- pass timestamp of frame start time to all update functions

- use timestamp instead of SDL_GetTicks to keep time in Timer class
- use seconds instead of milliseconds in Timer class
This commit is contained in:
ohsqueezy 2023-06-07 20:22:20 -04:00
parent 1ca956b5ac
commit 355ab4d8c4
20 changed files with 181 additions and 158 deletions

View File

@ -2,7 +2,7 @@
Animation::Animation()
{
timer.toggle(sb::Timer::OFF);
timer.off();
}
void Animation::play(float delay, bool play_once)
@ -10,12 +10,12 @@ void Animation::play(float delay, bool play_once)
this->delay = delay;
play_state = true;
paused = false;
previous_step_time = timer.ms_elapsed();
previous_step_time = timer.elapsed();
overflow = 0;
ending = play_once;
if (delay <= 0)
{
timer.toggle(true);
timer.on();
}
}
@ -31,19 +31,19 @@ void Animation::play_once(float delay)
void Animation::pause()
{
timer.toggle(false);
timer.off();
paused = true;
}
void Animation::unpause()
{
timer.toggle(true);
timer.on();
paused = false;
}
void Animation::reset()
{
timer.toggle(false);
timer.off();
play_state = false;
timer.reset();
}
@ -53,25 +53,25 @@ bool Animation::playing(bool include_delay) const
return play_state && !paused && (include_delay || delay <= 0);
}
void Animation::update()
void Animation::update(float timestamp)
{
timer.update();
timer.update(timestamp);
if (playing() && containing_object->is_active())
{
if (delay > 0)
{
delay -= timer.ms_last_frame();
delay -= timer.frame();
if (delay <= 0)
{
timer.toggle(true);
timer.on();
}
}
if (delay <= 0)
{
if (timer.ms_elapsed() - previous_step_time + overflow > frame_length)
if (timer.elapsed() - previous_step_time + overflow > frame_length)
{
overflow = timer.ms_elapsed() - previous_step_time + overflow - frame_length;
previous_step_time = timer.ms_elapsed();
overflow = timer.elapsed() - previous_step_time + overflow - frame_length;
previous_step_time = timer.elapsed();
step();
if (ending)
{

View File

@ -25,8 +25,7 @@ class Animation
private:
bool play_state = false, ending = false, paused = false;
int previous_step_time = 0;
float delay = 0, overflow = 0, frame_length = 0;
float delay = 0.0f, overflow = 0.0f, frame_length = 0.0f, previous_step_time = 0.0f;
callback step = nullptr;
Node* containing_object = nullptr;
sb::Timer timer = sb::Timer();
@ -35,19 +34,22 @@ public:
Animation();
/* Constructor that allows passing a pointer to an object and a pointer to one of its
/*!
* Constructor that allows passing a pointer to an object and a pointer to one of its
* member functions that will be used as the animation function. Frame length can be supplied
* in milliseconds, representing how often the animation function will run. If omitted, the
* animation function will run every time the Animation object is updated (generally, once per
* frame of the application). */
* in seconds, representing how often the animation function will run. If omitted, the
* animation function will run every time the Animation object is updated.
*/
template<typename T>
Animation(void(T::*member_function)(), T* object, float frame_length = 0) : frame_length(frame_length)
{
bind(member_function, object);
timer.toggle(sb::Timer::OFF);
timer.off();
}
/* Set the animation function by supplying an object and one of its member functions */
/*!
* Set the animation function by supplying an object and one of its member functions.
*/
template<typename T>
void bind(void(T::*f)(), T* o)
{
@ -62,7 +64,7 @@ public:
void unpause();
void reset();
bool playing(bool = true) const;
void update();
void update(float timestamp);
};

View File

@ -104,9 +104,9 @@ bool SoundEffect::playing(bool include_delay) const
return false;
}
void SoundEffect::update()
void SoundEffect::update(float timestamp)
{
play_animation.update();
play_animation.update(timestamp);
}
SoundEffect::~SoundEffect()
@ -126,10 +126,10 @@ void Audio::load_bgm()
load_directory(configuration()["audio"]["default-bgm-root"], bgm, this);
}
void Audio::update()
void Audio::update(float timestamp)
{
for (auto& [name, sound_effect] : sfx)
{
sound_effect.update();
sound_effect.update(timestamp);
}
}

View File

@ -56,7 +56,7 @@ struct SoundEffect : Node
void set_loop_count(int);
void set_loop_forever();
bool playing(bool = false) const;
void update();
void update(float timestamp);
~SoundEffect();
};
@ -70,7 +70,7 @@ struct Audio : Node
Audio(Node*);
void load_sfx();
void load_bgm();
void update();
void update(float timestamp);
template <typename T>
void load_directory(const fs::path& root, std::map<std::string, T>& storage, Node* parent = nullptr)

View File

@ -17,8 +17,6 @@
#include "glm/common.hpp"
#include "glm/gtx/integer.hpp"
#include "utility.hpp"
namespace sb
{

View File

@ -34,7 +34,7 @@ void Configuration::set_defaults()
{"suppress-any-key-on-mods", true},
{"system-any-key-ignore-commands", {"fullscreen", "screenshot", "record", "quit"}},
{"any-key-ignore-commands", nlohmann::json::array()},
{"default-unsuppress-delay", 700},
{"default-unsuppress-delay", 0.7},
{"ignore-repeat-keypress", true}
};
config["display"] = {
@ -69,10 +69,10 @@ void Configuration::set_defaults()
{"screenshot-extension", ".png"},
{"screenshot-zfill", 5},
{"screenshot-directory", "."},
{"gif-frame-length", 100},
{"gif-frame-length", 0.1},
{"video-directory", "."},
{"write-mp4", false},
{"max-stash-length", 5000},
{"max-stash-length", 5.0},
{"max-in-game-stashes", 3},
{"max-video-stashes", 40},
{"max-video-memory", 1000},
@ -92,7 +92,7 @@ void Configuration::set_defaults()
};
config["configuration"] = {
{"auto-refresh", false},
{"auto-refresh-interval", 1000}
{"auto-refresh-interval", 1.0}
};
}
@ -156,6 +156,7 @@ void Configuration::merge(const fs::path& path)
sb::Log::log(message, sb::Log::WARN);
}
#ifndef __ANDROID__
config_file_modification_time = fs::last_write_time(path);
}
else
{
@ -208,9 +209,9 @@ void Configuration::refresh()
}
}
void Configuration::update()
void Configuration::update(float timestamp)
{
auto_refresher.update();
auto_refresher.update(timestamp);
}
std::ostream& std::operator<<(std::ostream& out, const Configuration& configuration)

