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

Implementing circle collision detection

We are going to start by implementing circle collision detection because it is the fastest collision detection method available. It also fits well with our projectiles, which will be the most common kind of collider in our game. It will not do a great job on our ships, but later, we can improve that situation by implementing a compound collider that will use multiple circle colliders for each spaceship instead of just one. Because we only have two spaceships, this will give us the best of both worlds in our collision detection: the speed of circle collision detection, along with the accuracy of some of our better collision detection methods.

Let's start by adding a Collider class definition into our game.hpp file and creating a new collider.cpp file where we can define the functions used by our Collider class. Here's what our new Collider class will look like in the game.hpp file:

class Collider {
public:
double m_X;
double m_Y;
double m_Radius;

Collider(double radius);

bool HitTest( Collider *collider );
};

Here is the code we are putting in the collider.cpp file:

#include "game.hpp"
Collider::Collider(double radius) {
m_Radius = radius;
}

bool Collider::HitTest( Collider *collider ) {
double dist_x = m_X - collider->m_X;
double dist_y = m_Y - collider->m_Y;
double radius = m_Radius + collider->m_Radius;

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

The Collider class is a pretty simple circle collider. As we discussed earlier, a circle collider has an x and a y coordinate and a radius. The HitTest function does a pretty simple distance test to see whether the two circles are close enough to touch each other. We do this by squaring the x distance and squaring the y distance between the two colliders, which gives us the distance squared between the two points. We could take the square root to determine the actual distance, but a square root is a relatively slow function to perform, and it's much faster to square the sum of the radii to compare.

We will also need to talk about class inheritance briefly. If you look back at our code from earlier, we have a PlayerShip class and an EnemyShip class. These classes share most of their attributes. They all have x and y coordinates, x and y velocity, and many other attributes that are identical. Many of the functions use the same code copied and pasted. Instead of having this code defined twice, let's go back and create a Ship class that has all of the features that are common to our PlayerShip and EnemyShip classes. Then, we can refactor our EnemyShip and PlayerShip classes to inherit from our Ship class. Here is our new Ship class definition that we are adding to game.hpp:

class Ship: public Collider {
public:
Uint32 m_LastLaunchTime;
const int c_Width = 16;
const int c_Height = 16;
SDL_Texture *m_SpriteTexture;
Ship();
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;
void Render();
};

The first line, Ship class: public Collider, tells us that Ship will inherit all of the public and protected members of the Collider class. We are doing this because we would like to be able to perform a hit test. The Collider class also now defines the m_X and m_Y attribute variables that keep track of the x and y coordinates of our object. We have moved everything common to our EnemyShip and PlayerShip classes into the Ship class. You will notice that we have one virtual function, virtual void Move() = 0;. This line tells us that we will have a Move function in all classes that inherit from Ship, but we will need to define Move inside those classes instead of directly in the Ship class. That makes Ship an abstract class, which means that we cannot create an instance of Ship, but, instead, it is a class from which other classes will inherit.

Class inheritance, abstract classes, and virtual functions are all a part of a style of programming known as Object-Oriented Programming ( OOP). C++ was created in 1979 by Bjarne Stroustrup to add OOP to the C programming language. If you're not familiar with OOP, there are hundreds of books that go into great detail on this topic. I will only be able to cover it in a cursory manner in this book.

Next, we are going to modify the PlayerShip and EnemyShip classes in the game.hpp file to remove all of the methods and attributes we have moved into the parent Ship class. We will also modify these classes so that they inherit from Ship. Here is the new version of the class definitions:

class PlayerShip: public Ship {
public:
const char* c_SpriteFile = "sprites/Franchise.png";
const Uint32 c_MinLaunchTime = 300;
PlayerShip();
void Move();
};

class EnemyShip: public Ship {
public:
const char* c_SpriteFile = "sprites/BirdOfAnger.png";
const Uint32 c_MinLaunchTime = 300;
const int c_AIStateTime = 2000;
FSM_STUB m_AIState;
int m_AIStateTTL;

EnemyShip();
void AIStub();
void Move();
};

Now, we need to add a ship.cpp file and define all of the methods that will be common to EnemyShip and PlayerShip. These methods were in both PlayerShip and EnemyShip previously, but now we can have them all in one place. Here is what the ship.cpp file looks like:

#include "game.hpp"

Ship::Ship() : Collider(8.0) {
m_Rotation = PI;
m_DX = 0.0;
m_DY = 1.0;
m_VX = 0.0;
m_VY = 0.0;
m_LastLaunchTime = current_time;
}

void Ship::RotateLeft() {
m_Rotation -= delta_time;

if( m_Rotation < 0.0 ) {
m_Rotation += TWO_PI;
}
m_DX = sin(m_Rotation);
m_DY = -cos(m_Rotation);
}

void Ship::RotateRight() {
m_Rotation += delta_time;

if( m_Rotation >= TWO_PI ) {
m_Rotation -= TWO_PI;
}
m_DX = sin(m_Rotation);
m_DY = -cos(m_Rotation);
}

void Ship::Accelerate() {
m_VX += m_DX * delta_time;
m_VY += m_DY * delta_time;
}

void Ship::Decelerate() {
m_VX -= (m_DX * delta_time) / 2.0;
m_VY -= (m_DY * delta_time) / 2.0;
}
void Ship::CapVelocity() {
double vel = sqrt( m_VX * m_VX + m_VY * m_VY );

if( vel > MAX_VELOCITY ) {
m_VX /= vel;
m_VY /= vel;

m_VX *= MAX_VELOCITY;
m_VY *= MAX_VELOCITY;
}
}
void Ship::Render() {
dest.x = (int)m_X;
dest.y = (int)m_Y;
dest.w = c_Width;
dest.h = c_Height;

double degrees = (m_Rotation / PI) * 180.0;

int return_code = SDL_RenderCopyEx( renderer, m_SpriteTexture,
NULL, &dest,
degrees, NULL, SDL_FLIP_NONE );

if( return_code != 0 ) {
printf("failed to render image: %s\n", IMG_GetError() );
}
}

The only real difference between the versions of these classes that were in the player_ship.cpp and the enemy_ship.cpp files are that, instead of PlayerShip:: or EnemyShip:: in front of each of the function definitions, we now have Ship:: in front of the function definitions.

Next, we are going to need to modify player_ship.cpp and enemy_ship.cpp by removing all of the functions that we now have defined inside the ship.cpp file. Let's take a look at what the enemy_ship.cpp file looks like broken into two parts.  The first part is the #include of our game.hpp file and the EnemyShip constructor function:

#include "game.hpp"

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_LastLaunchTime = current_time;

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 );
}


