using System; using UnityEngine; namespace Unity.VisualScripting { /// /// Runs a timer and outputs elapsed and remaining measurements. /// [UnitCategory("Time")] [UnitOrder(7)] public sealed class Timer : Unit, IGraphElementWithData, IGraphEventListener { public sealed class Data : IGraphElementData { public float elapsed; public float duration; public bool active; public bool paused; public bool unscaled; public Delegate update; public bool isListening; } /// /// The moment at which to start the timer. /// If the timer is already started, this will reset it. /// If the timer is paused, this will resume it. /// [DoNotSerialize] public ControlInput start { get; private set; } /// /// Trigger to pause the timer. /// [DoNotSerialize] public ControlInput pause { get; private set; } /// /// Trigger to resume the timer. /// [DoNotSerialize] public ControlInput resume { get; private set; } /// /// Trigger to toggle the timer. /// If it is idle, it will start. /// If it is active, it will pause. /// If it is paused, it will resume. /// [DoNotSerialize] public ControlInput toggle { get; private set; } /// /// The total duration of the timer. /// [DoNotSerialize] public ValueInput duration { get; private set; } /// /// Whether to ignore the time scale. /// [DoNotSerialize] [PortLabel("Unscaled")] public ValueInput unscaledTime { get; private set; } /// /// Called when the timer is started.co /// [DoNotSerialize] public ControlOutput started { get; private set; } /// /// Called each frame while the timer is active. /// [DoNotSerialize] public ControlOutput tick { get; private set; } /// /// Called when the timer completes. /// [DoNotSerialize] public ControlOutput completed { get; private set; } /// /// The number of seconds elapsed since the timer started. /// [DoNotSerialize] [PortLabel("Elapsed")] public ValueOutput elapsedSeconds { get; private set; } /// /// The proportion of the duration that has elapsed (0-1). /// [DoNotSerialize] [PortLabel("Elapsed %")] public ValueOutput elapsedRatio { get; private set; } /// /// The number of seconds remaining until the timer is elapsed. /// [DoNotSerialize] [PortLabel("Remaining")] public ValueOutput remainingSeconds { get; private set; } /// /// The proportion of the duration remaining until the timer is elapsed (0-1). /// [DoNotSerialize] [PortLabel("Remaining %")] public ValueOutput remainingRatio { get; private set; } protected override void Definition() { isControlRoot = true; start = ControlInput(nameof(start), Start); pause = ControlInput(nameof(pause), Pause); resume = ControlInput(nameof(resume), Resume); toggle = ControlInput(nameof(toggle), Toggle); duration = ValueInput(nameof(duration), 1f); unscaledTime = ValueInput(nameof(unscaledTime), false); started = ControlOutput(nameof(started)); tick = ControlOutput(nameof(tick)); completed = ControlOutput(nameof(completed)); elapsedSeconds = ValueOutput(nameof(elapsedSeconds)); elapsedRatio = ValueOutput(nameof(elapsedRatio)); remainingSeconds = ValueOutput(nameof(remainingSeconds)); remainingRatio = ValueOutput(nameof(remainingRatio)); } public IGraphElementData CreateData() { return new Data(); } public void StartListening(GraphStack stack) { var data = stack.GetElementData(this); if (data.isListening) { return; } var reference = stack.ToReference(); var hook = new EventHook(EventHooks.Update, stack.machine); Action update = args => TriggerUpdate(reference); EventBus.Register(hook, update); data.update = update; data.isListening = true; } public void StopListening(GraphStack stack) { var data = stack.GetElementData(this); if (!data.isListening) { return; } var hook = new EventHook(EventHooks.Update, stack.machine); EventBus.Unregister(hook, data.update); data.update = null; data.isListening = false; } public bool IsListening(GraphPointer pointer) { return pointer.GetElementData(this).isListening; } private void TriggerUpdate(GraphReference reference) { using (var flow = Flow.New(reference)) { Update(flow); } } private ControlOutput Start(Flow flow) { var data = flow.stack.GetElementData(this); data.elapsed = 0; data.duration = flow.GetValue(duration); data.active = true; data.paused = false; data.unscaled = flow.GetValue(unscaledTime); AssignMetrics(flow, data); return started; } private ControlOutput Pause(Flow flow) { var data = flow.stack.GetElementData(this); data.paused = true; return null; } private ControlOutput Resume(Flow flow) { var data = flow.stack.GetElementData(this); data.paused = false; return null; } private ControlOutput Toggle(Flow flow) { var data = flow.stack.GetElementData(this); if (!data.active) { return Start(flow); } else { data.paused = !data.paused; return null; } } private void AssignMetrics(Flow flow, Data data) { flow.SetValue(elapsedSeconds, data.elapsed); flow.SetValue(elapsedRatio, Mathf.Clamp01(data.elapsed / data.duration)); flow.SetValue(remainingSeconds, Mathf.Max(0, data.duration - data.elapsed)); flow.SetValue(remainingRatio, Mathf.Clamp01((data.duration - data.elapsed) / data.duration)); } public void Update(Flow flow) { var data = flow.stack.GetElementData(this); if (!data.active || data.paused) { return; } data.elapsed += data.unscaled ? Time.unscaledDeltaTime : Time.deltaTime; data.elapsed = Mathf.Min(data.elapsed, data.duration); AssignMetrics(flow, data); var stack = flow.PreserveStack(); flow.Invoke(tick); if (data.elapsed >= data.duration) { data.active = false; flow.RestoreStack(stack); flow.Invoke(completed); } flow.DisposePreservedStack(stack); } } }