View File

@ -102,10 +102,10 @@ public:
/*!
* Enable auto refresh. Auto refresh watches the file at the given path for changes and loads them automatically every interval given
* in milliseconds.
* in seconds.
*
* @param file_to_refresh path to a configuration JSON
* @param interval amount of milliseconds between each refresh
* @param interval amount of seconds between each refresh
*/
void enable_auto_refresh(const fs::path& file_to_refresh, float interval);
@ -120,9 +120,11 @@ public:
void refresh();
/*!
* Update the auto refresher.
* Update the auto refresher with the given timestamp, which should be the timestamp passed to Game::update.
*
* @param timestamp seconds elapsed since the program started
*/
void update();
void update(float timestamp);
};

View File

@ -565,26 +565,27 @@ void Game::run()
#endif
}
void Game::frame(float ticks)
void Game::frame(float timestamp)
{
if (ticks - last_frame_timestamp + frame_time_overflow >= frame_length)
if (timestamp - last_frame_timestamp + frame_time_overflow >= frame_length)
{
last_frame_length = ticks - last_frame_timestamp;
last_frame_length = timestamp - last_frame_timestamp;
if (frame_length_history.size() == 5000)
{
frame_length_history.pop_back();
}
frame_length_history.insert(frame_length_history.begin(), last_frame_length);
frame_time_overflow = last_frame_length + frame_time_overflow - frame_length;
last_frame_timestamp = ticks;
last_frame_timestamp = timestamp;
if (last_frame_length < 1000)
{
recorder.update();
float timestamp_seconds = timestamp / 1000.0f;
recorder.update(timestamp_seconds);
_delegate.dispatch();
audio.update();
input.unsuppress_animation.update();
update();
_configuration.update();
audio.update(timestamp_seconds);
input.unsuppress_animation.update(timestamp_seconds);
update(timestamp_seconds);
_configuration.update(timestamp_seconds);
if (!is_gl_context)
{
SDL_SetRenderTarget(renderer, nullptr);
@ -597,10 +598,10 @@ void Game::frame(float ticks)
// frame_time_overflow = 0;
// }
frame_count_this_second++;
if (ticks - last_frame_count_timestamp >= 1000)
if (timestamp - last_frame_count_timestamp >= 1000)
{
current_frames_per_second = frame_count_this_second;
last_frame_count_timestamp = ticks;
last_frame_count_timestamp = timestamp;
frame_count_this_second = 0;
std::ostringstream message;
message << "Counted " << current_frames_per_second << " frames last second";

View File

@ -148,7 +148,7 @@ public:
void run();
void frame(float);
void flag_to_end();
virtual void update() {};
virtual void update(float timestamp) {};
void set_framerate(int);
float get_frame_length() const;
void handle_quit_event(SDL_Event&);
@ -165,9 +165,9 @@ public:
* @return weighted value
*/
template<typename T>
T weight(T amount)
T weight(T amount) const
{
return (last_frame_length / (1000.0 / 60)) * amount;
return (last_frame_length / 1000.0f) * amount;
}
};

View File

@ -125,10 +125,10 @@ void Node::suppress_input()
get_root()->get_input().suppress();
}
void Node::suppress_input_temporarily(int length)
void Node::suppress_input_temporarily(float length)
{
suppress_input();
if (length == 0)
if (length == 0.0f)
{
length = configuration()["input"]["default-unsuppress-delay"];
}

View File

@ -78,7 +78,7 @@ public:
const Game* get_root() const;
Box window_box(bool = false);
void suppress_input();
void suppress_input_temporarily(int = 0);
void suppress_input_temporarily(float length = 0.0f);
void unsuppress_input();
const std::string get_branch_as_string() const;
virtual std::string class_name() const { return "Node"; };

View File

@ -17,7 +17,7 @@ Recorder::Recorder(Node* parent) : Node(parent)
* been configured by the user. */
float Recorder::frame_length()
{
return configuration()["recording"].value("video-frame-length", get_root()->get_frame_length());
return configuration()["recording"].value("video-frame-length", get_root()->get_frame_length() / 1000.0f);
}
/* Handle commands for screenshot, record video and save video */
@ -259,7 +259,7 @@ void Recorder::write_stash_frames(Stash* stash)
if (ii == stash->frame_offset)
{
GifBegin(&gif_writer, gif_path.string().c_str(), frame->w,
frame->h, gif_frame_length / 10);
frame->h, gif_frame_length * 100);
}
else
{
@ -269,7 +269,7 @@ void Recorder::write_stash_frames(Stash* stash)
SDL_Surface* converted = SDL_ConvertSurfaceFormat(
frame, SDL_PIXELFORMAT_ABGR8888, 0);
GifWriteFrame(&gif_writer, (const uint8_t*) converted->pixels,
frame->w, frame->h, gif_frame_length / 10);
frame->w, frame->h, gif_frame_length * 100);
}
elapsed += frame_length();
delete[] stash->pixel_buffers.front();
@ -343,7 +343,7 @@ void Recorder::write_mp4()
std::string pixel_format = configuration()["recording"]["mp4-pixel-format"].get<std::string>();
fs::path images_match = current_video_directory / "%05d.png";
mp4_command << "ffmpeg -f s16le -ac 2 -ar 22050 -i " << current_audio_path.string() <<
" -f image2 -framerate " << (1000 / frame_length()) <<
" -f image2 -framerate " << (1.0f / frame_length()) <<
" -i " << images_match.string() << " -s " << size.x << "x" << size.y <<
" -c:v libx264 -crf 17 -pix_fmt " << pixel_format << " " <<
current_video_directory.string() << ".mp4";
@ -357,14 +357,14 @@ void Recorder::write_audio(Uint8* stream, int len)
audio_file.write(reinterpret_cast<char*>(stream), len);
}
void Recorder::update()
void Recorder::update(float timestamp)
{
if (is_recording and get_memory_size() > configuration()["recording"]["max-video-memory"])
{
end_recording();
}
animation.set_frame_length(frame_length());
animation.update();
animation.update(timestamp);
}
void Recorder::process_audio(void* context, Uint8* stream, int len)

View File

@ -75,7 +75,7 @@ public:
void finish_writing_video();
void write_mp4();
void write_audio(Uint8*, int);
void update();
void update(float timestamp);
virtual std::string class_name() const { return "Recorder"; }
};

