implementation of time dilation system for engines

pull/5/head
thatcosmonaut 2019-11-21 13:53:33 -08:00
parent 471117f4f4
commit fc50bf9b81
13 changed files with 398 additions and 11 deletions

View File

@ -0,0 +1,7 @@
using System;
namespace Encompass
{
[AttributeUsage(AttributeTargets.Class)]
public class IgnoresTimeDilation : Attribute { }
}

View File

@ -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;
}
}
}

View File

@ -19,10 +19,21 @@ namespace Encompass
internal readonly HashSet<Type> receiveTypes = new HashSet<Type>(); internal readonly HashSet<Type> receiveTypes = new HashSet<Type>();
internal readonly Dictionary<Type, int> writePriorities = new Dictionary<Type, int>(); internal readonly Dictionary<Type, int> writePriorities = new Dictionary<Type, int>();
/// <summary>
/// If false, the Engine will ignore time dilation.
/// </summary>
internal bool usesTimeDilation = true;
public bool TimeDilationActive { get => usesTimeDilation && timeManager.TimeDilationActive; }
/// <summary>
/// Used when activating time dilation. Lower priority overrides higher priority.
/// </summary>
internal int? timeDilationPriority = null;
private EntityManager entityManager; private EntityManager entityManager;
private MessageManager messageManager; private MessageManager messageManager;
private ComponentManager componentManager; private ComponentManager componentManager;
private ComponentMessageManager componentMessageManager; private ComponentMessageManager componentMessageManager;
private TimeManager timeManager;
protected Engine() protected Engine()
{ {
@ -106,6 +117,11 @@ namespace Encompass
this.componentMessageManager = componentMessageManager; this.componentMessageManager = componentMessageManager;
} }
internal void AssignTimeManager(TimeManager timeManager)
{
this.timeManager = timeManager;
}
/// <summary> /// <summary>
/// Runs once per World update with the calculated delta-time. /// Runs once per World update with the calculated delta-time.
/// </summary> /// </summary>
@ -673,5 +689,34 @@ namespace Encompass
{ {
componentManager.MarkForRemoval(componentID); 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<double, double, double, double, double> 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<double, double, double, double, double> easeOutFunction)
{
CheckTimeDilationPriorityExists();
timeManager.ActivateTimeDilation(factor, easeInTime, activeTime, easeOutTime, easeOutFunction, timeDilationPriority.Value);
}
public void ActivateTimeDilation(double factor, double easeInTime, System.Func<double, double, double, double, double> easeInFunction, double activeTime, double easeOutTime, System.Func<double, double, double, double, double> easeOutFunction)
{
CheckTimeDilationPriorityExists();
timeManager.ActivateTimeDilation(factor, easeInTime, easeInFunction, activeTime, easeOutTime, easeOutFunction, timeDilationPriority.Value);
}
} }
} }

View File

@ -1,6 +1,4 @@
using System.Reflection; namespace Encompass
namespace Encompass.Engines
{ {
internal class ComponentMessageEmitter<TComponent> : Engine where TComponent : struct, IComponent internal class ComponentMessageEmitter<TComponent> : Engine where TComponent : struct, IComponent
{ {

View File

@ -1,6 +1,6 @@
using System.Reflection; using System.Reflection;
namespace Encompass.Engines namespace Encompass
{ {
/// <summary> /// <summary>
/// A Spawner is a special type of Engine that runs a Spawn method in response to each Message it receives. /// A Spawner is a special type of Engine that runs a Spawn method in response to each Message it receives.

View File

@ -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)) { }
}
}

View File

@ -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)) { }
}
}

View File

@ -0,0 +1,37 @@
namespace Encompass
{
internal struct TimeDilationData
{
public double elapsedTime;
public double easeInTime;
public System.Func<double, double, double, double, double> easeInFunction;
public double activeTime;
public double easeOutTime;
public System.Func<double, double, double, double, double> 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;
}
}
}
}

View File

@ -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<double, double, double, double, double> 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<double, double, double, double, double> easeOutFunction, int priority)
{
ActivateTimeDilation(factor, easeInTime, Linear, activeTime, easeOutTime, easeOutFunction, priority);
}
public void ActivateTimeDilation(double factor, double easeInTime, System.Func<double, double, double, double, double> easeInFunction, double activeTime, double easeOutTime, System.Func<double, double, double, double, double> 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
};
}
}
}
}

View File

