Well not too long ago I stumbled across the excellent book Game Programming Patterns by Robert Nystrom (which is available in all sorts of formats including on the web for free so check it out!) and finally someone was writing things in a way which made sense to me!
What I think really made the difference was that Robert was speaking about the patterns as they relate to game design - which is something I think about constantly - so I could totally relate to the concepts.
What struck me as interesting upon finishing the book is that I had been creating/using recognized patterns on my own and I didn't even know it! For instance I had implemented both a Singleton and Observer-like pattern for my Enemy Manager.
The Enemy Manager class of course was a singleton in that only one could exist in a scene (we don't want 12 different sources trying to control the NPC's after all do we?) and the Observer pattern came in as the Enemies themselves were subscribing to notifications from the Enemy Manager regarding game states and if it was time to take a turn or not ( part of how I implement my phased base moment instead of strictly turn based or hard coding it by elapsed time).
But that's actually the whole point of design patterns - they are things that software engineers often find themselves doing over and over again and they realize "hey I've been doing the same thing as John over there - there's a pattern here we are all following in our designs!" So they establish a 'design pattern' so they can talk about its design concepts more easily as well as make as good of an implementation as possible by sharing it and improving it, and then re-using it in the future.
And so after putting 1 + 1 together (Robert's book and the insight that I had already been doing design patterns) I decided to take a more purposeful approach at implementing a design pattern next time .. which is what I'm talking about today.
But I'd like to talk about it in concrete terms rather than abstract coding ideas for two reasons:
- Robert's book (and many other books) have already done a better job explaining design patterns than I will likely be able to do for a few more years and;
- It's clear through my own experience that there are people who need really concrete examples to start fitting design patterns into their tool belt and I can add value here!
Why is it valuable to think about and learn about design patterns to me ? Because in my day job I work in a software engineering career and learning design patterns is going to get me farther in my career so it's my personal goal to always be learning something new and design patterns is on the list.
Whether I approve of design patterns or not they are used in the industry. I decided I'll learn them by implementing them in my games - an approach that I think really works because I'm having fun while I do it!
So without diving in to judgements about if you should use them or if they are good, evil, or indifferent .. here we go!
I think the first thing that is a barrier to using a design pattern is being able to identify a design issue that is coming up in your game that could benefit from a design pattern to solve it.
So I want to lay out what my design challenge was for The Rise of Dagon that lead to the use of the Observer pattern as an example in hopes that it might help you evaluate how you can use them yourself.
So in image 1 here let's take a look at the first problem:
Image 1: A floor pressure plate needs to open a remote door. |
As you see we have a typical pressure plate on the floor, when the player steps on it (or perhaps drops something heavy enough on it) the door off in the distance needs to open.
This problem by itself is fairly simple to solve one at a time, but imagine you have a game that has several dozens of doors in it? You would probably want to make some reusable script to make this easy to implement right? You certainly wouldn't custom code every door in the game!
Image 2: a wall trap needs to fire from the same kind of pressure plate |
In Image 2 we have a typical wall trap, it might shoot a spike, or a fireball as an example when the player steps on the same kind of pressure plate we noted in Image 1.
Now imagine we had written a very custom script for the plate to open the door (for problem #1) - we would now have to write another very custom script to fire this trap also!
If however we had written some sort of reusable script with for the door scenario .. we might be tempted to add a few extra parameters to it to handle the special situation of this wall trap? That certainly sound better than writing a dozen or so custom trap scripts doesn't it?
But wait.. we are not done quite yet!
Image 3: A covered pit trap - once again needs to be opened and closed by the same kind of pressure plate! |
Now if I were writing custom scripts .. by problem number 3 shown here in Image 3 (above) I would be pulling my hair out. How many pit traps in the game on top of all the doors and wall traps do we need to have before this just can't be even considered reasonable to be writing a custom solution?
And finally we have problem number 4 ...
Image 4: We need to be able to open this door with a lever seen on a nearby wall. |
Sounds easy enough, right?
But what if the lever was on the opposite wall of the room? Or even halfway across the dungeon level? All of a sudden its looking awfully convenient to have it sitting right there next to the door isn't it?
But yes, really, we do need to have levers all over the place! Specifically not near the door so we can create interesting game play situations for the player to solve.
Certainly I'll admit each of these could be solved with a fairly nice script that has some parameters to solve each of the four scenarios individually.
We could add parameters and public variables (in this case I'm using Unity 5) so I could put a public GameObject and drag it in as the target for each item that needed triggering ; this would create a fair amount of burden on me in the editor to associate everything together.
But now let's raise the complexity bar one level higher!
Two Plates - One Door! |
All of a sudden we have added an additional layer of complexity to the trigger problem!
What if there were 9 triggers? What if some had to be on and others off?
A custom script for each of these increasingly complex and individual needs should quickly convince you to engineer a better solution!
The first thing I like to do when I'm working on a problem like this is create a visualization - at work I have a whiteboard that I use for this purpose - but a regular $1.99 notepad I use at home works great too!
Creating software often creates really large areas of abstract ideas that can be hard to form in to concrete ideas - so taking a real physical medium and either doing a sketch or pseudo code is a great technique to help your mind form that concrete!
So I came up with a diagram like this :
Triggers on the left, Receivers on the right. |
What we see here is the items that can trigger something are on the left, while the item that can receive information about a trigger are on the right.
When I created an image like this - that was when the observer pattern became apparent to me.
I needed to be able to have the Receivers know when triggers were being hit. The way they do this is by subscribing to a message service -- or 'observing' it if you will that lets them know when a trigger has been pressed.
At this point four things were needed to make this work:
- Create an Observer class ; in this case well call it GameTriggers
- In the script for Triggers have them send the GameTrigger class a message that they have been turned on or off and their internal ID number
- In the script for the Receiver items subscribe/observe the GameTrigger events so they can be informed when triggers are hit
- Create a script that performs the actions needed when the appropriate trigger(s) are hit (whether one or ten) for the receiver useage case (door, trap, etc)
Now the GameTrigger class has work to do; it keeps a list (or whatever appropriate collection you want to use) of all the subscribers/observers for the kinds of messages that it handles.
When a message is sent to GameTriggers it iterates through the collection and sends a message to each of the subscribers - not caring if they are the one that actually needs that particular message or not; that is up to the receiver of the notification.
When the receiver gets the message it has logic that in my case says "is the trigger ID that was sent the one(s) I'm looking for? If so then do something about it!
That something could be "open the door" or "close the pit" or "fire the trap!".
Once I had this working for the first scenario it was very easy to put it in place for all of the others because the process of sending events and subscribing to them is the same for all of the use cases described!
Furthermore I can now use this same event/trigger system for other types of events in my game. There's no reason this has to be limited to use of opening doors and traps.
Some example uses for the trigger events:
- Turn a teleporter off/on
- Spawn a monster
- Alert a monster to the players presence (e.g the player walks in to a well lit area now they can be 'seen')
- Turn the lights off! (or on)
And that is how I implemented the Observer pattern in The Rise of Dagon. And I can definitely state that by implementing it in my game play - something I understand quite well - I was able to reach a much better understanding of the Observer pattern than I had been able to by reading books and web pages about it.
Nothing beats doing something yourself when it comes to learning!
No comments:
Post a Comment