It’s time for some housekeeping before the New Year! We’re going to change the design of our code completely in this Issue to support us as we venture forwards in 2011. We’ll also take some time to explain the water we added in Issue 4 and add some sounds effects along with a basic HUD.
We have mentioned code refactoring a few times historically. As a project grows it becomes harder to maintain, especially when taking a rapid development approach as we have done to date. It is now time to see some of the benefits of Object Oriented programming in action. We first discussed this back in Issue 2 but only really in passing/preparation.
The game code is now growing. It’s around 300 or so lines of Python. If we continue down this path it is only going to get worse. The problem, in a nutshell, is that we have everything in one class (the ‘MyApp’ class). The solution, also in a nutshell, is to split this class into several. Before we do that we’ll discuss the key points surrounding Object Oriented programming.
Lastly, watch out for our other changes! During this refactoring we have made some alterations and improvements. Watch out for them, in particular our collision detection improvements and updated camera trickery!
1. Sets of Three
Objects and Classes: If you have not heard of either before please refer back to Issue 2 of MGF Magazine where we first introduced them. You might also find the search box at the top-right of this page useful for digging out material we have previously covered. If you’re impatient, the short version is that Classes are ‘object templates’ or ‘skeletons’ while Objects are instances of Classes (declared variables of a given Class type). For example, we might have a Class ‘HumanDNA’ and an instance ‘OzzyOsbourne’.
An Object has:
- An Identity. Every object has a name. The variable name. In your game currently we create an instance of ‘MyApp’ and assign it to a variable called ‘app’. Thus, we have one object of type ‘MyApp’ and its identity is ‘app’.
- Behaviour. When you call a method on an object you instigate a behaviour. In your ‘MyApp’ class there is a method called “createEnvironment”. When it is called (executed) it creates the environment. We control an objects behaviour by controlling what methods we call on it and when.
- A State. The state of an object is captured via its member variables. For example, in ‘MyApp’ we keep track of the players speed via a variable – self.speed. The ‘state’ of an object is a snap-shot of all of its member variables and their values at any given moment.
Object Oriented programming allows for:
- Inheritance. As discussed back in Issue 2, a class can ‘inherit’ from a parent. The derived class has everything the parent class has ‘plus more’. MyApp inherits from ShowBase in your code – which is why the game window is automatically created for you (amongst other things!). ShowBase does a lot of things for us leaving us to concentrate mainly on game logic. Parent classes tend to hold ‘common case’ code while derived classes implement ‘specific cases’ (e.g., a 3D shoot em’ up as we are writing here!). Using our ‘HumanDNA’ example from above we might have a base class ‘DNA’. We could derive a new class ‘BipedDNA’ from which ‘HumanDNA’ would inherit as would ‘MonkeyDNA’. Whole ‘class trees’ can be defined in this way. All common code is abstracted into parent classes while specifics fall into derived classes. It is not, however, clear cut. There is nothing to stop us having an instance of ‘BipedDNA’. Despite it being a parent for HumanDNA and MonkeyDNA, it is still a class in its own right and an object of that type can be declared.
- Encapsulation. Otherwise known as ‘data hiding’. We have not yet discussed this in any detail. Using our summary of objects from above, encapsulation implies we should only interact with our objects via their behaviours (methods). Their underlying data (state) is not something we should have to worry about. For example, if we had a Car object we might want to simply say “car.accelerate()”. We don’t care what is going on internally in the accelerate behaviour. We don’t care what variables are involved or used in the Car object itself. The Car is like a black box – we interact with it via its interface (methods) but do not actually care how it works internally so long as it works! You’ll see why this is important below when we discuss maintainability.
- Polymorphism. A big word that essentially means “same interface, different behaviour”. Polymorphism is implemented using inheritance. Suppose you were writing a drawing application. The user can select to draw circles, squares, trapezoids (etc.) on the screen, drag them around, reposition, rescale and so on. In the code, it would make sense to keep all of the shapes together in one data structure (variable) for ease of access. In Python, we can achieve this via a list. You’ll recall we introduced dictionaries in Issue 3 as name/value pairs. A list is similar but is just values. Both are really types of what we call ‘arrays’ in programming. An array is a collection of values where items are accessed via indices. All of this is best illustrated with a code snippet:
class Shape: def __init__(self,name): self.name = name def draw(self): # nothing to do pass class Square(Shape): def draw(self): # some code to draw a square on screen class Circle(Animal): def draw(self): # draw circle on screen shapes = [Square('mysquare1'), Circle('mycircle1'), Square('mysquare2')] for shape in shapes: shape.draw() # the above code would draw 2 squares # and 1 circle on the screen
Running through the code, we begin declaring a Shape class. It has a ‘draw’ method that doesn’t do anything. We then have Square and Circle. Two new classes, both of which inherit from Shape. Thus, they provide everything Shape does and ‘a bit more’. They too provide a ‘draw’ method, only this time, it does something. For Square it draws a square, for Circle it draws a circle. Their methods override the version provided by the parent (Shape).
As an aside, it would be possible for either or both of Square and Circle to, in their draw methods, call the parent (Shape) draw method. You’ve done this already in the second line of your MyApp class where you called the ShowBase constructor. It’s not necessary in this case (example) but we thought it note worthy because it highlights that overriding is not the same as replacing.
We then create a list containing 3 shapes. Two squares, one circle. In a similar way as to how we iterated over our collision queue in Issue 4 we now iterate over our list of shapes. For each one, we simply call ‘draw’. We do not care whether or not it is a Square, Circle, whatever. In fact, we probably wouldn’t even know. In the ‘real world’ case of our drawing application the user can draw however many circles and squares they want in any permutation (order). ….and that’s it! Polymorphism is “same interface, different behaviour”. It is, however, a seriously powerful concept! You’ve used it already without even knowing. Remember our discussion of PandaNodes? That’s how they work. It doesn’t matter whether it’s a model, light, lens or ‘whatever’, they all support the same interface (setPos, getPos, etc.). Behind the scenes, the PandaNode is the base class while the others are derived from it:
Concepts out of the way, what are the benefits?
- Reuse. A good design will lend itself to reuse. If you had created a ‘Car’ class or ‘HumanDNA’ or ‘whatever’, wouldn’t it be nice if you could take the same class and use it in multiple projects? That is reuse. Inheritance is also reuse, separating the common code in a single application so it is not repeated. Repeating code is generally a bad idea because if the code turns out to be wrong or needs changing for some other reason, you have to locate all occurrences and change them. Always aim for a ‘single point of change’ when coding! In our game, we have a fighter plane, there is no reason why the same fighter plane could not be used in a completely different and unrelated project.
- Extensibility. We might not be able to directly use our fighter plane in a different project, it might not be exactly right. That does not stop us from creating a new class that inherits from our fighter plane and addresses its short-comings. A good design is easy to extend.
- Maintainability. As you have already seen, the game in development has become increasingly difficult to work with and maintain. By splitting out into multiple classes we find ourselves with a code base that is easier to work with and maintain. The bottom line is ‘code readability’. Going back to encapsulation, suppose we discovered that most of the code in ‘HumanDNA’ was wrong. We rewrite the entire class. It should have absolutely no impact on any other part of the project at hand so long as the interface (methods and their parameters) stays the same. Again, it is the black box “don’t know, don’t care” approach. It is for this reason you should always use methods to access member variables as opposed to directly changing an objects internal state. Using the fighter plane example, suppose the rewrite resulted in “no need for a speed variable”. If you had referenced that elsewhere in your code it would now fail. Always interact with an object via its methods only wherever possible.
Before we proceed it is worth mentioning two further concepts – Cohesion and Coupling. Keep both in mind whenever you are at the design stage of a project. Object oriented designs should be loosely coupled but highly cohesive. The fighter plane class is of little use in any other project if it depends on other parts of the game/project it was first created in. If there are dependencies that make it hard to reuse a class, it is said to be tightly coupled. Cohesion is exactly as it sounds – your objects working together in (as close is as possible to) perfect harmony. Your design should make logical sense and be cohesive. Sometimes, people develop all manner of classes and interactions that do not make sense and are not efficient. Your design should use suitable relationships and interactions (only) between objects that need it. In summary, your design should make good logical sense but should not forge dependencies wherever possible.
Quick Nav: Next page |