Top 10 celebrities who use component architecture!
The last blog I posted was Nov 2013. The blog section seemed stale for for a week or so so thought I'd share a change in design I recently did for our Dead Anyway game to spark conversation and ideas.
Our player script was just getting too massive and was doing too many different things directly inside of it. Adding features or modifying existing features was scary as hell. All the "helper" variables were adding up and the amount of hunting for what I needed in the script was pissing me off. So I decided to re-look at component architecture. After putting this into practice it's really makes programming more fun and changing things a lot easier and less fearful.
The first task was to look at this monster player code and break it down into domains. That meant looking at the high level functionality.
- Play sounds
- Take input
- Camera controls
- Movement controls
- Inventory
- FPS arms
- HUD
- Handle stats like health, hunger, thirst, etc
All of this was happening right in 1 script. Not cool. So I first hand to break all this functionality into their own scripts. However all this functionality works off each other. Even though they are separate when to do what and code X needs to know stuff about Y still exists. So if we want to break code out into it's own domain (and scripts) and reduce coupling on each other to avoid making fragile code AND they need to know about each other in various ways how would I do that?
Enter events. I treat each component as it's own little library that is designed to do it's one specific thing related to the game. So it of course has functions that act on it's data. But how would those functions get called? They would get called when events from other components were raised. So now a component has events to tell the outside world that something happened inside this component and functions to act on this components state. It doesn't care how it's functions get called and it doesn't care who is listening to it's events. It's blind to the outside world.
An example is the PlayerSound component. It loads sounds in it's init function and then it has functions like WalkForward(), WalkBackward(), StopWalking(), StrafeLeft(), StrafeRight(), Jump(), Eat(), Drink(). These functions simply play the right sounds. The PlayerSound code doesn't care about the input code and when you're in the PlayerSound script you don't either. Your mindset is just all about sound code at that point. You're thinking about what functions do I need for sound, and what possible events should I fire that have to do with sound? The sound script right now is about 100 lines of code (but it will grow in the future). It's nice and contained as it's own component.
Every component is like this. PlayerInput converts actions to events. It doesn't care who uses those events. It also has functions to map keys to actions. Who is calling those functions? Who cares. It's not my concern when I'm in coding the PlayerInput component. I just know I want to do that functionality at some point.
So how does this all get wired up you may ask? That's the interesting part. Your player script, instead of being an if nested disaster, simply becomes configuration code of linking component events to component functions (I call them actions). I noticed something interesting happen when I did this. Looking at the player script revealed what it's doing much simpler. It provided a nice high level overview of what the player is doing at a glance. I didn't have to read state variables and loops and branch statements to figure it out. I saw it all revealed to me. I feel like it works better with our brains. We think, when this happens do x, y, z. That's the component architecture with events exactly. That's all you see 1 line at a time. When this happens do this. Very little interpretation is needed.
Here is an example of what my player script looks like now:
PlayerLooting.onLooting:Subscribe(PlayerHUD, PlayerHUD.ShowProgressBar) PlayerLooting.onLooting:Subscribe(PlayerCamera, PlayerCamera.DisableUpdate) PlayerLooting.onLooting:Subscribe(PlayerController, PlayerController.DisableUpdate) PlayerLooting.onCancelLooting:Subscribe(PlayerController, PlayerController.EnableUpdate) PlayerLooting.onCancelLooting:Subscribe(PlayerCamera, PlayerCamera.EnableUpdate) PlayerLooting.onCancelLooting:Subscribe(PlayerHUD, PlayerHUD.HideProgressBar)
The PlayerLooting component has an event onLooting and onCancelLooting and those other components are subscribing to those events and telling those PlayerLooting events what functions they could call when that event is raised from within the PlayerLooting component.
You can plainly see the HUD shows the progressbar and the camera and controller controls are disabled. If we cancel looting we hide the progressbar and the controls are enable again.
Want to add a sound when looting? Just think about how easy that is to think about. No hunting for the right section and state in the old giant player script that is 1000+ lines. You simply:
- Add a loot sound variable and load a loot sound inside the PlayerSound script (you know exactly where to do this. if it's a sound it's done in PlayerSound component. easy right?)
- Make a function in PlayerSound to play said sound
- Make a function in PlayerSound to stop said sound
- Link the onLooting event to the PlayerSound play looting function
- Link the onCancelLooting event to the PlayerSound stop looting function
It's much more compartmentalized than the traditional design of putting all that stuff into the player script.
Here is the event code that I use:
if EventManager ~= nil then return end EventManager = {} function EventManager:Create(owner) local obj = {} obj.handlers = {} obj.owner = owner for k, v in pairs(EventManager) do obj[k] = v end return obj end function EventManager:Subscribe(owner, method) table.insert(self.handlers, { owner = owner, method = method }) end function EventManager:Raise(args) for i = 1, #self.handlers do self.handlers[i].method(self.handlers[i].owner, self.owner, args) end end
Inside a component to create an event you simply do:
-- note I just wanted to use Event but Josh exposed that for his UI stuff so I ended up with EventManager instead self.onmoveForward = EventManager:Create(self) -- events have a table that is an argument that will get passed to the function subscribed to events -- interestingly enough because it's a table, it's passed by reference so inside your action function if -- you change a value the component that raised the event can read that to perhaps act accordingly. -- I do that in a few instances self.onmoveFoward:Raise({ shiftKeyDown = false })
Then to subscribe to the event in the player script where all the components come together you simply call the Subscribe() function on said event passing in the component object and the component objects function/action to get called when that event is raised:
PlayerInput.onmoveFoward:Subscribe(PlayerSound, PlayerSound.PlayMoveFowardSound)
My future for this is to have an editor that reads all component events/actions and visually allows you to hook up events to actions. Then the player script simply reads the output file of said editor to hook things up. Then we'd have a nice visual of how our game works at a high level.
This was a long one but I'm really excited about making the switch. It was an easy transition to make and it makes adding/changing functionality really simple, and it gives me a high level overview of what's going on with the player which was increasingly getting more complex.
If you read this far on a weekend then go outside
- 8
16 Comments
Recommended Comments