Recently I've been reading through the book Game Programming Patterns by Robert Nystrom while implementing the patterns into an infinite runner that I wrote in Rust using the GGEZ game engine.
Before starting the game patterns proper the first part of the book covers some of the classic design patterns in Object Oriented Programming. This includes the Observer pattern.
As a quick recap, the observer pattern is all about having an event happen in one area of the code, and that event triggering an action in another area of the code. In a traditional object oriented language this could be done by passing instances of objects around. These objects would have to be mutable since the actions that would be triggered almost always affect the objects internal state.
I have a working example of the code that I'm going to show, you can find it on my GitHub. I'm using a game of pong (well, more like work-in-progress part of a game that could be pong...eventually) written using GGEZ. This example is working, and while it isn't a playable game it does properly use the observer pattern.
With that, let's go through the code.
Sending an Event
Let's begin by examining how we are sending an event. In this case when the ball touches the left or right edge of the screen we'll want to send an event saying that the player or ai scored. I have an enum to cover this.
pub enum GameEvent {
PlayerScored,
AiScored,
}
The ball struct has a update function that is running every frame. It will check if the ball is touching an edge and will use the event system struct to send the event.
pub fn update(
&mut self,
screen_width: f32,
screen_height: f32,
event_system: &EventSystem
) {
// I have removed the code from this example to move the ball and
// bounce it off the top of the top and bottom of the screen. You
// can view the code I removed at
// https://github.com/brooks-builds/observer_pattern_in_rust/blob/master/oop/src/ball.rs#L25
if self.location.x + self.radius >= screen_width {
self.velocity.x *= -1.0;
event_system.notify(GameEvent::PlayerScored);
} else if self.location.x - self.radius <= 0.0 {
self.velocity.x *= -1.0;
event_system.notify(GameEvent::AiScored);
}
}
This example shows that emitting an event doesn't require that the struct be aware that it is being observed. This will make it easy to add a lot of events throughout the code.
Let's move onto the observer of this event next, the score.
Observing an Event
I have the score as a struct that stores the ai and player scores respectively. It is also storing the Text drawables that will be displayed each frame to the screen.
pub struct Score {
// I'm leaving out some of the non-essential information
player: u8,
ai: u8,
}
I'm also using a trait to mark that this structure is an observable.
pub trait Observer {
fn on_notify(&mut self, event: &GameEvent);
}
It's a pretty simple trait, and while I could just add the on_notify
function to the Score struct proper, I'm adding it through the trait to make it easier to add onto any structures that I want to turn into observers.
I've implemented the trait to watch for the events and increment the scores.
impl Observer for Score {
fn on_notify(&mut self, event: &GameEvent) {
match event {
GameEvent::PlayerScored => {
self.player += 1;
self.player_text = Self::create_text(self.player);
}
GameEvent::AiScored => {
self.ai += 1;
self.ai_text = Self::create_text(self.ai);
}
}
}
}
Now the score can do it's own thing and whenever it receives the player scored or ai scored events it will increment the appropriate properties and update the text that will be drawn to the screen.
We still have the problem of how we are going to have two mutable references to the same score available to the drawing system and the event system at the same time. The answer, of course, is to cheat and use a tool that is meant for something else entirely.
Enter Arc and Mutex
Arcs and Mutexes are often used together to allow multiple threads to have a mutable reference to the same instance of a struct. They do reference counting at run-time, so there is a slight performance hit but they guarantee that only one mutable instance will be mutable at a time. This is done using the lock
method where the thread will be blocked until no other function is attempting to mutate the struct contained in the Mutex.
We're going to have to wrap our score struct in an Arc and Mutex and store it in the arena struct so that we can draw the score to the screen. Because any structs using the score need it to be wrapped, I'm wrapping it up inside the new
static function on the Score struct.
pub fn new(screen_width: f32, context: &mut Context) -> GameResult<WrappedScore> {
let player = 0;
let ai = 0;
let player_text = Score::create_text(player);
let (player_text_width, _player_text_height) = player_text.dimensions(context);
let ai_text = Score::create_text(ai);
Ok(Arc::new(Mutex::new(Score {
player_text,
ai_text,
player_location: Point2::new(screen_width / 2.0 - player_text_width as f32 - 5.0, 0.0),
ai_location: Point2::new(screen_width / 2.0 + 3.0, 0.0),
player,
ai,
})))
}
The game arena simply stores the wrapped Score. It does have to unwrap it when it goes to call the draw method on Score though.
pub fn draw(&self, context: &mut Context) -> GameResult<()> {
self.dividing_line.draw(context)?;
{
let score = self.wrapped_score.lock().unwrap();
score.draw(context)
}
}
That lock method call is where the Mutex is blocking the thread to make sure that no other threads currently have mutable access to the instance of Score. Since this game is written in one thread we don't have to worry, we know that we will be the only ones that have access mutably or not, and it won't be for very long.
When we hand a wrapped score to the event system we aren't handing it a clone of Score, we are handing it a clone of the Arc that is holding the Mutex that is holding the Score. In memory, it is the same Score. This is how we can pass around instances of objects back and forth in Rust while maintaining the safety that the borrow checker gives us.
pub fn new(
screen_width: f32,
screen_height: f32,
context: &mut Context,
event_system: &mut EventSystem,
) -> GameResult<Arena> {
let wrapped_score = Score::new(screen_width, context)?;
event_system.add_observer(wrapped_score.clone());
Ok(Arena {
dividing_line: DividingLine::new(screen_width, screen_height, context)?,
wrapped_score,
})
}
The proceeding code shows the arena creating the score, keeping a copy for itself and also registering a copy with the event system.
Handling the Events
Finally, let's take a look at the event system. It is storing the wrapped observers in a Vector, and looping through them to call the on_notify functions on them whenever an event is emitted.
However we don't want to have to be limited to just storing the Score, or any one struct that is acting as an observer. We want to be able to turn any struct into an observer in the future and have the event system just work. We can do this by using the trait that we added to the Score struct earlier. This will allow us to set the type stored as the trait instead of the struct.
pub struct EventSystem {
wrapped_observers: Vec<Arc<Mutex<dyn Observer>>>,
}
The dyn keyword is using dynamic dispatch to get a pointer to the struct and then substitute that in at runtime. Of course this has a small runtime cost but it allows us to do what we want.
That was the last bit of magic to make this work, here is the full event system file in it's entire glory.
use std::sync::{Arc, Mutex};
pub struct EventSystem {
wrapped_observers: Vec<Arc<Mutex<dyn Observer>>>,
}
impl EventSystem {
pub fn new() -> EventSystem {
EventSystem {
wrapped_observers: vec![],
}
}
pub fn notify(&self, event: GameEvent) {
for wrapped_observer in self.wrapped_observers.clone() {
let mut observer = wrapped_observer.lock().unwrap();
observer.on_notify(&event);
}
}
pub fn add_observer(&mut self, observer: Arc<Mutex<dyn Observer>>) {
self.wrapped_observers.push(observer);
}
}
pub trait Observer {
fn on_notify(&mut self, event: &GameEvent);
}
pub enum GameEvent {
PlayerScored,
AiScored,
}
As I mentioned at the beginning, if you want to run the code, or look at it all at the same time then you can find it at [https://github.com/brooks-builds/observer_pattern_in_rust/tree/master/oop/src](https://github.com/brooks-builds/observer_pattern_in_rust/tree/master/oop/src)
I've been reading through the Game Programming Patterns book and implementing each pattern first in a JavaScript + P5.js infinite runner, and then a Rust + GGEZ infinite runner. If you liked what you read I'd love to hear from you. I'm streaming every weekday morning at 7:am Mountain Time for around an hour on Twitch.
I also have a YouTube Playlist of all the archived streams.
Thanks for reading and happy coding!
Top comments (4)
Nice! Thanks for sharing!
I've been looking into game programming a bit myself recently. The observer pattern you created is interesting. It reminds me a bit of event listeners in the DOM. Not exactly the same but some similar principles.
Yeah, it is a very event driven architecture
Do you know of any resources/books that go into functional game programming concepts? I've had a hard time finding any good resources on this.
It's not functional programming per say, but I've been enjoying game programming patterns recently