Hands-On Game Development with WebAssembly
上QQ阅读APP看书,第一时间看更新

Implementing compound circle colliders

Now that our collision detection is working, and we have our ships and projectiles exploding on a collision, let's see how we can make our collision detection better. We chose circle collision detection for two reasons: the collision algorithm is fast, and it is simple. We could do better, however, by merely adding more circles to each ship. That will increase our collision detection time by a factor of n, where n is the average number of circles we have on each ship. That is because the only collision detection we do is between the projectiles and the ships. Even so, we don't want to go overboard with the number of circles we choose to use for each ship.

For the player ship, the front of the spaceship is covered well by the basic circle. However, we could get much better coverage of the back of the player's spaceship by adding a circle to each side:

Our player ship compound collider

The enemy ship is the opposite. The back of that spaceship is covered pretty well by a default circle, but the front could use some better coverage, so, for the enemy ship, we will add some additional circles in front:

Our enemy ship compound collider

The first thing we need to do is change the Collider class to include information from the parent of our collider. Here is the new version of the Collider class definition inside our game.hpp file:

class Collider {
public:
float* m_ParentRotation;
float* m_ParentX;
float* m_ParentY;
float m_X;
float m_Y;
float m_Radius;

bool CCHitTest( Collider* collider );
void SetParentInformation( double* rotation, double* x, double*
y );
Collider(double radius);
bool HitTest( Collider *collider );
};

We have added three-pointers to attributes of the parent of our Collider class. These will point to the x and y coordinates, as well as the Rotation of the collider's parent, which will either be the enemy ship, the player ship, or NULL. We will initialize those values to NULL in our constructor, and if the value is null, we will not modify the behavior of our collider. If, however, those values are set to something else, we will call the CCHitTest function to determine whether there is a collision. This version of the hit test will adjust the position of the collider to be relative to its parent's position and rotation before doing the collision test. Now that we have made the changes to the collider's definition, we will make changes to the functions inside the collider.cpp file to support the new compound colliders.

The first thing to do is modify our constructor to initialize the new pointers to NULL:

Collider::Collider(double radius) {
m_ParentRotation = NULL;
m_ParentX = NULL;
m_ParentY = NULL;
m_Radius = radius;
}

We have a new function to add to our collider.cpp file, the CCHitTest function, which will be our compound collider hit test. This version of the hit test will adjust the x and y coordinates of our collider to be relative to the position and rotation of our parent ship:

bool Collider::CCHitTest( Collider* collider ) {
float sine = sin(*m_ParentRotation);
float cosine = cos(*m_ParentRotation);
float rx = m_X * cosine - m_Y * sine;
float ry = m_X * sine + m_Y * cosine;
float dist_x = (*m_ParentX + rx) - collider->m_X;
float dist_y = (*m_ParentY + ry) - collider->m_Y;
float radius = m_Radius + collider->m_Radius;

if( dist_x * dist_x + dist_y * dist_y <= radius * radius ) {
return true;
}
return false;
}

The first thing this function does is take the sine and cosine of the parent's rotation and use that rotation to get a rotated version of x and y in the variables, rx and ry. We then adjust that rotated x and y position by the parent's x and y position, before calculating the distance between the two collider x and y positions. After we add this new CCHitTest function, we need to modify the HitTest function to call this version of the hit test if the parent values are set. Here is the latest version of HitTest:

bool Collider::HitTest( Collider *collider ) {
if( m_ParentRotation != NULL && m_ParentX != NULL && m_ParentY != NULL ) {
return CCHitTest( collider );
}

float dist_x = m_X - collider->m_X;
float dist_y = m_Y - collider->m_Y;
float radius = m_Radius + collider->m_Radius;

if( dist_x * dist_x + dist_y * dist_y <= radius * radius ) {
return true;
}
return false;
}

We have created a function to set all of these values called SetParentInformation. Here is the function definition:

void Collider::SetParentInformation( float* rotation, float* x, float* y ) {
m_ParentRotation = rotation;
m_ParentX = x;
m_ParentY = y;
}

To take advantage of these new kinds of colliders, we need to add a new vector of colliders into the Ship class. The following is the new class definition for Ship in the game.hpp file:

class Ship : public Collider {
public:
Uint32 m_LastLaunchTime;
const int c_Width = 32;
const int c_Height = 32;

SDL_Texture *m_SpriteTexture;
SDL_Rect src = {.x = 0, .y = 0, .w = 32, .h = 32 };
std::vector<Collider*> m_Colliders;
bool m_Alive = true;
Uint32 m_CurrentFrame = 0;

int m_NextFrameTime;
float m_Rotation;
float m_DX;
float m_DY;
float m_VX;
float m_VY;

void RotateLeft();
void RotateRight();
void Accelerate();
void Decelerate();
void CapVelocity();
virtual void Move() = 0;
Ship();
void Render();
bool CompoundHitTest( Collider* collider );
};

There are two differences between this version and the previous version of the Ship class. The first is the addition of the m_Colliders vector attribute:

 std::vector<Collider*> m_Colliders;

The second change is the new CompoundHitTest function added at the bottom of the class:

bool CompoundHitTest( Collider* collider );

For the change to our class, we will need to add a new function to our ship.cpp file:

bool Ship::CompoundHitTest( Collider* collider ) {
Collider* col;
std::vector<Collider*>::iterator it;
for( it = m_Colliders.begin(); it != m_Colliders.end(); it++ ) {
col = *it;
if( col->HitTest(collider) ) {
return true;
}
}
return false;
}

