itch.io is community of indie game creators and players

Devlogs

The Observer Pattern

Sando
A downloadable game

Another devpost about my experiences while making my first game of a decent size. There are so many mistakes that you dig yourself into a hole with! This was probably the most tedious fix, but the process of wrapping my head around it was one of my greatest "a-ha" moments I've had so far as a game developer. 

Getting Caught with my Hand in the Cookie Jar

The main gist of the issue is that you want something to occur when something else occurs. Simple enough, that's a brain-dead conditional if statement right?

if this then that end;

Okay, but now let's say when I do "this," you should do "that!" How will you know that I'm doing something? "Well, I'll just check every frame," you say, naively (please, play along, this is going somewhere).

if cake.isDoingThis then reader.doesThat() end;

Okay great, you're going to check every frame, but now let's take a step back and look at the scene. Let's say we are in a combat scene, and I start doing some action. The reader object can't do it in their own update function, because they can't see the cake object's state. So you check every frame in combat!

function combat:update(dt)
    if cake.isDoingThis then reader.doesThat() end
end;

Hmmm, there's something off here. Now combat is dipping its grubby little fingers into the functionality of other objects. This is a classic case of coupling. At first, I thought it wasn't a big deal. "Game development is messy!" I just needed to get this UI Component to work, then I wouldn't have to look at this messy code anymore. Here is what JUST THE INPUT READING looked like by the time my custom icon carousel was working. EDIT: another image that is too big... I will leave this here to work on my file size hubris

<img src="<a href="https://img.itch.zone/aW1nLzIxMjYyOTk5LnBuZw==/original/Amcalz.png">https://img.itch.zone/aW1nLzIxMjYyOTk5LnBuZw==/original/Amcalz.png</a>">

Who controlled the ActionUI's input reading? Oh boy, well...

Observer Pattern

The observer pattern achieves something you probably understand intuitively, even if it doesn't make much sense when you look at a diagram that explains it clearly. If you are the type that can understand just the diagram, then here:

If you're like me, and this diagram doesn't explain enough, I might have a decent analogy. Imagine you run a bakery. Everyone shows up at 4 A.M., ready to start preparing for the day. You need to mix dough, but you also need to start baking the dough that was prepared yesterday. You have donuts that need to be fried, too! On top of that, you have non-bakers who showed up early because the place needs to be cleaned, the coffee machine needs to be repaired, and someone (I) left a couple dishes in the sink because I ate dinner at the bakery before heading home and didn't clear the sink. Now, everyone in the bakery is listening to you. When you say, "GO," you want everyone to start on their relevant tasks for the day. These people are your Observors and, if they were blocks of code, they would have a lambda function defined that preps them for your signal. For example, if I were the mixer and you said to start, I would have something like this (using hump.signal for some assistance):

-- somewhere, anywhere, you give the signal
function manager:morningInstructions
    Signal.emit('go')
end;
function cake:init(stats)
    -- ...... initializations with my stats, then registering signals
    Signal.register('go', 
        function()
            self.startMixingTasks()
        end
    );
end;

When you say, "go," you don't have to tell me what to do, I already know what tasks I need to start. I get the ingredients, I measure them, I mix them. What's beautiful about this, is that everyone knows what they need to do, so long as they are registered as an observer to your go signal. For example, your pastry chef can have the same signal registered and they will do something different.

function marco:init(stats)
    -- ...... initializations with my stats, then registering signals
     Signal.register('go',          
        function()             
            self.startLaminatingCroissantDough()         
        end
     ); 
end;

Now, Cake is in control of what happens with mixing, and Marco is in control of the pastry dough. The manager doesn't need to be a part of the hierarchy that folds the dough and checks the temperature of the water! Now it's easier to abstract things behind functions because the manager doesn't need a flat structure to access everything in. Going back to my ActionUI example, here is how I was able to simplify some of the code to reduce code bloat and let base classes do more of the heavy lifting.

Much cleaner, way less code to read, and if you aren't sure what the buttons should be doing, that's because the answer is where it should be, in the button classes.

Takeaways

USE THE OBSERVER PATTERN. If you aren't using signal based communication, you're going to have an absolute headache when you realize that you've created so much technical debt, you've set yourself back weeks with a refactor because all of a sudden, your main character can't attack a mob without every single piece of your character changing state to say its okay. Now, you can just emit a signal and everything involved in the process will snap into action. There's a reason why so many engines, like Unity, have the Observer Pattern built into their functionality and force it on you as early as possible (see the Event system). Thanks if you read this far, and I hope to have some more fun updates about actual content next month!

With LÖVE ❣️

CakeJamble

Download Sando
Leave a comment