54 Commits

Author SHA1 Message Date
e9d4c3eb64 fix: coroutine managers now handle exceptions 2026-04-10 11:22:40 +03:00
50794d44ba fix: replace LoggerBase FilterLevel from Trace to Info 2026-04-09 14:01:30 +03:00
5cd3b938f7 fix: yaml type containers are not being recognized while reading back 2026-04-08 15:21:37 +03:00
f81bd61aa1 fix: yaml serialized messing up string fields 2026-04-08 14:24:40 +03:00
6d8ba5c80c feat: added a wip nested coroutine manager
This coroutine manager allows for nested IEnumerators, however it is experimental at the moment.
2026-04-07 18:53:02 +03:00
b713fe4f12 feat: task.ToYield extension method added 2026-04-07 16:19:04 +03:00
4b3a0fdde0 chore: renamed task completion status names 2026-04-07 16:18:35 +03:00
44ff916afe fix: WaitForTaskYield getting stuck 2026-04-06 17:07:00 +03:00
497eedab72 chore: added throwing task exception when faulted on WaitForTaskYield 2026-04-06 15:01:05 +03:00
3893a1d249 chore: forgotten test physics material 2026-04-04 21:21:57 +03:00
4255cc4893 fix: some bad idea 2026-04-04 20:39:29 +03:00
37f4f56cd6 feat(physics): IRigidbody Intertia and Inverse Mass & Intertia fields added 2026-04-04 19:38:29 +03:00
629d758dbc feat: ICollider.OnRecalculated event added 2026-04-04 19:02:25 +03:00
7fb6821a83 feat(math): Math.OneOver and Invert methods added 2026-04-04 18:52:05 +03:00
af2eed2200 feat(physics): added area & inertia calculations for the shape and circles 2026-04-04 18:51:55 +03:00
6db427f39b chore: converted PhysicsMaterial2D's to classes and renamed Default to be ReadOnly 2026-04-04 14:07:37 +03:00
53b342da46 feat: added CollisionDetectionInformation.ContactPoint 2026-04-04 00:13:30 +03:00
4c13578125 feat: basic impulse resolving for 2D collisions 2026-04-03 21:41:32 +03:00
a47f8e4944 fix: color lerping not working properly 2026-03-31 23:11:42 +03:00
24046cf60c fix: triangle batch not drawing transparent colors properly 2026-03-31 22:51:59 +03:00
2266f69927 fix: color lerping not working from bright to darker 2026-03-31 21:01:10 +03:00
c11aced8d2 feat: added default valueless Get to IConfiguration for nullables 2026-03-31 17:21:18 +03:00
fe0173b091 fix: SpriteBatcher not drawing correctly 2026-03-28 22:35:47 +03:00
0707665481 feat: added a basic Viewport field to ICamera 2026-03-28 22:35:01 +03:00
734649955e feat: added .ApplyMatrix extension method for Matrix4x4 2026-03-28 22:32:51 +03:00
105b87da3a fix: file & type name mismatches fixed 2026-03-08 15:12:16 +03:00
51534606c8 feat: added WaitForTask yield 2026-03-08 12:55:22 +03:00
1418927c32 refactor: renamed WaitForSeconds to a more general WaitForTime class 2026-03-08 12:53:02 +03:00
35c7eb9578 refactor: renamed NetworkBehaviour to CommonNetworkBehaviour 2026-03-06 11:55:10 +03:00
6ca3f22b17 feat: added UpdateManager.CallFirstActiveFrameImmediately method for early calls of behaviours 2026-03-06 11:51:15 +03:00
7ae8b4feb0 feat: added NetworkBehaviour for oth client & server communication 2026-03-05 23:48:57 +03:00
e84c6edce1 refactor: removed the IComparable from IIdentifiable and implemented in extension method 2026-03-04 20:16:07 +03:00
4326d5615e feat: INetworkCommunicatorServer.SendToClients extension method added 2026-03-04 12:32:58 +03:00
fa514531bf feat: added IIdentifiable comparison interface & IsIdentical extension method 2026-03-03 23:30:56 +03:00
1d3dd8b046 chore: fixed IIdentifiable file name and class name being different 2026-03-03 23:14:37 +03:00
785fee9b6b perf: memory allocation improvements on ITween.Loop method 2026-02-20 11:22:46 +03:00
9f54f89f6d fix: ITween.OnEnded getting multiple calls and getting unnecessary calls on repeats fixed 2026-02-20 11:22:00 +03:00
aadc87d78a perf: memory allocation improvements on ITween.LoopIndefinitely method 2026-02-20 11:00:49 +03:00
d653774357 fix: forgotten Save method for YamlConfiguration 2026-02-09 13:23:22 +03:00
45bd505da7 chore: renamed parameter names for ISerializer methods 2026-02-09 13:22:37 +03:00
3b1c291588 feat: IConfiguration for system and other configurations 2026-02-09 13:17:10 +03:00
32a7e9be24 fix: forgotten yaml converter for Matrix4x4 2026-02-09 13:14:45 +03:00
499f875903 chore: removed unused piece of code 2026-02-02 14:42:34 +03:00
b2cfb2a590 docs: added NetworkManager comments 2026-02-01 13:34:02 +03:00
1d6b9d2421 feat: added WaitForSeconds and WaitWhile yields 2026-01-31 13:08:59 +03:00
882f9e8b29 feat: added new yields 2026-01-31 13:08:13 +03:00
913af2a4a4 fix: added dynamic index updates during event invocation so there are no missing/duplicate invocations
This also makes the events very not ideal for multithreaded applications at the moment.
2026-01-31 00:49:00 +03:00
4e9fda3d7c refactor: auto property on FastListOrdered 2026-01-30 18:33:07 +03:00
7675f9acac chore: added missing Matrix4x4NetPacker 2026-01-30 18:02:34 +03:00
72f86478f2 feat!: added broadcast & routing support for NetworkManager
It used to only route or broadcast, now the same packet can be both routed to it's own behaviour AND at the same time the broadcast listeners can get alerted of the same package such as universe-wide managers
2026-01-30 13:43:48 +03:00
64e7321f0f fix: rotating file logger deleting from the wrong order 2026-01-30 10:54:56 +03:00
c355c666e0 refactor: fixed LiteNetLibServer using events to subscribe to PostUpdate instead of the interface 2026-01-29 22:29:19 +03:00
b9f3227f73 refactor: removed parameters on triangle batch calls on TriangleBatcher for multi window support 2026-01-28 22:19:04 +03:00
c68de39c83 fix: MonoGame view matrix calculation issues 2026-01-28 12:58:25 +03:00
93 changed files with 1405 additions and 295 deletions

View File

@@ -5,6 +5,11 @@ namespace Engine.Core;
/// </summary>
public interface ICamera
{
/// <summary>
/// The viewport of the <see cref="ICamera"/>.
/// </summary>
Vector2D Viewport { get; }
/// <summary>
/// View <see cref="Matrix4x4"/> of the <see cref="ICamera"/>.
/// </summary>

View File

@@ -0,0 +1,51 @@
using System.Collections.Generic;
namespace Engine.Core.Config;
public class BasicConfiguration : IConfiguration
{
public Event<IConfiguration, IConfiguration.ConfigUpdateArguments> OnAdded { get; } = new();
public Event<IConfiguration, IConfiguration.ConfigUpdateArguments> OnSet { get; } = new();
public Event<IConfiguration, IConfiguration.ConfigUpdateArguments> OnRemoved { get; } = new();
private readonly Dictionary<string, object?> values = [];
public IReadOnlyDictionary<string, object?> Values => values;
public T Get<T>(string key, T defaultValue) => Get<T>(key) ?? defaultValue;
public T? Get<T>(string key)
{
if (!values.TryGetValue(key, out object? value))
return default;
if (value is T castedObject)
return castedObject;
try { return (T?)System.Convert.ChangeType(value, typeof(T)); } catch { }
return default;
}
public object? Get(string key)
{
values.TryGetValue(key, out object? value);
return value;
}
public bool Has(string key) => values.ContainsKey(key);
public void Remove<T>(string key)
{
if (values.Remove(key))
OnRemoved.Invoke(this, new(key));
}
public void Set<T>(string key, T value)
{
if (!values.TryAdd(key, value))
values[key] = value;
else
OnAdded.Invoke(this, new(key));
OnSet.Invoke(this, new(key));
}
}

View File

@@ -0,0 +1,8 @@
using Engine.Core.Exceptions;
namespace Engine.Core.Config;
public static class ConfigurationExtensions
{
public static T GetRequired<T>(this IConfiguration configuration, string key) => configuration.Get<T>(key) ?? throw new NotFoundException($"Type of {typeof(T).FullName} with the key {key} was not present in the {configuration.GetType().FullName}");
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace Engine.Core.Config;
public interface IConfiguration
{
static IConfiguration System { get; set; } = new SystemConfiguration();
static IConfiguration Shared { get; set; } = new BasicConfiguration();
Event<IConfiguration, ConfigUpdateArguments> OnAdded { get; }
Event<IConfiguration, ConfigUpdateArguments> OnSet { get; }
Event<IConfiguration, ConfigUpdateArguments> OnRemoved { get; }
IReadOnlyDictionary<string, object?> Values { get; }
bool Has(string key);
object? Get(string key);
T Get<T>(string key, T defaultValue);
T? Get<T>(string key);
void Set<T>(string key, T value);
void Remove<T>(string key);
readonly record struct ConfigUpdateArguments(string Key);
}

View File

@@ -0,0 +1,11 @@
namespace Engine.Core.Config;
public class SystemConfiguration : BasicConfiguration, IConfiguration
{
public SystemConfiguration()
{
foreach (System.Collections.DictionaryEntry entry in System.Environment.GetEnvironmentVariables())
if (entry is { Key: string key, Value: not null })
Set(key, entry.Value);
}
}

View File

@@ -4,7 +4,7 @@ namespace Engine.Core.Debug;
public abstract class LoggerBase : ILogger
{
public ILogger.Level FilterLevel { get; set; } = ILogger.Level.Trace;
public ILogger.Level FilterLevel { get; set; } = ILogger.Level.Info;
public void Log(string message, ILogger.Level level = ILogger.Level.Info, bool force = false)
{

View File

@@ -55,7 +55,7 @@ public class RotatingFileLogger : ILogger
private static void RotateLastLogs(string directory, string prefix, int rotateLength)
{
IOrderedEnumerable<string> logs = System.IO.Directory.GetFiles(directory, $"{prefix}*.log")
.OrderBy(File.GetCreationTime);
.OrderByDescending(File.GetCreationTime);
foreach (string file in logs.Skip(rotateLength))
try

View File

@@ -0,0 +1,12 @@
namespace Engine.Core;
public static class IdentifiableExtensions
{
public static bool IsIdentical(this IIdentifiable? left, IIdentifiable? right)
{
if (left == null || right == null)
return false;
return left?.Id?.CompareTo(right?.Id) == 0;
}
}

View File

@@ -58,6 +58,9 @@ public class Event
public ILogger Logger { get; set => field = value ?? ILogger.Shared; } = ILogger.Shared;
private int currentOnceCallIndex = -1; // These are for the purpose of if a listener is added/removed during invocation the index is dynamically updated so no missing/duplicate invocations happen.
private int currentCallIndex = -1; // These are for the purpose of if a listener is added/removed during invocation the index is dynamically updated so no missing/duplicate invocations happen.
private readonly List<ListenerData> listeners = null!;
private readonly List<ListenerData> onceListeners = null!;
@@ -74,6 +77,9 @@ public class Event
if (insertIndex < 0)
insertIndex = ~insertIndex;
if (insertIndex < currentCallIndex)
currentCallIndex++;
listeners.Insert(insertIndex, listenerData);
}
@@ -90,6 +96,9 @@ public class Event
if (insertIndex < 0)
insertIndex = ~insertIndex;
if (insertIndex < currentOnceCallIndex)
currentOnceCallIndex++;
onceListeners.Insert(insertIndex, listenerData);
}
@@ -103,6 +112,8 @@ public class Event
if (listeners[i].Callback == listener)
{
listeners.RemoveAt(i);
if (i < currentCallIndex)
currentCallIndex--;
return;
}
}
@@ -117,6 +128,8 @@ public class Event
if (onceListeners[i].Callback == listener)
{
onceListeners.RemoveAt(i);
if (i < currentOnceCallIndex)
currentOnceCallIndex--;
return;
}
}
@@ -131,23 +144,23 @@ public class Event
/// </summary>
public void Invoke()
{
for (int i = listeners.Count - 1; i >= 0; i--)
try { listeners[i].Callback.Invoke(); }
for (currentCallIndex = listeners.Count - 1; currentCallIndex >= 0; currentCallIndex--)
try { listeners[currentCallIndex].Callback.Invoke(); }
catch (Exception exception)
{
string methodCallRepresentation = $"{listeners[i].Callback.Method.DeclaringType?.FullName}.{listeners[i].Callback.Method.Name}()";
EventHelpers.LogInvocationException(listeners[i].Callback.Target ?? this, Logger, exception, methodCallRepresentation);
string methodCallRepresentation = $"{listeners[currentCallIndex].Callback.Method.DeclaringType?.FullName}.{listeners[currentCallIndex].Callback.Method.Name}()";
EventHelpers.LogInvocationException(listeners[currentCallIndex].Callback.Target ?? this, Logger, exception, methodCallRepresentation);
}
for (int i = onceListeners.Count - 1; i >= 0; i--)
for (currentOnceCallIndex = onceListeners.Count - 1; currentOnceCallIndex >= 0; currentOnceCallIndex--)
{
try { onceListeners[i].Callback.Invoke(); }
try { onceListeners[currentOnceCallIndex].Callback.Invoke(); }
catch (Exception exception)
{
string methodCallRepresentation = $"{onceListeners[i].Callback.Method.DeclaringType?.FullName}.{onceListeners[i].Callback.Method.Name}()";
EventHelpers.LogInvocationException(onceListeners[i].Callback.Target ?? this, Logger, exception, methodCallRepresentation);
string methodCallRepresentation = $"{onceListeners[currentOnceCallIndex].Callback.Method.DeclaringType?.FullName}.{onceListeners[currentOnceCallIndex].Callback.Method.Name}()";
EventHelpers.LogInvocationException(onceListeners[currentOnceCallIndex].Callback.Target ?? this, Logger, exception, methodCallRepresentation);
}
onceListeners.RemoveAt(i);
onceListeners.RemoveAt(currentOnceCallIndex);
}
}
@@ -216,6 +229,9 @@ public class Event<TSender> where TSender : class
public ILogger Logger { get; set => field = value ?? ILogger.Shared; } = ILogger.Shared;
private int currentOnceCallIndex = -1; // These are for the purpose of if a listener is added/removed during invocation the index is dynamically updated so no missing/duplicate invocations happen.
private int currentCallIndex = -1; // These are for the purpose of if a listener is added/removed during invocation the index is dynamically updated so no missing/duplicate invocations happen.
private readonly List<ListenerData> listeners = null!;
private readonly List<ListenerData> onceListeners = null!;
@@ -232,6 +248,9 @@ public class Event<TSender> where TSender : class
if (insertIndex < 0)
insertIndex = ~insertIndex;
if (insertIndex < currentCallIndex)
currentCallIndex++;
listeners.Insert(insertIndex, listenerData);
}
@@ -248,6 +267,9 @@ public class Event<TSender> where TSender : class
if (insertIndex < 0)
insertIndex = ~insertIndex;
if (insertIndex < currentOnceCallIndex)
currentOnceCallIndex++;
onceListeners.Insert(insertIndex, listenerData);
}
@@ -261,6 +283,8 @@ public class Event<TSender> where TSender : class
if (listeners[i].Callback == listener)
{
listeners.RemoveAt(i);
if (i < currentCallIndex)
currentCallIndex--;
return;
}
}
@@ -275,6 +299,8 @@ public class Event<TSender> where TSender : class
if (onceListeners[i].Callback == listener)
{
onceListeners.RemoveAt(i);
if (i < currentOnceCallIndex)
currentOnceCallIndex--;
return;
}
}
@@ -290,23 +316,23 @@ public class Event<TSender> where TSender : class
/// <param name="sender">The caller that's triggering this event.</param>
public void Invoke(TSender sender)
{
for (int i = listeners.Count - 1; i >= 0; i--)
try { listeners[i].Callback.Invoke(sender); }
for (currentCallIndex = listeners.Count - 1; currentCallIndex >= 0; currentCallIndex--)
try { listeners[currentCallIndex].Callback.Invoke(sender); }
catch (Exception exception)
{
string methodCallRepresentation = $"{listeners[i].Callback.Method.DeclaringType?.FullName}.{listeners[i].Callback.Method.Name}({sender})";
EventHelpers.LogInvocationException(listeners[i].Callback.Target ?? sender, Logger, exception, methodCallRepresentation);
string methodCallRepresentation = $"{listeners[currentCallIndex].Callback.Method.DeclaringType?.FullName}.{listeners[currentCallIndex].Callback.Method.Name}({sender})";
EventHelpers.LogInvocationException(listeners[currentCallIndex].Callback.Target ?? sender, Logger, exception, methodCallRepresentation);
}
for (int i = onceListeners.Count - 1; i >= 0; i--)
for (currentOnceCallIndex = onceListeners.Count - 1; currentOnceCallIndex >= 0; currentOnceCallIndex--)
{
try { onceListeners[i].Callback.Invoke(sender); }
try { onceListeners[currentOnceCallIndex].Callback.Invoke(sender); }
catch (Exception exception)
{
string methodCallRepresentation = $"{onceListeners[i].Callback.Method.DeclaringType?.FullName}.{onceListeners[i].Callback.Method.Name}({sender})";
EventHelpers.LogInvocationException(onceListeners[i].Callback.Target ?? sender, Logger, exception, methodCallRepresentation);
string methodCallRepresentation = $"{onceListeners[currentOnceCallIndex].Callback.Method.DeclaringType?.FullName}.{onceListeners[currentOnceCallIndex].Callback.Method.Name}({sender})";
EventHelpers.LogInvocationException(onceListeners[currentOnceCallIndex].Callback.Target ?? sender, Logger, exception, methodCallRepresentation);
}
onceListeners.RemoveAt(i);
onceListeners.RemoveAt(currentOnceCallIndex);
}
}
@@ -382,6 +408,9 @@ public class Event<TSender, TArguments> where TSender : class
public ILogger Logger { get; set => field = value ?? ILogger.Shared; } = ILogger.Shared;
private int currentOnceCallIndex = -1; // These are for the purpose of if a listener is added/removed during invocation the index is dynamically updated so no missing/duplicate invocations happen.
private int currentCallIndex = -1; // These are for the purpose of if a listener is added/removed during invocation the index is dynamically updated so no missing/duplicate invocations happen.
private readonly List<ListenerData> listeners = null!;
private readonly List<ListenerData> onceListeners = null!;
@@ -398,6 +427,9 @@ public class Event<TSender, TArguments> where TSender : class
if (insertIndex < 0)
insertIndex = ~insertIndex;
if (insertIndex < currentCallIndex)
currentCallIndex++;
listeners.Insert(insertIndex, listenerData);
}
@@ -414,6 +446,9 @@ public class Event<TSender, TArguments> where TSender : class
if (insertIndex < 0)
insertIndex = ~insertIndex;
if (insertIndex < currentOnceCallIndex)
currentOnceCallIndex++;
onceListeners.Insert(insertIndex, listenerData);
}
@@ -427,6 +462,8 @@ public class Event<TSender, TArguments> where TSender : class
if (listeners[i].Callback == listener)
{
listeners.RemoveAt(i);
if (i < currentCallIndex)
currentCallIndex--;
return;
}
}
@@ -441,6 +478,8 @@ public class Event<TSender, TArguments> where TSender : class
if (onceListeners[i].Callback == listener)
{
onceListeners.RemoveAt(i);
if (i < currentOnceCallIndex)
currentOnceCallIndex--;
return;
}
}
@@ -457,23 +496,23 @@ public class Event<TSender, TArguments> where TSender : class
/// <param name="args">The arguments provided for this event.</param>
public void Invoke(TSender sender, TArguments args)
{
for (int i = listeners.Count - 1; i >= 0; i--)
try { listeners[i].Callback.Invoke(sender, args); }
for (currentCallIndex = listeners.Count - 1; currentCallIndex >= 0; currentCallIndex--)
try { listeners[currentCallIndex].Callback.Invoke(sender, args); }
catch (Exception exception)
{
string methodCallRepresentation = $"{listeners[i].Callback.Method.DeclaringType?.FullName}.{listeners[i].Callback.Method.Name}({sender}, {args})";
EventHelpers.LogInvocationException(listeners[i].Callback.Target ?? sender, Logger, exception, methodCallRepresentation);
string methodCallRepresentation = $"{listeners[currentCallIndex].Callback.Method.DeclaringType?.FullName}.{listeners[currentCallIndex].Callback.Method.Name}({sender}, {args})";
EventHelpers.LogInvocationException(listeners[currentCallIndex].Callback.Target ?? sender, Logger, exception, methodCallRepresentation);
}
for (int i = onceListeners.Count - 1; i >= 0; i--)
for (currentOnceCallIndex = onceListeners.Count - 1; currentOnceCallIndex >= 0; currentOnceCallIndex--)
{
try { onceListeners[i].Callback.Invoke(sender, args); }
try { onceListeners[currentOnceCallIndex].Callback.Invoke(sender, args); }
catch (Exception exception)
{
string methodCallRepresentation = $"{onceListeners[i].Callback.Method.DeclaringType?.FullName}.{onceListeners[i].Callback.Method.Name}({sender}, {args})";
EventHelpers.LogInvocationException(onceListeners[i].Callback.Target ?? sender, Logger, exception, methodCallRepresentation);
string methodCallRepresentation = $"{onceListeners[currentOnceCallIndex].Callback.Method.DeclaringType?.FullName}.{onceListeners[currentOnceCallIndex].Callback.Method.Name}({sender}, {args})";
EventHelpers.LogInvocationException(onceListeners[currentOnceCallIndex].Callback.Target ?? sender, Logger, exception, methodCallRepresentation);
}
onceListeners.RemoveAt(i);
onceListeners.RemoveAt(currentOnceCallIndex);
}
}

