/** * Timer class that generates a signal after a set interval, with an option * to generate recurring signals. * Copyright 2021 Anton Tarasenko *------------------------------------------------------------------------------ * This file is part of Acedia. * * Acedia is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, version 3 of the License, or * (at your option) any later version. * * Acedia is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with Acedia. If not, see . */ class Timer extends AcediaObject; /** * Because the `Timer` depends on the `Tick()` event, it has the same * resolution as a server's tick rate (frame rate for clients). This means that * the `OnElapsed()` signal will be emitted at an interval defined by the * `Tick()`. If the set interval between two signals is less than that, then * `Timer` might emit several signals at the same point of time. * Supposing server's tick rate is 30, but `Timer` is set to emit a signal * 60 time per second, then on average it will emit two signals each tick at * the same exact time. */ // Is timer currently tracking time until the next event? var private bool isTimerEnabled; // Should timer automatically reset after the next event to // also generate recurring signals? var private bool isTimerAutoReset; // Currently elapsed time since this timer has started waiting for the // next event var private float totalElapsedTime; // Time interval between timer's start and generating the signal var private float eventInterval; // This flag tells `Timer` to stop trying to emit messages that accumulated // between two `Tick()` updates. Used in case `Timer` was disables or // stopped during one of them. var private bool clearEventQueue; var private Timer_OnElapsed_Signal onElapsedSignal; protected function Constructor() { onElapsedSignal = Timer_OnElapsed_Signal( _.memory.Allocate(class'Timer_OnElapsed_Signal')); } protected function Finalizer() { _.memory.Free(onElapsedSignal); StopMe(); // Disconnects from listening to `_.unreal.OnTick()` } /** * Signal that will be emitted every time timer's interval is elapsed. * * [Signature] * void (Timer source) * * @param source `Timer` that emitted the signal. */ /* SIGNAL */ public final function Timer_OnElapsed_Slot OnElapsed(AcediaObject receiver) { return Timer_OnElapsed_Slot(onElapsedSignal.NewSlot(receiver)); } /** * This method is called every tick while the caller `Timer` is running and * can be overloaded to modify how passed time is affected by the * time dilation. * * For example, to make caller `Timer` count real time you need to passed time * by the `dilationCoefficient`: * `return timeDelta / dilationCoefficient;`. * * @param timeDelta In-game time that has passed since the last * `Tick()` event. To obtain real time should be divided by * `dilationCoefficient`. * @param dilationCoefficient Coefficient of time dilation for the passed * `Tick()`. Regular speed is `1.0` (corrected from native value `1.1` * for Unreal Engine 2). */ protected function float HandleTimeDilation( float timeDelta, float dilationCoefficient) { return timeDelta; } /** * Returns current interval between `OnElapsed()` signals for the * caller `Timer`. In seconds. * * @return How many seconds separate two `OnElapsed()` signals * (or starting a timer and next `OnElapsed()` event). */ public final function float GetInterval() { return eventInterval; } /** * Sets current interval between `OnElapsed()` signals for the * caller `Timer`. In seconds. * * Setting this value while the caller `Timer` is running resets it (same as * calling `StopMe().Start()`). * * @param newInterval How many seconds should separate two `OnElapsed()` * signals (or starting a timer and next `OnElapsed()` event)? * Setting a value `<= 0` disables the timer. * @return Caller `Timer` to allow for method chaining. */ public final function Timer SetInterval(float newInterval) { eventInterval = newInterval; if (eventInterval <= 0) { StopMe(); return self; } if (isTimerEnabled) { Start(); } return self; } /** * Checks whether the timer is currently enabled (emitting signals with * set interval). * * @return `true` if caller `Timer` is enabled and `false` otherwise. */ public final function bool IsEnabled() { return isTimerEnabled; } /** * Checks whether this `Timer` would automatically reset after the emitted * `OnElapsed()` signal, allowing for recurring signals. * * @return `true` if `Timer` will emit `OnElapse()` signal each time * the interval elapses and `false` otherwise. */ public final function bool IsAutoReset(float newInterval) { return isTimerAutoReset; } /** * Sets whether this `Timer` would automatically reset after the emitted * `OnElapsed()` signal, allowing for recurring signals. * * @param doAutoReset `true` if `Timer` will emit `OnElapse()` signal * each time the interval elapses and `false` otherwise. * @return Caller `Timer` to allow for method chaining. */ public final function Timer SetAutoReset(bool doAutoReset) { isTimerAutoReset = doAutoReset; return self; } /** * Starts emitting `OneElapsed()` signal. * * Does nothing if current timer interval (set by `SetInterval()`) is set * to a value that's `<= 0`. * * If caller `Timer` is already running, resets it (same as calling * `StopMe().Start()`). * * @return Caller `Timer` to allow for method chaining. */ public final function Timer Start() { if (eventInterval <= 0) { return self; } if (!isTimerEnabled) { _.unreal.OnTick(self).connect = Tick; } isTimerEnabled = true; totalElapsedTime = 0.0; return self; } /** * Stops emitting `OneElapsed()` signal. * * @return Caller `Timer` to allow for method chaining. */ public final function Timer StopMe() { _.unreal.OnTick(self).Disconnect(); isTimerEnabled = false; clearEventQueue = true; return self; } /** * Returns currently elapsed time since caller `Timer` has started waiting for * the next event. * * @return Elapsed time since caller `Timer` has started. */ public final function float GetElapsedTime() { return totalElapsedTime; } private final function Tick(float delta, float dilationCoefficient) { local int lifeVersion; if (onElapsedSignal == none || eventInterval <= 0.0) { StopMe(); return; } totalElapsedTime += HandleTimeDilation(delta, dilationCoefficient); clearEventQueue = false; while (totalElapsedTime > eventInterval && !clearEventQueue) { // It is important to modify _before_ the signal call in case `Timer` // is reset there and already has a zeroed `totalElapsedTime` totalElapsedTime -= eventInterval; // Stop `Timer` before emitting a signal, to allow user to potentially // restart it if (!isTimerAutoReset) { StopMe(); } // During signal emission caller `Timer` can get reallocated and // used to perform a completely different role. // In such a case we need to bail from this method as soom as // possible. lifeVersion = GetLifeVersion(); onElapsedSignal.Emit(self); if (!isTimerEnabled || lifeVersion != GetLifeVersion()) { return; } } } defaultproperties { }