Implementação de Gerenciador de Temporizadores no Framework ET

Visão Geral da Arquitetura

O sistema de temporizadores desenvolvido para o framework ET segue uma arquitetura em camadas composta por três elementos principais: a entidade temporizadora, o componente gerenciador e a fábrica de criação. Esta separação de responsabilidades permite controle granular sobre execuções agendadas com suporte a pausas, múltiplas repetições e consulta de estado.

Entidade Temporizadora

A classe ChronosEntity representa uma unidade de temporização independente, encapsulando toda a lógica de ciclo e estado:

using System;

namespace ETModel
{
    [ObjectSystem]
    public class ChronosAwakeSystem : AwakeSystem<ChronosEntity, float, uint, Action>
    {
        public override void Awake(ChronosEntity self, float tickSpan, uint repetitions, Action callback)
        {
            self.Initialize(tickSpan, repetitions, callback);
        }
    }

    public sealed class ChronosEntity : Entity
    {
        public const uint PERPETUAL = uint.MaxValue;

        private float _tickSpan = 0f;
        private uint _maxRepetitions = 1;
        private uint _finishedRepetitions = 0;
        private Action _userCallback = null;
        private Action _wrappedCallback = null;
        
        private bool _isFrozen = false;
        private float _accumulatedSpan = 0f;
        private float _currentCycleProgress = 0f;
        private long _lastSyncTicks = 0;

        public void Initialize(float tickSpan, uint repetitions, Action callback)
        {
            _tickSpan = Math.Max(tickSpan, 0f);
            _maxRepetitions = Math.Max(repetitions, 1u);
            _userCallback = callback;
            _lastSyncTicks = DateTime.UtcNow.Ticks;
        }

        public override void Dispose()
        {
            if (this.IsDisposed) return;
            _userCallback = null;
            _wrappedCallback = null;
            base.Dispose();
        }

        internal void SynchronizeCallback()
        {
            if (_maxRepetitions == PERPETUAL)
                _wrappedCallback = () => _userCallback?.Invoke();
            else
                _wrappedCallback = _userCallback;
        }

        internal void Tick()
        {
            if (_isFrozen) return;
            if (_wrappedCallback == null || _tickSpan <= 0f) return;

            if (_finishedRepetitions >= _maxRepetitions && _maxRepetitions != PERPETUAL)
            {
                _accumulatedSpan = _tickSpan * _maxRepetitions;
                _currentCycleProgress = _tickSpan;
                return;
            }

            long nowTicks = DateTime.UtcNow.Ticks;
            float delta = (nowTicks - _lastSyncTicks) / 10_000_000f;
            _lastSyncTicks = nowTicks;
            
            _accumulatedSpan += delta;
            _currentCycleProgress = _accumulatedSpan - (_finishedRepetitions * _tickSpan);

            while (_currentCycleProgress >= _tickSpan && 
                   (_finishedRepetitions < _maxRepetitions || _maxRepetitions == PERPETUAL))
            {
                _currentCycleProgress -= _tickSpan;
                _finishedRepetitions++;
                _wrappedCallback?.Invoke();
            }
        }

        public float TickSpan() => _tickSpan;
        public uint MaxRepetitions() => _maxRepetitions;
        public uint CompletedRepetitions() => _finishedRepetitions;
        public uint PendingRepetitions() => _maxRepetitions - _finishedRepetitions;
        
        public float TotalDuration() => 
            (_maxRepetitions == PERPETUAL) ? float.PositiveInfinity : _maxRepetitions * _tickSpan;
        
        public float ElapsedDuration() => _accumulatedSpan;
        public float RemainingDuration() => 
            (_maxRepetitions == PERPETUAL) ? float.PositiveInfinity : 
            Math.Max(TotalDuration() - _accumulatedSpan, 0f);
        
        public float CycleProgress() => _currentCycleProgress;
        public float CycleRemaining() => Math.Max(_tickSpan - _currentCycleProgress, 0f);
        
        public bool IsExhausted() => _wrappedCallback == null || RemainingDuration() <= 0f;
        public bool IsFrozen() => _isFrozen;
        public void SetFrozen(bool state) => _isFrozen = state;

        public static bool operator >(ChronosEntity a, ChronosEntity b) => 
            a?._tickSpan < b?._tickSpan;
        public static bool operator <(ChronosEntity a, ChronosEntity b) => 
            a?._tickSpan > b?._tickSpan;
    }
}

Componente Gerenciador

