158. 2D platform game #3 – Managing game objects
This post is all about programming.
Programming a game level with lots of objects can get messy very quickly. You can imagine a draw function with dozens of if statements testing whether you have collided with all the different objects.
Instead, this is my code in the draw function for drawing all the interactive objects (those which affect the player, like enemies, health and rewards), and checking for collisions.
for i,o in pairs(SS_Objects) do
o:draw()
o:collide(player)
end
Of course, there is a lot of code somewhere else to handle all these objects, but this approach keeps your main program tidy, and makes it easy to add new objects.
I can, for example, create an entirely new level map and run it straight away, without changing a line of code.
Let’s see how we can do this.
The basic design – tables and classes
Any time we have a lot of things we need to store, a table is a good place to start.
So I’ve set up a table for objects, called SS_Objects. (The SS_ prefix stands for side scroller, and is simply there to make it unlikely that someone using this library will define a variable with the same name as anything in this library).
This table contains a list of the objects, so that the draw function can loop through and draw them, as you see above.
But what about all the code that manages each object – how and where to draw it, how it moves and interacts with the player?
This is stored in a class. If you’re not sure why you would need classes, or even what they are, this game should help you see how useful they are. If you plan to do any serious programming, it’s very, very important to understand them. Classes aren’t just for huge programming projects – they can be very useful in fairly small projects like this, as well.
First of all, if you have no idea what a class is, I have written some posts here and here to try to explain them. And another here to show when to use a table or a class.
Assuming you have at least some basic understanding of classes, let’s move on.
The object class
My object classes need to do two main things
- specify what image to draw, and where
- check if the player has collided with the object, and if so, take action (reward or punish the player, or whatever)
So here is the basic outline of an object class called SS_A. I’ve numbered some lines and put notes underneath.
SS_A=class() function SS_A:init(x,y) --[1] self.pos=vec4(x-0.5,y-0.5,1,1) --[2] SS_Objects[self]=self --[3] end function SS_A:draw() --[4] SS_DrawSprite(SS_Sprites.SOMESPRITENAME,self.pos.x,self.pos.y) end function SS_A:collide(p) --[5] --code here to check for collision, take action --if the object needs to be deleted SS_Objects[self]=nil --[6] end
[1] – When the object is set up, it needs to know where it will be drawn, so it needs an x and y value as a minimum. Some objects will need more information. The x and y value is a tile position, so x=2, y=4 means the tile second from the left, four rows up from the bottom.
[2] – I store the x,y position. I’ll explain below why I deduct 0.5. The last two values of 1 are there in case you want to resize the object (eg o.5 means draw it half size).
[3] – Add our object to the SS_Objects table. I’ll explain this later, below.
[4] – the draw function will specify a sprite and the x,y position (in tiles, not pixels) – and maybe more. I wrote a special function called SS_DrawSprite because some objects need extra help, and it can take extra parameters I haven’t shown above. I’ll describe it later.
[5] – the collide function checks for collisions and, if they occur, does whatever is needed. The parameter p is the current position of the player.
[6] – delete the object. Explained below.
Choosing units to work with – tiles or pixels
One of the early problems I had, was choosing what units to work with. When you have a tiled screen, and your level design is a map with an object in each tile, it makes sense to use tile values rather than pixels – ie draw an object in tile 3,4 rather than at pixel 375,412. For example, if you ever changed the size of your tiles, or moved the whole map three pixels to the right, this would be easy if everything was measured in tile units, whereas if everything was in pixels, there would be a lot of work adjusting all the values.
We can then convert from tiles to pixels when we actually draw the objects.
But we want to draw most objects in the centre of the tile, so if we take the example of an object in the first tile x=1, y=1, the actual position for drawing is x=0.5, y=0.5. I make this adjustment when an object is set up (see note 2 in the code above). You might wonder why I don’t just include this in the SS_DrawSprite function, to save me repeating this code in every object class. The reason is that a lot of objects don’t take up a whole tile, so we don’t draw them in the centre. As a result, all my object classes start by adjusting the x,y value provided, to the correct drawing position.
How classes provide flexibility
We can put any code we like into the class functions for any object. This allows us to program any behaviour we need, without interfering with the rest of the code. The really nice thing about classes is that they allow you to write self contained code, and also to control what the rest of your program is allowed to do with them.
In this case, the main program is given just three functions for each object class – – init, draw and collide. This is what keeps the main draw function so neat. All it has to do is loop through the list of objects and run these functions.
Storing and managing a list of class objects
When I read in my level map, tile by tile, I figure out which object to add to that tile, and – supposing it is an object class called SS_A – I run this code
SS_A(c,r) --c,r are row and column tile position
This creates a new “instance” (object, if you prefer) of the SS_A class, running the init function. The code I showed above for the SS_A class includes this
SS_Objects[self]=self --[3]
The “self” value is simply a memory address (ie a long number) telling Codea where to find this particular class object. (If I add another copy of this same class, it will have a different self value).
So I store the address of the new object in the SS_Objects table, and I can use this in my main draw function, which, remember, looked like this.
for i,o in pairs(SS_Objects) do o:draw() o:collide(player) end
So the value of o in this loop will be the “self” address of each object, and I can use this to run the draw and collide functions of that object.
This is really no different from the way you usually use classes. If you say
a=SS_A(3,2) --create a new instance of SS_A --then in draw a:draw() --and later to delete a a=nil
then a is storing the “self” address. All I am doing is storing that address in a table instead. And because I’m doing that, I can just create an object with
SS_A(2,3)
without assigning the result to a variable as you would normally do, because we’ve done that inside the init function.
Deleting the object is also simple.
SS_Objects[self.id]=nil
This will delete the class object because the table is the only place its address is stored. However, it may take a couple of seconds before Lua’s garbage collector does this. If we want the deletion to occur immediately, we can force garbage collection with the command collectgarbage().
Putting it all together – Sample project
The code below walks a player along, colliding with three coins. It uses everything discussed above. You can try running it.
function setup() imgPlayer=readImage("Platformer Art:Guy Look Right") imgCoin=readImage("Platformer Art:Coin") --initialise tile size and object table tileSize=70 SS_Objects={} --add three coin class objects SS_A(3,2) SS_A(5,2) SS_A(7,2) --set initial position of player --last two values give width, height each side of the player --normally they would be 0.5, but the player doesn't take up --the full tile playerPos=vec4(1,2,0.4,0.4) end function draw() background(220) for i,o in pairs(SS_Objects) do o:draw() o:collide(playerPos) end --draw player and move him along 1/100 of a tile each time sprite(imgPlayer,playerPos.x*tileSize,playerPos.y*tileSize) playerPos.x=playerPos.x+1/100 end --object class SS_A=class() function SS_A:init(x,y) --adjust x,y positions to centre of tile --last two values are 1/2 width and height --normally they would just be 0.5 but our --picture doesn't take up the whole image self.pos=vec4(x-0.5,y-0.5,0.3,0.3) SS_Objects[self]=self --store object in table end function SS_A:draw() --convert from tile values to pixels and draw sprite(imgCoin,self.pos.x*tileSize,self.pos.y*tileSize) end function SS_A:collide(p) --delete object if player collides if SS_ObjectsOverlap(p,self.pos) then SS_Objects[self]=nil end end --AABB collision function function SS_ObjectsOverlap(a,b) --a,b are vec4(x,y,w/2,h/2) return not (a.x-a.z>=b.x+b.z or a.x+a.z<=b.x-b.z or a.y-b.w>=b.y+b.w or a.y+a.w<=b.y-b.w) end