From fc50bf9b8135978325da91a45d4ad78df161d400 Mon Sep 17 00:00:00 2001 From: thatcosmonaut Date: Thu, 21 Nov 2019 13:53:33 -0800 Subject: [PATCH] implementation of time dilation system for engines --- .../Attributes/IgnoresTimeDilation.cs | 7 + .../Attributes/TimeDilationPriority.cs | 15 ++ encompass-cs/Engine.cs | 45 ++++++ .../Engines/ComponentMessageEmitter.cs | 4 +- encompass-cs/Engines/Spawner.cs | 2 +- .../TimeDilationPriorityConflictException.cs | 12 ++ .../TimeDilationPriorityUndefinedException.cs | 12 ++ encompass-cs/TimeDilationData.cs | 37 +++++ encompass-cs/TimeManager.cs | 73 +++++++++ encompass-cs/World.cs | 13 +- encompass-cs/WorldBuilder.cs | 36 ++++- test/EngineTest.cs | 147 ++++++++++++++++++ test/SpawnerTest.cs | 6 +- 13 files changed, 398 insertions(+), 11 deletions(-) create mode 100644 encompass-cs/Attributes/IgnoresTimeDilation.cs create mode 100644 encompass-cs/Attributes/TimeDilationPriority.cs create mode 100644 encompass-cs/Exceptions/TimeDilationPriorityConflictException.cs create mode 100644 encompass-cs/Exceptions/TimeDilationPriorityUndefinedException.cs create mode 100644 encompass-cs/TimeDilationData.cs create mode 100644 encompass-cs/TimeManager.cs diff --git a/encompass-cs/Attributes/IgnoresTimeDilation.cs b/encompass-cs/Attributes/IgnoresTimeDilation.cs new file mode 100644 index 0000000..df6b579 --- /dev/null +++ b/encompass-cs/Attributes/IgnoresTimeDilation.cs @@ -0,0 +1,7 @@ +using System; + +namespace Encompass +{ + [AttributeUsage(AttributeTargets.Class)] + public class IgnoresTimeDilation : Attribute { } +} diff --git a/encompass-cs/Attributes/TimeDilationPriority.cs b/encompass-cs/Attributes/TimeDilationPriority.cs new file mode 100644 index 0000000..c833e9b --- /dev/null +++ b/encompass-cs/Attributes/TimeDilationPriority.cs @@ -0,0 +1,15 @@ +using System; + +namespace Encompass +{ + [AttributeUsage(AttributeTargets.Class)] + public class TimeDilationPriority : Attribute + { + public int timeDilationPriority; + + public TimeDilationPriority(int timeDilationPriority) + { + this.timeDilationPriority = timeDilationPriority; + } + } +} diff --git a/encompass-cs/Engine.cs b/encompass-cs/Engine.cs index 27ec75a..e26146f 100644 --- a/encompass-cs/Engine.cs +++ b/encompass-cs/Engine.cs @@ -19,10 +19,21 @@ namespace Encompass internal readonly HashSet receiveTypes = new HashSet(); internal readonly Dictionary writePriorities = new Dictionary(); + /// + /// If false, the Engine will ignore time dilation. + /// + internal bool usesTimeDilation = true; + public bool TimeDilationActive { get => usesTimeDilation && timeManager.TimeDilationActive; } + /// + /// Used when activating time dilation. Lower priority overrides higher priority. + /// + internal int? timeDilationPriority = null; + private EntityManager entityManager; private MessageManager messageManager; private ComponentManager componentManager; private ComponentMessageManager componentMessageManager; + private TimeManager timeManager; protected Engine() { @@ -106,6 +117,11 @@ namespace Encompass this.componentMessageManager = componentMessageManager; } + internal void AssignTimeManager(TimeManager timeManager) + { + this.timeManager = timeManager; + } + /// /// Runs once per World update with the calculated delta-time. /// @@ -673,5 +689,34 @@ namespace Encompass { componentManager.MarkForRemoval(componentID); } + + private void CheckTimeDilationPriorityExists() + { + if (!timeDilationPriority.HasValue) { throw new TimeDilationPriorityUndefinedException("Engines that activate time dilation must use the TimeDilationPriority attribute."); } + } + + public void ActivateTimeDilation(double factor, double easeInTime, double activeTime, double easeOutTime) + { + CheckTimeDilationPriorityExists(); + timeManager.ActivateTimeDilation(factor, easeInTime, activeTime, easeOutTime, timeDilationPriority.Value); + } + + public void ActivateTimeDilation(double factor, double easeInTime, System.Func easeInFunction, double activeTime, double easeOutTime) + { + CheckTimeDilationPriorityExists(); + timeManager.ActivateTimeDilation(factor, easeInTime, easeInFunction, activeTime, easeOutTime, timeDilationPriority.Value); + } + + public void ActivateTimeDilation(double factor, double easeInTime, double activeTime, double easeOutTime, System.Func easeOutFunction) + { + CheckTimeDilationPriorityExists(); + timeManager.ActivateTimeDilation(factor, easeInTime, activeTime, easeOutTime, easeOutFunction, timeDilationPriority.Value); + } + + public void ActivateTimeDilation(double factor, double easeInTime, System.Func easeInFunction, double activeTime, double easeOutTime, System.Func easeOutFunction) + { + CheckTimeDilationPriorityExists(); + timeManager.ActivateTimeDilation(factor, easeInTime, easeInFunction, activeTime, easeOutTime, easeOutFunction, timeDilationPriority.Value); + } } } diff --git a/encompass-cs/Engines/ComponentMessageEmitter.cs b/encompass-cs/Engines/ComponentMessageEmitter.cs index 27bec22..5addc68 100644 --- a/encompass-cs/Engines/ComponentMessageEmitter.cs +++ b/encompass-cs/Engines/ComponentMessageEmitter.cs @@ -1,6 +1,4 @@ -using System.Reflection; - -namespace Encompass.Engines +namespace Encompass { internal class ComponentMessageEmitter : Engine where TComponent : struct, IComponent { diff --git a/encompass-cs/Engines/Spawner.cs b/encompass-cs/Engines/Spawner.cs index 99e35bb..3ad21bf 100644 --- a/encompass-cs/Engines/Spawner.cs +++ b/encompass-cs/Engines/Spawner.cs @@ -1,6 +1,6 @@ using System.Reflection; -namespace Encompass.Engines +namespace Encompass { /// /// A Spawner is a special type of Engine that runs a Spawn method in response to each Message it receives. diff --git a/encompass-cs/Exceptions/TimeDilationPriorityConflictException.cs b/encompass-cs/Exceptions/TimeDilationPriorityConflictException.cs new file mode 100644 index 0000000..cf7c109 --- /dev/null +++ b/encompass-cs/Exceptions/TimeDilationPriorityConflictException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Encompass.Exceptions +{ + public class TimeDilationPriorityConflictException : Exception + { + public TimeDilationPriorityConflictException( + string format, + params object[] args + ) : base(string.Format(format, args)) { } + } +} diff --git a/encompass-cs/Exceptions/TimeDilationPriorityUndefinedException.cs b/encompass-cs/Exceptions/TimeDilationPriorityUndefinedException.cs new file mode 100644 index 0000000..1baf332 --- /dev/null +++ b/encompass-cs/Exceptions/TimeDilationPriorityUndefinedException.cs @@ -0,0 +1,12 @@ +using System; + +namespace Encompass.Exceptions +{ + public class TimeDilationPriorityUndefinedException : Exception + { + public TimeDilationPriorityUndefinedException( + string format, + params object[] args + ) : base(string.Format(format, args)) { } + } +} diff --git a/encompass-cs/TimeDilationData.cs b/encompass-cs/TimeDilationData.cs new file mode 100644 index 0000000..4939f43 --- /dev/null +++ b/encompass-cs/TimeDilationData.cs @@ -0,0 +1,37 @@ +namespace Encompass +{ + internal struct TimeDilationData + { + public double elapsedTime; + public double easeInTime; + public System.Func easeInFunction; + public double activeTime; + public double easeOutTime; + public System.Func easeOutFunction; + public double factor; + + public double Factor + { + get + { + double calculatedFactor = 1; + + if (elapsedTime < easeInTime) + { + calculatedFactor = easeInFunction(elapsedTime, 1, factor - 1, easeInTime); + } + else if (elapsedTime < easeInTime + activeTime) + { + calculatedFactor = factor; + } + else if (elapsedTime < easeInTime + activeTime + easeOutTime) + { + var elapsedOutTime = elapsedTime - easeInTime - activeTime; + calculatedFactor = easeOutFunction(elapsedOutTime, factor, 1 - factor, easeOutTime); + } + + return calculatedFactor; + } + } + } +} \ No newline at end of file diff --git a/encompass-cs/TimeManager.cs b/encompass-cs/TimeManager.cs new file mode 100644 index 0000000..7187091 --- /dev/null +++ b/encompass-cs/TimeManager.cs @@ -0,0 +1,73 @@ +namespace Encompass +{ + internal class TimeManager + { + private TimeDilationData timeDilationData = new TimeDilationData { factor = 1 }; + private bool newTimeDilationData = false; + private TimeDilationData nextFrameTimeDilationData = new TimeDilationData { factor = 1 }; + + private double Linear(double t, double b, double c, double d) + { + return c * t / d + b; + } + private int minPriority = int.MaxValue; + + public double TimeDilationFactor + { + get + { + return timeDilationData.Factor; + } + } + + public bool TimeDilationActive + { + get => TimeDilationFactor != 1; + } + + public void Update(double dt) + { + if (newTimeDilationData) + { + timeDilationData = nextFrameTimeDilationData; + } + + timeDilationData.elapsedTime += dt; + newTimeDilationData = false; + minPriority = int.MaxValue; + } + + public void ActivateTimeDilation(double factor, double easeInTime, double activeTime, double easeOutTime, int priority) + { + ActivateTimeDilation(factor, easeInTime, Linear, activeTime, easeOutTime, Linear, priority); + } + + public void ActivateTimeDilation(double factor, double easeInTime, System.Func easeInFunction, double activeTime, double easeOutTime, int priority) + { + ActivateTimeDilation(factor, easeInTime, easeInFunction, activeTime, easeOutTime, Linear, priority); + } + + public void ActivateTimeDilation(double factor, double easeInTime, double activeTime, double easeOutTime, System.Func easeOutFunction, int priority) + { + ActivateTimeDilation(factor, easeInTime, Linear, activeTime, easeOutTime, easeOutFunction, priority); + } + + public void ActivateTimeDilation(double factor, double easeInTime, System.Func easeInFunction, double activeTime, double easeOutTime, System.Func easeOutFunction, int priority) + { + if (priority <= minPriority) + { + newTimeDilationData = true; + nextFrameTimeDilationData = new TimeDilationData + { + elapsedTime = 0, + easeInTime = easeInTime, + easeInFunction = easeInFunction, + activeTime = activeTime, + easeOutTime = easeOutTime, + easeOutFunction = easeOutFunction, + factor = factor + }; + } + } + } +} diff --git a/encompass-cs/World.cs b/encompass-cs/World.cs index 1d8768a..2a3a099 100644 --- a/encompass-cs/World.cs +++ b/encompass-cs/World.cs @@ -12,6 +12,7 @@ namespace Encompass private readonly ComponentManager componentManager; private readonly MessageManager messageManager; private readonly ComponentMessageManager componentMessageManager; + private readonly TimeManager timeManager; private readonly RenderManager renderManager; internal World( @@ -20,6 +21,7 @@ namespace Encompass ComponentManager componentManager, MessageManager messageManager, ComponentMessageManager componentMessageManager, + TimeManager timeManager, RenderManager renderManager ) { @@ -28,6 +30,7 @@ namespace Encompass this.componentManager = componentManager; this.messageManager = messageManager; this.componentMessageManager = componentMessageManager; + this.timeManager = timeManager; this.renderManager = renderManager; } @@ -38,10 +41,18 @@ namespace Encompass public void Update(double dt) { messageManager.ProcessDelayedMessages(dt); + timeManager.Update(dt); foreach (var engine in enginesInOrder) { - engine.Update(dt); + if (engine.usesTimeDilation) + { + engine.Update(dt * timeManager.TimeDilationFactor); + } + else + { + engine.Update(dt); + } } messageManager.ClearMessages(); diff --git a/encompass-cs/WorldBuilder.cs b/encompass-cs/WorldBuilder.cs index b214475..8e1d510 100644 --- a/encompass-cs/WorldBuilder.cs +++ b/encompass-cs/WorldBuilder.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Reflection; using System.Linq; using Encompass.Exceptions; -using Encompass.Engines; using MoonTools.Core.Graph; using MoonTools.Core.Graph.Extensions; @@ -26,6 +25,7 @@ namespace Encompass private readonly EntityManager entityManager; private readonly MessageManager messageManager; private readonly ComponentMessageManager componentMessageManager; + private readonly TimeManager timeManager; private readonly DrawLayerManager drawLayerManager; private readonly RenderManager renderManager; @@ -42,6 +42,7 @@ namespace Encompass messageManager = new MessageManager(); componentMessageManager = new ComponentMessageManager(); entityManager = new EntityManager(componentManager, componentMessageManager); + timeManager = new TimeManager(); renderManager = new RenderManager(componentManager, drawLayerManager, entityManager); } @@ -97,6 +98,7 @@ namespace Encompass engine.AssignComponentManager(componentManager); engine.AssignMessageManager(messageManager); engine.AssignComponentMessageManager(componentMessageManager); + engine.AssignTimeManager(timeManager); engines.Add(engine); engineGraph.AddNode(engine); @@ -232,8 +234,27 @@ namespace Encompass var writePriorities = new Dictionary>(); var writeMessageToEngines = new Dictionary>(); + var timeDilationPriorities = new Dictionary>(); + foreach (var engine in engines) { + var timeDilationPriorityAttribute = engine.GetType().GetCustomAttribute(); + + if (timeDilationPriorityAttribute != null) + { + engine.timeDilationPriority = timeDilationPriorityAttribute.timeDilationPriority; + if (!timeDilationPriorities.ContainsKey(timeDilationPriorityAttribute.timeDilationPriority)) + { + timeDilationPriorities.Add(timeDilationPriorityAttribute.timeDilationPriority, new HashSet()); + } + timeDilationPriorities[timeDilationPriorityAttribute.timeDilationPriority].Add(engine); + } + + if (engine.GetType().GetCustomAttribute() != null) + { + engine.usesTimeDilation = false; + } + var defaultWritePriorityAttribute = engine.GetType().GetCustomAttribute(false); var writeTypes = engine.sendTypes.Where((type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ComponentWriteMessage<>)); @@ -323,6 +344,18 @@ namespace Encompass throw new EngineWriteConflictException(errorString); } + foreach (var timeDilationEngines in timeDilationPriorities) + { + var priority = timeDilationEngines.Key; + var engines = timeDilationEngines.Value; + if (engines.Count > 1) + { + var errorString = "Multiple Engines have the same Time Dilation Priority value: "; + errorString += string.Join(", ", engines); + throw new TimeDilationPriorityConflictException(errorString); + } + } + var engineOrder = new List(); foreach (var engine in engineGraph.TopologicalSort()) { @@ -335,6 +368,7 @@ namespace Encompass componentManager, messageManager, componentMessageManager, + timeManager, renderManager ); diff --git a/test/EngineTest.cs b/test/EngineTest.cs index b42ba35..c23f6e9 100644 --- a/test/EngineTest.cs +++ b/test/EngineTest.cs @@ -910,5 +910,152 @@ namespace Tests resultComponents.Should().BeEmpty(); } + + static double dilatedDeltaTime; + + [TimeDilationPriority(0)] + class ActivateTimeDilationEngine : Engine + { + public override void Update(double dt) + { + if (!TimeDilationActive) + { + ActivateTimeDilation(0.2, 1, 1, 1); + } + else + { + dilatedDeltaTime = dt; + } + } + } + + [Test] + public void ActivateTimeDilation() + { + var worldBuilder = new WorldBuilder(); + worldBuilder.AddEngine(new ActivateTimeDilationEngine()); + + var world = worldBuilder.Build(); + + world.Update(0.01); // activate time dilation + + world.Update(0.5); + + dilatedDeltaTime.Should().BeApproximately(0.3, 0.01); + + world.Update(0.5); + + dilatedDeltaTime.Should().BeApproximately(0.1, 0.01); + + world.Update(1); + + world.Update(0.5); + + dilatedDeltaTime.Should().BeApproximately(0.3, 0.01); + } + + class ActivateTimeDilationWithoutPriorityEngine : Engine + { + public override void Update(double dt) + { + ActivateTimeDilation(0.2, 1, 1, 1); + } + } + + [Test] + public void ActivateTimeDilationWithoutPriorityThrows() + { + var worldBuilder = new WorldBuilder(); + worldBuilder.AddEngine(new ActivateTimeDilationWithoutPriorityEngine()); + + var world = worldBuilder.Build(); + + Assert.Throws(() => world.Update(0.01)); + } + + [TimeDilationPriority(0)] + class ActivateTimeDilationLowerPriorityEngine : Engine + { + public override void Update(double dt) + { + if (!TimeDilationActive) + { + ActivateTimeDilation(0.2, 1, 1, 1); + } + else + { + dilatedDeltaTime = dt; + } + } + } + + [TimeDilationPriority(1)] + class ActivateTimeDilationHigherPriorityEngine : Engine + { + public override void Update(double dt) + { + if (!TimeDilationActive) + { + ActivateTimeDilation(0.5, 1, 1, 1); + } + else + { + dilatedDeltaTime = dt; + } + } + } + + [Test] + public void MultipleActivateTimeDilation() + { + var worldBuilder = new WorldBuilder(); + worldBuilder.AddEngine(new ActivateTimeDilationLowerPriorityEngine()); + worldBuilder.AddEngine(new ActivateTimeDilationHigherPriorityEngine()); + + var world = worldBuilder.Build(); + + world.Update(0.01); // activate time dilation + + world.Update(0.5); + + dilatedDeltaTime.Should().BeApproximately(0.3, 0.01); + } + + [Test] + public void MultipleActivateTimeDilationWithDuplicatePriority() + { + var worldBuilder = new WorldBuilder(); + worldBuilder.AddEngine(new ActivateTimeDilationEngine()); + worldBuilder.AddEngine(new ActivateTimeDilationLowerPriorityEngine()); + + Assert.Throws(() => worldBuilder.Build()); + } + + static double undilatedDeltaTime; + + [IgnoresTimeDilation] + class IgnoresTimeDilationEngine : Engine + { + public override void Update(double dt) + { + undilatedDeltaTime = dt; + } + } + + [Test] + public void IgnoresTimeDilation() + { + var worldBuilder = new WorldBuilder(); + worldBuilder.AddEngine(new ActivateTimeDilationEngine()); + worldBuilder.AddEngine(new IgnoresTimeDilationEngine()); + + var world = worldBuilder.Build(); + + world.Update(0.01); // activate time dilation + + world.Update(0.5); + + undilatedDeltaTime.Should().Be(0.5); + } } } diff --git a/test/SpawnerTest.cs b/test/SpawnerTest.cs index a5cfedb..a7f8158 100644 --- a/test/SpawnerTest.cs +++ b/test/SpawnerTest.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Encompass; -using Encompass.Engines; +using Encompass; using NUnit.Framework; namespace Tests