View File

@@ -16,8 +16,7 @@ public class FastListOrdered<TIndex, TItem> : IList<TItem>, IReadOnlyList<TItem>
private readonly Func<TItem, TIndex> getIndexFunc = null!;
private readonly IComparer<TIndex> sortBy = null!;
private int count = 0;
public int Count => count;
public int Count { get; private set; } = 0;
public bool IsReadOnly { get; set; } = false;
@@ -35,10 +34,10 @@ public class FastListOrdered<TIndex, TItem> : IList<TItem>, IReadOnlyList<TItem>
private (TIndex TIndex, int i) GetAt(Index index)
{
int actualIndex = index.IsFromEnd
? count - index.Value
? Count - index.Value
: index.Value;
if (actualIndex < 0 || actualIndex >= count)
if (actualIndex < 0 || actualIndex >= Count)
throw new IndexOutOfRangeException();
int leftIndex = actualIndex;
@@ -75,7 +74,7 @@ public class FastListOrdered<TIndex, TItem> : IList<TItem>, IReadOnlyList<TItem>
items[key] = list = [];
list.Add(item);
count++;
Count++;
}
public void Insert(int index, TItem item)
@@ -88,7 +87,7 @@ public class FastListOrdered<TIndex, TItem> : IList<TItem>, IReadOnlyList<TItem>
items[tIndex] = list = [];
list.Insert(index, item);
count++;
Count++;
}
public bool Remove(TItem item)
@@ -103,7 +102,7 @@ public class FastListOrdered<TIndex, TItem> : IList<TItem>, IReadOnlyList<TItem>
if (!list.Remove(item))
return false;
count--;
Count--;
return true;
}
@@ -114,7 +113,7 @@ public class FastListOrdered<TIndex, TItem> : IList<TItem>, IReadOnlyList<TItem>
(TIndex tIndex, int i) = GetAt(index);
items[tIndex].RemoveAt(i);
count--;
Count--;
}
public void Clear()
@@ -125,7 +124,7 @@ public class FastListOrdered<TIndex, TItem> : IList<TItem>, IReadOnlyList<TItem>
foreach ((TIndex index, FastList<TItem> list) in items)
list.Clear();
count = 0;
Count = 0;
}
public bool Contains(TItem item)

View File

@@ -30,6 +30,16 @@ public static class Math
/// </summary>
public const float DegreeToRadian = Pi / 180f;
/// <inheritdoc cref="Invert{T}(T)" />
public static T OneOver<T>(T value) where T : INumber<T> => T.One / value;
/// <summary>
/// Gets 1 / value of given <see cref="T"/>.
/// </summary>
/// <param name="value">The value <see cref="T"/>.</param>
/// <returns>1 / value of given <see cref="T"/>.</returns>
public static T Invert<T>(T value) where T : INumber<T> => T.One / value;
/// <summary>
/// Gets one minus of given <see cref="T"/>.
/// </summary>

View File

@@ -8,6 +8,12 @@ public static class MathExtensions
/// <inheritdoc cref="Math.OneMinus{T}(T)" />
public static T OneMinus<T>(this T value) where T : INumber<T> => Math.OneMinus(value);
/// <inheritdoc cref="Math.OneOver{T}(T)" />
public static T OneOver<T>(this T value) where T : INumber<T> => Math.OneOver(value);
/// <inheritdoc cref="Math.Invert{T}(T)" />
public static T Invert<T>(this T value) where T : INumber<T> => Math.OneMinus(value);
/// <inheritdoc cref="Math.Add{T}(T, T)" />
public static T Add<T>(this T left, T value) where T : INumber<T> => Math.Add(left, value);

View File

@@ -34,6 +34,16 @@ public readonly struct Circle(Vector2D center, float radius) : IEquatable<Circle
/// </summary>
public readonly float Diameter => 2f * Radius;
/// <summary>
/// Gets the area of the <see cref="Circle"/>.
/// </summary>
public readonly float Area => Math.Pi * RadiusSquared;
/// <summary>
/// Gets the geometric interia of the <see cref="Circle"/>.
/// </summary>
public readonly float GeometricInertia => .5f * RadiusSquared;
/// <summary>
/// A predefined unit <see cref="Circle"/> with a center at the origin and a radius of 1.
/// </summary>

View File

@@ -95,7 +95,11 @@ public readonly struct ColorRGB(byte r, byte g, byte b) : IEquatable<ColorRGB>
int greenDiff = to.G - from.G;
int blueDiff = to.B - from.B;
return from + new ColorRGB((byte)(redDiff * t), (byte)(greenDiff * t), (byte)(blueDiff * t));
return new(
(byte)(from.R + redDiff * t),
(byte)(from.G + greenDiff * t),
(byte)(from.B + blueDiff * t)
);
}
/// <summary>

View File

@@ -1,4 +1,5 @@
using System;
using Engine.Core.Debug;
namespace Engine.Core;
@@ -125,7 +126,12 @@ public readonly struct ColorRGBA(byte r, byte g, byte b, byte a = 255) : IEquata
int blueDiff = to.B - from.B;
int alphaDiff = to.A - from.A;
return from + new ColorRGBA((byte)(redDiff * t), (byte)(greenDiff * t), (byte)(blueDiff * t), (byte)(alphaDiff * t));
return new(
(byte)(from.R + redDiff * t),
(byte)(from.G + greenDiff * t),
(byte)(from.B + blueDiff * t),
(byte)(from.A + alphaDiff * t)
);
}
/// <summary>

View File

@@ -436,4 +436,9 @@ public static class Matrix4x4Extensions
/// <inheritdoc cref="Matrix4x4.ToRightHanded(Matrix4x4) />
public static Matrix4x4 ToRightHanded(this Matrix4x4 matrix) => Matrix4x4.ToRightHanded(matrix);
/// <summary>
/// Multiplies two <see cref="Matrix4x4"/>'s.
/// </summary>
public static Matrix4x4 ApplyMatrix(this Matrix4x4 left, Matrix4x4 right) => left * right;
}

View File

@@ -22,6 +22,31 @@ public class Shape2D(List<Vector2D> vertices) : IEnumerable<Vector2D>
private readonly List<Vector2D> _vertices = vertices;
/// <summary>
/// Gets the area of the <see cref="Shape2D"/>.
/// </summary>
public float Area
{
get
{
float area = 0f;
for (int i = 0; i < _vertices.Count; i++)
{
Vector2D a = _vertices[i];
Vector2D b = _vertices[(i + 1) % _vertices.Count];
area += a.Cross(b);
}
return area.Abs() * .5f;
}
}
/// <summary>
/// Gets the geometric interia of the <see cref="Shape2D"/>.
/// </summary>
public float GeometricInertia => GetGeometricInertia(this, Vector2D.Zero);
/// <summary>
/// Gets the vertices of the <see cref="Shape2D"/>.
/// </summary>
@@ -112,6 +137,40 @@ public class Shape2D(List<Vector2D> vertices) : IEnumerable<Vector2D>
return new Triangle(p1, p2, p3);
}
/// <summary>
/// Gets the geometric interia of the <see cref="Shape2D"/>.
/// </summary>
/// <param name="shape">The shape to get the geometrical interia of.</param>
/// <param name="centerOfMass">The point in space to calculate the geometrical interia from.</param>
/// <returns>The geometrical interia of the <see cref="Shape2D"/>.</returns>
public static float GetGeometricInertia(Shape2D shape, Vector2D centerOfMass)
{
float geometricInertia = 0f;
for (int i = 0; i < shape._vertices.Count; i++)
{
Vector2D p1 = centerOfMass.FromTo(shape._vertices[i]);
Vector2D p2 = centerOfMass.FromTo(shape._vertices[(i + 1) % shape._vertices.Count]);
float cross = p1.Cross(p2);
float dot = p1.Dot(p1) + p1.Dot(p2) + p2.Dot(p2);
geometricInertia += cross * dot;
}
return geometricInertia.Abs() / 12f;
}
/// <summary>
/// Gets the interia of the <see cref="Shape2D"/>.
/// </summary>
/// <param name="shape">The shape to get the interia of.</param>
/// <param name="centerOfMass">The point in space to calculate the geometrical interia from.</param>
/// <param name="mass">Mass of the shape.</param>
/// <returns>The interia of the <see cref="Shape2D"/>.</returns>
public static float GetInertia(Shape2D shape, Vector2D centerOfMass, float mass)
=> GetGeometricInertia(shape, centerOfMass) * mass;
/// <summary>
/// Triangulates the given convex <see cref="Shape2D"/>.
/// </summary>
@@ -297,6 +356,12 @@ public static class Shape2DExtensions
/// <inheritdoc cref="Shape2D.GetSuperTriangle(Shape2D)" />
public static Triangle ToSuperTriangle(this Shape2D shape) => Shape2D.GetSuperTriangle(shape);
/// <inheritdoc cref="Shape2D.GetGeometricInertia(Shape2D, Vector2D)" />
public static float GetGeometricInertia(this Shape2D shape, Vector2D centerOfMass) => Shape2D.GetGeometricInertia(shape, centerOfMass);
/// <inheritdoc cref="Shape2D.GetInertia(Shape2D, Vector2D, float)" />
public static float GetInertia(this Shape2D shape, Vector2D centerOfMass, float mass) => Shape2D.GetInertia(shape, centerOfMass, mass);
/// <inheritdoc cref="Shape2D.TriangulateConvex(Shape2D, IList{Triangle})" />
public static void ToTrianglesConvex(this Shape2D shape, IList<Triangle> triangles) => Shape2D.TriangulateConvex(shape, triangles);

