A library for tick based game engines.
A tick based game engine runs all of it's calculations at fixed intervals. This is in contrast to most modern game engines which run their calculations every frame. Tick based game engines are used because it allows for calculations to be run on a different thread than the rendering thread. allowing for the game to render at a consistent speed even if the rendering thread is slow, and for the rendering thread to run quickly even if the calculations are slow.
When multiple frames happen per tick interpolation is used to make the game appear to run smoothly. This is done by storing the tick data for the previous tick, and the most recent tick and interpolating between the two to get what the renderer should show.
The core of saunter is the loop. The loop calls the tick function on the provided listener. Your listener should store the state of your game engine or the current scene in your engine. To create a listener implement Listener.
```rust struct ExampleListener { val: f32, } impl Listener for ExampleListener { // The type of the tick that tick function returns type TickType = ExampleTick; // The type of event that is passed to tick in the Event::Other(EventType) variant type EventType = ExampleEvent;
fn tick(
&mut self,
dt: f32,
events: &mut Vec<Event<Self::EventType>>,
time: Instant,
) -> Result<Self::TickType, SaunterError> {
self.val = 1.0 - self.val;
log::info!("{}", self.val);
Ok(Self::TickType {val: self.val})
}
} ```
You may have noticed the type Tick being used a lot here. Ticks store a snapshot of the state of the listener. The tick function returns the tick that was just processed. Similarly to making a listener we implement the Tick trait on our tick type.
```rust struct ExampleTick { // Ticks store the time they were made so that they can be enterpolated pub time: Instant,
pub val: f32,
}
impl Tick for ExampleTick {
// This is called by ticks when b is the most recent tick and self is the last to enterpolate between the two
fn lerp(&self, b: &Self, t: f32) -> Result
fn get_time(&self) -> &Instant {
&self.time
}
} ```
Now we can create the loop! The easiest way to create a loop is using Loop::init()
rust
let (mut tick_loop, event_sender, ticks) = Loop::init(
// The listener that the loop will call tick() on
Box::new(listener),
// The state of the engine or scene when the program is started. AKA the first tick
first_tick,
// The number of ticks to occur per second (TPS), does not have to be an integer
tps,
);
Now that you have a loop all that is left is to send it events. To do this you can use the event_sender that was returned by Loop::init()
. The event sender is a Sender<Event<EventType>>
where EventType is the type of event you specified in your listener. EventType is wrapped in a saunter::event::Event
. This is to guarentee that you can send a close event to the loop. To send an event you can use the send()
method on the event sender.
The ticks type is used to store the most recent and last tick. It has a lerp funtion that returns a new tick enterpolated by the amount specified between the two ticks for use in rendering, or whatever you choose. The ticks returned by Loop::init()
is not actually a ticks, it is a Arc<RwLock<Ticks<...>>>
, This is because it is constantly being updated by the tickloop. For this reason, only ever have a read lock on ticks. To help with optimization, it is best practise to immediatly drop the read lock when you are done with it.