Tiled Level Sample - Part 2

Overview

This sample builds on the first tiled level sample by introducing a better system for handling objects. It also introduces two new objects, coins and blobs. The coins are collectable objects and the blobs are enemies. In addition to extending some parts of the code in part 1 two files have been introduced that deal with the new objects, Coin.cs and Blob.cs.

Object management

In the first part of this sample there was only one object, the player. A reference to the player object was held in the main Game class and its Update() and Draw() methods were explicitly called. We could continue in this way and store all the objects we create in explicit references in the Game class but we would eventually find we had a large number references and we'd be doing very similar things to all of them.

In the first part of the sample the Player class was derived from the GameObject class. This may have seemed unnecessary at the time but what it lets us do now is store a List of GameObject's in the Game class. The Update() and Draw() methods of Player are declared in GameObject as virtual so we don't actually need a reference to a Player object to call the methods. Polymorphism lets us call those methods through the base class. So long as we override these methods where appropriate when creating new objects derived from GameObject then they can be added to our list of GameObject's and updated and drawn correctly.

We now declare a 'List<GameObject> objects' variable in the Game class. So long as an object is derived from GameObject we can add it to the list by using the Add() method, ie. objects.Add(new Player()). Instead of calling player.Draw() and player.Update() in the Game class we loop round all the objects in the 'objects' variable and call their Draw() and Update() methods.

Why not use DrawableGameComponent?

In XNA there is a class called DrawableGameComponent which you can derive your game objects from. It provides Draw() and Update() methods which you can override and will be called at the appropriate time to draw and update the object. This would prevent looping through the objects in the Game class and save a bit of code, so why haven't I done this? The honest answer is that I prefer having more control over the objects.

If we were to pause the game then we'd still want to call the Draw() methods on objects but not the Update() methods. While there is an 'enabled' flag in GameComponent (the class DrawableGameComponent is derived from) you'd have to go through all the objects and set this flag when the user pauses and unpauses. Not a great hassle but I prefer having a flag in the class which maintains the list of objects so I can just skip the step where I update all the objects. This also proved useful when developing Shuggy and I wanted everything to run twice as fast if the user held the action button in certain levels. Using this system I can simply call Update() twice on every object for every call to Game.Update() making all objects move twice for every time they're drawn.

Extending GameObject

Now that the Game class interacts with objects purely through the GameObject base class it really needs to be extended to be a bit more flexible and cope with any different objects we might want to create.

The Update() method has been changed to return a type defined in GameObject, 'UpdateAction'. For now the only purpose of this will be to let the caller know if the object should be deleted. A flag has been added to the GameObject which will cause the appropriate value to be returned by Update() to delete the object.

A method called Hit() has been added which lets two GameObjects be checked against each other to see if they have collided. This just checks against the rectangular boundary of each object. The Player object calls this method on all objects and if a collision is found it calls another virtual method, HitPlayer(). This lets objects easily react if the player has hit them. The method returns a value of 'HitAction' type which can effectively pass a message back to the player object about what happened as a result of the collision. For now the only interesting value that can be passed back is one indicating the player is hurt, ie. he hit an enemy.

Finally, a 'depth' variable has been added that determines the rendering order for objects. This is the same value that gets passed straight down to the SpriteBatch.Draw() call. The value defaults to 0.5f but objects can be moved backwards or forwards in the rendering order by changing the value in their constructor.

Coin class

Now that the GameObject base class has been beefed up there isn't that much going in the Coin class. The coin graphic is loaded by the LoadContent() method just like the Player object. The bulk of the code in the constructor is actually for finding an appropriate random position to place the coin since we have a randomly created level. We also set the depth so that enemies will be rendered in front of the coins.

The HitPlayer() method is overridden so that we can set the 'Delete' flag in the coin object ensuring that it will be removed from the object list making it appear to get collected.

Blob class

The Blob enemy moves backwards and forwards along the platforms and occassionaly jumps. Similar code to the coin class ensures the Blob will start at a reasonable position but also sets the Blob's size to 2x2 since it is larger than the player and coin objects. The HitPlayer() method is overridden here as well but in this case returns a value indicating that the player has been hurt.

The Update() method is overridden to allow the Blob to move. Like the player object, we update the 'vector' variable and call the Move() method within GameObject. We can be sure this method does the appropriate collision detection and returns a value indicating if the object collided with a wall in any given direction.

A variable called 'right' is used to remember if the Blob was heading left or right. If this is true then we fix the X component of the vector to a fixed value. If it's false then we set it to minus the same fixed value. After calling the GameObject.Move() method we check if the Blob hit something going either left or right and invert the 'right' variable so it starts heading the other way.

Every frame we increase the Y component of the vector by a fixed amount to simulate gravity. We don't need to worry about setting this to zero when the Blob hits something because the GameObject.Move() method will do this for us.

In order to jump we must be sure that we're on a solid surface. We can do this easily by checking if the GameObject.Move() method returned a value of Direction.Down indicating we'd hit an object below us. This will happen every frame because the object is continually being pulled downwards into the floor by gravity. If this is the case then we will randomly jump every now and then by checking if a random number between 0 and 127 happens to come out at 64. This means the Blob will jump roughly once every 128 frames. The actual jump is started by setting the Y component of the vector to a larger negative value.

Hitting the player

As mentioned earlier the player object now checks its position against all other objects to see if there is a collision. The HitPlayer() method is called if there was a collision and now the player has to react appropriately if a value of HitAction.Damage is returned. For the purposes of this sample all that happens when the player hits an enemy is that he flashes white briefly. This is achived by changing the 'texture' variable used by the GameObject.Draw() method to a different graphic. In order to change back to the original graphic a counter called 'hitcount' is used. This variable is set to 40 on hitting an enemy and decreases with every call to Update() until it reaches zero when the graphic is changed back.