View File

@ -51,13 +51,6 @@ public:
std::vector<Segment> subsegments(int) const;
inline bool operator<(const Segment& segment) const { return operator<(segment.length()); }
/*
std::cout << (a < b) << (a > b) << (b < c) << (b <= c) << (c > b) << (c >= b) << (d < 1) << (d < 2) <<
(d >= a) << (d > c) << std::endl;
should print 0101010101
*/
template<typename N>
inline bool operator<(const N& other) const { return length() < other; }
@ -76,3 +69,11 @@ namespace std
{
std::ostream& operator<<(std::ostream&, const Segment&);
}
/* Add Segment class to the sb namespace. This should be the default location, but Segment is left in the global namespace
* for backward compatibility.
*/
namespace sb
{
using ::Segment;
}

View File

@ -17,8 +17,6 @@
#include "glm/common.hpp"
#include "glm/gtx/integer.hpp"
#include "utility.hpp"
namespace sb
{

View File

@ -830,7 +830,7 @@ void Sprite::set_draw_children_on_frame(bool on_frame)
draw_children_on_frame = on_frame;
}
void Sprite::update(const std::vector<Box>& subsections)
void Sprite::update(float timestamp, const std::vector<Box>& subsections)
{
if (is_active())
{
@ -838,10 +838,10 @@ void Sprite::update(const std::vector<Box>& subsections)
{
move_weighted(step);
}
frame_animation.update();
blink_animation.update();
wipe_animation.update();
toggle_hidden_animation.update();
frame_animation.update(timestamp);
blink_animation.update(timestamp);
wipe_animation.update(timestamp);
toggle_hidden_animation.update(timestamp);
SDL_Texture* texture = get_current_frame();
if (is_loaded() && !is_hidden() && get_current_frameset().get_frame_count())
{
@ -892,7 +892,7 @@ void Sprite::update(const std::vector<Box>& subsections)
child_relative_position = child.sprite.get_nw();
child.sprite.move(get_nw());
}
child.sprite.update();
child.sprite.update(timestamp);
if (!draw_children_on_frame)
{
child.sprite.set_nw(child_relative_position);
@ -901,9 +901,9 @@ void Sprite::update(const std::vector<Box>& subsections)
}
}
void Sprite::update()
void Sprite::update(float timestamp)
{
update({});
update(timestamp, {});
}
void Sprite::render_subsection(SDL_Renderer* renderer, SDL_Texture* texture, const Box& subsection, const Box& box)

View File

@ -140,8 +140,8 @@ public:
bool has_child(std::string) const;
void remove_child(std::string);
void set_draw_children_on_frame(bool);
virtual void update(const std::vector<Box>&);
virtual void update();
virtual void update(float timestamp, const std::vector<Box>&);
virtual void update(float timestamp);
void render_subsection(SDL_Renderer*, SDL_Texture*, const Box&, const Box&);
void set_to_leave_memory_allocated();
void set_to_deallocate_memory();

View File

@ -84,10 +84,8 @@ namespace sb
private:
using Reaction = std::function<return_type(bool, arguments...)>;
inline static const bool STATE_OFF = false;
inline static const bool STATE_ON = true;
bool state = STATE_OFF;
bool state = false;
Reaction reaction;
public:
@ -102,6 +100,12 @@ namespace sb
Switch(bool state, Reaction reaction = Reaction()) : state(state), reaction(reaction) {}
/*!
* Toggle state, triggering the reaction function.
*
* @param args arbitrary list of arguments that are expected by the reaction function
* @return return the return value of the reaction function
*/
return_type flip(arguments... args)
{
state = !state;
@ -121,13 +125,14 @@ namespace sb
/*!
* @return when called as a boolean, return state
*/
operator bool()
operator bool() const
{
return on();
return state;
}
/*!
* Assign the switch object's state using the assignment operator with a boolean value.
* Assign the switch object's state using the assignment operator with a boolean value, without triggering the reaction function.
* To trigger the reaction function, use `Switch::flip` instead.
*
* @param state state the switch will be set to
*/
@ -137,14 +142,6 @@ namespace sb
return *this;
}
/*!
* @return true if state is Switch::STATE_ON, false otherwise
*/
bool on()
{
return state;
}
operator std::string() const
{
return state ? "true" : "false";

View File

@ -1,77 +1,55 @@
#include "Timer.hpp"
/* Initialize a Timer object for keeping a general time count in milliseconds or seconds which can be paused
* arbitrarily. The time initializes to zero. By default, the timer begins timing at initialization (this
* may change in the future). The ticks and previous_ticks parameters are initially synchronized with the SDL
* ticks function, which returns the number of milliseconds since the program began, and the SDL ticks function
* is used to keep the time updated. */
sb::Timer::Timer()
{
ticks = SDL_GetTicks();
ticks_previous = ticks;
}
/* Check whether Timer is keeping time or not */
bool sb::Timer::state()
sb::Timer::operator bool() const
{
return timing;
}
/* Toggle timing on/off */
void sb::Timer::toggle()
{
toggle(!state());
toggle(!*this);
}
/* Set state explicitly to sb::Timer::ON (true) or sb::Timer::OFF (false) */
void sb::Timer::toggle(bool state)
{
timing = state;
}
/* Reset time elapsed to zero */
void sb::Timer::on()
{
toggle(ON);
}
void sb::Timer::off()
{
toggle(OFF);
}
void sb::Timer::reset()
{
ticks_elapsed = 0;
ticks = SDL_GetTicks();
ticks_previous = ticks;
frame_duration = 0;
_elapsed = 0;
}
/* Return milliseconds elapsed on timer */
float sb::Timer::ms_elapsed()
float sb::Timer::elapsed() const
{
return ticks_elapsed;
return _elapsed;
}
/* Return seconds elapsed on timer */
float sb::Timer::seconds_elapsed()
{
return ms_elapsed() / 1000.0f;
}
/* Return the length of the previous frame in milliseconds (the time between the last two calls to
* the timer update function) */
float sb::Timer::ms_last_frame()
float sb::Timer::frame() const
{
return frame_duration;
}
/* Return the length of the previous frame in seconds (the time between the last two calls to the
* timer update function) */
float sb::Timer::seconds_last_frame()
void sb::Timer::update(float timestamp)
{
return ms_last_frame() / 1000.0f;
}
/* Add time to the timer by measuring the time between this call and the previous call to update */
void sb::Timer::update()
{
ticks = SDL_GetTicks();
frame_duration = ticks - ticks_previous;
if (state())
if (previous_is_recorded)
{
ticks_elapsed += frame_duration;
frame_duration = timestamp - timestamp_previous;
if (*this)
{
_elapsed += frame();
}
}
ticks_previous = ticks;
timestamp_previous = timestamp;
previous_is_recorded = true;
}

View File

@ -10,17 +10,20 @@
#pragma once
#include "SDL.h"
namespace sb
{
/*!
* Timer in seconds which can be paused arbitrarily.
*
* It must be updated every frame with the timestamp passed to Game::update, regardless of whether it is actively timing or not.
*/
class Timer
{
private:
bool timing = Timer::ON;
int ticks = 0, ticks_previous = 0, frame_duration = 0, ticks_elapsed = 0;
bool timing = false, previous_is_recorded = false;
float timestamp = 0.0f, timestamp_previous = 0.0f, frame_duration = 0.0f, _elapsed = 0.0f;
public:
@ -30,16 +33,58 @@ namespace sb
ON
};
Timer();
bool state();
/*!
* @return boolean indicating whether timer object is keeping time or not
*/
operator bool() const;
/*!
* Toggle timing on/off
*/
void toggle();
void toggle(bool);
/*!
* @param state set timing state explictly by passing a boolean
*/
void toggle(bool state);
/*!
* Start timing.
*/
void on();
/*!
* Stop timing.
*/
void off();
/*!
* Reset time elapsed to zero.
*/
void reset();
float ms_elapsed();
float seconds_elapsed();
float ms_last_frame();
float seconds_last_frame();
void update();
/*!
* @return seconds elapsed on timer
*/
float elapsed() const;
/*!
* @return length of the previous frame in seconds
*/
float frame() const;
/*!
* Update the timer's elapsed time, using the amount of seconds elapsed in the program at the start of the frame to keep track.
*
* The timer should be updated every frame to keep track of the timestamp, regardless of whether it is currently timing or not.
*
* The timestamp should be forwarded from the value passed to Game::update, so that it is consistently the timestamp at the
* beginning of the frame.
*
* @param timestamp seconds elapsed since the start of the program
*/
void update(float timestamp);
/*!
* Applies delta timing to a value: returns the value as weighted by the amount of time passed since the
@ -50,9 +95,9 @@ namespace sb
* @return weighted value
*/
template<typename Type>
Type weigh(Type amount)
Type delta(Type amount) const
{
return seconds_last_frame() * amount;
return frame() * amount;
}
};