O ChronosCoordinator atua como singleton responsável pela atualização centralizada de todas as entidades temporizadoras ativas:

using System;
using System.Collections.Generic;
using System.Linq;

namespace ETModel
{
    [ObjectSystem]
    public class CoordinatorAwakeSystem : AwakeSystem<ChronosCoordinator>
    {
        public override void Awake(ChronosCoordinator self) => self.Bootstrap();
    }

    [ObjectSystem]
    public class CoordinatorUpdateSystem : UpdateSystem<ChronosCoordinator>
    {
        public override void Update(ChronosCoordinator self) => self.ProcessFrame();
    }

    public class ChronosCoordinator : Component
    {
        public static ChronosCoordinator ActiveInstance { get; private set; }
        
        private readonly List<ChronosEntity> _activeTimers = new List<ChronosEntity>();
        private bool _globalFreeze = false;

        public void Bootstrap()
        {
            ActiveInstance = this;
        }

        public void ProcessFrame()
        {
            if (_globalFreeze) return;

            foreach (var timer in _activeTimers.ToArray())
            {
                timer.Tick();
                if (timer.IsExhausted() || timer.IsDisposed)
                {
                    _activeTimers.Remove(timer);
                }
            }
        }

        public void Register(ChronosEntity timer)
        {
            if (timer != null && !timer.IsDisposed)
                _activeTimers.Add(timer);
        }

        public ChronosEntity FindByCallback(Action callback)
        {
            return _activeTimers.FirstOrDefault(t => 
                ReferenceEquals(t.GetType().GetField("_userCallback")?.GetValue(t), callback));
        }

        public void Unregister(ChronosEntity timer)
        {
            _activeTimers.Remove(timer);
            timer?.Dispose();
        }

        public void Unregister(Action callback)
        {
            var target = FindByCallback(callback);
            if (target != null) Unregister(target);
        }

        public int ActiveCount => _activeTimers.Count;

        public override void Dispose()
        {
            if (IsDisposed) return;
            base.Dispose();
            
            foreach (var timer in _activeTimers)
                timer.Dispose();
            _activeTimers.Clear();
            ActiveInstance = null;
        }

        // Métodos de consulta por callback
        public float? QueryTickSpan(Action cb) => FindByCallback(cb)?.TickSpan();
        public uint? QueryRemainingRepetitions(Action cb) => FindByCallback(cb)?.PendingRepetitions();
        public float? QueryRemainingTime(Action cb) => FindByCallback(cb)?.RemainingDuration();
        public bool QueryIsFrozen(Action cb) => FindByCallback(cb)?.IsFrozen() ?? false;
        
        public void SetFreezeState(Action cb, bool freeze)
        {
            var timer = FindByCallback(cb);
            if (timer != null) timer.SetFrozen(freeze);
        }
    }
}

Fábrica de Instanciação

A classe estática ChronosBuilder simplifica a criação e registro automáticco de temporizadores:

using System;

namespace ETModel
{
    public static class ChronosBuilder
    {
        public static ChronosEntity Schedule(float interval, uint repetitions, Action callback)
        {
            var coordinator = Game.Scene.GetComponent<ChronosCoordinator>();
            if (coordinator == null) return null;

            var timer = ComponentFactory.Create<ChronosEntity, float, uint, Action>(
                interval, repetitions, callback);
            
            timer.SynchronizeCallback();
            coordinator.Register(timer);
            return timer;
        }

        public static ChronosEntity ScheduleOnce(float delay, Action callback) => 
            Schedule(delay, 1, callback);

        public static ChronosEntity SchedulePerpetual(float interval, Action callback) => 
            Schedule(interval, ChronosEntity.PERPETUAL, callback);
    }
}

Exemplos de Utilização

// Temporizador único após 2 segundos
ChronosBuilder.ScheduleOnce(2.0f, () => {
    Log.Info("Execução única completada");
});

// Temporizador repetitivo 5 vezes a cada 0.5 segundos
ChronosBuilder.Schedule(0.5f, 5, () => {
    Log.Info("Ciclo executado");
});

// Temporizador infinito com controle de pausa
var perpetual = ChronosBuilder.SchedulePerpetual(1.0f, () => {
    Log.Info("Heartbeat");
});

// Pausar temporizador existente
ChronosCoordinator.ActiveInstance?.SetFreezeState(perpetual.GetType()
    .GetField("_userCallback")?.GetValue(perpetual) as Action, true);

Tags: ET Framework Game Development C# Entity Component System Timer Management

Publicado em 8-7 19:33