@ -12,6 +12,7 @@ namespace Encompass
private readonly ComponentManager componentManager; private readonly ComponentManager componentManager;
private readonly MessageManager messageManager; private readonly MessageManager messageManager;
private readonly ComponentMessageManager componentMessageManager; private readonly ComponentMessageManager componentMessageManager;
private readonly TimeManager timeManager;
private readonly RenderManager renderManager; private readonly RenderManager renderManager;
internal World( internal World(
@ -20,6 +21,7 @@ namespace Encompass
ComponentManager componentManager, ComponentManager componentManager,
MessageManager messageManager, MessageManager messageManager,
ComponentMessageManager componentMessageManager, ComponentMessageManager componentMessageManager,
TimeManager timeManager,
RenderManager renderManager RenderManager renderManager
) )
{ {
@ -28,6 +30,7 @@ namespace Encompass
this.componentManager = componentManager; this.componentManager = componentManager;
this.messageManager = messageManager; this.messageManager = messageManager;
this.componentMessageManager = componentMessageManager; this.componentMessageManager = componentMessageManager;
this.timeManager = timeManager;
this.renderManager = renderManager; this.renderManager = renderManager;
} }
@ -38,11 +41,19 @@ namespace Encompass
public void Update(double dt) public void Update(double dt)
{ {
messageManager.ProcessDelayedMessages(dt); messageManager.ProcessDelayedMessages(dt);
timeManager.Update(dt);
foreach (var engine in enginesInOrder) foreach (var engine in enginesInOrder)
{
if (engine.usesTimeDilation)
{
engine.Update(dt * timeManager.TimeDilationFactor);
}
else
{ {
engine.Update(dt); engine.Update(dt);
} }
}
messageManager.ClearMessages(); messageManager.ClearMessages();
componentMessageManager.ClearMessages(); componentMessageManager.ClearMessages();

View File

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Reflection; using System.Reflection;
using System.Linq; using System.Linq;
using Encompass.Exceptions; using Encompass.Exceptions;
using Encompass.Engines;
using MoonTools.Core.Graph; using MoonTools.Core.Graph;
using MoonTools.Core.Graph.Extensions; using MoonTools.Core.Graph.Extensions;
@ -26,6 +25,7 @@ namespace Encompass
private readonly EntityManager entityManager; private readonly EntityManager entityManager;
private readonly MessageManager messageManager; private readonly MessageManager messageManager;
private readonly ComponentMessageManager componentMessageManager; private readonly ComponentMessageManager componentMessageManager;
private readonly TimeManager timeManager;
private readonly DrawLayerManager drawLayerManager; private readonly DrawLayerManager drawLayerManager;
private readonly RenderManager renderManager; private readonly RenderManager renderManager;
@ -42,6 +42,7 @@ namespace Encompass
messageManager = new MessageManager(); messageManager = new MessageManager();
componentMessageManager = new ComponentMessageManager(); componentMessageManager = new ComponentMessageManager();
entityManager = new EntityManager(componentManager, componentMessageManager); entityManager = new EntityManager(componentManager, componentMessageManager);
timeManager = new TimeManager();
renderManager = new RenderManager(componentManager, drawLayerManager, entityManager); renderManager = new RenderManager(componentManager, drawLayerManager, entityManager);
} }
@ -97,6 +98,7 @@ namespace Encompass
engine.AssignComponentManager(componentManager); engine.AssignComponentManager(componentManager);
engine.AssignMessageManager(messageManager); engine.AssignMessageManager(messageManager);
engine.AssignComponentMessageManager(componentMessageManager); engine.AssignComponentMessageManager(componentMessageManager);
engine.AssignTimeManager(timeManager);
engines.Add(engine); engines.Add(engine);
engineGraph.AddNode(engine); engineGraph.AddNode(engine);
@ -232,8 +234,27 @@ namespace Encompass
var writePriorities = new Dictionary<Type, HashSet<int>>(); var writePriorities = new Dictionary<Type, HashSet<int>>();
var writeMessageToEngines = new Dictionary<Type, List<Engine>>(); var writeMessageToEngines = new Dictionary<Type, List<Engine>>();
var timeDilationPriorities = new Dictionary<int, HashSet<Engine>>();
foreach (var engine in engines) foreach (var engine in engines)
{ {
var timeDilationPriorityAttribute = engine.GetType().GetCustomAttribute<TimeDilationPriority>();
if (timeDilationPriorityAttribute != null)
{
engine.timeDilationPriority = timeDilationPriorityAttribute.timeDilationPriority;
if (!timeDilationPriorities.ContainsKey(timeDilationPriorityAttribute.timeDilationPriority))
{
timeDilationPriorities.Add(timeDilationPriorityAttribute.timeDilationPriority, new HashSet<Engine>());
}
timeDilationPriorities[timeDilationPriorityAttribute.timeDilationPriority].Add(engine);
}
if (engine.GetType().GetCustomAttribute<IgnoresTimeDilation>() != null)
{
engine.usesTimeDilation = false;
}
var defaultWritePriorityAttribute = engine.GetType().GetCustomAttribute<DefaultWritePriority>(false); var defaultWritePriorityAttribute = engine.GetType().GetCustomAttribute<DefaultWritePriority>(false);
var writeTypes = engine.sendTypes.Where((type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ComponentWriteMessage<>)); var writeTypes = engine.sendTypes.Where((type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ComponentWriteMessage<>));
@ -323,6 +344,18 @@ namespace Encompass
throw new EngineWriteConflictException(errorString); 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<Engine>(); var engineOrder = new List<Engine>();
foreach (var engine in engineGraph.TopologicalSort()) foreach (var engine in engineGraph.TopologicalSort())
{ {
@ -335,6 +368,7 @@ namespace Encompass
componentManager, componentManager,
messageManager, messageManager,
componentMessageManager, componentMessageManager,
timeManager,
renderManager renderManager
); );

View File

@ -910,5 +910,152 @@ namespace Tests
resultComponents.Should().BeEmpty(); 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<TimeDilationPriorityUndefinedException>(() => 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<TimeDilationPriorityConflictException>(() => 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);
}
} }
} }

View File

@ -1,8 +1,4 @@
using System; using Encompass;
using System.Collections.Generic;
using System.Text;
using Encompass;
using Encompass.Engines;
using NUnit.Framework; using NUnit.Framework;
namespace Tests namespace Tests