This class will fires a certain amount of ticks per second. I would greatly appreciate any suggentions, I'm more then welcome for any feedback since I haven't coded for long in C#.
It is intentional that if there is a heavy load on the ticks that the ticks per second don't match the desired input. It is more of a maximum were it should aim to run at, but because of that this class needs to be as performant as possible and with this we come to my first problem with this code:
I'm not really happy with the Thread.Sleep
in the Pulse
Method, which is my main concern, but could'nt figure out a better way. I needed it to reduce stress on the CPU on lower TPS..
Also the thing that this is using Time ticks to calculate the ticks for the engine makes it a little bit annouing to read but don't really know what to name it since both are ticks..
TickEngine.cs
using System;
using System.Threading.Tasks;
using System.Threading;
using System.Runtime.Serialization;
namespace TickEngine
{
class TickEngine
{
public event EventHandler<TickEventArgs> OnStart = null!;
public event EventHandler<TickEventArgs> OnPreTick = null!;
public event EventHandler<TickEventArgs> OnTick = null!;
public event EventHandler<TickEventArgs> OnPostTick = null!;
public event EventHandler<TickEventArgs> OnStop = null!;
private readonly int _resolution;
private long _maxTick;
private TimeSpan _delay;
private ulong _tickCnt = 0;
private float _deltaTickSum = 0;
private bool _isEnabled = false;
private long _timestamp = 0;
private uint _tps;
/// <summary>
/// Is the Engine running
/// </summary>
public bool IsEnabled { get { return _isEnabled; } }
/// <summary>
/// Total ticks that have been processed since engine was started
/// </summary>
public ulong Tick { get { return _tickCnt; } }
/// <summary>
/// Ticks per Second
/// </summary>
public uint TPS
{
get { return _tps; }
set
{
_tps = value;
_maxTick = TimeSpan.TicksPerSecond / _tps;
_delay = new TimeSpan(TimeSpan.TicksPerSecond / _tps / _resolution);
}
}
/// <summary>
/// Initialzise the Tick engine
/// </summary>
/// <param name="tps">How many Ticks per Second should the Engine process</param>
/// <param name="resolution">How many Checks per Tickinterval, higher number more CPU usage, but higher accuracy</param>
public TickEngine(uint tps, int resolution = 3)
{
_resolution = resolution;
TPS = tps;
}
/// <summary>
/// Start the tick engine, need to handle own pulse loop
/// </summary>
/// <exception cref="TickEngineException">If engine is already running</exception>
public void Start()
{
if (_isEnabled)
throw new TickEngineException("Tick engine is already running");
_tickCnt = 0;
_deltaTickSum = 0;
_timestamp = DateTime.Now.Ticks;
_isEnabled = true;
OnStart?.Invoke(this, new TickEventArgs { tick = _tickCnt });
}
/// <summary>
/// Start the tick engine with async pulse loop
/// </summary>
/// <exception cref="TickEngineException">If engine is already running</exception>
public void StartWithPulse()
{
if (_isEnabled)
throw new TickEngineException("Tick engine is already running");
Start();
Task.Run(() =>
{
while (_isEnabled)
{
Pulse();
}
});
}
/// <summary>
/// Perform a pulse on the engine if enough time has passed a tick will be fired
/// </summary>
public void Pulse()
{
Update();
Thread.Sleep(_delay);
}
/// <summary>
/// Stop tick engine
/// </summary>
/// <exception cref="TickEngineException">Tick engine is Not running</exception>
public void Stop()
{
if (!_isEnabled)
throw new TickEngineException("Tick engine is NOT running");
_isEnabled = false;
OnStop?.Invoke(this, new TickEventArgs { tick = _tickCnt });
}
/// <summary>
/// Update the timing and check if a tick(engine) will be fired.
/// </summary>
private void Update()
{
_deltaTickSum += DeltaTick();
if (_deltaTickSum >= _maxTick)
{
_deltaTickSum -= _maxTick;
_tickCnt++;
TickEventArgs args = new TickEventArgs { tick = _tickCnt };
OnPreTick?.Invoke(this, args);
OnTick?.Invoke(this, args);
OnPostTick?.Invoke(this, args);
}
}
/// <summary>
/// Calculate ticks(time) that have passed
/// </summary>
/// <returns>Returns the ticks(time) that have passed since last call</returns>
private long DeltaTick()
{
long a = DateTime.Now.Ticks;
long delta = a - _timestamp;
_timestamp = a;
return delta;
}
public class TickEventArgs : EventArgs
{
public ulong tick;
}
[Serializable]
public class TickEngineException : Exception
{
public TickEngineException()
: base()
{ }
public TickEngineException(string message)
: base(message)
{ }
protected TickEngineException(SerializationInfo info, StreamingContext ctxt)
: base(info, ctxt)
{ }
}
}
}
Example Program.cs
using System;
using System.Diagnostics;
namespace TickEngineExample // Note: actual namespace depends on the project name.
{
internal class Program
{
static Stopwatch asyncStopwatch = new Stopwatch();
static Stopwatch syncStopwatch = new Stopwatch();
static readonly uint tps = 50;
static readonly uint endTick = tps * 10;
static void Main(string[] args)
{
AsyncExample();
SyncExample();
Console.ReadLine();
}
static void SyncExample()
{
//Creates a Tick Engine that fires 50 ticks per Second
TickEngine.TickEngine engine = new TickEngine.TickEngine(tps);
//Set Event
engine.OnStart += (sender, e) =>
{
Console.WriteLine("[Sync] Started");
syncStopwatch.Start();
};
engine.OnTick += (sender, e) =>
{
Console.WriteLine("[Sync] Tick: " + e.tick);
};
engine.OnPostTick += (sender, e) =>
{
if (e.tick == endTick)
engine.Stop();
};
engine.OnStop += (sender, e) =>
{
syncStopwatch.Stop();
Console.WriteLine("[Sync] Stopped after " + syncStopwatch.Elapsed.TotalSeconds + " Seconds");
};
engine.Start();
while (engine.IsEnabled)
{
engine.Pulse();
}
}
static void AsyncExample()
{
asyncStopwatch.Start();
//Creates a Tick Engine that fires 50 ticks per Second
TickEngine.TickEngine engine = new TickEngine.TickEngine(tps);
engine.OnStart += (sender, e) =>
{
Console.WriteLine("[Async] Started");
asyncStopwatch.Start();
};
engine.OnTick += (sender, e) =>
{
Console.WriteLine("[Async] Tick: " + e.tick);
};
engine.OnPostTick += (sender, e) =>
{
if (e.tick == endTick)
engine.Stop();
};
engine.OnStop += (sender, e) =>
{
asyncStopwatch.Stop();
Console.WriteLine("[Async] Stopped after " + asyncStopwatch.Elapsed.TotalSeconds + " Seconds");
};
engine.StartWithPulse();
}
}
}
```
Stopwatch
class to me, why didn't you use it instead? \$\endgroup\$