A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4#nullable enable
5
6using osu.Framework.Extensions.TypeExtensions;
7using System;
8
9namespace osu.Framework.Timing
10{
11 /// <summary>
12 /// Takes a clock source and separates time reading on a per-frame level.
13 /// The CurrentTime value will only change on initial construction and whenever ProcessFrame is run.
14 /// </summary>
15 public class FramedClock : IFrameBasedClock, ISourceChangeableClock
16 {
17 public IClock Source { get; private set; }
18
19 /// <summary>
20 /// Construct a new FramedClock with an optional source clock.
21 /// </summary>
22 /// <param name="source">A source clock which will be used as the backing time source. If null, a StopwatchClock will be created. When provided, the CurrentTime of <paramref name="source"/> will be transferred instantly.</param>
23 /// <param name="processSource">Whether the source clock's <see cref="ProcessFrame"/> method should be called during this clock's process call.</param>
24 public FramedClock(IClock? source = null, bool processSource = true)
25 {
26 this.processSource = processSource;
27 Source = source ?? new StopwatchClock(true);
28
29 ChangeSource(Source);
30 }
31
32 public FrameTimeInfo TimeInfo => new FrameTimeInfo { Elapsed = ElapsedFrameTime, Current = CurrentTime };
33
34 public double FramesPerSecond { get; private set; }
35
36 public virtual double CurrentTime { get; protected set; }
37
38 protected virtual double LastFrameTime { get; set; }
39
40 public double Rate => Source.Rate;
41
42 protected double SourceTime => Source.CurrentTime;
43
44 public double ElapsedFrameTime => CurrentTime - LastFrameTime;
45
46 public bool IsRunning => Source.IsRunning;
47
48 private readonly bool processSource;
49
50 private double timeUntilNextCalculation;
51 private double timeSinceLastCalculation;
52 private int framesSinceLastCalculation;
53
54 private const int fps_calculation_interval = 250;
55
56 public void ChangeSource(IClock source)
57 {
58 CurrentTime = LastFrameTime = source.CurrentTime;
59 Source = source;
60 }
61
62 public virtual void ProcessFrame()
63 {
64 if (processSource && Source is IFrameBasedClock framedSource)
65 framedSource.ProcessFrame();
66
67 if (timeUntilNextCalculation <= 0)
68 {
69 timeUntilNextCalculation += fps_calculation_interval;
70
71 if (framesSinceLastCalculation == 0)
72 FramesPerSecond = 0;
73 else
74 FramesPerSecond = (int)Math.Ceiling(framesSinceLastCalculation * 1000f / timeSinceLastCalculation);
75 timeSinceLastCalculation = framesSinceLastCalculation = 0;
76 }
77
78 framesSinceLastCalculation++;
79 timeUntilNextCalculation -= ElapsedFrameTime;
80 timeSinceLastCalculation += ElapsedFrameTime;
81
82 LastFrameTime = CurrentTime;
83 CurrentTime = SourceTime;
84 }
85
86 public override string ToString() => $@"{GetType().ReadableName()} ({Math.Truncate(CurrentTime)}ms, {FramesPerSecond} FPS)";
87 }
88}