In which we make classes for the player and an enemy
Now, we have the start of some gameplay, and in particular of a player-character, taking form. But everything is haphazardly placed in our main “Game” class. If we keep building our player-character as we have been, and especially if we also build our enemies in a similar fashion, our code could get very messy, I fear.
So, it’s time for some object-oriented development.
As I said at the start of these lessons, this is not a game-design tutorial. Furthermore, little of what I intend to cover in this lesson is terribly Panda-specific. As a result, I intend to somewhat skim over this part. However, I do intend to at least in brief describe what I’m doing here!
To make our code neater and easier to navigate, we’ll put these new classes into a file of their own, named “GameObject.py”
For the purposes of this tutorial, we will be creating three types of entity:
The player
An enemy that moves towards the player and attacks when in range
And a “trap” that moves in straight lines across the level, damaging player and other enemy-type alike.
These three all have a number of traits in common: they all have 3D models, they all have colliders, they all move, and so on.
So, we will create a base-class that contains these common elements, called “GameObject”. Our player and enemies will be sub-classes of this base-class.
Furthermore, our two enemy-types both have their own elements in common, in particular that they will run some sort of logic that governs their actions. So they will derive from a common sub-class of “GameObject”, named “Enemy”.
In short, the “GameObject” class will have two sub-classes: “Player” and “Enemy”. The “Enemy” class itself will have two sub-classes: “WalkingEnemy” and “TrapEnemy”. We’ll create the “TrapEnemy” class only later–for now we’ll just make the “WalkingEnemy” class.
The shells of our new classes look like this:
In a number of places below, we’ll want to access “cTrav” and our “pusher” handler. This is done via a Panda-provided globally-accessible variable called “base”, which provides a reference to the game’s “ShowBase” object–and since that’s our “Game” object, that reference is also a reference to our game.
“GameObject” will store a given character’s actor and collider, as well as handling its velocity and movement, and the basics of its health. It will also provide a “cleanup” method, for when we want to remove a character.
Something that is worth noting above is the pair of “Python-tag”-related methods–these two:
What I’m doing here is storing a reference to the GameObject in the collider. When a collision happens, we have access to the colliders involved–but we likely want access to the related GameObject, too. This provides that access.
However, there’s a caveat here! The GameObject is storing a reference to the collider, and by adding a Python-tag pointing to the GameObject, the collider now has a reference to the GameObject. That means that we have a circular reference, which can result in the object not being properly garbage-collected.
Thus, when we clean up the GameObject, we clear the Python-tag, and so break the circle, allowing the objects to be garbage-collected.
The “Player” class holds pretty much the same player-logic as we’ve thus far had in our “Game” class, save that the movement-controls now alter its velocity, rather than just moving it:
Regarding that animation code, the basic idea is that if a character is “walking”, then it should loop its “walk” animation, if it wasn’t already. If it’s not walking, then, as long as it’s not playing another animation, it should loop its “stand” animation, if it wasn’t already.
There are probably better, or at least more-elegant, ways of doing this (a state-machine–which Panda has a class for–comes to mind). But for our purposes, this will do.
You may also notice further animation code, to similar effect, in the “Enemy” class below.
The “Enemy” class provides a stub “runLogic” method, which is intended to be overridden by its sub-classes, and calls this method in its “update” method. It also provides a “score” value, for when it’s killed:
And finally, the “WalkingEnemy” class overrides the “runLogic” method of the “Enemy” class, providing code that has it walk towards the player until it reaches an “attack distance”. It also turns to face the player, using the vector between their positions, and the “signedAngleDeg” (i.e. “get the signed angle, in degrees”) method provided by Panda’s vector-classes.
With all that done, we want to use these classes in our game.
First we remove the player-code that we had in the “Game” class: “tempActor”, its collider, the logic that adds that collider to “pusher” and “cTrav”, and the code that checks the key-map to move “tempActor”. Note that “GameObject”, above, now handles that actor and collider logic, and “Player” handles the key-map checking.
Then we import our “GameObject” module, and–for now–create a temporary instance of each of the “Player” and “WalkingEnemy” classes. This isn’t how we’ll handle them in the end, but it will serve for testing as we build up the classes.
So, in “Game.py”:
If you run the game now, you should be able to move the player, much as before–but you’ll also be chased around (harmlessly–for now) by an enemy!
Next, let’s see how we handle collision events, via our trap enemy…