View File

@@ -4,15 +4,15 @@ namespace Engine.Core.Serialization;
public interface ISerializer
{
object Deserialize(string configuration);
object Deserialize(string configuration, Type type);
T Deserialize<T>(string configuration);
object Deserialize(string content);
object Deserialize(string content, Type type);
T Deserialize<T>(string content);
string Serialize(object instance);
ProgressiveTask<object> DeserializeAsync(string configuration);
ProgressiveTask<object> DeserializeAsync(string configuration, Type type);
ProgressiveTask<T> DeserializeAsync<T>(string configuration);
ProgressiveTask<object> DeserializeAsync(string content);
ProgressiveTask<object> DeserializeAsync(string content, Type type);
ProgressiveTask<T> DeserializeAsync<T>(string content);
ProgressiveTask<string> SerializeAsync(object instance);
}

View File

@@ -0,0 +1,9 @@
using System.Collections;
namespace Engine.Core;
public interface ICoroutineManager
{
IEnumerator StartCoroutine(IEnumerator enumerator);
void StopCoroutine(IEnumerator enumerator);
}

View File

@@ -1,6 +1,8 @@
using System.Collections;
using System.Collections.Generic;
using Engine.Core.Debug;
namespace Engine.Core;
public class CoroutineManager : Behaviour, IUpdate
@@ -22,11 +24,21 @@ public class CoroutineManager : Behaviour, IUpdate
{
for (int i = enumerators.Count - 1; i >= 0; i--)
{
if (enumerators[i].Current is ICoroutineYield coroutineYield && coroutineYield.Yield())
continue;
try
{
if (enumerators[i].Current is ICoroutineYield coroutineYield && coroutineYield.Yield())
continue;
if (!enumerators[i].MoveNext())
if (!enumerators[i].MoveNext())
enumerators.RemoveAt(i);
}
catch (System.Exception exception)
{
ILogger.Shared.LogError(this, $"Coroutine failed, removing from execution.");
ILogger.Shared.LogException(this, exception);
ILogger.Shared.LogTrace(exception.StackTrace);
enumerators.RemoveAt(i);
}
}
}

View File

@@ -0,0 +1,92 @@
using System.Collections;
using System.Collections.Generic;
using Engine.Core.Debug;
namespace Engine.Core;
public class NestedCoroutineManager : Behaviour, IUpdate, ICoroutineManager
{
private readonly List<CoroutineStack> stacks = [];
private readonly Pool<CoroutineStack> pool = new(() => new());
public IEnumerator StartCoroutine(IEnumerator enumerator)
{
CoroutineStack stack = pool.Get();
stack.EntryPoint = enumerator;
stack.Stack.Push(enumerator);
stacks.Add(stack);
return enumerator;
}
public void StopCoroutine(IEnumerator enumerator)
{
for (int i = 0; i < stacks.Count; i++)
if (stacks[i].EntryPoint == enumerator)
{
RemoveCoroutineAt(i);
return;
}
}
private void RemoveCoroutineAt(int i)
{
stacks[i].Reset();
stacks.RemoveAt(i);
}
void IUpdate.Update()
{
for (int i = stacks.Count - 1; i >= 0; i--)
{
Stack<IEnumerator> stack = stacks[i].Stack;
if (stack.Count == 0)
{
RemoveCoroutineAt(i);
continue;
}
try
{
IEnumerator top = stack.Peek();
if (top.Current is ICoroutineYield coroutineYield && coroutineYield.Yield())
continue;
if (top.Current is IEnumerator nested)
{
stack.Push(nested);
continue;
}
if (!top.MoveNext())
{
stack.Pop();
if (stack.Count != 0)
stack.Peek().MoveNext();
continue;
}
}
catch (System.Exception exception)
{
ILogger.Shared.LogError(this, $"Coroutine failed, removing from execution.");
ILogger.Shared.LogException(this, exception);
ILogger.Shared.LogTrace(exception.StackTrace);
RemoveCoroutineAt(i);
}
}
}
private class CoroutineStack
{
public IEnumerator EntryPoint = null!;
public Stack<IEnumerator> Stack = new();
public void Reset() { EntryPoint = null!; Stack.Clear(); }
}
public NestedCoroutineManager() => Priority = int.MinValue;
}

View File

@@ -44,6 +44,17 @@ public class UpdateManager : Behaviour, IEnterUniverse, IExitUniverse
universe.OnPostUpdate.RemoveListener(OnPostUpdate);
}
/// <summary>
/// Call the <see cref="IFirstFrameUpdate"/> early if it's in queue to be called by this the <see cref="UpdateManager"/>.
/// It will not be called in the next natural cycle.
/// </summary>
/// <param name="instance">The instance that will be called now rather than later.</param>
public void CallFirstActiveFrameImmediately(IFirstFrameUpdate instance)
{
if (toCallFirstFrameUpdates.Remove(instance))
instance.FirstActiveFrame();
}
private void OnFirstUpdate(IUniverse sender, IUniverse.UpdateArguments args)
{
for (int i = toCallFirstFrameUpdates.Count - 1; i >= 0; i--)

View File

@@ -0,0 +1,35 @@
using System.Threading.Tasks;
using static Engine.Core.WaitForTaskYield;
namespace Engine.Core;
public class WaitForTaskYield(Task task, TaskCompletionStatus completionStatus = TaskCompletionStatus.Any) : ICoroutineYield
{
public bool Yield()
{
switch (completionStatus)
{
case TaskCompletionStatus.Success:
if (task.IsCanceled)
throw new("Task has been canceled.");
if (task.IsFaulted)
throw task.Exception ?? new("Task has faulted.");
return !task.IsCompletedSuccessfully;
case TaskCompletionStatus.Fail:
if (task.IsCompletedSuccessfully)
throw new("Task was completed successfully.");
return !task.IsFaulted;
}
return !task.IsCompleted;
}
public enum TaskCompletionStatus
{
Any,
Success,
Fail
}
}

View File

@@ -0,0 +1,14 @@
using System;
namespace Engine.Core;
public class WaitForTimeYield(float seconds = 0f, float milliseconds = 0f, float minutes = 0f, float hours = 0f) : ICoroutineYield
{
private readonly DateTime triggerTime = DateTime.UtcNow
.AddHours(hours)
.AddMinutes(minutes)
.AddSeconds(seconds)
.AddMilliseconds(milliseconds);
public bool Yield() => DateTime.UtcNow < triggerTime;
}

View File

@@ -0,0 +1,10 @@
using System;
namespace Engine.Core;
public class WaitUntilYield(Func<bool> condition) : ICoroutineYield
{
private readonly Func<bool> condition = condition;
public bool Yield() => !condition.Invoke();
}

View File

@@ -2,7 +2,7 @@ using System;
namespace Engine.Core;
public class CoroutineYield(Func<bool> condition) : ICoroutineYield
public class WaitWhileYield(Func<bool> condition) : ICoroutineYield
{
private readonly Func<bool> condition = condition;

View File

@@ -0,0 +1,10 @@
using System.Threading.Tasks;
using static Engine.Core.WaitForTaskYield;
namespace Engine.Core;
public static class YieldExtensions
{
public static WaitForTaskYield ToYield(this Task task, TaskCompletionStatus completionStatus = TaskCompletionStatus.Any) => new(task, completionStatus);
}

View File

@@ -178,6 +178,7 @@ public abstract class LiteNetLibCommunicatorBase : Behaviour, IEnterUniverse, IE
netPacketProcessor.RegisterNestedType(Vector3DNetPacker.Write, Vector3DNetPacker.Read);
netPacketProcessor.RegisterNestedType(Vector3DIntNetPacker.Write, Vector3DIntNetPacker.Read);
netPacketProcessor.RegisterNestedType(Vector4DNetPacker.Write, Vector4DNetPacker.Read);
netPacketProcessor.RegisterNestedType(Matrix4x4NetPacker.Write, Matrix4x4NetPacker.Read);
}
public INetworkCommunicator SubscribeToPackets<T>(Event<IConnection, T>.EventHandler callback)

View File

@@ -6,7 +6,7 @@ using Engine.Core.Debug;
namespace Engine.Systems.Network;
public class LiteNetLibServer : LiteNetLibCommunicatorBase, INetworkCommunicatorServer
public class LiteNetLibServer : LiteNetLibCommunicatorBase, INetworkCommunicatorServer, IPostUpdate
{
public string Password { get; private set; } = string.Empty;
public int MaxConnectionCount { get; private set; } = 2;
@@ -104,17 +104,8 @@ public class LiteNetLibServer : LiteNetLibCommunicatorBase, INetworkCommunicator
}
}
private void PollEvents(IUniverse sender, IUniverse.UpdateArguments args) => Manager.PollEvents();
public override void EnterUniverse(IUniverse universe)
public void PostUpdate()
{
base.EnterUniverse(universe);
universe.OnPostUpdate.AddListener(PollEvents);
}
public override void ExitUniverse(IUniverse universe)
{
base.ExitUniverse(universe);
universe.OnPostUpdate.RemoveListener(PollEvents);
Manager.PollEvents();
}
}

View File

@@ -0,0 +1,61 @@
using LiteNetLib.Utils;
using Engine.Core;
namespace Engine.Systems.Network.Packers;
internal static class Matrix4x4NetPacker
{
internal static void Write(NetDataWriter writer, Matrix4x4 data)
{
writer.Put(data.M11);
writer.Put(data.M12);
writer.Put(data.M13);
writer.Put(data.M14);
writer.Put(data.M21);
writer.Put(data.M22);
writer.Put(data.M23);
writer.Put(data.M24);
writer.Put(data.M31);
writer.Put(data.M32);
writer.Put(data.M33);
writer.Put(data.M34);
writer.Put(data.M41);
writer.Put(data.M42);
writer.Put(data.M43);
writer.Put(data.M44);
}
internal static Matrix4x4 Read(NetDataReader reader)
{
float m11 = reader.GetFloat();
float m12 = reader.GetFloat();
float m13 = reader.GetFloat();
float m14 = reader.GetFloat();
float m21 = reader.GetFloat();
float m22 = reader.GetFloat();
float m23 = reader.GetFloat();
float m24 = reader.GetFloat();
float m31 = reader.GetFloat();
float m32 = reader.GetFloat();
float m33 = reader.GetFloat();
float m34 = reader.GetFloat();
float m41 = reader.GetFloat();
float m42 = reader.GetFloat();
float m43 = reader.GetFloat();
float m44 = reader.GetFloat();
return new Matrix4x4(
m11, m12, m13, m14,
m21, m22, m23, m24,
m31, m32, m33, m34,
m41, m42, m43, m44
);
}
}

View File

@@ -1,5 +1,4 @@
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Engine.Core;
@@ -41,7 +40,7 @@ public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFr
}
} = Matrix4x4.Identity;
public Viewport Viewport
public Vector2D Viewport
{
get;
set
@@ -72,8 +71,8 @@ public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFr
// TODO This causes delay since OnPreDraw calls assuming this is called in in Update
public Vector2D ScreenToWorldPosition(Vector2D screenPosition)
{
float x = 2f * screenPosition.X / Viewport.Width - 1f;
float y = 1f - 2f * screenPosition.Y / Viewport.Height;
float x = 2f * screenPosition.X / Viewport.X - 1f;
float y = 1f - 2f * screenPosition.Y / Viewport.Y;
Vector4D normalizedCoordinates = new(x, y, 0f, 1f);
Matrix4x4 invertedViewProjectionMatrix = (ProjectionMatrix * ViewMatrix).Inverse;
@@ -96,8 +95,8 @@ public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFr
if (clip.W != 0f)
clip /= clip.W;
float screenX = (clip.X + 1f) * .5f * Viewport.Width;
float screenY = (1f - clip.Y) * .5f * Viewport.Height;
float screenX = (clip.X + 1f) * .5f * Viewport.X;
float screenY = (1f - clip.Y) * .5f * Viewport.Y;
return new(screenX, screenY);
}
@@ -106,17 +105,17 @@ public class MonoGameCamera2D : Behaviour, ICamera2D, IFirstFrameUpdate, ILastFr
public void FirstActiveFrame()
{
Graphics = BehaviourController.UniverseObject.Universe.FindRequiredBehaviour<MonoGameWindowContainer>().Window.Graphics;
Viewport = Graphics.GraphicsDevice.Viewport;
Viewport = new(Graphics.GraphicsDevice.Viewport.Width, Graphics.GraphicsDevice.Viewport.Height);
Transform = BehaviourController.GetRequiredBehaviour<ITransform2D>();
}
public void PreDraw()
{
ProjectionMatrix = Matrix4x4.CreateOrthographicViewCentered(Viewport.Width, Viewport.Height);
ViewMatrix =
Matrix4x4.CreateTranslation(new Vector3D(-Transform.Position.X, -Transform.Position.Y, 0f))
.ApplyRotationZ(Transform.Rotation * Math.DegreeToRadian)
.ApplyScale(Transform.Scale.X.Max(Transform.Scale.Y))
.ApplyScale(Zoom);
ProjectionMatrix = Matrix4x4.CreateOrthographicViewCentered(Viewport.X, Viewport.Y);
ViewMatrix = Matrix4x4.Identity
.ApplyTranslation(new Vector3D(-Transform.Position.X, -Transform.Position.Y, 0f))
.ApplyRotationZ(Transform.Rotation * Math.DegreeToRadian)
.ApplyScale(Transform.Scale.X.Max(Transform.Scale.Y))
.ApplyScale(Zoom);
}
}

View File