In the second part of our enemy_ship.cpp file we have the Move and AIStub functions:

void EnemyShip::Move() {
AIStub();

if( m_AIState == TURN_LEFT ) {
RotateLeft();
}

if( m_AIState == TURN_RIGHT ) {
RotateRight();
}

if( m_AIState == ACCELERATE ) {
Accelerate();
}

if( m_AIState == DECELERATE ) {
Decelerate();
}

CapVelocity();
m_X += m_VX;

if( m_X > 320 ) {
m_X = -16;
}
else if( m_X < -16 ) {
m_X = 320;
}

m_Y += m_VY;

if( m_Y > 200 ) {
m_Y = -16;
}
else if( m_Y < -16 ) {
m_Y = 200;
}

if( m_AIState == SHOOT ) {
Projectile* projectile;

if( current_time - m_LastLaunchTime >= c_MinLaunchTime ) {
m_LastLaunchTime = current_time;
projectile = projectile_pool->GetFreeProjectile();

if( projectile != NULL ) {
projectile->Launch( m_X, m_Y, m_DX, m_DY );
}
}
}
}

void EnemyShip::AIStub() {
m_AIStateTTL -= diff_time;

if( m_AIStateTTL <= 0 ) {
// for now get a random AI state.
m_AIState = (FSM_STUB)(rand() % 5);
m_AIStateTTL = c_AIStateTime;
}
}

Now that we have seen what is in the enemy_ship.cpp file, let's take a look at what the new player_ship.cpp file looks like:

#include "game.hpp"
PlayerShip::PlayerShip() {
m_X = 160.0;
m_Y = 100.0;
SDL_Surface *temp_surface = IMG_Load( c_SpriteFile );

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 );
}

void PlayerShip::Move() {
current_time = SDL_GetTicks();
diff_time = current_time - last_time;
delta_time = (double)diff_time / 1000.0;
last_time = current_time;

if( left_key_down ) {
RotateLeft();
}

if( right_key_down ) {
RotateRight();
}

if( up_key_down ) {
Accelerate();
}

if( down_key_down ) {
Decelerate();
}

CapVelocity();
m_X += m_VX;

if( m_X > 320 ) {
m_X = -16;
}
else if( m_X < -16 ) {
m_X = 320;
}

m_Y += m_VY;

if( m_Y > 200 ) {
m_Y = -16;
}
else if( m_Y < -16 ) {
m_Y = 200;
}

if( space_key_down ) {
Projectile* projectile;

if( current_time - m_LastLaunchTime >= c_MinLaunchTime ) {
m_LastLaunchTime = current_time;
projectile = projectile_pool->GetFreeProjectile();
if( projectile != NULL ) {
projectile->Launch( m_X, m_Y, m_DX, m_DY );
}
}
}
}

Next, let's modify the Move function in our ProjectilePool class so that, every time it moves a Projectile, it also tests to see whether it hit one of our ships:

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->HitTest( player ) ) {
printf("hit player\n");
}
if( projectile->HitTest( enemy ) ) {
printf("hit enemy\n");
}
}
}
}

For right now, we are only going to print to the console when either the player or the enemy collides with a projectile. That will tell us whether our collision detection is working correctly. In later sections, we will add animations to destroy our ships when they collide with the projectile.

There is one last change we need to make to the Launch function on our Projectile class. When we launch a projectile from our ships, we give the projectile an x and a y position and an x and y velocity based on the direction the ship was facing. We need to take that direction and move the starting point of the projectile. That is to prevent the projectile from hitting the ship that launched it by moving it out of the collision detection circle for the ship:

void Projectile::Launch(double x, double y, double dx, double dy) {
m_X = x + dx * 9;
m_Y = y + dy * 9;
m_VX = velocity * dx;
m_VY = velocity * dy;
m_TTL = alive_time;
m_Active = true;
}

In the next section, we will detect when our ship collides with a projectile and run an explosion animation.