The Battle for Wesnoth  1.19.25+dev
animated.hpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2004 - 2025
3  by Philippe Plantier <ayin@anathas.org>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
16 /** @file animated.hpp
17  * Frame-based animation system with support for looping, acceleration, and time manipulation.
18  * The animation system uses two parallel timelines:
19  * - Normal timeline: Real wall-clock time for UI animations, effects, etc.
20  * - Accelerated timeline: Can be sped up/slowed down for unit movement, attacks, etc. */
21 
22 #pragma once
23 
24 #include <algorithm>
25 #include <chrono>
26 #include <vector>
27 
28 // ============================================================================
29 // Global Animation Timeline Management
30 // ============================================================================
31 
32 /** Updates both animation timelines.
33  * Should be called once per frame before advancing any animations.
34  * @param acceleration Multiplier for the accelerated timeline (1.0 = normal speed) */
35 void update_animation_timers(double acceleration = 1.0);
36 
37 /** Gets the current time point for animations.
38  * @param uses_acceleration If true, returns accelerated timeline; otherwise normal timeline
39  * @return Current time point on the selected timeline */
40 std::chrono::steady_clock::time_point get_current_animation_tick(bool uses_acceleration);
41 
42 
43 // ============================================================================
44 // Frame-Based Animation Template
45 // ============================================================================
46 
47 template<typename T>
48 class animated
49 {
50 public:
51  typedef std::pair<std::chrono::milliseconds, T> frame_description;
52  typedef std::vector<frame_description> anim_description;
53 
54  // ========================================================================
55  // Construction
56  // ========================================================================
57 
58  /** Creates an animation with no frames; add_frame() populates it afterward.
59  * @param start_time Logical start time of the animation timeline (default 0ms) */
60  animated(const std::chrono::milliseconds& start_time = std::chrono::milliseconds{0});
61 
62  /** Creates an animation with its frame list already built from cfg.
63  * @param cfg Sequence of (duration, value) pairs to build the frame list from
64  * @param start_time Logical start time of the animation timeline
65  * @param force_change See add_frame(). Applies to the whole sequence built from cfg. */
66  explicit animated(const anim_description& cfg,
67  const std::chrono::milliseconds& start_time = std::chrono::milliseconds{0},
68  bool force_change = false);
69 
70  virtual ~animated() = default;
71 
72  /** Appends a frame, starting where the previous one ends (or at start_time, if first).
73  * @param duration How long this frame should be displayed
74  * @param value The data/state for this frame
75  * @param force_change Forces is_static() false even if this is the only frame added so
76  * far; use when the frame's own value can change over time despite the frame count. */
77  void add_frame(const std::chrono::milliseconds& duration, const T& value, bool force_change = false);
78 
79  // ========================================================================
80  // Playback Control
81  // ========================================================================
82 
83  /** Anchors the timeline to the current tick, then begins playback.
84  * @param start_time_offset How far into the animation to start (0 = from beginning)
85  * @param cycles If true, loop back to the first frame on reaching the end; if false, stop there */
86  void start_animation(const std::chrono::milliseconds& start_time_offset, bool cycles = false);
87 
88  /** Freezes the timeline in place. Playback continues from the same point via resume_animation(). */
90 
91  /** Shifts the timeline forward by the time spent paused, then continues playback. */
93 
94  /** Moves current_frame_index_ forward to match the current tick, one frame at a time
95  * (or in whole cycles, if far enough behind). Marks the animation finished() if it
96  * runs out of frames without cycles(). */
98 
99  // ========================================================================
100  // Playback State Queries
101  // ========================================================================
102 
103  /** True once the current tick has reached the current frame's end, meaning
104  * advance_to_current_frame() has a new frame to move to. */
105  bool need_update() const;
106 
107  /** True if playback has run past the last frame, is not yet started, or has no frames.
108  * Always true for a cycling animation, since it never runs out. */
109  bool animation_finished() const;
110 
111  /** True if the animation loops back to the first frame instead of stopping at the last. */
112  bool cycles() const { return cycles_; }
113 
114  // ========================================================================
115  // Time Queries and Manipulation
116  // ========================================================================
117 
118  /** Time since playback began, per get_playback_tick(). 0 if not started.
119  * Capped at the limit set by set_duration_limit(), if any. */
120  std::chrono::milliseconds get_elapsed_time() const;
121 
122  /** Shifts the timeline so the current tick corresponds to the given animation time,
123  * then restarts frame tracking from the beginning to resync.
124  * Used to desynchronize otherwise-identical animations for visual variety.
125  * @param time e.g. 300ms starts the animation as if it had been playing for 300ms already */
126  void apply_time_offset(const std::chrono::milliseconds& time);
127 
128  /** @param time Elapsed time past which animation_finished() reports true regardless of
129  * actual progress (0 = no limit). Does not affect frame playback, only that query. */
130  void set_duration_limit(const std::chrono::milliseconds& time);
131 
132  /** get_end_time() minus get_begin_time(): how long one pass through all frames takes. */
133  std::chrono::milliseconds get_animation_duration() const;
134 
135  /** Animation-time offset of the first frame's start. Default 0ms; see set_begin_time(). */
136  std::chrono::milliseconds get_begin_time() const;
137 
138  /** Animation-time offset where the last frame ends.
139  * For 3 seconds of frames starting at 500ms (see get_begin_time()), this returns 3500ms. */
140  std::chrono::milliseconds get_end_time() const;
141 
142  /** Rebases the timeline onto a new begin time, offsetting every frame's start_time_
143  * by the difference so their durations and order are unchanged. */
144  void set_begin_time(const std::chrono::milliseconds& new_begin_time);
145 
146  // ========================================================================
147  // Frame Queries
148  // ========================================================================
149 
150  /** Animation-time offset where the current frame starts. */
151  std::chrono::milliseconds get_current_frame_begin_time() const;
152 
153  /** Animation-time offset where the current frame ends. */
154  std::chrono::milliseconds get_current_frame_end_time() const;
155 
156  /** How far into its own duration the current frame is, clamped to [0, duration]. */
157  std::chrono::milliseconds get_time_in_current_frame() const;
158 
159  const T& get_current_frame() const;
160  const T& get_first_frame() const;
161  const T& get_frame(std::size_t n) const;
162  const T& get_last_frame() const;
163 
164  std::size_t get_frames_count() const;
165 
166  // ========================================================================
167  // Optimization and Configuration
168  // ========================================================================
169 
170  /** Clears the flag returned by is_static(). Needed because is_static_ is set purely from
171  * frame count: a single frame can still change visually over time through means this
172  * class doesn't track (e.g. unit_frame's interpolated offset/alpha/image parameters),
173  * in which case the owner must call this to keep need_update() reporting true. */
174  void force_change() { is_static_ = false; }
175 
176  /** True when the single-unchanging-frame optimization is enabled: need_update() will
177  * always report false, since there is never a different frame to advance to.
178  * Set from add_frame()/the constructor when the animation has exactly one frame, unless
179  * force_change requests otherwise; cleared permanently by add_frame() past the first frame,
180  * or by force_change(). */
181  bool is_static() const { return is_static_; }
182 
183  /** Selects which of the two global timelines get_current_animation_tick()
184  * reads for this animation: accelerated if true, normal if false. */
185  void set_uses_acceleration(bool uses_accel) { uses_acceleration_ = uses_accel; }
186 
187  /** Which timeline this animation currently reads; see set_uses_acceleration(). */
188  bool uses_acceleration() const { return uses_acceleration_; }
189 
190  static const T void_value_; // MSVC: the frame constructor below requires this to be public
191 
192 protected:
193  friend class unit_animation;
194 
195  /** Drops whole frames from the front while their end time is still before starting_time,
196  * then advances the begin time by their durations. May overshoot starting_time if it
197  * falls inside a remaining frame, rather than split that frame.
198  * @param starting_time Animation time the new begin time should reach */
199  void remove_frames_until(const std::chrono::milliseconds& starting_time);
200 
201  /** Reduces or extends the frame list so get_end_time() becomes ending_time: drops frames
202  * after the cut and shortens the one it falls in, extends the last frame if ending_time is
203  * later, or (if ending_time is before the current begin time) collapses to a zero-length
204  * first frame.
205  * @param ending_time Animation time the new end time should reach */
206  void set_end_time(const std::chrono::milliseconds& ending_time);
207 
208 private:
209  /** The tick get_elapsed_time() and get_time_in_current_frame() treat as "now": frozen at
210  * pause_tick_ while paused_, otherwise the live tick from get_current_animation_tick().
211  * Playback bookkeeping (start/pause/resume/advance) reads get_current_animation_tick()
212  * directly instead, since it needs the live clock even while paused_. */
213  std::chrono::steady_clock::time_point get_playback_tick() const;
214 
215  // Internal frame representation
216  struct frame
217  {
218  std::chrono::milliseconds duration_; // How long this frame lasts
219  T value_; // The frame data
220  std::chrono::milliseconds start_time_; // When this frame starts (in animation time)
221  };
222 
223  // Animation timeline configuration
224  std::chrono::milliseconds starting_frame_time_; // Logical start time of the animation
225  std::chrono::milliseconds max_animation_time_; // Optional duration cap (0 = unlimited)
226 
227  // Frame storage and tracking
228  std::vector<frame> frames_; // All frames in sequence
229  std::size_t current_frame_index_; // Which frame is currently displayed
230 
231  // Playback state
232  bool started_; // Whether values are initialized by start_animation()
233  bool paused_; // True while frozen at pause_tick_; see get_playback_tick()
234  bool finished_; // Set by advance_to_current_frame() when a non-cycling animation runs out of frames
235  bool cycles_; // Whether the animation loops/cycles
236  bool uses_acceleration_; // Which timeline to follow, normal or accelerated.
237 
238  // Timeline synchronization
239  // The animation's position is tracked by relating animation time to real timeline ticks.
240  std::chrono::steady_clock::time_point animation_start_tick_; // Real tick corresponding to animation time 0 (can be adjusted).
241  std::chrono::steady_clock::time_point next_frame_tick_; // Real tick when next frame should display
242  std::chrono::steady_clock::time_point pause_tick_; // Real tick when pause_animation() was called
243 
244  // Optimization flag
245  bool is_static_; // True if all frames identical (skip updates)
246 };
247 
248 // NOTE: this needs to be down here or the templates won't build.
249 #include "animated.tpp"
void update_animation_timers(double acceleration=1.0)
Updates both animation timelines.
Definition: animated.cpp:38
std::chrono::steady_clock::time_point get_current_animation_tick(bool uses_acceleration)
Gets the current time point for animations.
Definition: animated.cpp:49
bool is_static() const
True when the single-unchanging-frame optimization is enabled: need_update() will always report false...
Definition: animated.hpp:181
std::chrono::milliseconds get_time_in_current_frame() const
How far into its own duration the current frame is, clamped to [0, duration].
const T & get_first_frame() const
const T & get_frame(std::size_t n) const
animated(const anim_description &cfg, const std::chrono::milliseconds &start_time=std::chrono::milliseconds{0}, bool force_change=false)
Creates an animation with its frame list already built from cfg.
std::vector< frame > frames_
Definition: animated.hpp:228
bool uses_acceleration() const
Which timeline this animation currently reads; see set_uses_acceleration().
Definition: animated.hpp:188
void resume_animation()
Shifts the timeline forward by the time spent paused, then continues playback.
std::chrono::milliseconds get_begin_time() const
Animation-time offset of the first frame's start.
std::chrono::milliseconds get_current_frame_begin_time() const
Animation-time offset where the current frame starts.
virtual ~animated()=default
std::chrono::milliseconds max_animation_time_
Definition: animated.hpp:225
bool uses_acceleration_
Definition: animated.hpp:236
std::pair< std::chrono::milliseconds, T > frame_description
Definition: animated.hpp:51
void set_uses_acceleration(bool uses_accel)
Selects which of the two global timelines get_current_animation_tick() reads for this animation: acce...
Definition: animated.hpp:185
bool started_
Definition: animated.hpp:232
std::chrono::steady_clock::time_point next_frame_tick_
Definition: animated.hpp:241
void add_frame(const std::chrono::milliseconds &duration, const T &value, bool force_change=false)
Appends a frame, starting where the previous one ends (or at start_time, if first).
std::size_t current_frame_index_
Definition: animated.hpp:229
void apply_time_offset(const std::chrono::milliseconds &time)
Shifts the timeline so the current tick corresponds to the given animation time, then restarts frame ...
void pause_animation()
Freezes the timeline in place.
bool is_static_
Definition: animated.hpp:245
std::vector< frame_description > anim_description
Definition: animated.hpp:52
void start_animation(const std::chrono::milliseconds &start_time_offset, bool cycles=false)
Anchors the timeline to the current tick, then begins playback.
std::chrono::steady_clock::time_point get_playback_tick() const
The tick get_elapsed_time() and get_time_in_current_frame() treat as "now": frozen at pause_tick_ whi...
void set_end_time(const std::chrono::milliseconds &ending_time)
Reduces or extends the frame list so get_end_time() becomes ending_time: drops frames after the cut a...
const T & get_last_frame() const
void force_change()
Clears the flag returned by is_static().
Definition: animated.hpp:174
std::chrono::steady_clock::time_point pause_tick_
Definition: animated.hpp:242
bool finished_
Definition: animated.hpp:234
bool need_update() const
True once the current tick has reached the current frame's end, meaning advance_to_current_frame() ha...
bool animation_finished() const
True if playback has run past the last frame, is not yet started, or has no frames.
std::chrono::milliseconds get_elapsed_time() const
Time since playback began, per get_playback_tick().
std::chrono::milliseconds starting_frame_time_
Definition: animated.hpp:224
void remove_frames_until(const std::chrono::milliseconds &starting_time)
Drops whole frames from the front while their end time is still before starting_time,...
bool cycles_
Definition: animated.hpp:235
const T & get_current_frame() const
bool paused_
Definition: animated.hpp:233
std::size_t get_frames_count() const
void set_begin_time(const std::chrono::milliseconds &new_begin_time)
Rebases the timeline onto a new begin time, offsetting every frame's start_time_ by the difference so...
std::chrono::steady_clock::time_point animation_start_tick_
Definition: animated.hpp:240
std::chrono::milliseconds get_animation_duration() const
get_end_time() minus get_begin_time(): how long one pass through all frames takes.
void set_duration_limit(const std::chrono::milliseconds &time)
static const T void_value_
Definition: animated.hpp:190
std::chrono::milliseconds get_end_time() const
Animation-time offset where the last frame ends.
std::chrono::milliseconds get_current_frame_end_time() const
Animation-time offset where the current frame ends.
animated(const std::chrono::milliseconds &start_time=std::chrono::milliseconds{0})
Creates an animation with no frames; add_frame() populates it afterward.
bool cycles() const
True if the animation loops back to the first frame instead of stopping at the last.
Definition: animated.hpp:112
void advance_to_current_frame()
Moves current_frame_index_ forward to match the current tick, one frame at a time (or in whole cycles...
const config * cfg
std::chrono::milliseconds duration_
Definition: animated.hpp:218
std::chrono::milliseconds start_time_
Definition: animated.hpp:220
static map_location::direction n