The Battle for Wesnoth  1.19.26+dev
animated.tpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2005 - 2025
3  by Guillaume Melquiond <guillaume.melquiond@gmail.com>
4  Copyright (C) 2004 by Philippe Plantier <ayin@anathas.org>
5  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
6 
7  This program is free software; you can redistribute it and/or modify
8  it under the terms of the GNU General Public License as published by
9  the Free Software Foundation; either version 2 of the License, or
10  (at your option) any later version.
11  This program is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY.
13 
14  See the COPYING file for more details.
15 */
16 
17 /**
18  * @file animated.tpp
19  * Template implementation for frame-based animations.
20  */
21 
22 
23 // ============================================================================
24 // Static Members
25 // ============================================================================
26 
27 template<typename T>
28 const T animated<T>::void_value_ = T();
29 
30 
31 // ============================================================================
32 // Construction
33 // ============================================================================
34 
35 template<typename T>
36 inline animated<T>::animated(const std::chrono::milliseconds& start_time)
37  : starting_frame_time_(start_time)
38  , max_animation_time_(0)
39  , frames_()
40  , current_frame_index_(0)
41  , started_(false)
42  , paused_(false)
43  , finished_(false)
44  , cycles_(false)
45  , uses_acceleration_(false)
46  , animation_start_tick_()
47  , next_frame_tick_()
48  , pause_tick_()
49  , is_static_(true)
50 {
51 }
52 
53 template<typename T>
54 inline animated<T>::animated(const animated<T>::anim_description& cfg, const std::chrono::milliseconds& start_time, bool force_change)
55  : starting_frame_time_(start_time)
56  , max_animation_time_(0)
57  , frames_()
58  , current_frame_index_(0)
59  , started_(false)
60  , paused_(false)
61  , finished_(false)
62  , cycles_(false)
63  , uses_acceleration_(false)
64  , animation_start_tick_()
65  , next_frame_tick_()
66  , pause_tick_()
67  , is_static_(true)
68 {
69  for(const auto& [duration, value] : cfg) {
70  add_frame(duration, value, force_change);
71  }
72 }
73 
74 template<typename T>
75 inline void animated<T>::add_frame(const std::chrono::milliseconds& duration, const T& value, bool force_change)
76 {
77  // NOTE: We cannot use emplace_back here, because the value may be a reference into the same vector,
78  // which case emplace_back could invalidate it before the new frame is constructed.
79 
80  if(!frames_.empty()) {
81  const auto& last_frame = frames_.back();
82  frames_.push_back(frame{duration, value, last_frame.start_time_ + last_frame.duration_});
83  is_static_ = false;
84  } else {
85  is_static_ = !force_change;
86  frames_.push_back(frame{duration, value, starting_frame_time_});
87  }
88 }
89 
90 
91 // ============================================================================
92 // Playback Control
93 // ============================================================================
94 
95 template<typename T>
96 inline void animated<T>::start_animation(const std::chrono::milliseconds& start_time_offset, bool cycles)
97 {
98  started_ = true;
99  paused_ = false;
100  finished_ = false;
101  cycles_ = cycles;
102  current_frame_index_ = 0;
103  const auto current_tick = get_current_animation_tick(uses_acceleration_);
104  animation_start_tick_ = current_tick - start_time_offset;
105  if(!frames_.empty()) {
106  next_frame_tick_ = animation_start_tick_ + frames_[0].start_time_ + frames_[0].duration_;
107  }
108 }
109 
110 template<typename T>
111 inline void animated<T>::pause_animation()
112 {
113  if(!started_ || paused_) {
114  return;
115  }
116  paused_ = true;
117  pause_tick_ = get_current_animation_tick(uses_acceleration_);
118 }
119 
120 template<typename T>
121 inline void animated<T>::resume_animation()
122 {
123  if(!paused_) {
124  return;
125  }
126 
127  const auto current_tick = get_current_animation_tick(uses_acceleration_);
128  const auto pause_duration = current_tick - pause_tick_;
129 
130  // Shift the timeline forward by the pause duration
131  animation_start_tick_ += pause_duration;
132  next_frame_tick_ += pause_duration;
133 
134  paused_ = false;
135 }
136 
137 template<typename T>
138 inline void animated<T>::advance_to_current_frame()
139 {
140  if(frames_.empty()) {
141  return;
142  }
143 
144  const auto current_tick = get_current_animation_tick(uses_acceleration_);
145 
146  // If a cycling animation is more than a whole cycle behind (e.g., it went un-updated
147  // while offscreen), skip forward in whole cycles so the loop below never has to walk
148  // more than one cycle of frames.
149  if(cycles_) {
150  const std::chrono::milliseconds animation_duration = get_animation_duration();
151  if(animation_duration > std::chrono::milliseconds{0} && current_tick > (next_frame_tick_ + animation_duration)) {
152  const auto behind = current_tick - next_frame_tick_;
153  next_frame_tick_ += (behind / animation_duration) * animation_duration; // The int division truncates to whole cycles.
154  }
155  }
156 
157  // Advance frame by frame until we reach the frame containing the current time
158  while(current_tick >= next_frame_tick_) {
159  std::size_t next_index = current_frame_index_ + 1;
160 
161  // Non-looping animation finished, stay on last frame
162  if(!cycles_ && next_index == frames_.size()) {
163  finished_ = true;
164  return;
165  }
166 
167  // Advance to the next frame, wrapping around when looping
168  current_frame_index_ = next_index % frames_.size();
169  next_frame_tick_ += frames_[current_frame_index_].duration_;
170  }
171 }
172 
173 
174 // ============================================================================
175 // Playback State Queries
176 // ============================================================================
177 
178 template<typename T>
179 inline bool animated<T>::need_update() const
180 {
181  if(is_static_ || !started_ || paused_ || finished_ || frames_.empty()) {
182  return false;
183  }
184  return get_current_animation_tick(uses_acceleration_) >= next_frame_tick_;
185 }
186 
187 template<typename T>
188 inline bool animated<T>::animation_finished() const
189 {
190  return finished_ || frames_.empty() || !started_ || cycles_ || (get_elapsed_time() >= get_end_time());
191 }
192 
193 
194 // ============================================================================
195 // Time Queries and Manipulation
196 // ============================================================================
197 
198 template<typename T>
199 inline std::chrono::steady_clock::time_point animated<T>::get_playback_tick() const
200 {
201  return paused_ ? pause_tick_ : get_current_animation_tick(uses_acceleration_);
202 }
203 
204 template<typename T>
205 inline std::chrono::milliseconds animated<T>::get_elapsed_time() const
206 {
207  if(!started_) {
208  return starting_frame_time_;
209  }
210 
211  // Calculate elapsed time since animation started
212  const auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(get_playback_tick() - animation_start_tick_);
213 
214  // Apply maximum time cap if set
215  if(max_animation_time_ > std::chrono::milliseconds{0} && elapsed > max_animation_time_) {
216  return max_animation_time_;
217  }
218 
219  return elapsed;
220 }
221 
222 template<typename T>
223 inline void animated<T>::apply_time_offset(const std::chrono::milliseconds& time)
224 {
225  // Adjust the anchor point to make the current tick correspond to the requested time offset
226  animation_start_tick_ -= time;
227 
228  // Restart animation from beginning; advance_to_current_frame() will sync on next call
229  if(!frames_.empty()) {
230  current_frame_index_ = 0;
231  finished_ = false;
232  next_frame_tick_ = animation_start_tick_ + frames_[0].start_time_ + frames_[0].duration_;
233  }
234 }
235 
236 template<typename T>
237 inline void animated<T>::set_duration_limit(const std::chrono::milliseconds& time)
238 {
239  max_animation_time_ = time;
240 }
241 
242 template<typename T>
243 inline std::chrono::milliseconds animated<T>::get_animation_duration() const
244 {
245  return get_end_time() - get_begin_time();
246 }
247 
248 template<typename T>
249 inline std::chrono::milliseconds animated<T>::get_begin_time() const
250 {
251  return starting_frame_time_;
252 }
253 
254 template<typename T>
255 inline std::chrono::milliseconds animated<T>::get_end_time() const
256 {
257  if(frames_.empty()) {
258  return starting_frame_time_;
259  }
260  const auto& last_frame = frames_.back();
261  return last_frame.start_time_ + last_frame.duration_;
262 }
263 
264 template<typename T>
265 inline void animated<T>::set_begin_time(const std::chrono::milliseconds& new_begin_time)
266 {
267  const auto shift = new_begin_time - starting_frame_time_;
268  starting_frame_time_ = new_begin_time;
269  for(auto& frame : frames_) {
270  frame.start_time_ += shift;
271  }
272 }
273 
274 // ============================================================================
275 // Frame Queries
276 // ============================================================================
277 
278 template<typename T>
279 inline const T& animated<T>::get_current_frame() const
280 {
281  if(frames_.empty()) {
282  return void_value_;
283  }
284  return frames_[current_frame_index_].value_;
285 }
286 
287 template<typename T>
288 inline std::chrono::milliseconds animated<T>::get_current_frame_begin_time() const
289 {
290  if(frames_.empty()) {
291  return starting_frame_time_;
292  }
293  return frames_[current_frame_index_].start_time_;
294 }
295 
296 template<typename T>
297 inline std::chrono::milliseconds animated<T>::get_current_frame_end_time() const
298 {
299  if(frames_.empty()) {
300  return starting_frame_time_;
301  }
302  const auto& frame = frames_[current_frame_index_];
303  return frame.start_time_ + frame.duration_;
304 }
305 
306 template<typename T>
307 inline std::chrono::milliseconds animated<T>::get_time_in_current_frame() const
308 {
309  if(frames_.empty() || !started_) {
310  return std::chrono::milliseconds{0};
311  }
312  const auto current_tick = get_playback_tick();
313  const auto& frame = frames_[current_frame_index_];
314 
315  // How much of this frame has elapsed, clamped to [0, duration].
316  const auto time_until_next = std::chrono::duration_cast<std::chrono::milliseconds>(next_frame_tick_ - current_tick);
317  const auto elapsed_in_frame = frame.duration_ - time_until_next;
318  return std::clamp(elapsed_in_frame, std::chrono::milliseconds{0}, frame.duration_);
319 }
320 
321 template<typename T>
322 inline const T& animated<T>::get_first_frame() const
323 {
324  if(frames_.empty()) {
325  return void_value_;
326  }
327  return frames_[0].value_;
328 }
329 
330 template<typename T>
331 inline const T& animated<T>::get_frame(std::size_t n) const
332 {
333  if(n >= frames_.size()) {
334  return void_value_;
335  }
336  return frames_[n].value_;
337 }
338 
339 template<typename T>
340 inline const T& animated<T>::get_last_frame() const
341 {
342  if(frames_.empty()) {
343  return void_value_;
344  }
345  return frames_.back().value_;
346 }
347 
348 template<typename T>
349 inline std::size_t animated<T>::get_frames_count() const
350 {
351  return frames_.size();
352 }
353 
354 
355 // ============================================================================
356 // Protected Methods (for unit_animation)
357 // ============================================================================
358 
359 template<typename T>
360 void animated<T>::remove_frames_until(const std::chrono::milliseconds& new_starting_time)
361 {
362  // Remove frames from the beginning until we reach the target time
363  while(starting_frame_time_ < new_starting_time && !frames_.empty()) {
364  starting_frame_time_ += frames_[0].duration_;
365  frames_.erase(frames_.begin());
366  }
367 }
368 
369 template<typename T>
370 inline void animated<T>::set_end_time(const std::chrono::milliseconds& new_ending_time)
371 {
372  if(frames_.empty()) {
373  return;
374  }
375 
376  // Cut-off before the animation begins: keep nothing but a zero-length first frame
377  if(new_ending_time <= starting_frame_time_) {
378  frames_.erase(std::next(frames_.begin()), frames_.end());
379  frames_.front().duration_ = std::chrono::milliseconds{0};
380  return;
381  }
382 
383  // Cut-off at or past the current end: extend the last frame to reach it
384  const auto current_end_time = get_end_time();
385  if(new_ending_time >= current_end_time) {
386  frames_.back().duration_ += new_ending_time - current_end_time;
387  return;
388  }
389 
390  // The cut-off lands within a frame: walk to it
391  auto current_start_time = starting_frame_time_;
392  auto it = frames_.begin();
393 
394  while(it != frames_.end()) {
395  const auto next_end_time = current_start_time + it->duration_;
396 
397  // Check if the cut-off point is within or at the end of this frame
398  if(next_end_time >= new_ending_time) {
399  // Truncate this frame to time remaining (a no-op if the cut-off is exactly at its end)
400  it->duration_ = new_ending_time - current_start_time;
401 
402  // Erase all SUBSEQUENT frames (keep the current one)
403  frames_.erase(std::next(it), frames_.end());
404  return;
405  }
406 
407  current_start_time = next_end_time;
408  ++it;
409  }
410 }