2D Platform Games Part 12: A Framework for Interactive Game Objects
IMPORTANT! To run the pre-compiled EXEs in this article, you must have Windows 7 Service Pack 1 with Platform Update for Windows 7 installed, or Windows 8.
This article builds upon the demo project created in 2D Platform Games Part 11: Collision Detection Edge Cases for The Uninitiated. Start with 2D Platform Games Part 1: Collision Detection for Dummies if you just stumbled upon this page at random!
Download source code and compiled EXE for the code in this article as well as the complete source code and compiled EXE for the level editor.
We’ve spent a lot of time adding different types of platforms and collision detection behaviour to our project, but of course the real meat of any platform game is in the objects you can interact with like coins, baddies, levers and switches and so on.
Goals and Terminology
Over the next 5 parts of this series, we will build a framework in which we can place interactive game objects like enemies and collectibles, and look at all the complexities of this topic which must be tackled for a complete working implementation, including:
- Defining a class hierarchy for interactive game objects (which I’ll call game entities from hereon to differentiate them from actual C++ objects and platform geometry and instances) (Part 12 – this article)
- Handling the movement logic of static game entities (those which do not move in the game world), game entities with pre-defined paths (for example enemies which move backwards and forwards in a fixed pattern) (Part 13) and game entities which can move around the game world of their own accord under the rules of physics we have already defined (which I’ll call free-roaming) (Part 14)
- Handling animation of game entities with a unified animation function (for example, changing the sprites used for animation depending on the direction the game entity is travelling in) (Part 13)
- Unifying the collision processing code for entity-platform, entity-entity and player-entity collisions (Part 14, Part 16)
- Allowing entities to have custom behaviour on collisions with other entities, platforms or the player (for example, making a coin disappear and adding its value to the player’s score when the player collides with it) (Part 16)
- Unifying the internal representation of the game world to simplify the code and make it more easily extensible (Part 16)
We will also review the collision detection code and go through some really tricky edge cases that only come up when non-player game entities are added to the world, in Part 15.
Finally, I shall also present a new level editor which will allow you to place game entities and modify their properties, in turn giving way to a new level definition format for our level files (the level editor also contains a lot of other minor improvements and bug fixes).
Do be sure to work through these 5 parts in order as each builds on the code from the previous parts.
In this part we’re mostly going to just look at the internal organization of game entities without actually adding any new visible features to our project. Although this is a platform game series, the concepts and code structure can be applied to most any type of game, however the implementation will be completely different for different game genres. When we talk about a framework, it’s important to note we are not talking about a re-usable framework as such, since most games have a much larger percentage of custom code than other types of application, due to their specialist nature. Nevertheless, the principles apply to any game where you need to manage a large number of entities with related but different behaviour. For example, the code can be re-purposed to manage NPCs (non-player characters) in an RPG, specifying for example their dialogue, whether they are a merchant or quest-giver (and which quests they give and under what conditions) and so on.
Organizing Game Entities
An obvious question might be, why would we want to store entity data in class objects at all, especially when you consider that say a coin has fundamentally different properties and behaviour to an enemy? Is it object-oriented programming for the sake of it with no real benefits?
As it happens, these entity types as I shall call them (for example, a coin is an entity type; an enemy is an entity type) have more in common than it might first appear. They all have:
- a starting position in the game world
- a flag indicating whether they are still in the game world or not
- (possibly customized) collision behaviour against the player
- animations which work in a similar-ish fashion
and so on.
Many entities will also be moving around and need their speed and acceleration tracked, and static (non-moving) entities are easy to integrate into this system because they simply have a speed of zero.
When we place a game entity of a specific entity type into the game world, we call this creating an instance of the entity type. For example, we define a coin as an entity type; if we then want to place 100 coins in a level in various locations, we create 100 instances of the coin entity type (or 100 entity instances of a coin, or 100 coin instances; these phrases are interchangeable and mean the same thing). These instances will have certain properties which are specific to the instance being created, eg. position in the level.
There are lots of dilemmas to consider when structuring the entity hierarchy. Some entity types may have properties which are not shared by other entity types, yet are the same for all instances of the entity type which has the property (for example, all coins (the entity type) may have a base value of 100 points (the property shared by all coins, ie. all instances of the entity type), while enemies don’t have a base value as they are not collectible items). On the other hand, some entity types may have properties which may be different for each instance of the entity type (for example, each switch may have a target door it opens, and this target door will be different for each switch created).
We quickly begin to see that there are properties which are:
- shared by all entity types and which are different for each entity type but constant for all instances of a specific entity type
Example: each entity type (coins, enemies, switches) will have a specific set of sprites for animation, and those sprites will be the same sprites (constant) for all instances of that entity type - shared by all entity types and unique for every instance
Example: the starting co-ordinates of the instance in the game world – regardless of its entity type – must be specified - defined only for certain entity types but which are constant for all instances of that entity type
Example: the coin base points value is the same for all coins, but only coins have a base points value; switches and teleportation portals for example do not - defined only for certain entity types and are unique for each instance of that entity type
Example: the door opened by flicking a particular switch is unique for each switch, but only switches have a “door-to-open” property specified
We may also have:
- entity types whose instances start with default properties that may be overridden by the instance
Example: if a coin is customizable, it may have a base value of 100 points which can be overridden by instances of individual coins - properties unique to each instance which change as the game progresses
Example: the current co-ordinates and animation frame of each individual entity instance; for enemies, perhaps the amount of health remaining
In addition to sorting all of this out into a class hierarchy, we need to provide behavioural hooks both at the entity type (collect coin, add points value to score) and instance (flick switch, open target door) levels, and organize the hierarchy in such a way that only the required properties are stored in the level file, both to avoid duplication of shared properties and to make those properties adjustable later if we need to globally change the behaviour of an entity type.
In other words, we have a mess on our hands before we even start. However, the benefits of using classes for the job are clear:
- Encapsulation – the properties and behaviour (functions/methods, essentially) of each entity type can be organized and re-used neatly in a self-contained way
- Inheritance – duplicate code can be eliminated by relating similar entity types together through derivation, and deriving all entity types from a generic base entity type class which implements the default properties shared by everything
- Polymorphism – the game code, including collision detection, rendering and behavioural processing will not need to care what type of entity is being operated on, as they can all be treated the same. This reduces code duplication, increases ease of maintenance and promotes extensibility: new entity types will be much easier to add, as they only require using an existing class with changed default properties, or deriving a new class which will be handled via polymorphism by the rest of the game code.
In other words, all three of the traditional “pillars of OOP” apply well to our use case.
There are also some downsides to the class-based approach:
- Performance – potentially heavy use of virtual functions to enable polymorphic behaviour increases the performance overhead (although only by a very marginal amount on modern PCs)
- RTTI (Run-time Type Information) / Type Reflection may be required – the whole point of polymorphism is that we shouldn’t need to know the precise type of the object we are dealing with. However, with the level editor in particular, we will need to inspect the class type of each entity so that the user can be presented with editable properties sheets relevant to the specific entity type and so on. If we choose to go even further and integrate platforms (world geometry) and entities into a single list of world objects, there are collision detection situations which apply to platforms but not entities, so we must be able to differentiate them.
The type information problems can in fact be solved by careful design. For example, we could provide a virtual function overridden by the base platform and entity classes which report how collisions should be detected. For the level editor, we could implement virtual functions for each entity type which return a list of editable properties, their types and range bounds, then program the level editor to dynamically create a window (or dialog box or property sheet) from this information. Such a solution, however, is very laborious and time-consuming, and adds a lot of unnecessary bloat to the classes just for the sake of the level editor (we workaround the problem below by embedding an entity type identifier field into each entity type class, which the level editor can inspect to find out its actual type even if it is referencing it from a base class pointer).
Class Hierarchies Reviewed
If you need it, refer to the article C++ Class Hierarchies: Derivation (“is-a”) vs Composition (“has-a”) and Is a Square a Rectangle? on this site for a quick crash course refresher on designing class hierarchies. We will be making extensive use of the techniques and design justifications described therein below.
Before We Start
First it is important to understand that the framework we will construct to handle all the situations outlined above is not necessary for simple games. There is no need to go into the complexities discussed if you only have one or two types of enemies and a few collectibles (coins, power-ups etc.) in your entire game. The approach described here is generally applicable to “medium-sized” games where there are perhaps some dozens or a couple of hundred different entity types. To implement this framework for very simple games is overkill.
Please note also that I do not show every single changed line of code from previous parts in the series below, just the relevant stuff; there are numerous boilerplate and organizational changes and I encourage you to WinDiff (or other diff tool of your choice) the source trees for Part 11 and Part 12 (this part) to study the changes in more detail.
Framework Class Hierarchy Overview
Figure 1 shows the layout of the framework in C++ classes, with base classes in blue and derived classes in orange. Take some time to refer back to this diagram as I explain it in more detail below. We’ll disregard the implementation of the actual player for now and return to that in Part 16.
The basic idea is that we divide up all the properties and behaviours we need into three class groups: those with base types of GameEntityTypeDefinition, GameEntityDefinition and GameEntity. We then relate them to each other so that a live instance of an entity can find its own unique default properties and behaviours, and properties and behaviours shared by every entity of its own entity type.
Example Game Entity Types
For now, we are going to create some entity types that demonstrate the need for the different types of property and behaviour sharing and exclusivity we discussed above. Our game will contain:
- Stars – the in-game currency that you can collect which gives you points. Stars do not move around the game world (they are static)
- Evil Burgers – a baddie which moves around the game world on a fixed path backwards and forwards. It can be killed by jumping on top of it
- Rolling Barrels – a baddie which moves around the game world according to the laws of physics. It cannot be destroyed so the player must dodge it
- Portals – a teleporter which moves the player instantly to another location in the game world
Figure 2 shows how these are rolled up into our framework, with detailed explanations below.
Entity Type Organization
GameEntityType is an enum with one entry for each possible type of game entity, including the player. We use it as a key for the GameEntityTypes dictionary which will contain one entry for each possible game entity type, with that type’s shared default properties.
GameEntityTypeDefinition defines properties and behaviours which all game entities of the same type share and forms the values of the GameEntityTypes dictionary. One GameEntityTypeDefinition for each possible entity type is created with type-specific values when the application starts, and these values are shared by every entity with the same type.
The base class contains properties which all entities of all types require, such as sprite and animation data.
The class may be derived to provide additional properties and behaviours shared by all game entities of a particular type but which are not needed by all entity types; these properties and behaviours are shared among all instances of the type defined.
Let’s see how all this works in code.
Defining the GameEntityType enum (GameEntities.h):
// Game entity types enum GameEntityType { GET_Unknown = 0, GET_Star = 1, GET_EvilBurger = 2, GET_Barrel = 3, GET_Portal = 4 };
Defining the GameEntityTypeDefinition class (this is just an initial stab at the properties we might need and we will fiddle around with it later) (GameEntityTypes.h):
// Properties of each game entity type (needed by game only) class GameEntityTypeDefinition { public: GameEntityTypeDefinition() : sprites(""), width(-1), height(-1), afLeft(-1), afRight(-1), afUp(-1), afDown(-1), afStatic(-1), arowLeft(-1), arowRight(-1), arowUp(-1), arowDown(-1), arowStatic(-1), apLeft(-1), apRight(-1), apUp(-1), apDown(-1), apStatic(-1), amfLeft(0), amfRight(0), amfUp(0), amfDown(0), apauseLeft(false), apauseRight(false), apauseUp(false), apauseDown(false), spawnRadius(0) {} // Convenience functions void SetSpriteSheet(std::string sn, int w, int h) { sprites = sn; width = w; height = h; } void SetAnimationLeft(int frames = 1, int row = 0, int periodMs = 1000, bool forwardsBackwards = true, bool pauseWhenStill = false, int speedFactor = 0) { afLeft = frames; arowLeft = row; apLeft = periodMs; afbLeft = forwardsBackwards; apauseLeft = pauseWhenStill; amfLeft = speedFactor; } void SetAnimationRight(int frames = 1, int row = 0, int periodMs = 1000, bool forwardsBackwards = true, bool pauseWhenStill = false, int speedFactor = 0) { afRight = frames; arowRight = row; apRight = periodMs; afbRight = forwardsBackwards; apauseRight = pauseWhenStill; amfRight = speedFactor; } void SetAnimationUp(int frames = 1, int row = 0, int periodMs = 1000, bool forwardsBackwards = true, bool pauseWhenStill = false, int speedFactor = 0) { afUp = frames; arowUp = row; apUp = periodMs; afbUp = forwardsBackwards; apauseUp = pauseWhenStill; amfUp = speedFactor; } void SetAnimationDown(int frames = 1, int row = 0, int periodMs = 1000, bool forwardsBackwards = true, bool pauseWhenStill = false, int speedFactor = 0) { afDown = frames; arowDown = row; apDown = periodMs; afbDown = forwardsBackwards; apauseDown = pauseWhenStill; amfDown = speedFactor; } void SetAnimationStatic(int frames = 1, int row = 0, int periodMs = 1000, bool forwardsBackwards = true) { afStatic = frames; arowStatic = row; apStatic = periodMs; afbStatic = forwardsBackwards; } void SetSpawnRadius(int r) { spawnRadius = r; } // Sprite sheet for object std::string sprites; // Width and height int width; int height; // Collision points D2D1_POINT_2F testPoints[8]; // Sprite sheet row to use, number of frames, animation period for each animation type, // and whether to run the animation forwards or backwards (true = forwards) // (Setting frames to zero forces the use of a single sprite) int afLeft, afRight, afUp, afDown, afStatic; int arowLeft, arowRight, arowUp, arowDown, arowStatic; int apLeft, apRight, apUp, apDown, apStatic; int afbLeft, afbRight, afbUp, afbDown, afbStatic; // If the animation should depend on the object's movement speed, set a speed factor here, // otherwise set to zero int amfLeft, amfRight, amfUp, amfDown; // Whether the animation should pause at the current frame when the object stops moving // in the specified direction, or to keep animating bool apauseLeft, apauseRight, apauseUp, apauseDown; // Spawn radius of the object type // If none is supplied, uses the default of the screen resolution plus 'a bit' // Object will also de-spawn when the player is outside this radius int spawnRadius; // Collision behaviour with player // Which sides of the object have been struck, and the width and height ("strength") of the collision/intersection virtual void onPlayerCollisionWithType(bool cTop, bool cBottom, bool cLeft, bool cRight, int w, int h) {} };
The details aren’t too important but as you can see, the base entity type defines sprite animation data, the object width and height and collision test points (check Part 1 for more details on this) – things that will be required by every single instance of every entity type.
Defining derived types (GameEntityTypes.h):
// Generic collectible currency class GameEntityTypeDefinition_Currency : public GameEntityTypeDefinition { public: // The currency value for all objects of this type int pointsValue; public: GameEntityTypeDefinition_Currency() : pointsValue(0) {} // Collision processing virtual void onPlayerCollisionWithType(bool cTop, bool cBottom, bool cLeft, bool cRight, int w, int h) { #ifdef _DEBUG DebugPrint("You scored " + StringFactory(pointsValue) + " points!"); #endif } }; // Generic baddie class GameEntityTypeDefinition_Baddie : public GameEntityTypeDefinition { public: // The amount of damage done to the player on collision int damageToPlayer; // Sides from which the baddie is vulnerable to being attacked (collided) by the player // eg. set vulnFromAbove to allow the player to kill the baddie by jumping on it bool vulnFromLeft; bool vulnFromRight; bool vulnFromAbove; bool vulnFromBelow; public: GameEntityTypeDefinition_Baddie() : damageToPlayer(0), vulnFromLeft(false), vulnFromRight(false), vulnFromAbove(false), vulnFromBelow(false) {} // Collision processing virtual void onPlayerCollisionWithType(bool cTop, bool cBottom, bool cLeft, bool cRight, int w, int h) { #ifdef _DEBUG if ((cTop && vulnFromAbove) || (cBottom && vulnFromBelow) || (cLeft && vulnFromLeft) || (cRight && vulnFromRight)) DebugPrint("You attacked a baddie and killed it!"); else DebugPrint("A baddie hit you and did " + StringFactory(damageToPlayer) + " ego damage :P"); #endif } };
We define a Currency derived type which has a points value to add to the player’s score when the currency item is collected, and a Baddie derived type which specifies how many hit points to damage the player by and on which sides the enemy is vulnerable to attack. Note that we just use stub functions for the collision processing for now.
Notice that these properties are not only shared but the same for all instances of a particular currency type and a particular enemy type respectively, ie. all Star currency types may have a value of 100, and all BigStar currency types may have a value of 500. The reason we do not define a Portal type here is because, while portals share a target co-ordinates property to transport the player to, this property is not the same for all instances of a portal – it is different for every portal.
Defining the entity type definition dictionary (SharedPlatformCode.h):
typedef boost::ptr_unordered_map<GameEntityType, GameEntityTypeDefinition> GameEntityTypeList; ... // in some class definition: // List of game entity type definitions static GameEntityTypeList GameEntityTypes;
Initializing Game Entity Types
When the application starts, we populate the entity type definition dictionary with one entry for each possible entity type as follows (GameEntityDefinitions.cpp):
// Game entity type definitions void SharedTools::createGameEntityTypeDefinitions() { GameEntityType get; // Star // A static animated object which gives a points value if the player touches it get = GET_Star; GameEntityTypeDefinition_Currency *get_Star = new GameEntityTypeDefinition_Currency(); get_Star->SetSpriteSheet("objects.png", 20, 20); get_Star->SetAnimationStatic(8, 0, 750); get_Star->pointsValue = 100; D2D1_POINT_2F tp_get_Star[] = { { 10, 0 }, { 10, 0 }, { 5, 20 }, { 15, 20 }, { 0, 10 }, { 0, 10 }, { 20, 10 }, { 20, 10 } }; memcpy(get_Star->testPoints, tp_get_Star, sizeof(D2D1_POINT_2F)*8); GameEntityTypes.insert(get, get_Star); // Evil burger // A generic baddie with left and right animations. It damages the player on contact, // but can be killed by jumping on it. get = GET_EvilBurger; GameEntityTypeDefinition_Baddie *get_EvilBurger = new GameEntityTypeDefinition_Baddie(); get_EvilBurger->SetSpriteSheet("objects-med.png", 40, 40); get_EvilBurger->SetAnimationLeft(8, 2, 1000, true); get_EvilBurger->SetAnimationRight(8, 2, 1000, false); get_EvilBurger->damageToPlayer = 20; get_EvilBurger->vulnFromAbove = true; // Jump on burger to kill it D2D1_POINT_2F tp_get_EvilBurger[] = { { 5, 0 }, { 35, 0 }, { 0, 40 }, { 40, 40 }, { 0, 5 }, { 0, 35,}, { 40, 15 }, { 40, 35 } }; memcpy(get_EvilBurger->testPoints, tp_get_EvilBurger, sizeof(D2D1_POINT_2F)*8); GameEntityTypes.insert(get, get_EvilBurger); // Barrel // A generic baddie which damages the player on collision and can't be killed. get = GET_Barrel; GameEntityTypeDefinition_Baddie *get_Barrel = new GameEntityTypeDefinition_Baddie(); get_Barrel->SetSpriteSheet("objects-med.png", 40, 40); get_Barrel->SetAnimationLeft(8, 3, 0, true, true, 100000); get_Barrel->SetAnimationRight(8, 3, 0, false, true, 100000); get_Barrel->damageToPlayer = 50; D2D1_POINT_2F tp_get_Barrel[] = { { 15, 0 }, { 25, 0, }, { 15, 40 }, { 25, 40 }, { 0, 15 }, { 0, 25 }, { 40, 15 }, { 40, 25 } }; memcpy(get_Barrel->testPoints, tp_get_Barrel, sizeof(D2D1_POINT_2F)*8); GameEntityTypes.insert(get, get_Barrel); // Portal // A static object which transports the player to another location on collision get = GET_Portal; GameEntityTypeDefinition *get_Portal = new GameEntityTypeDefinition(); get_Portal->SetSpriteSheet("ice-texture.png", 80, 80); get_Portal->SetAnimationStatic(1, 0, 0); D2D1_POINT_2F tp_get_Portal[] = { { 15, 0 }, { 25, 0, }, { 15, 40 }, { 25, 40 }, { 0, 15 }, { 0, 25 }, { 40, 15 }, { 40, 25 } }; memcpy(get_Portal->testPoints, tp_get_Portal, sizeof(D2D1_POINT_2F)*8); GameEntityTypes.insert(get, get_Portal); }
We populate GameEntityTypes with one entry for each entity type. Note that the Portal type is created using the standard GameEntityTypeDefinition base class as it does not have any additional properties that have constant values for all portal instances.
The Star type has a static (not moving in any direction) animation and gives 100 points to the player on collision. The EvilBurger type has left and right animations, does 20 hit points of damage to the player on collision, and is vulnerable to attack from above (by jumping on it). The Barrel type has left and right animations, does 50 hit points of damage to the player on collision, and cannot be destroyed (Note: having left and right animations does not mean the entity cannot move up or down; it only means that the animation displayed depends on whether the entity is facing left or right). The Portal type has a static animation of 1 frame (meaning that it does not animate – it uses a single sprite) and no other shared properties.
Now we have our entity types defined, we can go on to create individual instances of these entity types in a specific level.
Entity Instance Organization
GameEntityDefinition defines an instance of a single game entity in the game world. It has an entity type field (of type GameEntityType) from which its corresponding GameEntityTypeDefinition (or derived class thereof) can be looked up for the information defined above.
GameEntityDefinition may be derived to provide additional properties and behaviours shared by all instances of a particular entity type, but whose values must be set individually for each instance. The portal is the example we are using here, where each instance must have its target location set individually.
An array of GameEntityDefinitions represents all of the entity instances for a specific level or area in the game.
Instances of GameEntityDefinition will describe (among other things) the movement and collision behaviour of each entity instance. To simplify this, we define a few enums first.
Recall that we will have three possibilities for an entity’s movement: static (not moving), moving on a fixed pre-defined path, or free-roaming according to the game’s physics model. MovementType defines the movement formula for an entity which moves on a fixed path (GameEntities.h):
// Movement types typedef enum { MT_Unknown = 0, MT_Linear = 1, MT_Cosine = 2, MT_Sine = 3 } MovementType;
Sine and cosine movements are great for objects which speed up and slow down in an oscillating fashion, or move in a circular or elliptical pattern.
FreeRoamModel defines how free-roaming (non-fixed paths) entities will move (GameEntities.h):
// Free-roam physics models enum FreeRoamModel { FRM_Unknown = 0, FRM_Standard = -1, // Use standard physics model FRM_Platform = -2 // Use physics model of current platform };
The Standard model uses the default game physics regardless of the platform the entity is on. The Platform model uses the physics model defined by the platform the entity is on (see Part 5 on platform surface dynamics for more information). We use negative numbers in the enumeration because a positive number will be used to give entities their own custom-defined rate of acceleration.
FreeRoamCollision defines what happens when the entity collides with a platform in the game world (GameEntities.h):
// Free-roam geometry collision behaviour enum FreeRoamCollision { FRC_Unknown = 0, // Turn around when object collides with a wall, floor or ceiling FRC_TurnAround = 1, // Reset to initial position when object collides with a wall, floor or ceiling FRC_Reset = 2, // De-spawn when object collides with a wall, floor or ceiling until it is next in the player's spawn radius FRC_Despawn = 3, // Attempt to jump and continue moving in current direction of movement // when object collides with a wall, floor or ceiling FRC_Jump = 4 };
The comments should make this fairly self-explanatory.
The business end of entity definitions is performed in GameEntityDefinition as described above. Once again we make an initial stab at a class definition and will refine it later as we go along (GameEntities.h):
// The initial definition of an individual game entity // Needed by both the game and the level editor class GameEntityDefinition { public: GameEntityDefinition() : typeId(GET_Unknown), id(-1), topLeftX(-1), topLeftY(-1), facingX(1), facingY(1), usePhysics(false), useFreeRoam(false), usePathing(false), frAccXf(FRM_Standard), frMaxSpeedX(FRM_Standard), frTargetPlayer(false), frtpRadiusX(0.f), frtpRadiusY(0.f), frcBehaviour(FRC_TurnAround), frJumpSpeed(0.f), frcMomentumFactor(1.0f), pathFuncX(MT_Linear), pathFuncY(MT_Linear), pathIntervalX(-1), pathIntervalY(-1), pathBaseX(0.f), pathBaseY(0.f), pathAmplitudeX(0.f), pathAmplitudeY(0.f), pathCropLowerX(0.f), pathCropLowerY(0.f), pathCropUpperX(1.f), pathCropUpperY(1.f), pathCycleTypeX(Animation::Reverse), pathCycleTypeY(Animation::Reverse) {} // Game entity type GameEntityType typeId; // Game entity ID int id; // Initial position in the game world int topLeftX; int topLeftY; // Which direction the object is initially facing (1 for positive along the axis, -1 for negative) int facingX, facingY; // Whether the object should obey the laws of physics bool usePhysics; // Free-roam parameters // Whether the object is free-roam bool useFreeRoam; // Forward X acceleration and top speed when using free-roam // Specify -1 to use the game's standard physics model float frAccXf; float frMaxSpeedX; // Always move in the direction of the player (enable/disable homing free-roam) bool frTargetPlayer; // Radius within which the object will target the player if frTargetPlayer is set // Set to 0 to always follow float frtpRadiusX, frtpRadiusY; // Behaviour on collision with world geometry during free-roam FreeRoamCollision frcBehaviour; // Initial upwards force when object begins to jump during free-roam float frJumpSpeed; // The percentage of speed to retain when an object turns around // (0 = stop completely, 1 = bounce in the opposite direction at the same speed currently moving) float frcMomentumFactor; // Pathing parameters // Pathed objects can either use or ignore physics // Whether the object uses pre-defined pathing bool usePathing; // Pathing parameters for X and Y // Movement function MovementType pathFuncX, pathFuncY; // Movement parameters int pathIntervalX, pathIntervalY; double pathBaseX, pathBaseY; double pathAmplitudeX, pathAmplitudeY; double pathCropLowerX, pathCropLowerY; double pathCropUpperX, pathCropUpperY; Animation::CycleType pathCycleTypeX, pathCycleTypeY; // Collision behaviour with player // Which sides of the object have been struck, and the width and height ("strength") of the collision/intersection virtual void onPlayerCollisionWithEntity(bool cTop, bool cBottom, bool cLeft, bool cRight, int w, int h) {} };
In this class, we define:
- The entity type of the instance (typeId)
- A unique ID number with which the entity can be identified (id)
- The entity’s initial co-ordinates in terms of the sprite’s top-left pixel (topLeftX and topLeftY)
- Whether the entity is facing left or right (facingX) and up or down (facingY)
- Whether the entity obeys the game’s physics model (usePhysics)
- Whether the entity uses free-roaming for movement behaviour (useFreeRoam)
- Customized X-axis acceleration and top speed (frAccXf, frMaxSpeedX) when free-roaming; if they are set to one of the FreeRoamModel values, use the Standard or Platform physics models instead
- Whether to home in on the player (frTargetPlayer)
- The radius in which the entity will home in on the player, in game world units, when free-roaming (frtpRadiusX, frtpRadiusY). If the player is outside this radius, the entity will revert to normal free-roaming
- How the entity will respond to collisions with platforms and other obstacles (“world geometry”) when free-roaming (frcBehaviour)
- The escape velocity, relative to gravity, of the entity if it attempts to jump when free-roaming (frJumpSpeed). This indirectly defines the object’s weight
- The co-efficient of restitution (CoR) of the entity when it bounces off world geometry (frcMomentumFactor). The lower the CoR, the more energy (speed) is lost when the entity begins to move in the opposite direction. Note, this is not a true co-efficient of restitution as CoR requires both the colliding objects to have damping factors defined, but we have not done so for platforms, so we assume their CoR is 1 (ie. no energy is absorbed by a platform when an entity collides with it)
- Whether the object uses fixed pathing (usePathing). Note that this is not mutually exclusive with obeying the game’s physics model
- The length of the horizontal and vertical paths in milliseconds (pathIntervalX, pathIntervalY), their start positions (pathBaseX, pathBaseY), the amount of movement in total from the start position in each axis (pathAmplitudeX, pathAmplitudeY), cropping factors if only part of the path should be used (where 0.0 is the start of the path and 1.0 is the end of the path) (pathCropLowerX, pathCropLowerY, pathCropUpperX, pathCropUpperY), whether the path should be executed once, looped indefinitely or looped and reversed when the end of the path is reached (pathCycleTypeX, pathCycleTypeY)
- A virtual function which can be used to define custom behaviour when the player collides with the entity or vice versa (onPlayerCollisionWithEntity)
All of these properties are required by every instance of every entity, but most of the values will be unique for each entity, which is why we don’t define them in GameEntityTypeDefinition.
We derive from GameEntityDefinition to create instances of entity types that have properties which are shared only by that type and whose values are unique for each instance of the type. Here is the definition for the portal (GameEntities.h):
// Portal class GameEntityDefinition_Portal : public GameEntityDefinition { public: // The ID of the game entity to transport the player to on collision int targetEntityId; GameEntityDefinition_Portal() : targetEntityId(-1) {} // Collision processing virtual void onPlayerCollisionWithEntity(bool cTop, bool cBottom, bool cLeft, bool cRight, int w, int h) { #ifdef _DEBUG DebugPrint("You touched a portal and will be moved to object with ID " + StringFactory(targetEntityId) + "!"); #endif } };
We add one field to the derived class – targetEntityId – which specifies the ID number of the entity instance to which the player will be teleported when she collides with the portal.
Quizzical readers may at this point wonder why GameEntityDefinition doesn’t just include its associated GameEntityTypeDefinition as a member or pointer member. The answer is basically to do with type reflection, object cloning and serialization in the level editor. I won’t go into the level editor code here but see the supplied source code for the details. Another solution is to use the CRTP, but see my article C++: Polymorphic cloning and the CRTP (Curiously Recurring Template Pattern for an explanation of CRTP and to see what a mess that creates. I tried a number of solutions for this problem but in the end a simple dictionary was the simplest if not most elegant solution.
Live Game Entity Instances
GameEntityDefinition instances store the default behavioural parameters and properties of each game entity instance. When the game is actually running, there are a number of things we will need to regularly update such as the instance’s position, animation state and whether it is currently spawned (active) in the game world or not. We will also need to perform operations on the instance such as spawning it, updating its position and animation, rendering it and so on. For this we create a GameEntity class as follows (SimplePlatformer.h):
// A game entity created from a GameEntityDefinition_* // Only needed by the game class GameEntity { private: // Needed so we can provide a reference initialization in the default constructor static GameEntityDefinition _GED_Empty; // Application data MyProgram *app; public: // Needed to allow GameEntitys to be stored in an unordered_map GameEntity() : type(SharedTools::GameEntityTypes[GET_Unknown]), def(_GED_Empty) {} // Creation function GameEntity(MyProgram *app, GameEntityDefinition &def); // Reset/re-spawn entity void Reset(); // Definition for the entity type GameEntityTypeDefinition &type; // Definition for the entity GameEntityDefinition &def; // Is the object currently active or not bool spawned; // Update the object void Update(int updateTick); // Render the object void Draw(); };
The constructor takes a reference to a GameEntityDefinition so we define an ’empty’ one in _GED_Empty for when the default constructor is needed (the default constructor is required since the instances are stored in another unordered_map, for which class entries must have a default constructor). From the GameEntityDefinition, the constructor uses the typeId field to look up the corresponding GameEntityTypeDefinition from the GameEntityTypes dictionary and include a reference to it as a member of GameEntity. This just saves us from looking it up every time we need it.
There is a 1-to-1 mapping of GameEntityDefinitions to GameEntitys: for each GameEntityDefinition there is exactly one and only one corresponding GameEntity. The reason we separate the classes at all is to avoid saving unnecessary data in the level files. This works exactly the same as how we created exactly one Platform for each stored GeometryDefinition earlier in the series.
The class methods are implemented as follows (SimplePlatformer.cpp):
// Create a game entity from a game entity definition // The items set here never change once the entity is created GameEntity::GameEntity(MyProgram *app, GameEntityDefinition &def) : def(def), type(SharedTools::GameEntityTypes[def.typeId]), app(app) { Reset(); } // Set properties of the game entity that must be reset whenever it is spawned or re-loaded void GameEntity::Reset() { // Not spawned spawned = false; } // Per-frame update game entities void GameEntity::Update(int updateTick) { } // Draw a game entity void GameEntity::Draw() { }
As you can see, everything is a stub function which we’ll fill in later except that Reset() puts the instance in a non-spawned state and the parameterized constructor fetches the entity type from GameEntityTypes as described above.
Later in the series we will show how the GameEntity class can be derived both to represent the player, and to add additional ‘live’ properties for instances of a particular entity type, such as the remaining health for an enemy, but for now we’ll just use the base type for all entities and flesh out its properties and methods over the course of the next articles in the series.
Everything Must Have a Unique ID, Including Platforms
Every game entity instance and platform (piece of world geometry) must now have a unique identifier or ID number. For entity instances, the reason is simple: some instances may refer to other instances – for example the portal takes the player to another instance’s location – so we need a way to identify them.
For world geometry, there are a couple of reasons: first, similar to above, some instances may refer to world geometry – for example a switch that opens a door (which is world geometry) – so we need a way to identify which world geometry the instance affects. Next, to avoid having to iterate over the entire list of world geometry to find a particular item for this purpose, we change the storage from a vector to an unordered_map (which is the same as how entity instances are stored), where the geometry ID is the map key. The problem with this is that the order in which geometry is stored in the map is not guaranteed to be consistent with the order they are inserted. Since the rendering code iterates over the map from the first to last item and renders in that order, the drawing order of world geometry may change unexpectedly. This is a problem if some geometry is supposed to be in front of or behind other geometry. To solve this, we create a new std::list which does have a guaranteed order, and fill it with ints specifying the IDs of and in what order each piece of geometry should be rendered, then iterate over this list when rendering instead (the geometry with the first ID in the list will be rendered first and the geometry with the last ID will be rendered last).
In the source code, an id field has been added to GeometryDefinition and Platform to give world geometry unique IDs, the worldGeometry<Platform> vector in SimplePlatformer.h is changed to an unordered_map and the field list<int> worldGeometryDrawOrder is added in SimplePlatformer.h to store the list of world geometry IDs in the desired draw order. The rendering code is updated to iterate over worldGeometryDrawOrder and find the next piece of geometry to render from its ID, rather than iterating over worldGeometry directly.
Storing Data In The Level Definition
Since the contents of each GameEntityTypeDefinition are generated when the game starts up and are fixed across the entire game, there is no need to serialize them into the level files. Similarly, GameEntitys are generated on the fly from their corresponding GameEntityDefinitions so these should not be stored either. In fact, the only extra item we need to store store is a ptr_vector of GameEntityDefinitions to give the initial state of each game entity instance. We use standard Boost serialization techniques to load and save (in the editor) levels – see Part 9: Storing Levels in Files / Level Editors and Part 10: Improved Level Management and Storage if you are unfamiliar with this, and to see how the existing level format is laid out and stored. We use the Boost version tracking mechanism described in Part 10 to ensure backwards compatibility with older level file formats.
Let’s look at the updated LevelDefinition class (SharedPlatformCode.h):
// Definition of an entire level class LevelDefinition { public: // Player start position in level float playerStartX, playerStartY; // Geometry definitions for each platform vector<GeometryDefinition> worldGeometry; // Entity definitions for each entity boost::ptr_vector<GameEntityDefinition> worldEntities; // Background and mediumground filenames WCHAR backgroundFile[1000]; WCHAR mediumgroundFile[1000]; // Load/save a level from/to file static LevelDefinition Load(std::string filename); void Save(std::string filename); private: // Allow Boost::serialization access to private members friend class boost::serialization::access; // This template describes how to serialize and deserialize the level into/from a file template <typename Archive> void serialize(Archive &ar, const unsigned int version) { ar & playerStartX; ar & playerStartY; ar & worldGeometry; if (version >= 1) ar & worldEntities; else worldEntities.clear(); ar & backgroundFile; ar & mediumgroundFile; } }; // Version 0: initial // BOOST_CLASS_VERSION(LevelDefinition, 0) // Version 1: serializes game entities BOOST_CLASS_VERSION(LevelDefinition, 1) // Prevents warnings from Boost when serializing enums BOOST_CLASS_TRACKING(LevelDefinition, boost::serialization::track_never)
New and changed lines are highlighted. We increment the version number by 1 and add conditional serialization of the game entities for compatibility with older file versions.
In GeometryDefinition we update the serialize() code to serialize the world geometry’s ID (or set it to -1 for previous file formats) and increment the version number here too (in SharedPlatformCode.h – not shown here).
The implementations of LevelDefinition::Load() and LevelDefinition::Save() are the same as before except we have changed the file format from text to binary to reduce unnecessary bloat in the file size, and world geometry is given arbitrarily generated IDs when loading levels from an older file format (in SharedPlatformCode.cpp – not shown here).
We now define how GameEntityDefinitions will be serialized by adding a serialize() method to GameEntityDefinition (GameEntities.h):
class GameEntityDefinition { ... private: // Allow Boost::serialization access to private members friend class boost::serialization::access; // This template describes how to serialize and deserialize the game entity definition into/from a file template <typename Archive> void serialize(Archive &ar, const unsigned int version) { ar & id; ar & typeId; ar & topLeftX & topLeftY; ar & facingX & facingY; ar & usePhysics; ar & useFreeRoam; ar & frAccXf & frMaxSpeedX; ar & frTargetPlayer & frtpRadiusX & frtpRadiusY; ar & frcBehaviour; ar & frJumpSpeed; ar & frcMomentumFactor; ar & usePathing; ar & pathFuncX & pathIntervalX & pathBaseX & pathAmplitudeX & pathCropLowerX & pathCropUpperX & pathCycleTypeX; ar & pathFuncY & pathIntervalY & pathBaseY & pathAmplitudeY & pathCropLowerY & pathCropUpperY & pathCycleTypeY; } }; BOOST_CLASS_VERSION(GameEntityDefinition, 0) };
(in the actual source code this class is on version 3, and you can look through the full serialization code to see the iterations I went to when tweaking the initial version of the class)
All derived classes of GameEntityDefinition must be serialized too. Here is the code for the portal (GameEntities.h):
class GameEntityDefinition_Portal : public GameEntityDefinition { ... private: friend class boost::serialization::access; // This template describes how to serialize and deserialize the entity into/from a file template <typename Archive> void serialize(Archive &ar, const unsigned int version) { ar & boost::serialization::base_object(*this); ar & targetEntityId; } }; BOOST_CLASS_VERSION(GameEntityDefinition_Portal, 0)
Note that the code first serializes the base class, then the derived class’s own extra fields.
Since we are serializing each instance of GameEntityDefinition via a ptr_vector of GameEntityDefinitions, we are asking Boost to serialize a potentially unknown derived type via a base class pointer. This will cause derived classes (such as our portal implementation) not to be serialized properly as Boost defaults to serializing the base class fields only, with the extra fields in the derived class being lost. To work around this problem, we register all of the classes we may want to serialize with Boost in advance via a provided macro (GameEntities.cpp):
#include "GameEntities.h" // List of game entity type definitions that require serialization into a level file BOOST_CLASS_EXPORT_GUID(GameEntityDefinition, "GED_Default") BOOST_CLASS_EXPORT_GUID(GameEntityDefinition_Portal, "GED_Portal")
This simply associates an arbitrary string literal to each serializable class (including the base class itself) which Boost uses as an identifier in the saved file when serializing the class object. For more information, see the Registration and Export sub-sections of the Pointers to Objects of Derived Classes section in the Boost Serializable Concept documentation.
Next, in the post-load code in the game itself (MyProgram::LoadLevel), we convert the loaded ptr_vector of world entities into an unordered_map and generate a GameEntity from each GameEntityDefinition (SimplePlatformer.cpp):
// Configure game entities (make entities from definitions) worldEntities.clear(); for (auto it = level.worldEntities.begin(); it != level.worldEntities.end(); it++) worldEntities.insert(std::make_pair<int, GameEntity>(it->id, GameEntity(this, *it)));
We also re-write the world geometry post-load code completely to place the geometry into an unordered_map and automatically generate the worldGeometryDrawOrder list (SimplePlatformer.cpp):
// Configure platforms and draw order // NOTE: Draw order is needed because unordered_map won't necessarily store the // objects in the order they were loaded when iterating through it worldGeometry.clear(); worldGeometryDrawOrder.clear(); for (auto it = level.worldGeometry.begin(); it != level.worldGeometry.end(); it++) { worldGeometry.insert(std::pair<int, Platform>(it->id, Platform(*it))); worldGeometryDrawOrder.push_back(it->id); }
This completes the task of updating our level definition class and files, and making sure everything is loaded and saved correctly.
Sprite Sheet Management
All entities of a given type will need the same set of sprites (or sprite sheet) for rendering their animations, and some may be shared across types, but storing them for each entity or even each entity type may waste a lot of memory in duplicates. Instead, we make another dictionary, this time of sprite sheets and again using unordered_map as the dictionary type. We associate each sprite sheet with a string as the dictionary key, and we have already defined a string (sprites) referencing the desired set of sprites in GameEntityTypeDefinition and set the value for each entity type in createGameEntityTypeDefinitions() (GameEntityDefinitions.cpp). We also store the player’s own sprite sheets in the same way (instead of in the separate player variable in the main game class) and adjust the existing rendering code accordingly.
In general, we use the filename of the sprite sheet as the string key, and the word “Player” for the player’s sprite sheet. These are arbitrary choices and can be anything you like.
The dictionary is defined as follows (SimplePlatformer.h):
// All the spritesheets in use unordered_map<std::string, Image> spriteSheets;
In the post-level load code, we scan over all of the entity instances to gather a list of sprite sheet filenames, and load each of them once (if not already found) in the dictionary as follows (SimplePlatformer.cpp):
// Only load one copy of each sprite sheet otherwise we'd have potentially hundreds of duplicates for (auto it = level.worldEntities.begin(); it != level.worldEntities.end(); it++) if (spriteSheets.find(SharedTools::GameEntityTypes[it->typeId].sprites) == spriteSheets.end()) { WCHAR *spriteFile = StringToWCHAR(SharedTools::GameEntityTypes[it->typeId].sprites); spriteSheets[SharedTools::GameEntityTypes[it->typeId].sprites] = MakeImage(spriteFile); delete [] spriteFile; }
Whenever we want to render a frame of animation for a spawned instance, we will use the string key stored in the instance’s corresponding GameEntityTypeDefinition to look up the correct sprite sheet in the spriteSheets dictionary. The implementation of this is shown later on in the series.
The player sprites are loaded in the main game initialization very simply as follows (SimplePlatformer.cpp):
spriteSheets["Player"] = MakeImage(L"stickman-anim.png");
Finally, we replace all instances of eg. player->DrawPartWH(…) (which draws a frame from the player’s sprite sheet) with spriteSheets[“Player”]->DrawPartWH(…) throughout the entire game code in SimplePlatformer.cpp.
Hooking In Entity Updates and Rendering to the Game Code
All of this hard work won’t actually do anything unless we hook up calls to each GameEntity‘s Update() and Draw() functions in the main game code. Fortunately, thanks to the fact we have developed a class hierarchy which takes of polymorphism, this is trivially accomplished.
In UpdateObjects() after the platforms are updated but before the collision detection code (SimplePlatformer.cpp):
// Update the position/animation of game entities for (auto it = worldEntities.begin(); it != worldEntities.end(); it++) it->second.Update(updateTick);
In DrawScene() after the background is rendered but before the platforms are rendered (SimplePlatformer.cpp):
// Draw the game entities for (auto it = worldEntities.begin(); it != worldEntities.end(); it++) it->second.Draw();
Right now, this does nothing because we haven’t written the code for GameEntity::Update() or GameEntity::Draw() yet – this will be covered in parts 13 and 14.
Should Platforms (World Geometry) and Game Entities be stored in a single game object list?
There are arguments for and against this design decision. On the plus side, geometry and entities share some of the behaviours: they have co-ordinates, they may move, they may be spawned and de-spawned and merging the classes allows collision detection to be handled in a unified way. On the other hand, geometry and entities have quite different purposes and we have to draw a line on how generalized we want our code to be at some point.
In the framework above, we have kept them separate, but in Part 16 we will indeed merge everything into a single list of game objects with a single base class from which we will make both Platform and GameEntity derive (currently they are separate base classes). One other advantage of doing this is that the draw order of geometry and entities can be interspersed so that entities can be rendered in front of some geometry objects but behind others. In the current code, all entities are rendered first and all geometry is considered to be in front of them.
Should the Player be a Game Entity too?
Once again there are arguments for and against. The player is the only object in the game world which accepts user input and can move around according to the user’s wishes rather then using pre-defined rules. But once again, it shares much in common with general entity types like sprite sheet usage, animation configuration, co-ordinates and some collision code. If we want to truly unify player-entity, player-geometry and entity-entity collisions in a polymorphic way, making the player its own game entity may be a good choice to avoid duplicating code.
For now, we have kept the player seperate, but in Part 16 we will re-write the game code so that the player is also a game entity.
The Purpose of Spawn Radius
Earlier we briefly mentioned that each entity type has a spawn radius. This is the circular (or “rectangular” if you choose to implement it that way) radius from the player at the centre point where entities are spawned. The reason for this is optimization: in a large level there may be hundreds of coins, enemies and other instances, and re-calculating them all every frame is hugely wasteful when you can’t see the majority of them. Therefore, we effectively cheat by not activating (spawning) instances until they are reasonably close to the player; usually just outside the visible area of the level on the screen. When an entity leaves the spawn radius, it is reset to its original position and other default parameters, then de-activated (de-spawned) until it next enters the spawn radius (you need a bit of a buffer outside the visible area otherwise the player may see weird behavioural artifacts as instances near the edge of the spawn radius constantly de-spawn and re-spawn if the player is moving backwards and forwards over the same area).
The Results
If you run the new code (link at the top of the article) you will see that all of this new work does… absolutely nothing. That’s because we haven’t actually implemented the code to draw or update any entities yet, but fear not! Parts 13 and 14 are devoted to exactly this topic, so keep reading!
Footnote: Changes to the Level Editor for this part
- Switches to edit/selection mode when resize/move/rotate/scale/skew is clicked
- Middle button now selects/move object even when not in selection mode
- Objects can be re-sized (as opposed to scaled); extents not guaranteed to change correctly when object is scaled, rotated or skewed
- Geometry properties is now two tabs with the left showing positional data which can be edited in real-time
- Loads/saves game entities (no rendering or editing yet)
- Supports copy & paste (Ctrl+C / Ctrl+V)
- Warns if user tries to close editor without saving and gives opportunity to save, discard or cancel
I hope you found this overview useful. Until next time!
Amazing. The best “tutorial” (it’s more than just a tutorial) that I have ever seen.
Wow, thanks a lot for sharing all your experience, I think it is great what you are doing 🙂
Hi Katy, thank you verty mutch, your Platformer is very cool, great work!