In our game, physics will be a simple matter of entity-to-world and entity-to-entity collision in two dimensions. In Level 4, we have water tiles, but that will be a special case of entity-to-world.
What are “entities”? As we mentioned in Physics Engine, the game world consists of the “level,” the “player,” and the “animals.” To simplify things, we will say the game world consists of the “level” and “entities.” An entity is anything that exists in the game world within a particular level.
The player is an entity because she is in the world but can move around independently. The animals are also entities that can move around on their own. There can also be stationary entities, such as a button. Because a button has an animation and logic behind it (pressed and unpressed), it cannot be fully represented as merely a part of the tile-engine portion of the level; it must be an entity.
All entities need to have a position, a way to render themselves, and an update function that gets called every frame to allow their internal logic to progress.
We will begin by creating a class named Entity
(which you already saw referenced in
TileWorld
):
//Entity.h @interface Entity : NSObject { CGPoint worldPos; CGPoint velocity; Sprite* sprite; TileWorld* world; } @property (nonatomic, retain) Sprite* sprite; @property (nonatomic) CGPoint position; - (id) initWithPos:(CGPoint) pos sprite:(Sprite*)sprite; - (void) drawAtPoint:(CGPoint) offset; - (void) update:(CGFloat) time; - (void) setWorld:(TileWorld*) newWorld; @end
Notice the pointer to TileWorld
. This allows the entity to check for entity-to-world collisions on its own.
The implementation of the Entity
class is very simple, with the
exception of the render function, which adds its position to a camera
offset before calling drawAtPoint:
on
its Sprite
:
//Entity.m - (void) drawAtPoint:(CGPoint) offset { offset.x += worldPos.x; offset.y += worldPos.y; [sprite drawAtPoint:offset]; }
The rest of the functions are simple or empty because Entity
is a base class. We will create other
classes that inherit from Entity
and
implement more interesting functionality.
Entities that can move but are limited by the physical properties of the level must detect entity-to-world collisions. But first we must have a way to know what physical property each level tile has.
We do this in a way similar to the unique tile indexes of the tile graphics. Just as we had a file with texture indexes for each tile, we can have a file with physics flags for each tile. Our game design incorporates the following physics properties for tiles: normal, impassable, and water. Normal is applied to tiles that can be walked upon, impassable is used for walls and tiles that are otherwise inaccessible to entities, and water is for special tiles that represent water.
Giving values (0 = normal, 1 = impassable, 2 = water), we could represent the first level with the following data:
1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
Not very exciting, but you get the point. Entities cannot enter any of the tiles with a “1,” but they can walk freely in any tile with a “0.”
Now that we know what each tile is supposed to represent physically, we can perform entity-to-world collision detection (remember the separation between collision “detection” and collision “resolution”).
The most naive approach is to do a rectangle-to-rectangle
collision between the entity and each level tile. However, this quickly
becomes inefficient as you have to perform levelWidth*levelHeight
checks for all entities
at every frame. Fortunately, we can make a number of
optimizations.
Since this check will be performed every update loop, we can assume that our entity is in a stable state at the start of our check (because any collisions would have been resolved during the last frame). Therefore, if the entity is not moving (its velocity is zero), we can skip the process altogether (for the current loop).
Meanwhile, if the entity is moving, we only need to check for collisions along its intended path. Even better, if we know that our entities aren’t moving faster than one tile length per frame, we only need to check the end of the path on each frame (because it will give the same result as the middle of the path if we’re not passing through more than one tile).
Unfortunately for us, the iPhone SDK does not come with a helpful 2D vector class. Therefore, we have to provide our own functions for vector math located in PointMath.h:
add
Performs vector addition on two
CGPoint
vectorssub
Performs vector subtraction on two
CGPoint
vectorsscale
Performs scalar multiplication between a
CGPoint
and a scalar floatdistsquared
Returns the squared magnitude of a
CGPoint
vectorunit
Returns a unit vector based on a
CGPoint
vectortowards
A utility function that gets the vector between two
CGPoint
s and derives its unit vector
The following is an example of how we will check for collisions in
our entity code using our functions for vector math (remember that the
time
parameter of our update function
represents time passed since the last update):
CGPoint distThisFrame = scale(velocity, time); CGPoint projection = add( worldPos, distThisFrame ); Tile* newTile = [world tileAt:projection]; if( overtile == nil || (overtile->flags & UNWALKABLE) != 0 ) { //tile is out of bounds or impassable projection = worldPos; //don't move } worldPos = projection;
First we calculate the amount of space that will be moved during this frame. Note that we scale our velocity by the time passed during the last frame, instead of merely using the entire velocity. This is because our speed is in units per second, but our update function is not called once per second (hopefully it’s being called 33 times per second). If we used the full velocity, the entities would be moving far too quickly, and more importantly, they would speed up or slow down if our game loop was faster or slower.
Next, we use vector math to add the distThisFrame
to our current worldPos
to get the projected position at the
end of the current loop. Now we need to check to see whether that
projected point is going to be in a safe tile or if it is
impassable.
We use the tileAt:
function
of TileWorld
to grab the
tile that is underneath that point. If the point is out of the bounds of
the level, it will return nil.
We check to see whether the result is nil or whether it has an
UNWALKABLE
physics flag. If either of
these cases is true, we should not let the entity walk onto the tile, so
we set the projection to our current position.
Finally, we accept the projection vector as our new current position.
Although impassable tile collisions can simply be resolved as soon as they are detected, other special types of tiles need extra game logic. To handle these situations, we can send the entity a message when it collides with a certain type of tile. This allows us to write code to allow entities to handle special tiles in their own way.
If a hippopotamus is walking on land and collides with a water tile, it can move to a swimming state and keep going. In contrast, a cougar might decide it doesn’t want to get wet and the AI can use the message to turn the cougar around and find another path.
Entity-to-world collision is fairly straightforward. Your only goal is to keep entities from entering impassable tiles, and notifying them when they’re on special tiles such as water. Entity-to-entity collision, however, is more complex.
With entity-to-entity collision, there are many results from different types of collisions. If an animal touches another animal, we could simply choose not to let the animals pass through each other, or we could ignore the situation if we want to allow animals to walk over each other. Or if they are two different types of animals, they might attack each other.
When collision resolution takes many different forms due to game design, the line between physics code and game logic code gets blurred. Similar to special tiles, we can handle this through special messages alerting entities of events so that the entities themselves can handle the resolution logic.
However, we might also want to resolve these collisions strictly
at the GameState
level. A
single entity knows only as much about the world as we give it access
to. However, the GameState
envelops
the entire world, including all of the entities, the level, and all of
the game logic code.
To clarify, consider the following example. For the first level, we want to determine when one of the emu chicks has walked onto the nest at the top of the level. To represent the nest, we will have a special kind of entity called a trigger area. A trigger area has a bounding box for determining physical collisions just like a regular entity, but it doesn’t necessarily have a graphical representation. Because the nest is already represented in the tiles of the level background, we don’t need to draw anything for this trigger area. Instead, we merely perform entity-to-entity collision between emu chicks and the nest trigger area.
However, when we detect this collision, we want to remove the emu
entity from the level. Simply sending a message to the entity won’t
allow it to remove itself. We need the resolution to happen at the
GameState
level.
In addition, not all entities require collision detection against each other. Using the previous example, although an emu chick should be tested for collision against the nest trigger area, the main character should not be. Because of the wide variety of expected results from entity-to-entity collision, it should be handled on a case-by-case basis.
In the next section, we will examine the implementation of these cases.
Get iPhone Game Development now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.