The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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
27template<typename T>
28const T animated<T>::void_value_ = T();
29
30
31// ============================================================================
32// Construction
33// ============================================================================
34
35template<typename T>
36inline 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
53template<typename T>
54inline 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
74template<typename T>
75inline 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
95template<typename T>
96inline 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
110template<typename T>
111inline 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
120template<typename T>
121inline 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
137template<typename T>
138inline 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
178template<typename T>
179inline 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
187template<typename T>
188inline 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
198template<typename T>
199inline 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
204template<typename T>
205inline 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
222template<typename T>
223inline 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
236template<typename T>
237inline void animated<T>::set_duration_limit(const std::chrono::milliseconds& time)
238{
239 max_animation_time_ = time;
240}
241
242template<typename T>
243inline std::chrono::milliseconds animated<T>::get_animation_duration() const
244{
245 return get_end_time() - get_begin_time();
246}
247
248template<typename T>
249inline std::chrono::milliseconds animated<T>::get_begin_time() const
250{
251 return starting_frame_time_;
252}
253
254template<typename T>
255inline 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
264template<typename T>
265inline 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
278template<typename T>
279inline 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
287template<typename T>
288inline 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
296template<typename T>
297inline 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
306template<typename T>
307inline 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
321template<typename T>
322inline 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
330template<typename T>
331inline 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
339template<typename T>
340inline 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
348template<typename T>
349inline 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
359template<typename T>
360void 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
369template<typename T>
370inline 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}