@@ -49,7 +49,7 @@ public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFr
}
} = Matrix4x4.Identity;
public Viewport Viewport
public Vector2D Viewport
{
get;
set
@@ -57,7 +57,7 @@ public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFr
if (field.Equals(value))
return;
Viewport previousViewport = field;
Vector2D previousViewport = field;
field = value;
SetForRecalculation();
OnViewportChanged.Invoke(this, new(previousViewport));
@@ -114,21 +114,21 @@ public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFr
Matrix projection = ProjectionMatrix.ToXnaMatrix();
Matrix view = ViewMatrix.ToXnaMatrix();
Vector3 worldNear = Viewport.Unproject(nearPoint, projection, view, Matrix.Identity);
Vector3 worldFar = Viewport.Unproject(farPoint, projection, view, Matrix.Identity);
Vector3 worldNear = Graphics.GraphicsDevice.Viewport.Unproject(nearPoint, projection, view, Matrix.Identity);
Vector3 worldFar = Graphics.GraphicsDevice.Viewport.Unproject(farPoint, projection, view, Matrix.Identity);
Vector3 direction = Vector3.Normalize(worldFar - worldNear);
return new(worldNear.ToVector3D(), direction.ToVector3D());
}
public Vector2D WorldToScreenPosition(Vector3D worldPosition) => Viewport.Project(worldPosition.ToVector3(), ProjectionMatrix.ToXnaMatrix(), ViewMatrix.ToXnaMatrix(), Matrix.Identity).ToVector3D();
public Vector2D WorldToScreenPosition(Vector3D worldPosition) => Graphics.GraphicsDevice.Viewport.Project(worldPosition.ToVector3(), ProjectionMatrix.ToXnaMatrix(), ViewMatrix.ToXnaMatrix(), Matrix.Identity).ToVector3D();
public void LastActiveFrame() => Transform.OnTransformUpdated.RemoveListener(SetDirtyOnTransformUpdate);
public void FirstActiveFrame()
{
Transform = BehaviourController.GetRequiredBehaviour<ITransform3D>();
Graphics = BehaviourController.UniverseObject.Universe.FindRequiredBehaviour<MonoGameWindowContainer>().Window.Graphics;
Viewport = Graphics.GraphicsDevice.Viewport;
Viewport = new(Graphics.GraphicsDevice.Viewport.Width, Graphics.GraphicsDevice.Viewport.Height);
Transform.OnTransformUpdated.AddListener(SetDirtyOnTransformUpdate);
}
@@ -167,7 +167,7 @@ public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFr
private void CalculateProjection()
{
float yScale = 1f / (float)Math.Tan(fieldOfView / 2f);
float xScale = yScale / Viewport.AspectRatio;
float xScale = yScale / (Viewport.X / Viewport.Y);
ProjectionMatrix = new Matrix4x4(
xScale, 0, 0, 0,
@@ -179,5 +179,5 @@ public class MonoGameCamera3D : Behaviour, ICamera3D, IFirstFrameUpdate, ILastFr
public readonly record struct ViewChangedArguments(Matrix4x4 PreviousView);
public readonly record struct ProjectionChangedArguments(Matrix4x4 PreviousProjection);
public readonly record struct ViewportChangedArguments(Viewport PreviousViewport);
public readonly record struct ViewportChangedArguments(Vector2D PreviousViewport);
}

View File

@@ -1,5 +1,7 @@
using System.Collections.Generic;
using Microsoft.Xna.Framework.Graphics;
using Engine.Core;
namespace Engine.Integration.MonoGame;
@@ -12,6 +14,7 @@ public class SpriteBatcher : Behaviour, IFirstFrameUpdate, IDraw
private ISpriteBatch spriteBatch = null!;
private MonoGameCamera2D camera2D = null!;
private readonly RasterizerState rasterizerState = new() { CullMode = CullMode.CullClockwiseFace };
private readonly ActiveBehaviourCollectorOrdered<int, IDrawableSprite> drawableSprites = new(GetPriority(), SortByPriority());
public void FirstActiveFrame()
@@ -26,7 +29,13 @@ public class SpriteBatcher : Behaviour, IFirstFrameUpdate, IDraw
public void Draw()
{
spriteBatch.Begin(transformMatrix: camera2D.ViewMatrix.ToXnaMatrix());
Matrix4x4 transformMatrix = Matrix4x4.Identity
.ApplyTranslation(new Vector2D(camera2D.Viewport.X, camera2D.Viewport.Y) * .5f)
.ApplyScale(new Vector3D(1f, -1f, 1f))
.ApplyMatrix(camera2D.ViewMatrix)
.Transposed;
spriteBatch.Begin(transformMatrix: transformMatrix.ToXnaMatrix(), rasterizerState: rasterizerState);
for (int i = 0; i < drawableSprites.Count; i++)
drawableSprites[i].Draw(spriteBatch);
spriteBatch.End();

View File

@@ -19,8 +19,11 @@ public class MonoGameTriangleBatch : Behaviour, ITriangleBatch, IFirstFrameUpdat
private BasicEffect basicEffect = null!;
private readonly RasterizerState rasterizerState = new() { CullMode = CullMode.None };
private ICamera camera = null!;
public void FirstActiveFrame()
{
camera = Universe.FindRequiredBehaviour<ICamera>();
GraphicsDevice graphicsDevice = Universe.FindRequiredBehaviour<MonoGameWindowContainer>().Window.GraphicsDevice;
this.graphicsDevice = graphicsDevice;
basicEffect = new(graphicsDevice);
@@ -36,7 +39,7 @@ public class MonoGameTriangleBatch : Behaviour, ITriangleBatch, IFirstFrameUpdat
Vector2 A = triangle.A.ToVector2();
Vector2 B = triangle.B.ToVector2();
Vector2 C = triangle.C.ToVector2();
Color color = colorRGBA.ToColor();
Color color = colorRGBA.ToPreMultipliedColor();
vertices[verticesIndex++] = new(new(A.X, A.Y, 0f), color);
vertices[verticesIndex++] = new(new(B.X, B.Y, 0f), color);
@@ -45,10 +48,8 @@ public class MonoGameTriangleBatch : Behaviour, ITriangleBatch, IFirstFrameUpdat
public void Begin(Matrix4x4? view = null, Matrix4x4? projection = null)
{
Viewport viewport = graphicsDevice.Viewport;
this.view = (view ?? Matrix4x4.Identity).Transposed.ToXnaMatrix();
this.projection = (projection ?? Matrix4x4.CreateOrthographicViewCentered(viewport.Width, viewport.Height)).Transposed.ToXnaMatrix();
this.view = (view ?? camera.ViewMatrix).Transposed.ToXnaMatrix();
this.projection = (projection ?? camera.ProjectionMatrix).Transposed.ToXnaMatrix();
}
public void End() => Flush();
@@ -59,6 +60,8 @@ public class MonoGameTriangleBatch : Behaviour, ITriangleBatch, IFirstFrameUpdat
return;
graphicsDevice.RasterizerState = rasterizerState;
graphicsDevice.BlendState = BlendState.AlphaBlend;
basicEffect.Projection = projection;
basicEffect.View = view;
vertexBuffer.SetData(vertices);
@@ -66,8 +69,10 @@ public class MonoGameTriangleBatch : Behaviour, ITriangleBatch, IFirstFrameUpdat
graphicsDevice.SetVertexBuffer(vertexBuffer);
foreach (EffectPass pass in basicEffect.CurrentTechnique.Passes)
{
pass.Apply();
graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, verticesIndex / 3);
graphicsDevice.DrawPrimitives(PrimitiveType.TriangleList, 0, verticesIndex / 3);
}
verticesIndex = 0;
}

View File

@@ -11,7 +11,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class BehaviourControllerConverter : EngineTypeYamlSerializerBase<IBehaviourController>
public class BehaviourControllerConverter : EngineTypeYamlConverterBase<IBehaviourController>
{
private const string BEHAVIOURS_SCALAR_NAME = "Behaviours";

View File

@@ -9,7 +9,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class BehaviourConverter : EngineTypeYamlSerializerBase<IBehaviour>
public class BehaviourConverter : EngineTypeYamlConverterBase<IBehaviour>
{
public override IBehaviour? Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public abstract class EngineTypeYamlSerializerBase<T> : IEngineTypeYamlConverter
public abstract class EngineTypeYamlConverterBase<T> : IEngineTypeYamlConverter
{
protected const string SERIALIZED_SCALAR_NAME = "Properties";

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class AABB2DConverter : EngineTypeYamlSerializerBase<AABB2D>
public class AABB2DConverter : EngineTypeYamlConverterBase<AABB2D>
{
public override AABB2D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class AABB3DConverter : EngineTypeYamlSerializerBase<AABB3D>
public class AABB3DConverter : EngineTypeYamlConverterBase<AABB3D>
{
public override AABB3D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class CircleConverter : EngineTypeYamlSerializerBase<Circle>
public class CircleConverter : EngineTypeYamlConverterBase<Circle>
{
public override Circle Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class ColorHSVAConverter : EngineTypeYamlSerializerBase<ColorHSVA>
public class ColorHSVAConverter : EngineTypeYamlConverterBase<ColorHSVA>
{
private static readonly int SUBSTRING_START_LENGTH = nameof(ColorHSVA).Length + 1;

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class ColorHSVConverter : EngineTypeYamlSerializerBase<ColorHSV>
public class ColorHSVConverter : EngineTypeYamlConverterBase<ColorHSV>
{
private static readonly int SUBSTRING_START_LENGTH = nameof(ColorHSV).Length + 1;

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class ColorRGBAConverter : EngineTypeYamlSerializerBase<ColorRGBA>
public class ColorRGBAConverter : EngineTypeYamlConverterBase<ColorRGBA>
{
private static readonly int SUBSTRING_START_LENGTH = nameof(ColorRGBA).Length + 1;

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class ColorRGBConverter : EngineTypeYamlSerializerBase<ColorRGB>
public class ColorRGBConverter : EngineTypeYamlConverterBase<ColorRGB>
{
private static readonly int SUBSTRING_START_LENGTH = nameof(ColorRGB).Length + 1;

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class Line2DConverter : EngineTypeYamlSerializerBase<Line2D>
public class Line2DConverter : EngineTypeYamlConverterBase<Line2D>
{
public override Line2D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class Line2DEquationConverter : EngineTypeYamlSerializerBase<Line2DEquation>
public class Line2DEquationConverter : EngineTypeYamlConverterBase<Line2DEquation>
{
public override Line2DEquation Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class Line3DConverter : EngineTypeYamlSerializerBase<Line3D>
public class Line3DConverter : EngineTypeYamlConverterBase<Line3D>
{
public override Line3D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{

View File

@@ -0,0 +1,33 @@
using System;
using Engine.Core;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class Matrix4x4Converter : EngineTypeYamlConverterBase<Matrix4x4>
{
private static readonly int SUBSTRING_START_LENGTH = nameof(Matrix4x4).Length + 1;
public override Matrix4x4 Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{
string value = parser.Consume<Scalar>().Value;
string insideParenthesis = value[SUBSTRING_START_LENGTH..^1];
string[] values = insideParenthesis.Split(", ");
return new Matrix4x4(
float.Parse(values[0]), float.Parse(values[1]), float.Parse(values[2]), float.Parse(values[3]),
float.Parse(values[4]), float.Parse(values[5]), float.Parse(values[6]), float.Parse(values[7]),
float.Parse(values[8]), float.Parse(values[9]), float.Parse(values[10]), float.Parse(values[11]),
float.Parse(values[12]), float.Parse(values[13]), float.Parse(values[14]), float.Parse(values[15])
);
}
public override void WriteYaml(IEmitter emitter, object? value, Type type, ObjectSerializer serializer)
{
Matrix4x4 m = (Matrix4x4)value!;
emitter.Emit(new Scalar($"{nameof(Matrix4x4)}({m.M11}, {m.M12}, {m.M13}, {m.M14},{m.M21}, {m.M22}, {m.M23}, {m.M24},{m.M31}, {m.M32}, {m.M33}, {m.M34},{m.M41}, {m.M42}, {m.M43}, {m.M44})"));
}
}

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class Projection1DConverter : EngineTypeYamlSerializerBase<Projection1D>
public class Projection1DConverter : EngineTypeYamlConverterBase<Projection1D>
{
public override Projection1D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class QuaternionConverter : EngineTypeYamlSerializerBase<Quaternion>
public class QuaternionConverter : EngineTypeYamlConverterBase<Quaternion>
{
private static readonly int SUBSTRING_START_LENGTH = nameof(Quaternion).Length + 1;

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class Ray2DConverter : EngineTypeYamlSerializerBase<Ray2D>
public class Ray2DConverter : EngineTypeYamlConverterBase<Ray2D>
{
public override Ray2D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class Ray3DConverter : EngineTypeYamlSerializerBase<Ray3D>
public class Ray3DConverter : EngineTypeYamlConverterBase<Ray3D>
{
public override Ray3D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{

View File

@@ -9,7 +9,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class Shape2DConverter : EngineTypeYamlSerializerBase<Shape2D>
public class Shape2DConverter : EngineTypeYamlConverterBase<Shape2D>
{
public override Shape2D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class Sphere3DConverter : EngineTypeYamlSerializerBase<Sphere3D>
public class Sphere3DConverter : EngineTypeYamlConverterBase<Sphere3D>
{
public override Sphere3D Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class TriangleConverter : EngineTypeYamlSerializerBase<Triangle>
public class TriangleConverter : EngineTypeYamlConverterBase<Triangle>
{
public override Triangle Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class Vector2DConverter : EngineTypeYamlSerializerBase<Vector2D>
public class Vector2DConverter : EngineTypeYamlConverterBase<Vector2D>
{
private static readonly int SUBSTRING_START_LENGTH = nameof(Vector2D).Length + 1;

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class Vector2DIntConverter : EngineTypeYamlSerializerBase<Vector2DInt>
public class Vector2DIntConverter : EngineTypeYamlConverterBase<Vector2DInt>
{
private static readonly int SUBSTRING_START_LENGTH = nameof(Vector2DInt).Length + 1;

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class Vector3DConverter : EngineTypeYamlSerializerBase<Vector3D>
public class Vector3DConverter : EngineTypeYamlConverterBase<Vector3D>
{
private static readonly int SUBSTRING_START_LENGTH = nameof(Vector3D).Length + 1;

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class Vector3DIntConverter : EngineTypeYamlSerializerBase<Vector3DInt>
public class Vector3DIntConverter : EngineTypeYamlConverterBase<Vector3DInt>
{
private static readonly int SUBSTRING_START_LENGTH = nameof(Vector3DInt).Length + 1;

View File

@@ -8,7 +8,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class Vector4DConverter : EngineTypeYamlSerializerBase<Vector4D>
public class Vector4DConverter : EngineTypeYamlConverterBase<Vector4D>
{
private static readonly int SUBSTRING_START_LENGTH = nameof(Vector4D).Length + 1;

View File

@@ -12,7 +12,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class SerializedClassConverter : EngineTypeYamlSerializerBase<SerializedClass>
public class SerializedClassConverter : EngineTypeYamlConverterBase<SerializedClass>
{
public override SerializedClass? Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{
@@ -37,17 +37,25 @@ public class SerializedClassConverter : EngineTypeYamlSerializerBase<SerializedC
}
}
foreach ((string key, object @object) in publicDictionary)
if (@object is IDictionary<object, object> dictionary && dictionary.TryGetValue(nameof(TypeContainer.Type), out object? typeField) && dictionary.TryGetValue(nameof(TypeContainer.Value), out object? valueField))
publicDictionary[key] = new TypeContainer() { Type = typeField!.ToString()!, Value = valueField };
foreach ((string key, object @object) in privateDictionary)
if (@object is IDictionary<object, object> dictionary && dictionary.TryGetValue(nameof(TypeContainer.Type), out object? typeField) && dictionary.TryGetValue(nameof(TypeContainer.Value), out object? valueField))
privateDictionary[key] = new TypeContainer() { Type = typeField!.ToString()!, Value = valueField };
Type classType = TypeFactory.GetType(serializedClass.Type);
foreach ((string key, object @object) in publicDictionary)
if (@object is TypeContainer typeContainer)
serializedClass.Public.Add(key, Serializer.InternalDeserialize(typeContainer.Value!.ToString()!, TypeFactory.GetType(typeContainer.Type)));
serializedClass.Public.Add(key, Serializer.InternalDeserialize(Serializer.Serialize(typeContainer.Value!), TypeFactory.GetType(typeContainer.Type)));
else
serializedClass.Public.Add(key, Serializer.InternalDeserialize(@object.ToString()!, (classType.GetProperty(key, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)?.PropertyType ?? classType.GetField(key, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance)?.FieldType)!));
foreach ((string key, object @object) in privateDictionary)
if (@object is TypeContainer typeContainer)
serializedClass.Private.Add(key, Serializer.InternalDeserialize(typeContainer.Value!.ToString()!, TypeFactory.GetType(typeContainer.Type)));
serializedClass.Private.Add(key, Serializer.InternalDeserialize(Serializer.Serialize(typeContainer.Value!), TypeFactory.GetType(typeContainer.Type)));
else
serializedClass.Private.Add(key, Serializer.InternalDeserialize(@object.ToString()!, (classType.GetProperty(key, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Default)?.PropertyType ?? classType.GetField(key, System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Default)?.FieldType)!));
@@ -65,14 +73,14 @@ public class SerializedClassConverter : EngineTypeYamlSerializerBase<SerializedC
Dictionary<string, object> publics = [];
Dictionary<string, object> privates = [];
foreach ((string key, object? @object) in serializedClass.Public.Where(v => !v.GetType().HasAttribute<IgnoreSerializationAttribute>()))
if (@object?.GetType().IsClass == false)
foreach ((string key, object? @object) in serializedClass.Public)
if (@object?.GetType().IsClass == false || @object is string)
publics.Add(key, @object!);
else
publics.Add(key, new TypeContainer(@object));
foreach ((string key, object? @object) in serializedClass.Private.Where(v => !v.GetType().HasAttribute<IgnoreSerializationAttribute>()))
if (@object?.GetType().IsClass == false)
foreach ((string key, object? @object) in serializedClass.Private)
if (@object?.GetType().IsClass == false || @object is string)
privates.Add(key, @object!);
else
privates.Add(key, new TypeContainer(@object));

View File

@@ -9,7 +9,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class StateEnableConverter : EngineTypeYamlSerializerBase<IStateEnable>
public class StateEnableConverter : EngineTypeYamlConverterBase<IStateEnable>
{
public override IStateEnable? Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{

View File

@@ -9,7 +9,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class TypeContainerConverter : EngineTypeYamlSerializerBase<TypeContainer>
public class TypeContainerConverter : EngineTypeYamlConverterBase<TypeContainer>
{
public override TypeContainer Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{

View File

@@ -11,7 +11,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class UniverseConverter : EngineTypeYamlSerializerBase<IUniverse>
public class UniverseConverter : EngineTypeYamlConverterBase<IUniverse>
{
public override IUniverse? Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{

View File

@@ -11,7 +11,7 @@ using YamlDotNet.Serialization;
namespace Engine.Serializers.Yaml;
public class UniverseObjectSerializer : EngineTypeYamlSerializerBase<IUniverseObject>
public class UniverseObjectConverter : EngineTypeYamlConverterBase<IUniverseObject>
{
public override IUniverseObject? Read(IParser parser, Type type, ObjectDeserializer rootDeserializer)
{

View File

@@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.IO;
using Engine.Core.Config;
namespace Engine.Serializers.Yaml;
public class YamlConfiguration : BasicConfiguration
{
public readonly string FilePath;
private readonly YamlSerializer yamlSerializer = new();
public YamlConfiguration(string filePath)
{
if (!filePath.EndsWith(".yaml"))
filePath += ".yaml";
FilePath = filePath;
bool isRelativePath = Path.GetFullPath(filePath).CompareTo(filePath) != 0;
if (isRelativePath)
FilePath = Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, filePath));
if (Path.GetDirectoryName(FilePath) is string directoryPath)
Directory.CreateDirectory(directoryPath);
if (!File.Exists(FilePath))
return;
string yamlFileText = File.ReadAllText(FilePath);
Dictionary<string, string> valuePairs = yamlSerializer.Deserialize<Dictionary<string, string>>(yamlFileText);
foreach ((string key, string value) in valuePairs)
Set(key, value);
}
public void Save()
{
File.WriteAllText(FilePath, yamlSerializer.Serialize(Values));
}
}

View File

@@ -62,65 +62,65 @@ public class YamlSerializer : Core.Serialization.ISerializer
}
}
public object Deserialize(string configuration)
public object Deserialize(string content)
{
lock (Lock)
{
identifiableRegistry.Reset();
object result = deserializer.Deserialize(configuration)!;
object result = deserializer.Deserialize(content)!;
identifiableRegistry.AssignAll();
return result;
}
}
public object Deserialize(string configuration, Type type)
public object Deserialize(string content, Type type)
{
lock (Lock)
{
identifiableRegistry.Reset();
object result = deserializer.Deserialize(configuration, type)!;
object result = deserializer.Deserialize(content, type)!;
identifiableRegistry.AssignAll();
return result;
}
}
public T Deserialize<T>(string configuration)
public T Deserialize<T>(string content)
{
lock (Lock)
{
identifiableRegistry.Reset();
T result = deserializer.Deserialize<T>(configuration);
T result = deserializer.Deserialize<T>(content);
identifiableRegistry.AssignAll();
return result;
}
}
public ProgressiveTask<object> DeserializeAsync(string configuration)
public ProgressiveTask<object> DeserializeAsync(string content)
{
lock (Lock)
{
progressionTracker.Reset();
Task<object> task = Task.Run(() => Deserialize(configuration));
Task<object> task = Task.Run(() => Deserialize(content));
return new ProgressiveTask<object>(progressionTracker, task);
}
}
public ProgressiveTask<object> DeserializeAsync(string configuration, Type type)
public ProgressiveTask<object> DeserializeAsync(string content, Type type)
{
lock (Lock)
{
progressionTracker.Reset();
Task<object> task = Task.Run(() => Deserialize(configuration, type));
Task<object> task = Task.Run(() => Deserialize(content, type));
return new ProgressiveTask<object>(progressionTracker, task);
}
}
public ProgressiveTask<T> DeserializeAsync<T>(string configuration)
public ProgressiveTask<T> DeserializeAsync<T>(string content)
{
lock (Lock)
{
progressionTracker.Reset();
Task<T> task = Task.Run(() => Deserialize<T>(configuration));
Task<T> task = Task.Run(() => Deserialize<T>(content));
return new ProgressiveTask<T>(progressionTracker, task);
}
}
@@ -135,8 +135,8 @@ public class YamlSerializer : Core.Serialization.ISerializer
}
}
internal object InternalDeserialize(string configuration, Type type)
internal object InternalDeserialize(string content, Type type)
{
return deserializer.Deserialize(configuration, type)!;
return deserializer.Deserialize(content, type)!;
}
}

View File

@@ -7,6 +7,11 @@ namespace Engine.Physics2D;
/// </summary>
public interface ICollider2D : IBehaviour
{
/// <summary>
/// Event triggered when the collider is recalculated.
/// </summary>
Event<ICollider2D> OnRecalculated { get; }
/// <summary>
/// Event triggered when a collision is detected.
/// </summary>
@@ -30,6 +35,16 @@ public interface ICollider2D : IBehaviour
/// </summary>
IRigidBody2D? RigidBody2D { get; }
/// <summary>
/// The area of the <see cref="ICollider2D"/>.
/// </summary>
float Area { get; }
/// <summary>
/// The geometric inertia of the <see cref="ICollider2D"/>.
/// </summary>
float GeometricInertia { get; }
/// <summary>
/// The value indicating whether the <see cref="ICollider2D"/> is a trigger.
/// </summary>

View File

@@ -27,6 +27,21 @@ public interface IRigidBody2D : IBehaviour2D
/// </summary>
float Mass { get; set; }
/// <summary>
/// The inverse mass (1 / Mass) of the <see cref="IRigidBody2D"/>.
/// </summary>
float InverseMass { get; }
/// <summary>
/// The Invertia of the <see cref="IRigidBody2D"/>.
/// </summary>
float Inertia { get; }
/// <summary>
/// The inverse Invertia (1 / Invertia) of the <see cref="IRigidBody2D"/>.
/// </summary>
float InverseInertia { get; }
/// <summary>
/// The value indicating whether the <see cref="IRigidBody2D"/> is static/immovable.
/// </summary>

View File

@@ -4,6 +4,7 @@ namespace Engine.Physics2D;
public abstract class Collider2DBase : Behaviour2D, ICollider2D
{
public Event<ICollider2D> OnRecalculated { get; } = new();
public Event<ICollider2D, CollisionDetectionInformation> OnCollisionDetected { get; } = new();
public Event<ICollider2D, CollisionDetectionInformation> OnCollisionResolved { get; } = new();
public Event<ICollider2D, ICollider2D> OnTriggered { get; } = new();
@@ -20,6 +21,9 @@ public abstract class Collider2DBase : Behaviour2D, ICollider2D
public IRigidBody2D? RigidBody2D { get; protected set; } = null;
public bool IsTrigger { get; set; } = false;
public abstract float Area { get; }
public abstract float GeometricInertia { get; }
public void Recalculate()
{
if (!NeedsRecalculation)
@@ -27,6 +31,7 @@ public abstract class Collider2DBase : Behaviour2D, ICollider2D
CalculateCollider();
NeedsRecalculation = false;
OnRecalculated.Invoke(this);
}
public abstract void CalculateCollider();

View File

@@ -15,7 +15,15 @@ public class Collider2DCircle : Collider2DBase, ICircleCollider2D
}
} = Circle.UnitCircle;
public override void CalculateCollider() => CircleWorld = Transform.Transform(CircleLocal);
private float area = 0f; public override float Area => area;
private float geometricInertia = 0f; public override float GeometricInertia => geometricInertia;
public override void CalculateCollider()
{
CircleWorld = Transform.Transform(CircleLocal);
area = CircleWorld.Area;
geometricInertia = CircleWorld.GeometricInertia;
}
public Collider2DCircle() { }
public Collider2DCircle(Circle circle) => CircleLocal = circle;

View File

@@ -15,7 +15,16 @@ public class Collider2DShape : Collider2DBase, IShapeCollider2D
}
} = Shape2D.Square;
public override void CalculateCollider() => ShapeLocal.Transform(Transform, ShapeWorld);
private float area = 0f; public override float Area => area;
private float geometricInertia = 0f; public override float GeometricInertia => geometricInertia;
public override void CalculateCollider()
{
ShapeLocal.Transform(Transform, ShapeWorld);
area = ShapeWorld.Area;
geometricInertia = ShapeWorld.GetGeometricInertia(Transform.Position);
}
public Collider2DShape() { }
public Collider2DShape(Shape2D shape) { ShapeLocal = shape; }

View File

@@ -7,14 +7,16 @@ public readonly struct CollisionDetectionInformation
(
ICollider2D Detector,
ICollider2D Detected,
Vector2D ContactPoint,
Vector2D Normal,
float Penetration
)
{
public ICollider2D Detector { get; init; } = Detector;
public ICollider2D Detected { get; init; } = Detected;
public Vector2D ContactPoint { get; init; } = ContactPoint;
public Vector2D Normal { get; init; } = Normal;
public float Penetration { get; init; } = Penetration;
public CollisionDetectionInformation Reverse() => new(Detected, Detector, -Normal, Penetration);
public CollisionDetectionInformation Reverse() => new(Detected, Detector, ContactPoint, -Normal, Penetration);
}

View File

@@ -1,3 +1,5 @@
using System.Collections.Generic;
using Engine.Core;
using Engine.Physics2D;
@@ -41,12 +43,13 @@ public class CollisionDetector2D : ICollisionDetector2D
private static bool DetectShapeShapeOneWay(IShapeCollider2D left, IShapeCollider2D right, ref CollisionDetectionInformation collisionInformation)
{
System.Collections.Generic.IReadOnlyList<Vector2D> vertices = left.ShapeWorld.Vertices;
IReadOnlyList<Vector2D> vertices = left.ShapeWorld.Vertices;
int count = vertices.Count;
for (int indexProjection = 0; indexProjection < count; indexProjection++)
{
Vector2D projectionVector = vertices[indexProjection].FromTo(vertices[(indexProjection + 1) % count]).Perpendicular().Normalized;
Vector2D leftEdge = vertices[indexProjection].FromTo(vertices[(indexProjection + 1) % count]);
Vector2D projectionVector = leftEdge.Perpendicular().Normalized;
Projection1D leftProjection = left.ShapeWorld.ToProjection(projectionVector);
Projection1D rightProjection = right.ShapeWorld.ToProjection(projectionVector);
@@ -55,17 +58,61 @@ public class CollisionDetector2D : ICollisionDetector2D
return false;
if (collisionInformation.Detector is null || Math.Abs(collisionInformation.Penetration) > Math.Abs(depth))
collisionInformation = new(left, right, projectionVector, depth);
{
Vector2D contactPoint = FindShapeToShapeContactPoint(left, right, projectionVector);
collisionInformation = new(left, right, contactPoint, projectionVector, depth);
}
}
return true;
}
private static Vector2D FindShapeToShapeContactPoint(IShapeCollider2D left, IShapeCollider2D right, Vector2D contactProjectionVector)
{
IReadOnlyList<Vector2D> leftVertices = left.ShapeWorld.Vertices;
IReadOnlyList<Vector2D> rightVertices = right.ShapeWorld.Vertices;
Line2D leftSupportLine = GetSupportLine(leftVertices, contactProjectionVector);
Line2D rightSupportLine = GetSupportLine(rightVertices, -contactProjectionVector);
if (leftSupportLine.Direction.Dot(rightSupportLine.Direction).Abs() > .99f)
return (leftSupportLine.From + leftSupportLine.To + rightSupportLine.From + rightSupportLine.To) / 4f;
return leftSupportLine.IntersectionPoint(rightSupportLine);
}
private static Line2D GetSupportLine(IReadOnlyList<Vector2D> vertices, Vector2D contactProjectionVector)
{
System.Span<Vector2D> points = stackalloc Vector2D[2];
System.Span<float> distances = stackalloc float[2] { float.MaxValue, float.MaxValue };
for (int i = 0; i < vertices.Count; i++)
{
Vector2D point = vertices[i];
float distance = contactProjectionVector.Dot(point);
if (distance < distances[0])
{
points[1] = points[0];
distances[1] = distances[0];
points[0] = point;
distances[0] = distance;
}
else if (distance < distances[1])
{
points[1] = point;
distances[1] = distance;
}
}
return new(points[0], points[1]);
}
private static bool DetectShapeCircle(IShapeCollider2D shapeCollider, ICircleCollider2D circleCollider, out CollisionDetectionInformation collisionInformation)
{
collisionInformation = default;
System.Collections.Generic.IReadOnlyList<Vector2D> vertices = shapeCollider.ShapeWorld.Vertices;
IReadOnlyList<Vector2D> vertices = shapeCollider.ShapeWorld.Vertices;
int count = vertices.Count;
for (int indexProjection = 0; indexProjection < count; indexProjection++)
@@ -78,8 +125,10 @@ public class CollisionDetector2D : ICollisionDetector2D
if (!shapeProjection.Overlaps(circleProjection, out float depth))
return false;
Vector2D contactPoint = circleCollider.CircleWorld.Center + projectionVector * circleCollider.CircleWorld.Radius;
if (collisionInformation.Detector is null || Math.Abs(collisionInformation.Penetration) > Math.Abs(depth))
collisionInformation = new(shapeCollider, circleCollider, projectionVector, depth);
collisionInformation = new(shapeCollider, circleCollider, contactPoint, projectionVector, depth);
}
{
@@ -91,8 +140,10 @@ public class CollisionDetector2D : ICollisionDetector2D
if (!shapeProjection.Overlaps(circleProjection, out float depth))
return false;
Vector2D contactPoint = circleCollider.CircleWorld.Center + shapeToCircleProjectionVector * circleCollider.CircleWorld.Radius;
if (collisionInformation.Detector is null || Math.Abs(collisionInformation.Penetration) > Math.Abs(depth))
collisionInformation = new(shapeCollider, circleCollider, shapeToCircleProjectionVector, depth);
collisionInformation = new(shapeCollider, circleCollider, contactPoint, shapeToCircleProjectionVector, depth);
}
return true;
@@ -110,7 +161,10 @@ public class CollisionDetector2D : ICollisionDetector2D
bool collision = leftProjection.Overlaps(rightProjection, out float depth);
if (collision)
collisionInformation = new(left, right, leftToRightCenterProjectionVector, depth);
{
Vector2D contactPoint = left.CircleWorld.Center + leftToRightCenterProjectionVector * left.CircleWorld.Radius;
collisionInformation = new(left, right, contactPoint, leftToRightCenterProjectionVector, depth);
}
return collision;
}

View File

@@ -6,8 +6,6 @@ public class CollisionResolver2D : ICollisionResolver2D
{
public void Resolve(CollisionDetectionInformation collisionInformation)
{
Vector2D displacementVector = collisionInformation.Normal * collisionInformation.Penetration;
ICollider2D left = collisionInformation.Detector;
ICollider2D right = collisionInformation.Detected;
@@ -17,6 +15,20 @@ public class CollisionResolver2D : ICollisionResolver2D
if (isLeftStatic && isRightStatic)
return;
Displace(collisionInformation, left, right, isLeftStatic, isRightStatic);
Bounce(collisionInformation, left, right, isLeftStatic, isRightStatic);
left.Recalculate();
right.Recalculate();
left.Resolve(collisionInformation);
right.Resolve(collisionInformation);
}
private static void Displace(CollisionDetectionInformation collisionInformation, ICollider2D left, ICollider2D right, bool isLeftStatic, bool isRightStatic)
{
Vector2D displacementVector = collisionInformation.Normal * collisionInformation.Penetration;
if (isLeftStatic)
right.Transform.Position += displacementVector;
else if (isRightStatic)
@@ -33,11 +45,32 @@ public class CollisionResolver2D : ICollisionResolver2D
right.Transform.Position += leftMomentumPercentage * displacementVector;
left.Transform.Position -= rightMomentumPercentage * displacementVector;
}
}
left.Recalculate();
right.Recalculate();
private static void Bounce(CollisionDetectionInformation collisionInformation, ICollider2D left, ICollider2D right, bool isLeftStatic, bool isRightStatic)
{
Vector2D leftVelocity = left.RigidBody2D?.Velocity ?? Vector2D.Zero;
Vector2D rightVelocity = right.RigidBody2D?.Velocity ?? Vector2D.Zero;
left.Resolve(collisionInformation);
right.Resolve(collisionInformation);
Vector2D relativeVelocity = leftVelocity - rightVelocity;
float velocityAlongNormal = Vector2D.Dot(relativeVelocity, collisionInformation.Normal);
if (velocityAlongNormal > 0)
{
collisionInformation = collisionInformation.Reverse();
velocityAlongNormal = -velocityAlongNormal;
}
float e = (left.RigidBody2D?.Material.Restitution ?? 0f).Add(right.RigidBody2D?.Material.Restitution ?? 0f).Divide(2f);
float leftMassEffective = isLeftStatic ? float.PositiveInfinity : left.RigidBody2D?.Mass ?? float.Epsilon;
float rightMassEffective = isRightStatic ? float.PositiveInfinity : right.RigidBody2D?.Mass ?? float.Epsilon;
float impulse = -(1f + e) * velocityAlongNormal / ((1f / leftMassEffective) + (1f / rightMassEffective));
if (!isLeftStatic)
left.RigidBody2D?.Velocity += impulse / leftMassEffective * collisionInformation.Normal;
if (!isRightStatic)
right.RigidBody2D?.Velocity -= impulse / rightMassEffective * collisionInformation.Normal;
}
}

View File

@@ -1,7 +1,7 @@
namespace Engine.Physics2D;
public readonly struct PhysicsMaterial2D(float Friction, float Restitution) : IPhysicsMaterial2D
public class PhysicsMaterial2D(float Friction, float Restitution) : IPhysicsMaterial2D
{
public readonly float Friction { get; init; } = Friction;
public readonly float Restitution { get; init; } = Restitution;
public float Friction { get; set; } = Friction;
public float Restitution { get; set; } = Restitution;
}

View File

@@ -1,8 +0,0 @@
namespace Engine.Physics2D;
public readonly struct PhysicsMaterial2DDefault : IPhysicsMaterial2D
{
public readonly float Friction => .1f;
public readonly float Restitution => .1f;
}

View File

@@ -0,0 +1,9 @@
namespace Engine.Physics2D;
public readonly struct ReadOnlyPhysicsMaterial2D(float Friction, float Restitution) : IPhysicsMaterial2D
{
public readonly float Friction { get; } = Friction;
public readonly float Restitution { get; } = Restitution;
public readonly static ReadOnlyPhysicsMaterial2D Default = new(.1f, .1f);
}

View File

@@ -1,16 +1,75 @@
using System.Collections.Generic;
using Engine.Core;
namespace Engine.Physics2D;
public class RigidBody2D : Behaviour2D, IRigidBody2D
public class RigidBody2D : Behaviour2D, IRigidBody2D, IFirstFrameUpdate, ILastFrameUpdate
{
private const float LOWEST_ALLOWED_MASS = 0.00001f;
public IPhysicsMaterial2D Material { get; set; } = new PhysicsMaterial2DDefault();
public IPhysicsMaterial2D Material { get; set; } = ReadOnlyPhysicsMaterial2D.Default;
public Vector2D Velocity { get; set; } = Vector2D.Zero;
public float AngularVelocity { get; set; } = 0f;
public bool IsStatic { get; set; } = false;
public float Mass { get; set => field = Math.Max(value, LOWEST_ALLOWED_MASS); } = 1f;
public float Mass { get; set { field = Math.Max(value, LOWEST_ALLOWED_MASS); } } = 1f;
public float InverseMass { get; protected set; } = 1f;
public float Inertia { get; protected set; } = 1f;
public float InverseInertia { get; protected set; } = 1f;
private readonly List<ICollider2D> childColliders = [];
public void LastActiveFrame() => DisconnectColliders();
public void FirstActiveFrame()
{
ReconnectColliders();
UpdateValues();
}
private void ReconnectColliders()
{
DisconnectColliders();
BehaviourController.GetBehavioursInChildren(childColliders);
foreach (ICollider2D collider in childColliders)
collider.OnRecalculated.AddListener(RecalculateCallback);
}
private void DisconnectColliders()
{
foreach (ICollider2D collider in childColliders)
collider.OnRecalculated.RemoveListener(RecalculateCallback);
}
private void RecalculateCallback(ICollider2D _) => UpdateValues();
private void UpdateValues()
{
InverseMass = Mass.OneOver();
Vector2D center = Transform.Position;
Inertia = 0f;
float totalColliderArea = 0f;
foreach (ICollider2D collider in childColliders)
totalColliderArea += collider.Area;
foreach (ICollider2D collider in childColliders)
{
float colliderMass = Mass * (collider.Area / totalColliderArea);
float colliderInertia = collider.GeometricInertia * colliderMass;
float distanceSquared = center.FromTo(collider.Transform.Position).MagnitudeSquared;
Inertia += colliderInertia + colliderMass * distanceSquared;
}
if (childColliders.Count == 0)
Inertia = 1f;
InverseInertia = Inertia.OneOver();
}
}

View File

@@ -10,14 +10,10 @@ public class TriangleBatcher : Behaviour, IFirstFrameUpdate, ILastFrameUpdate, I
private static System.Func<IBehaviour, int> GetPriority() => (b) => b.Priority;
private readonly BehaviourCollector<ITriangleBatch> triangleBatches = new();
private ICamera camera = null!;
private readonly ActiveBehaviourCollectorOrdered<int, IDrawableTriangle> drawableShapes = new(GetPriority(), SortByAscendingPriority());
public void FirstActiveFrame()
{
camera = Universe.FindRequiredBehaviour<ICamera>();
drawableShapes.Assign(Universe);
triangleBatches.Assign(Universe);
}
@@ -27,7 +23,7 @@ public class TriangleBatcher : Behaviour, IFirstFrameUpdate, ILastFrameUpdate, I
for (int i = 0; i < triangleBatches.Count; i++)
{
ITriangleBatch triangleBatch = triangleBatches[i];
triangleBatch.Begin(camera.ViewMatrix, camera.ProjectionMatrix);
triangleBatch.Begin();
for (int j = 0; j < drawableShapes.Count; j++)
drawableShapes[j].Draw(triangleBatch);
triangleBatch.End();

View File

@@ -0,0 +1,6 @@
namespace Engine.Systems.Network;
public interface IPacketListenerClientEntity<T> : INetworkEntity where T : IEntityNetworkPacket
{
void OnEntityClientPacketArrived(IConnection sender, T packet);
}

View File

@@ -0,0 +1,6 @@
namespace Engine.Systems.Network;
public interface IPacketListenerServerEntity<T> : INetworkEntity where T : IEntityNetworkPacket
{
void OnEntityServerPacketArrived(IConnection sender, T packet);
}

View File

@@ -0,0 +1,40 @@
using Engine.Core;
namespace Engine.Systems.Network;
/// <summary>
/// Basic network behaviour that supports both client & server behaviour. Finds both
/// the <see cref="INetworkCommunicatorClient"/> and the <see cref="INetworkCommunicatorServer"/>
/// in the universe in it's first active frame. Recommended to use <see cref="ClientBehaviour"/> or <see cref="ServerBehaviour"/>
/// <br/>
/// Disclaimer: It implements <see cref="IFirstFrameUpdate"/> and <see cref="ILastFrameUpdate"/> in virtual methods.
/// </summary>
public class CommonNetworkBehaviour : Behaviour, IFirstFrameUpdate, ILastFrameUpdate
{
protected INetworkCommunicatorServer? server = null!;
protected INetworkCommunicatorClient? client = null!;
protected bool IsServer { get; private set; } = false;
protected bool IsClient { get; private set; } = false;
public INetworkCommunicatorServer Server => server ?? throw new Core.Exceptions.NotFoundException($"Universe does not contain a {nameof(INetworkCommunicatorServer)}");
public INetworkCommunicatorClient Client => client ?? throw new Core.Exceptions.NotFoundException($"Universe does not contain a {nameof(INetworkCommunicatorClient)}");
public virtual void FirstActiveFrame()
{
client = Universe.FindBehaviour<INetworkCommunicatorClient>();
server = Universe.FindBehaviour<INetworkCommunicatorServer>();
IsServer = server is not null;
IsClient = client is not null;
}
public virtual void LastActiveFrame()
{
client = null!;
server = null!;
IsServer = false;
IsClient = false;
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace Engine.Systems.Network;
public static class CommunicatorExtensions
{
public static void SendToClients<T>(this INetworkCommunicatorServer server, IEnumerable<IConnection> connections, T packet, PacketDelivery packetDelivery = PacketDelivery.ReliableInOrder) where T : class, new()
{
foreach (IConnection connection in connections)
server.SendToClient(connection, packet, packetDelivery);
}
}

View File

@@ -10,25 +10,127 @@ namespace Engine.Systems.Network;
/// <summary>
/// Intermediary manager that looks up in it's hierarchy for a <see cref="INetworkCommunicator"/> to route/broadcast it's received packets to their destinations.
/// </summary>
/// TODO: I need to peer check this class, I don't exactly remember the state I was in when I was originally writing it and left it uncommented and the current comments are added later on.
/// It's a fairly complex manager that relies heavily on Reflection and lots of generic method delegation which is making it very hard to read back.
public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetworkManager
{
private readonly Dictionary<Type, Dictionary<Type, List<MethodInfo>>> clientPacketArrivalMethods = [];
private readonly Dictionary<Type, Dictionary<Type, List<MethodInfo>>> serverPacketArrivalMethods = [];
#region Packet Router/Broadcaster to Listener Delegates
private readonly Dictionary<Type, Dictionary<string, object>> clientPacketRouters = [];
private readonly Dictionary<Type, Dictionary<string, object>> serverPacketRouters = [];
/// <summary>
/// Behaviour Type → Packet Type → List of <see cref="IPacketListenerClient{T}"/> listener methods (broadcast packets, client-side)
/// </summary>
private readonly Dictionary<Type, Dictionary<Type, List<MethodInfo>>> clientBroadcastPacketListenerMethods = [];
private readonly List<(Type packetType, Delegate @delegate)> packetRetrievalDelegates = [];
/// <summary>
/// Behaviour Type → Packet Type → List of <see cref="IPacketListenerServer{T}"/> listener methods (broadcast packets, server-side)
/// </summary>
private readonly Dictionary<Type, Dictionary<Type, List<MethodInfo>>> serverBroadcastPacketListenerMethods = [];
/// <summary>
/// Behaviour Type → Packet Type → List of <see cref="IPacketListenerClientEntity{T}"/> listener methods (entity packets, client-side)
/// </summary>
private readonly Dictionary<Type, Dictionary<Type, List<MethodInfo>>> clientEntityPacketListenerMethods = [];
/// <summary>
/// Behaviour Type → Packet Type → List of <see cref="IPacketListenerServerEntity{T}"/> listener methods (entity packets, server-side)
/// </summary>
private readonly Dictionary<Type, Dictionary<Type, List<MethodInfo>>> serverEntityPacketListenerMethods = [];
#endregion
#region Packet Router/Broadcaster Events
/// <summary>
/// Packet Type → Behaviour.Id → Broadcaster Event (broadcast, client-side)
/// </summary>
private readonly Dictionary<Type, Dictionary<string, object>> clientPacketBroadcastEvents = [];
/// <summary>
/// Packet Type → Behaviour.Id → Broadcaster Event (broadcast, server-side)
/// </summary>
private readonly Dictionary<Type, Dictionary<string, object>> serverPacketBroadcastEvents = [];
/// <summary>
/// Maps an <see cref="IEntityNetworkPacket"/> type to a set of routing events,
/// keyed by <see cref="IIdentifiable.Id"/>, for CLIENT entity listeners.
/// The packet is routed to the correct <see cref="INetworkEntity"/> instance
/// by matching <see cref="IEntityNetworkPacket.EntityId"/>.
/// </summary>
private readonly Dictionary<Type, Dictionary<string, object>> clientEntityPacketRouterEvents = [];
/// <summary>
/// Maps an <see cref="IEntityNetworkPacket"/> type to a set of routing events,
/// keyed by <see cref="IIdentifiable.Id"/>, for SERVER entity listeners.
/// The packet is routed to the correct <see cref="INetworkEntity"/> instance
/// by matching <see cref="IEntityNetworkPacket.EntityId"/>.
/// </summary>
private readonly Dictionary<Type, Dictionary<string, object>> serverEntityPacketRouterEvents = [];
#endregion
#region Packet Retrieval Delegates
/// <summary>
/// Stores delegates that connect incoming broadcast packets from <see cref="INetworkCommunicator"/>
/// to <see cref="OnPacketReceived{T}(IConnection, T)"/>.
/// These are used to subscribe/unsubscribe from <see cref="INetworkCommunicator"/> events.
/// </summary>
private readonly List<PacketRetrievalDelegatePair> broadcastPacketRetrievalSubscriptionDelegates = [];
/// <summary>
/// Stores delegates that connect incoming entity packets from <see cref="INetworkCommunicator"/>
/// to <see cref="OnPacketReceived{T}(IConnection, T)"/>.
/// These are used to subscribe/unsubscribe from <see cref="INetworkCommunicator"/> events.
/// </summary>
private readonly List<PacketRetrievalDelegatePair> entityPacketRetrievalSubscriptionDelegates = [];
/// <summary>
/// Stores delegates that connect incoming all packets from <see cref="INetworkCommunicator"/> to
/// <see cref="OnPacketReceived{T}(IConnection, T)"/>. This is a combination of all subscription
/// delegates filtered so there are no duplicates packet entries.
/// </summary>
private readonly List<PacketRetrievalDelegatePair> uniqueRetrievalSubscriptionDelegates = [];
#endregion
#region Method Caches
/// <summary>
/// Packet type → <see cref="ClearRouter{T}(object)"/> method.
/// </summary>
private readonly Dictionary<Type, MethodInfo> clearRoutesMethods = [];
private readonly Dictionary<Type, MethodInfo> registerPacketListenersMethods = [];
/// <summary>
/// Packet type → <see cref="RegisterBroadcastPacketListenerEvent{T}(INetworkEntity, Event{IConnection, T}, NetworkType)"/> method.
/// </summary>
private readonly Dictionary<Type, MethodInfo> registerBroadcastPacketListenersMethods = [];
/// <summary>
/// Packet type → <see cref="RegisterEntityPacketListenerEvent{T}(INetworkEntity, Event{IConnection, T}, NetworkType)"/> method.
/// </summary>
private readonly Dictionary<Type, MethodInfo> registerEntityPacketListenersMethods = [];
#endregion
#region Network Entity Collector
/// <summary>
/// All active network <see cref="INetworkEntity"/>, keyed by <see cref="IIdentifiable.Id"/>.
/// </summary>
private readonly Dictionary<string, INetworkEntity> _networkEntities = [];
public IReadOnlyDictionary<string, INetworkEntity> NetworkEntities => _networkEntities;
/// <summary>
/// Collector responsible for detecting <see cref="INetworkEntity"/>s entering/leaving the universe.
/// </summary>
private readonly BehaviourCollector<INetworkEntity> _networkEntityCollector = new();
public IBehaviourCollector<INetworkEntity> NetworkEntityCollector => _networkEntityCollector;
#endregion
#region Network Communicator
public INetworkCommunicator NetworkCommunicator
{
get;
@@ -40,68 +142,74 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork
INetworkCommunicator? previousCommunicator = field;
field = value;
if (previousCommunicator is not null) UnsubscribeCommunicatorMethods(previousCommunicator);
if (field is not null) SubscribeCommunicatorMethods(field);
// Unsubscribe packet delegates from old communicator
if (previousCommunicator is not null)
InvokeCommunicatorMethods(nameof(INetworkCommunicator.UnsubscribeFromPackets), previousCommunicator);
// Subscribe packet delegates to new communicator
if (field is not null)
InvokeCommunicatorMethods(nameof(INetworkCommunicator.SubscribeToPackets), field);
}
} = null!;
#region Communicator Subscriptions
private void SubscribeCommunicatorMethods(INetworkCommunicator networkCommunicator)
{
MethodInfo subscribeToPacketsMethod = typeof(INetworkCommunicator)
.GetMethod(nameof(INetworkCommunicator.SubscribeToPackets), BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)!;
foreach ((Type packetType, Delegate @delegate) in packetRetrievalDelegates)
{
MethodInfo genericSubscribeMethod = subscribeToPacketsMethod.MakeGenericMethod(packetType);
genericSubscribeMethod.Invoke(networkCommunicator, [@delegate]);
}
}
private void UnsubscribeCommunicatorMethods(INetworkCommunicator networkCommunicator)
/// <summary>
/// Dynamically invokes <see cref="INetworkCommunicator.SubscribeToPackets{T}(Event{IConnection, T}.EventHandler)"/>
/// or <see cref="INetworkCommunicator.UnsubscribeFromPackets{T}(Event{IConnection, T}.EventHandler)"/>
/// on the provided <see cref="INetworkCommunicator"/> for all known packet types.
/// </summary>
private void InvokeCommunicatorMethods(string methodName, INetworkCommunicator networkCommunicator)
{
MethodInfo unsubscribeFromPacketsMethod = typeof(INetworkCommunicator)
.GetMethod(nameof(INetworkCommunicator.UnsubscribeFromPackets), BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)!;
.GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)!;
foreach ((Type packetType, Delegate @delegate) in packetRetrievalDelegates)
foreach ((Type packetType, Delegate @delegate) in uniqueRetrievalSubscriptionDelegates)
{
MethodInfo genericUnsubscribeMethod = unsubscribeFromPacketsMethod.MakeGenericMethod(packetType);
genericUnsubscribeMethod.Invoke(networkCommunicator, [@delegate]);
MethodInfo genericMethod = unsubscribeFromPacketsMethod.MakeGenericMethod(packetType);
genericMethod.Invoke(networkCommunicator, [@delegate]);
}
}
#endregion
#region Packet Routing
////////////////////////////////////////////////////////////////
#region Packet Routing/Broadcasting
/// <summary>
/// Entry point for ALL incoming packets from the <see cref="NetworkCommunicator"/>.
/// </summary>
private void OnPacketReceived<T>(IConnection sender, T entityDataPacket)
{
BroadcastPacket(sender, entityDataPacket);
if (entityDataPacket is IEntityNetworkPacket entityPacket)
RoutePacket(sender, entityDataPacket, entityPacket);
else
BroadcastPacket(sender, entityDataPacket);
}
private void RoutePacket<T>(IConnection sender, T entityDataPacket, IEntityNetworkPacket entityPacket)
{
if (NetworkCommunicator is INetworkCommunicatorClient)
RoutePacket(clientPacketRouters, entityPacket.EntityId, sender, entityDataPacket);
RoutePacket(clientEntityPacketRouterEvents, entityPacket.EntityId, sender, entityDataPacket);
if (NetworkCommunicator is INetworkCommunicatorServer)
RoutePacket(serverPacketRouters, entityPacket.EntityId, sender, entityDataPacket);
RoutePacket(serverEntityPacketRouterEvents, entityPacket.EntityId, sender, entityDataPacket);
}
private void BroadcastPacket<T>(IConnection sender, T entityDataPacket)
{
if (NetworkCommunicator is INetworkCommunicatorClient)
BroadcastPacket(clientPacketRouters, sender, entityDataPacket);
BroadcastPacket(clientPacketBroadcastEvents, sender, entityDataPacket);
if (NetworkCommunicator is INetworkCommunicatorServer)
BroadcastPacket(serverPacketRouters, sender, entityDataPacket);
BroadcastPacket(serverPacketBroadcastEvents, sender, entityDataPacket);
}
private static void BroadcastPacket<T>(
Dictionary<Type, Dictionary<string, object>> packetRouters,
private void BroadcastPacket<T>(
Dictionary<Type, Dictionary<string, object>> packetBroadcasters,
IConnection sender,
T entityDataPacket)
{
if (!packetRouters.TryGetValue(entityDataPacket!.GetType(), out Dictionary<string, object>? routers))
if (!packetBroadcasters.TryGetValue(entityDataPacket!.GetType(), out Dictionary<string, object>? routers))
return;
foreach ((string behaviourId, object routerEventReference) in routers)
@@ -111,7 +219,7 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork
}
}
private static void RoutePacket<T>(
private void RoutePacket<T>(
Dictionary<Type, Dictionary<string, object>> packetRouters,
string entityId,
IConnection sender,
@@ -126,65 +234,114 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork
Event<IConnection, T> routerEvent = (Event<IConnection, T>)routerEventReference;
routerEvent.Invoke(sender, entityDataPacket!);
}
#endregion
#region Packet Routers
/// <summary>
/// Registers routing events for the behaviour based on cached packet listener methods.
/// </summary>
private void RegisterPacketRoutersFor(
INetworkEntity behaviour,
Dictionary<Type, Dictionary<string, object>> packetRouters,
Dictionary<Type, Dictionary<Type, List<MethodInfo>>> packetArrivalMethods,
NetworkType networkType)
Dictionary<Type, Dictionary<Type, List<MethodInfo>>> packetListenerMethods,
NetworkType networkType,
Dictionary<Type, MethodInfo> registerPacketListenerListenersMethods)
{
if (!packetArrivalMethods.TryGetValue(behaviour.GetType(), out Dictionary<Type, List<MethodInfo>>? arrivalMethods))
if (!packetListenerMethods.TryGetValue(behaviour.GetType(), out Dictionary<Type, List<MethodInfo>>? listenerMethods))
return;
foreach ((Type packetType, List<MethodInfo> methods) in arrivalMethods)
foreach (MethodInfo receiveMethod in methods)
foreach (Type packetType in listenerMethods.Keys)
{
if (!packetRouters.TryGetValue(packetType, out Dictionary<string, object>? routers))
{
if (!packetRouters.TryGetValue(packetType, out Dictionary<string, object>? routers))
{
routers = [];
packetRouters.Add(packetType, routers);
}
object packetListenerEvent = CreateEventAndRegister(packetType, behaviour, networkType);
routers.Add(behaviour.Id, packetListenerEvent);
routers = [];
packetRouters.Add(packetType, routers);
}
object packetListenerEvent = CreateEventAndRegister(packetType, behaviour, networkType, registerPacketListenerListenersMethods);
routers.Add(behaviour.Id, packetListenerEvent);
}
}
private object CreateEventAndRegister(Type packetType, INetworkEntity behaviour, NetworkType networkType)
/// <summary>
/// Creates an Event<IConnection, TPacket> and attaches listener callbacks.
/// </summary>
private object CreateEventAndRegister(
Type packetType,
INetworkEntity behaviour,
NetworkType networkType,
Dictionary<Type, MethodInfo> registerPacketListenersMethods)
{
Type genericEventType = typeof(Event<,>).MakeGenericType(typeof(IConnection), packetType);
object packetListenerEvent = Activator.CreateInstance(genericEventType)!;
if (!registerPacketListenersMethods.TryGetValue(packetType, out MethodInfo? registerPacketListenerMethod))
throw new($"{nameof(RegisterPacketListenerEvent)} for {packetType.Name} has not been cached.");
throw new($"Packet Listener Events for {packetType.Name} has not been cached.");
registerPacketListenerMethod.Invoke(this, [behaviour, packetListenerEvent, networkType]);
return packetListenerEvent;
}
private static void RegisterPacketListenerEvent<T>(
/// <summary>
/// Registers broadcast packet listeners on the behaviour.
/// </summary>
private static void RegisterBroadcastPacketListenerEvent<T>(
INetworkEntity behaviour,
Event<IConnection, T> packetListenerEvent,
NetworkType networkType)
{
switch (networkType)
{
case NetworkType.Client: packetListenerEvent.AddListener((sender, packet) => ((IPacketListenerClient<T>)behaviour).OnClientPacketArrived(sender, packet)); break;
case NetworkType.Server: packetListenerEvent.AddListener((sender, packet) => ((IPacketListenerServer<T>)behaviour).OnServerPacketArrived(sender, packet)); break;
case NetworkType.Client:
packetListenerEvent.AddListener(
(sender, packet) => ((IPacketListenerClient<T>)behaviour).OnClientPacketArrived(sender, packet));
break;
case NetworkType.Server:
packetListenerEvent.AddListener(
(sender, packet) => ((IPacketListenerServer<T>)behaviour).OnServerPacketArrived(sender, packet));
break;
}
}
/// <summary>
/// Registers entity-specific packet listeners on a network behaviour.
/// </summary>
private static void RegisterEntityPacketListenerEvent<T>(
INetworkEntity behaviour,
Event<IConnection, T> packetListenerEvent,
NetworkType networkType
) where T : IEntityNetworkPacket
{
switch (networkType)
{
case NetworkType.Client:
packetListenerEvent.AddListener(
(sender, packet) => ((IPacketListenerClientEntity<T>)behaviour).OnEntityClientPacketArrived(sender, packet));
break;
case NetworkType.Server:
packetListenerEvent.AddListener(
(sender, packet) => ((IPacketListenerServerEntity<T>)behaviour).OnEntityServerPacketArrived(sender, packet));
break;
}
}
/// <summary>
/// Unregisters all routing events associated with the behaviour.
/// </summary>
private void UnregisterPacketRoutersFor(
INetworkEntity behaviour,
Dictionary<Type, Dictionary<string, object>> packetRouters,
Dictionary<Type, Dictionary<Type, List<MethodInfo>>> packetArrivalMethods)
Dictionary<Type, Dictionary<Type, List<MethodInfo>>> packetListenerMethods)
{
if (!packetArrivalMethods.TryGetValue(behaviour.GetType(), out Dictionary<Type, List<MethodInfo>>? arrivalMethods))
if (!packetListenerMethods.TryGetValue(behaviour.GetType(), out Dictionary<Type, List<MethodInfo>>? listenerMethods))
return;
foreach ((Type packetType, List<MethodInfo> methods) in arrivalMethods)
foreach ((Type packetType, List<MethodInfo> methods) in listenerMethods)
{
if (!packetRouters.TryGetValue(packetType, out Dictionary<string, object>? routers))
continue;
@@ -199,6 +356,9 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork
}
}
/// <summary>
/// Clears all listeners from a router event.
/// </summary>
private static void ClearRouter<T>(object routerEventReference)
{
Event<IConnection, T> routerEvent = (Event<IConnection, T>)routerEventReference;
@@ -208,124 +368,192 @@ public class NetworkManager : Behaviour, IEnterUniverse, IExitUniverse, INetwork
#endregion
#region Engine Callbacks
private void OnCollected(IBehaviourCollector<INetworkEntity> sender, IBehaviourCollector<INetworkEntity>.BehaviourCollectedArguments args)
/// <summary>
/// Called when an <see cref="INetworkEntity"/> enters the universe.
/// Registers all packet routing for that entity.
/// </summary>
private void OnCollected(
IBehaviourCollector<INetworkEntity> sender,
IBehaviourCollector<INetworkEntity>.BehaviourCollectedArguments args)
{
INetworkEntity collectedBehaviour = args.BehaviourCollected;
if (!_networkEntities.TryAdd(collectedBehaviour.Id, collectedBehaviour))
throw new($"Unable to add {collectedBehaviour.Id} to {nameof(NetworkManager)}");
RegisterPacketRoutersFor(collectedBehaviour, clientPacketRouters, clientPacketArrivalMethods, NetworkType.Client);
RegisterPacketRoutersFor(collectedBehaviour, serverPacketRouters, serverPacketArrivalMethods, NetworkType.Server);
RegisterPacketRoutersFor(collectedBehaviour, clientPacketBroadcastEvents, clientBroadcastPacketListenerMethods, NetworkType.Client, registerBroadcastPacketListenersMethods);
RegisterPacketRoutersFor(collectedBehaviour, clientEntityPacketRouterEvents, clientEntityPacketListenerMethods, NetworkType.Client, registerEntityPacketListenersMethods);
RegisterPacketRoutersFor(collectedBehaviour, serverPacketBroadcastEvents, serverBroadcastPacketListenerMethods, NetworkType.Server, registerBroadcastPacketListenersMethods);
RegisterPacketRoutersFor(collectedBehaviour, serverEntityPacketRouterEvents, serverEntityPacketListenerMethods, NetworkType.Server, registerEntityPacketListenersMethods);
}
/// <summary>
/// Called when an <see cref="INetworkEntity"/> is removed from the universe.
/// Cleans up all routing.
/// </summary>
private void OnRemoved(IBehaviourCollector<INetworkEntity> sender, IBehaviourCollector<INetworkEntity>.BehaviourRemovedArguments args)
{
INetworkEntity removedBehaviour = args.BehaviourRemoved;
if (!_networkEntities.Remove(args.BehaviourRemoved.Id))
if (!_networkEntities.Remove(removedBehaviour.Id))
return;
UnregisterPacketRoutersFor(removedBehaviour, clientPacketRouters, clientPacketArrivalMethods);
UnregisterPacketRoutersFor(removedBehaviour, serverPacketRouters, serverPacketArrivalMethods);
UnregisterPacketRoutersFor(removedBehaviour, clientPacketBroadcastEvents, clientBroadcastPacketListenerMethods);
UnregisterPacketRoutersFor(removedBehaviour, clientEntityPacketRouterEvents, clientEntityPacketListenerMethods);
UnregisterPacketRoutersFor(removedBehaviour, serverPacketBroadcastEvents, serverBroadcastPacketListenerMethods);
UnregisterPacketRoutersFor(removedBehaviour, serverEntityPacketRouterEvents, serverEntityPacketListenerMethods);
}
public void ExitUniverse(IUniverse universe) => _networkEntityCollector.Unassign();
public void EnterUniverse(IUniverse universe)
{
_networkEntityCollector.Assign(universe);
NetworkCommunicator = BehaviourController.GetRequiredBehaviourInParent<INetworkCommunicator>();
}
#endregion
#region Initialization
public NetworkManager()
{
CachePacketRetrievalDelegates();
CacheRetrievalSubscriptionDelegates();
CacheRegistrationMethods();
CachePacketArrivalMethods();
CachePacketListenerMethods();
_networkEntityCollector.OnCollected.AddListener(OnCollected);
_networkEntityCollector.OnRemoved.AddListener(OnRemoved);
}
private void CachePacketRetrievalDelegates()
/// <summary>
/// Caches all retrieval subscription delegates for packets.
/// </summary>
private void CacheRetrievalSubscriptionDelegates()
{
IEnumerable<Type> packetTypes = AppDomain.CurrentDomain.GetAssemblies().SelectMany(a => a.GetTypes())
.Where(t => typeof(INetworkPacket).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract && !t.IsGenericType);
CachePacketRetrievalDelegates(typeof(INetworkPacket), broadcastPacketRetrievalSubscriptionDelegates);
CachePacketRetrievalDelegates(typeof(IEntityNetworkPacket), entityPacketRetrievalSubscriptionDelegates);
MethodInfo onPacketArrivedMethod = GetType()
.GetMethod(nameof(OnPacketReceived), BindingFlags.NonPublic | BindingFlags.Instance)!;
uniqueRetrievalSubscriptionDelegates.AddRange(broadcastPacketRetrievalSubscriptionDelegates.Concat(entityPacketRetrievalSubscriptionDelegates).DistinctBy(pair => pair.PacketType));
}
foreach (Type packetType in packetTypes)
/// <summary>
/// Creates delegates for all concrete packet types that forward packets to <see cref="OnPacketReceived{T}(IConnection, T)"/>.
/// </summary>
private void CachePacketRetrievalDelegates(Type packetType, List<PacketRetrievalDelegatePair> retrievalDelegates)
{
IEnumerable<Type> packetTypes =
AppDomain.CurrentDomain.GetAssemblies()
.SelectMany(a => a.GetTypes())
.Where(t =>
packetType.IsAssignableFrom(t) &&
!t.IsInterface &&
!t.IsAbstract &&
!t.IsGenericType
);
MethodInfo onPacketArrivedMethod = GetType().GetMethod(nameof(OnPacketReceived), BindingFlags.NonPublic | BindingFlags.Instance)!;
foreach (Type type in packetTypes)
{
MethodInfo genericOnPacketArrivedMethod = onPacketArrivedMethod.MakeGenericMethod(packetType);
MethodInfo genericOnPacketArrivedMethod = onPacketArrivedMethod.MakeGenericMethod(type);
Type genericDelegateType = typeof(Event<,>.EventHandler).MakeGenericType(typeof(IConnection), type);
Type genericDelegateType = typeof(Event<,>.EventHandler).MakeGenericType(typeof(IConnection), packetType);
Delegate genericPacketReceivedDelegate = Delegate.CreateDelegate(genericDelegateType, this, genericOnPacketArrivedMethod);
packetRetrievalDelegates.Add((packetType, genericPacketReceivedDelegate));
retrievalDelegates.Add((type, genericPacketReceivedDelegate));
}
}
/// <summary>
/// Caches all registration and cleanup methods for packets.
/// </summary>
private void CacheRegistrationMethods()
{
CacheRegistrationMethods(registerPacketListenersMethods, nameof(RegisterPacketListenerEvent));
CacheRegistrationMethods(clearRoutesMethods, nameof(ClearRouter));
CacheRegistrationMethods(registerBroadcastPacketListenersMethods, nameof(RegisterBroadcastPacketListenerEvent), broadcastPacketRetrievalSubscriptionDelegates);
CacheRegistrationMethods(registerEntityPacketListenersMethods, nameof(RegisterEntityPacketListenerEvent), entityPacketRetrievalSubscriptionDelegates);
CacheRegistrationMethods(clearRoutesMethods, nameof(ClearRouter), uniqueRetrievalSubscriptionDelegates);
}
private void CacheRegistrationMethods(Dictionary<Type, MethodInfo> registrationMethods, string methodName)
/// <summary>
/// Creates generic method instances for each packet type listener to be registered into the <see cref="NetworkEntity"/>.
/// </summary>
private void CacheRegistrationMethods(
Dictionary<Type, MethodInfo> listenerRegistrationMethods,
string methodName,
List<PacketRetrievalDelegatePair> packetRetrievalDelegates)
{
MethodInfo registerPacketMethod = typeof(NetworkManager).GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Static)!;
foreach ((Type packetType, Delegate @delegate) in packetRetrievalDelegates)
{
MethodInfo genericMethod = registerPacketMethod.MakeGenericMethod(packetType);
registrationMethods.Add(packetType, genericMethod);
listenerRegistrationMethods.TryAdd(packetType, genericMethod);
}
}
private void CachePacketArrivalMethods()
/// <summary>
/// Caches packet listener methods for all packet listener interfaces.
/// </summary>
private void CachePacketListenerMethods()
{
CachePacketArrivalMethods(clientPacketArrivalMethods, typeof(IPacketListenerClient<>), nameof(IPacketListenerClient<INetworkEntity>.OnClientPacketArrived));
CachePacketArrivalMethods(serverPacketArrivalMethods, typeof(IPacketListenerServer<>), nameof(IPacketListenerServer<INetworkEntity>.OnServerPacketArrived));
CachePacketListenerMethods(clientBroadcastPacketListenerMethods, typeof(IPacketListenerClient<>), nameof(IPacketListenerClient<>.OnClientPacketArrived));
CachePacketListenerMethods(serverBroadcastPacketListenerMethods, typeof(IPacketListenerServer<>), nameof(IPacketListenerServer<>.OnServerPacketArrived));
CachePacketListenerMethods(clientEntityPacketListenerMethods, typeof(IPacketListenerClientEntity<>), nameof(IPacketListenerClientEntity<>.OnEntityClientPacketArrived));
CachePacketListenerMethods(serverEntityPacketListenerMethods, typeof(IPacketListenerServerEntity<>), nameof(IPacketListenerServerEntity<>.OnEntityServerPacketArrived));
}
private static void CachePacketArrivalMethods(Dictionary<Type, Dictionary<Type, List<MethodInfo>>> packetArrivalMethods, Type listenerType, string packetArrivalMethodName)
/// <summary>
/// Discovers all types implementing a given packet listener interface and caches their methods.
/// </summary>
private static void CachePacketListenerMethods(
Dictionary<Type, Dictionary<Type, List<MethodInfo>>> packetListenerMethods,
Type listenerType,
string packetListenerMethodName)
{
foreach (Type listenerClass in GetGenericsWith(listenerType))
{
Dictionary<Type, List<MethodInfo>> packetRouters = [];
packetArrivalMethods.Add(listenerClass, packetRouters);
Dictionary<Type, List<MethodInfo>> listenerMethodDictionary = [];
packetListenerMethods.Add(listenerClass, listenerMethodDictionary);
foreach (Type packetListener in GetGenericInterfacesWith(listenerType, listenerClass))
{
Type packetType = packetListener.GetGenericArguments().First();
List<MethodInfo> arrivalMethods = packetListener
.GetMethods()
.Where(m => m.Name == packetArrivalMethodName)
.ToList();
List<MethodInfo> listenerMethods = [.. packetListener.GetMethods().Where(m => m.Name == packetListenerMethodName)];
packetRouters.Add(packetType, arrivalMethods);
listenerMethodDictionary.Add(packetType, listenerMethods);
}
}
}
/// <summary>
/// Finds all types that implement a generic interface.
/// </summary>
private static IEnumerable<Type> GetGenericsWith(Type type)
=> AppDomain.CurrentDomain
.GetAssemblies()
.SelectMany(a =>
a.GetTypes().Where(
t => t.GetInterfaces().Any(
i => i.IsGenericType && i.GetGenericTypeDefinition() == type
)
)
);
i => i.IsGenericType && i.GetGenericTypeDefinition() == type)));
// Gets all generic interfaces of a specific definition on a type
private static IEnumerable<Type> GetGenericInterfacesWith(Type interfaceType, Type type)
=> type.GetInterfaces().Where(
i => i.IsGenericType && i.GetGenericTypeDefinition() == interfaceType
);
=> type.GetInterfaces().Where(i => i.IsGenericType && i.GetGenericTypeDefinition() == interfaceType);
#endregion
// Identifies whether packet routing is client-side or server-side
private enum NetworkType { Client, Server }
private readonly record struct PacketRetrievalDelegatePair(Type PacketType, Delegate Delegate)
{
public static implicit operator (Type packetType, Delegate @delegate)(PacketRetrievalDelegatePair value) => (value.PacketType, value.Delegate);
public static implicit operator PacketRetrievalDelegatePair((Type packetType, Delegate @delegate) value) => new(value.packetType, value.@delegate);
}
}

View File

@@ -27,8 +27,16 @@ internal class Tween : ITween
field = value;
switch (value)
{
case TweenState.Completed: OnCompleted?.Invoke(this); OnEnded?.Invoke(this); break;
case TweenState.Cancelled: OnCancelled?.Invoke(this); OnEnded?.Invoke(this); break;
case TweenState.Completed:
OnCompleted?.Invoke(this);
if (State == TweenState.Completed)
OnEnded?.Invoke(this);
break;
case TweenState.Cancelled:
OnCancelled?.Invoke(this);
if (State == TweenState.Cancelled)
OnEnded?.Invoke(this);
break;
case TweenState.Paused: OnPaused?.Invoke(this); break;
case TweenState.Playing:
if (previousState == TweenState.Idle)

View File

@@ -4,35 +4,48 @@ namespace Engine.Systems.Tween;
public static class TweenExtensions
{
private static readonly System.Collections.Generic.Dictionary<ITween, int> loopDictionary = [];
public static ITween Loop(this ITween tween, int count)
{
Tween tweenConcrete = (Tween)tween;
int counter = count;
if (!loopDictionary.TryAdd(tween, count))
throw new($"Tween already has a loop in progress.");
tweenConcrete.OnCompleted.AddListener(_ =>
{
if (counter-- <= 0)
return;
tweenConcrete.Reset();
tweenConcrete.State = TweenState.Playing;
});
tween.OnCompleted.AddListener(looperDelegate);
tween.OnEnded.AddListener(looperEndDelegate);
return tween;
}
private static readonly Core.Event<ITween>.EventHandler looperEndDelegate = sender => loopDictionary.Remove(sender);
private static readonly Core.Event<ITween>.EventHandler looperDelegate = sender =>
{
int counter = loopDictionary[sender] = loopDictionary[sender] - 1;
if (counter <= 0)
{
loopDictionary.Remove(sender);
return;
}
Tween tweenConcrete = (Tween)sender;
tweenConcrete.Reset();
tweenConcrete.State = TweenState.Playing;
};
public static ITween LoopInfinitely(this ITween tween)
{
Tween tweenConcrete = (Tween)tween;
tweenConcrete.OnCompleted.AddListener(_ =>
{
tweenConcrete.Reset();
tweenConcrete.State = TweenState.Playing;
});
tween.OnCompleted.AddListener(repeaterDelegate);
return tween;
}
private static readonly Core.Event<ITween>.EventHandler repeaterDelegate = sender =>
{
Tween tweenConcrete = (Tween)sender;
tweenConcrete.Reset();
tweenConcrete.State = TweenState.Playing;
};
public static ITween Ease(this ITween tween, IEasing easing)
{
Tween tweenConcrete = (Tween)tween;

View File

@@ -7,7 +7,7 @@ namespace Engine.Systems.Tween;
public class TweenManager : Behaviour, IEnterUniverse, IExitUniverse, ITweenManager
{
private CoroutineManager coroutineManager = null!;
private ICoroutineManager coroutineManager = null!;
private readonly Dictionary<ITween, IEnumerator> runningCoroutines = [];
private readonly Queue<Tween> pool = new();
@@ -75,7 +75,7 @@ public class TweenManager : Behaviour, IEnterUniverse, IExitUniverse, ITweenMana
public void EnterUniverse(IUniverse universe)
{
coroutineManager = universe.FindRequiredBehaviour<CoroutineManager>();
coroutineManager = universe.FindRequiredBehaviour<ICoroutineManager>();
}
public void ExitUniverse(IUniverse universe)

View File

@@ -2,4 +2,4 @@ using Engine.Core;
namespace Engine.Systems.Tween;
public class WaitForTweenCompleteCoroutineYield(ITween tween) : CoroutineYield(() => tween.State == TweenState.Completed);
public class WaitForTweenCompleteCoroutineYield(ITween tween) : WaitUntilYield(() => tween.State == TweenState.Completed);

View File

@@ -2,4 +2,4 @@ using Engine.Core;
namespace Engine.Systems.Tween;
public class WaitWhileTweenActiveCoroutineYield(ITween tween) : CoroutineYield(() => tween.State.CheckFlag(TweenState.Completed | TweenState.Cancelled));
public class WaitForTweenDoneCoroutineYield(ITween tween) : WaitUntilYield(() => tween.State.CheckFlag(TweenState.Completed | TweenState.Cancelled));