Now that we have our game framework in object oriented format, it’s time to extend it further so that we can add game entities, load resources and run our game simulations via the framework. Let’s begin by looking at what all needs to be added to our basic game framework. Remember, I am not going to post the entire code from now on. You can refer previous chapters to find out what each function does, or you can refer the full source code posted at the end of this chapter.
In chapter 3 we briefly talked about Events and Actions. In this chapter we will integrate an event system within our framework. Our entire framework will be event based, so it’s better to get into this now and make it the backbone of our framework. We will call it — well, the Event System. A note I would like to add here is — there are many ways to model a event system each having it’s advantages and disadvantages. We will be using a rather simple but a popular python event system which is easy and concise to implement. The code is as follows –
class Event(): '''The Event class.''' def __init__(self): '''Create a list of handlers.''' self.handlers = [] def __iadd__(self, handler): '''Adds a handler to the list of handlers.''' self.handlers.append(handler) return self def __isub__(self, handler): '''Removes a handler to the list of handlers.''' self.handlers.remove(handler) return self def Fire(self, *args, **keywargs): '''Calls all the handlers associated with this event.''' for handler in self.handlers: handler(*args, **keywargs) def ClearObjectHandlers(self, obj): '''Clears the handlers for a particular object.''' for handler in self.handlers: if handler.im_self == obj: self -= handler def ClearAllHandlers(self) : '''Clears all the handlers currently associated with the Event.''' self.handlers[:] = []
As you can see it’s not too complicated. An event is modeled as a class that maintains a list of event handlers which it invokes one by one when fired. The __iadd__ and __isub__ are overloaded arithmetic operators += and -= that append and remove event handlers to the event handler list. The usage of the class will become clear when we look at an example. Let’s take a scenario from an epic space battle and model two simple classes, one that fires events and other that responds to those events.
class Captain(): def __init__(self): self.ArmWeapons = Event() self.FireWeapons = Event() class StarShip(): def __init__(self, name, captain): self.name = name # bind the captain's events to event handlers captain.ArmWeapons += self.OnArmWeapons captain.FireWeapons += self.OnFireWeapons def OnArmWeapons(self): print self.name + ' : \"Phasers armed and ready.\"' def OnFireWeapons(self): print self.name + ': \"Firing Phasers.\"'
Here we have two classes Captain and StarShip. The Captain is the controller of the StarShip and it’s but obvious that the ship respond to his events. The Captain has two events — ArmWeapons and FireWeapons, which are connected to the ship’s event handlers OnArmWeapons and OnFireWeapons respectively in the StarShip’s constructor. Let’s check further how the Captain can invoke the StarShip’s event handlers.
if __name__ == '__main__': Picard = Captain() # let's make Picard the captain of the Enterprise Enterprise = StarShip('Enterprise', captain=Picard) print '\"This is Picard, the captain of the Enterprise. Prepare for battle.\"' Picard.ArmWeapons.Fire() print '\"This is Picard. Fire at will.\"' Picard.FireWeapons.Fire()
The output of the above code is –
"This is Picard, the captain of the Enterprise. Prepare for battle." Enterprise : "Phasers armed and ready." "This is Picard. Fire at will." Enterprise: "Firing Phasers."
As you can see we have 2 objects Picard and Enterprise. The Picard object is an instance of the class Captain and the Enterprise object is an instance of the class StarShip. We passed the Picard object as a captain in the Enterprise’s constructor. The event handlers of the StarShip Enterprise were bound to the events of Captain Picard there. When Picard fired the events, the event handlers of Enterprise were invoked.
It’s important that you understand the association of the two objects and how the two object communicated via events. Such inter-object communication via events will be the basis of our game. This is one way interaction, but objects can also communicated both ways via events.
But what happens if Picard takes charge of more than one ship? Will the same event system work in that case? Well, only one way to find out –
# Ok, now let's make Picard the commander of 4 more ships Voyager = StarShip('Voyager', Picard) Defiant = StarShip('Defiant', Picard) Excalibur = StarShip('Excalibur', Picard) Challenger = StarShip('Challenger', Picard) print '\"This is Picard, the captain of the Enterprise. I am taking command of all the ships. Prepare for battle.\"' Picard.ArmWeapons.Fire() print '\"This is Picard. All ships fire at will.\"' Picard.FireWeapons.Fire()
… and the output is
"This is Picard, the captain of the Enterprise. I am taking command of all the ships. Prepare for battle." Enterprise : "Phasers armed and ready." Voyager : "Phasers armed and ready." Defiant : "Phasers armed and ready." Excalibur : "Phasers armed and ready." Challenger : "Phasers armed and ready." "This is Picard. All ships fire at will." Enterprise: "Firing Phasers." Voyager: "Firing Phasers." Defiant: "Firing Phasers." Excalibur: "Firing Phasers." Challenger: "Firing Phasers."
We made Picard the captain of 4 more ships (along with the Enterprise) and all the ships correctly responded to his Events. The event was fired only once, but all the event handlers were invoked. So our event system seems to correctly invoke more than one handlers connected to an event. Fine, let’s move on and integrate it with our framework.
Since we will be working with a lot of events throughout our game, I chose to create a separate file event.py and put the event code in it. It’s exactly the same code that we wrote for the Event class earlier in the chapter. Along with this there are some changes that need to be made to our GameManager class. As you may recall SFML gives us windows and system events. Unfortunately SFML events are not compatible with our event system out-of-the-box. We need to translate those SFML events into our events — and that will be our first task. First we create the events themselves –
def Init(self): ... ... self.closeEvent = event.Event() self.resizedEvent = event.Event() self.lostFocusEvent = event.Event() self.gainedFocusEvent = event.Event() self.textEnteredEvent = event.Event() self.keyPressedEvent = event.Event() self.keyReleasedEvent = event.Event() self.mouseWheelEvent = event.Event() self.mouseLButtonDownEvent = event.Event() self.mouseRButtonDownEvent = event.Event() self.mouseMButtonDownEvent = event.Event() self.mouseLButtonUpEvent = event.Event() self.mouseRButtonUpEvent = event.Event() self.mouseMButtonUpEvent = event.Event() self.mouseMoveEvent = event.Event() self.mouseEnteredEvent = event.Event() self.mouseLeaveEvent = event.Event() self.joyButtonPressedEvent = event.Event() self.joyButtonReleasedEvent = event.Event() self.joyMovedEvent = event.Event() ... ...
This is done in the Init() function of the GameManager. Now any game object that wants to receive events can add it’s handlers to these events of the GameManager. But, when do these events fire? They are fired when SFML gives us an event. We do the translation in the function TranslateSFMLEvents, which is the first thing called inside the game loop. The code for that is something like this –
def TranslateSFMLEvents(self): '''Translates the SFML events to Game Events.''' sfmlEvent = sf.Event() while self.window.GetEvent(sfmlEvent): if sfmlEvent.Type == sf.Event.Closed: self.closeEvent.Fire() elif sfmlEvent.Type == sf.Event.Resized: self.resizedEvent.Fire(sfmlEvent.Size.Width, sfmlEvent.Size.Height) elif sfmlEvent.Type == sf.Event.LostFocus: self.lostFocusEvent.Fire() ... ... ...
(The entire code is available at the bottom of the chapter.)
If you observe closely, you will see that for some events the Fire function takes arguments. Well the fact is that events are used to communicate information and for some events there is sometimes the need to pass arguments. For example, for the Resize event, new width and height is passed as an argument to the function Fire. Correspondingly every handler attached to that event Resize will receive width and height as arguments. Most events that we will be using and creating, will have arguments in the Fire function. That’s just an easy way to communicate instead of cryptic callbacks.
We have a long list of events in the Init function of our GameManager. Most of these events will be used later on but for now let’s use the closeEvent to close our window. So our original code for closing the window turns event based into something like this.
def Init(self): ... ... ... self.closeEvent += self.OnClose .... def OnClose(self): self.running = False
That’s about it in this chapter. We will be using this event system even with things like Rendering and Updates later on. For now you can fiddle around with the SFML events and see how each of those events are invoked. In our code the GameManager simply prints out the event and it’s arguments.
2 responses to “A Dash of Game Development – 6. The Event System.”
Wow, you explained that very well. You should publish this as a book when you complete it! Great stuff in here!
🙂 Gald you liked it.
Well, I don’t know about a book but the whole idea about these articles is to avoid long explanations and let the game developer/programmer experiment with actual code.
Maybe I will put it in a shot book when I finish.
Thanks