This CompoundHitTest function is a pretty simple function that loops over all of our additional colliders and performs a hit test on them. This line creates a vector of collider pointers. We will now modify our EnemyShip and PlayerShip constructors to add some colliders into this vector. First, we will add some new lines to the EnemyShip constructor inside the enemy_ship.cpp file:

EnemyShip::EnemyShip() {
m_X = 60.0;
m_Y = 50.0;
m_Rotation = PI;
m_DX = 0.0;
m_DY = 1.0;
m_VX = 0.0;
m_VY = 0.0;
m_AIStateTTL = c_AIStateTime;
m_Alive = true;
m_LastLaunchTime = current_time;

Collider* temp_collider = new Collider(2.0);
temp_collider->SetParentInformation( &(this->m_Rotation),
&(this->m_X), &(this->m_Y) );
temp_collider->m_X = -6.0;
temp_collider->m_Y = -6.0;
m_Colliders.push_back( temp_collider );
temp_collider = new Collider(2.0);
temp_collider->SetParentInformation( &(this->m_Rotation),
&(this->m_X), &(this->m_Y) );
temp_collider->m_X = 6.0;
temp_collider->m_Y = -6.0;
m_Colliders.push_back( temp_collider );

SDL_Surface *temp_surface = IMG_Load( c_SpriteFile );

if( !temp_surface ) {
printf("failed to load image: %s\n", IMG_GetError() );
return;
}
else {
printf("success creating enemy ship surface\n");
}
m_SpriteTexture = SDL_CreateTextureFromSurface( renderer,
temp_surface );
if( !m_SpriteTexture ) {
printf("failed to create texture: %s\n", IMG_GetError() );
return;
}
else {
printf("success creating enemy ship texture\n");
}
SDL_FreeSurface( temp_surface );
}

The code that we added creates new colliders and sets the parent information for those colliders as pointers to the x and y coordinates, as well as the radius to the addresses of those values inside of this object. We set the m_X and m_Y values for this collider relative to the position of this object, and then we push the new colliders into the m_Colliders vector attribute:

Collider* temp_collider = new Collider(2.0);
temp_collider->SetParentInformation( &(this->m_Rotation),
&(this->m_X), &(this->m_Y) );
temp_collider->m_X = -6.0;
temp_collider->m_Y = -6.0;
m_Colliders.push_back( temp_collider );
temp_collider = new Collider(2.0);
temp_collider->SetParentInformation( &(this->m_Rotation),
&(this->m_X), &(this->m_Y) );
temp_collider->m_X = 6.0;
temp_collider->m_Y = -6.0;
m_Colliders.push_back( temp_collider );

We will now do something similar for the PlayerShip constructor inside the player_ship.cpp file:

PlayerShip::PlayerShip() {
m_X = 160.0;
m_Y = 100.0;
SDL_Surface *temp_surface = IMG_Load( c_SpriteFile );

Collider* temp_collider = new Collider(3.0);
temp_collider->SetParentInformation( &(this->m_Rotation),
&(this->m_X), &(this->m_Y) );
temp_collider->m_X = -6.0;
temp_collider->m_Y = 6.0;
m_Colliders.push_back( temp_collider );
temp_collider = new Collider(3.0);
temp_collider->SetParentInformation( &(this->m_Rotation),
&(this->m_X), &(this->m_Y) );
temp_collider->m_X = 6.0;
temp_collider->m_Y = 6.0;
m_Colliders.push_back( temp_collider );

if( !temp_surface ) {
printf("failed to load image: %s\n", IMG_GetError() );
return;
}
m_SpriteTexture = SDL_CreateTextureFromSurface( renderer,
temp_surface );

if( !m_SpriteTexture ) {
printf("failed to create texture: %s\n", IMG_GetError() );
return;
}
SDL_FreeSurface( temp_surface );
}

Now, we have to change our projectile pool to run the collision detection on these new compound colliders in our ships. Here is the modified version of the MoveProjectiles function inside the projectile_pool.cpp file:

void ProjectilePool::MoveProjectiles() {
Projectile* projectile;
std::vector<Projectile*>::iterator it;

for( it = m_ProjectileList.begin(); it != m_ProjectileList.end();
it++ ) {
projectile = *it;
if( projectile->m_Active ) {
projectile->Move();
if( projectile->m_CurrentFrame == 0 &&
player->m_CurrentFrame == 0 &&
( projectile->HitTest( player ) ||
player->CompoundHitTest( projectile ) ) ) {
player->m_CurrentFrame = 1;
player->m_NextFrameTime = ms_per_frame;
projectile->m_CurrentFrame = 1;
projectile->m_NextFrameTime = ms_per_frame;
}
if( projectile->m_CurrentFrame == 0 &&
enemy->m_CurrentFrame == 0 &&
( projectile->HitTest( enemy ) ||
enemy->CompoundHitTest( projectile ) ) ) {
enemy->m_CurrentFrame = 1;
enemy->m_NextFrameTime = ms_per_frame;
projectile->m_CurrentFrame = 1;
projectile->m_NextFrameTime = ms_per_frame;
}
}
}
}

Because we continue to inherit Collider in our Ship class, we still will perform a regular hit test on our player and enemy ships. We have added a call to CompoundHitTest in our Ship class that loops over our m_Colliders vector and performs a collision hit test on each of the colliders in that vector.

Our compound collider solution is not generalized, and, for the most part, neither is our collision detection. We are only detecting collisions between our ships and our projectiles. We are not currently performing any collision detection between our ships. To have a generalized approach to collision detection, we would need to implement spacial segmenting. That would prevent the number of collision checks from growing exponentially with each additional collider added to our game.