Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
SFML Game Development.pdf
Скачиваний:
209
Добавлен:
28.03.2016
Размер:
4.19 Mб
Скачать

Chapter 7

Adding projectiles

Finally, time to add what makes a game fun. Shooting down stuff is essential for our game. The code to interact with the World class is already defined, thanks to the actions in Player and to the existing Entity base class. All that's left is to define the projectiles themselves.

We start with the Projectile class. We have normal machine gun bullets and homing missiles represented by the same class. This class inherits from the Entity class and is quite small, since it doesn't have anything special that differentiates it from other entities apart from collision tests, which we will talk about later.

class Projectile : public Entity

{

public:

enum Type

{

AlliedBullet,

 

EnemyBullet,

 

Missile,

 

TypeCount

 

};

 

public:

 

 

Projectile(Type type,

 

const TextureHolder& textures);

void

guideTowards(sf::Vector2f position);

bool

isGuided() const;

virtual unsigned int

getCategory() const;

virtual sf::FloatRect getBoundingRect() const;

float

getMaxSpeed() const;

int

getDamage() const;

private:

 

virtual void

updateCurrent(sf::Time dt,

 

CommandQueue& commands);

virtual void

drawCurrent(sf::RenderTarget& target,

 

sf::RenderStates states) const;

[ 161 ]

www.it-ebooks.info

Warfare Unleashed – Implementing Gameplay

private:

 

Type

mType;

sf::Sprite

mSprite;

sf::Vector2f

mTargetDirection;

};

Nothing fun or exciting here; we add some new helper functions such as the one to guide the missile towards a target. So let's have a quick look at the implementation. You might notice, we use the same data tables that we used in the Aircraft class to store data.

Projectile::Projectile(Type type, const TextureHolder& textures) : Entity(1)

,mType(type)

,mSprite(textures.get(Table[type].texture))

{

centerOrigin(mSprite);

}

The constructor simply creates a sprite with the texture we want for the projectile. We will check out the guide function when we actually implement the behavior of missiles. The rest of the functions don't hold anything particularly interesting. Draw the sprite and return a category for the commands and other data needed.

To get an overview of the class hierarchy in the scene graph, here is an inheritance diagram of the current scene node types. The data table structures which are directly related to their corresponding entities are shown at the bottom of the following diagram:

SceneNode

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

SpriteNode

 

Entity

 

TextNode

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Projectile

 

Aircraft

 

Pickup

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

ProjectileData

 

AircraftData

 

PickupData

 

 

 

 

 

 

 

 

[ 162 ]

www.it-ebooks.info

Chapter 7

Firing bullets and missiles

So let's try and shoot some bullets in the game. We start with adding two new actions in the Player class: Fire and LaunchMissile. We define the default key bindings for these to be the Space bar and M keys.

Player::Player()

{

//Set initial key bindings mKeyBinding[sf::Keyboard::Left] = MoveLeft; mKeyBinding[sf::Keyboard::Right] = MoveRight; mKeyBinding[sf::Keyboard::Up] = MoveUp; mKeyBinding[sf::Keyboard::Down] = MoveDown; mKeyBinding[sf::Keyboard::Space] = Fire; mKeyBinding[sf::Keyboard::M] = LaunchMissile;

//...

}

void Player::initializeActions()

{

// ...

mActionBinding[Fire].action = derivedAction<Aircraft>( std::bind(&Aircraft::fire, _1)); mActionBinding[LaunchMissile].action =derivedAction<Aircraft>( std::bind(&Aircraft::launchMissile, _1));

}

So when we press the keys bound to those two actions, a command will be fired which calls the aircraft's fire() and launchMissile() functions. However, we cannot put the actual code that fires the bullet or missile in those two functions. The reason is, because if we could, we would have no concept of how much time has elapsed. We don't want to fire a projectile for every frame. We want there to be some cool down until the next time we fire a bullet, to accomplish that we need to use the delta time passed in the aircraft's update() function.

Instead, we mark what we want to fire by setting the Boolean flags mIsFiring

or mIsLaunchingMissile to true in the Aircraft::fire() and the

Aircraft::launchMissile() functions, respectively. Then we perform the actual logic in the update() function using commands. In order to make the code clearer to read, we have extracted it to its own function.

void Aircraft::checkProjectileLaunch(sf::Time dt, CommandQueue& commands)

{

if (mIsFiring && mFireCountdown <= sf::Time::Zero)

[ 163 ]

www.it-ebooks.info

Warfare Unleashed – Implementing Gameplay

{

commands.push(mFireCommand);

mFireCountdown += sf::seconds(1.f / (mFireRateLevel+1)); mIsFiring = false;

}

else if (mFireCountdown > sf::Time::Zero)

{

mFireCountdown -= dt;

}

if (mIsLaunchingMissile)

{

commands.push(mMissileCommand); mIsLaunchingMissile = false;

}

}

We have a cool down for the bullets. When enough time has elapsed since the last bullet was fired, we can fire another bullet. The actual creation of the bullet is done using a command which we will look at later. After we spawn the bullet, we reset the countdown. Here, we use += instead of =; with a simple assignment, we would discard a little time remainder in each frame, generating a bigger error as time goes by. The time of the countdown is calculated using a member variable

mFireCountdown in Aircraft. Like that, we can improve the aircraft's fire rate easily. So if the fire rate level is one, then we can fire a bullet every half a second, increase it to level two, and we get every third of a second. We also have to remember to keep ticking down the countdown member, even if the user is not trying to fire.

Otherwise, the countdown would get stuck when the user released the Space bar.

Next is the missile launch. We don't need a countdown here, because in the Player class, we made the input an event-based (not real-time based) input.

bool Player::isRealtimeAction(Action action)

{

switch (action)

{

case MoveLeft: case MoveRight: case MoveDown: case MoveUp: case Fire:

return true;

[ 164 ]

www.it-ebooks.info

Chapter 7

default:

return false;

}

}

Since the switch statement does not identify LaunchMissile as a real-time input, the user has to release the M key before he can shoot another missile. The user wants to save his missiles for the moment he needs them.

So, let's look at the commands that we perform, in order to actually shoot the projectiles. We define them in the constructor in order to have access to the texture holder. This shows one of the strengths of lambda expressions in C++11.

Aircraft::Aircraft(Type type, const TextureHolder& textures)

{

mFireCommand.category = Category::SceneAirLayer; mFireCommand.action =

[this, &textures] (SceneNode& node, sf::Time)

{

createBullets(node, textures);

};

mMissileCommand.category = Category::SceneAirLayer; mMissileCommand.action =

[this, &textures] (SceneNode& node, sf::Time)

{

createProjectile(node, Projectile::Missile, 0.f, 0.5f, textures);

};

}

Now, we can pass the texture holder to the projectiles without any extra difficulty, and we don't even have to keep an explicit reference to the resources. This makes the Aircraft class and our code a lot simpler, since the reference does not need to exist in the update() function.

The commands are sent to the air layer in the scene graph. This is the node where we want to create our projectiles. The missile is a bit simpler to create than bullets, that's why we call directly Aircraft::createProjectile(). So how do we create bullets then?

void Aircraft::createBullets(SceneNode& node, const TextureHolder& textures) const

{

Projectile::Type type = isAllied()

? Projectile::AlliedBullet : Projectile::EnemyBullet;

[ 165 ]

www.it-ebooks.info

Warfare Unleashed – Implementing Gameplay

switch (mSpreadLevel)

{

case 1:

createProjectile(node, type, 0.0f, 0.5f, textures); break;

case 2:

createProjectile(node, type, -0.33f, 0.33f, textures); createProjectile(node, type, +0.33f, 0.33f, textures); break;

case 3:

createProjectile(node, type, -0.5f, 0.33f, textures); createProjectile(node, type, 0.0f, 0.5f, textures); createProjectile(node, type, +0.5f, 0.33f, textures); break;

}

}

For projectiles, we provide different levels of fire spread in order to make the game more interesting. The player can feel that progress is made, and that his aircraft becomes more powerful as he is playing. The function calls createProjectile() just as it was done for the missile.

So how do we actually create the projectile and attach it to the scene graph?

void Aircraft::createProjectile(SceneNode& node, Projectile::Type type, float xOffset, float yOffset, const TextureHolder& textures) const

{

std::unique_ptr<Projectile> projectile( new Projectile(type, textures));

sf::Vector2f offset(

xOffset * mSprite.getGlobalBounds().width, yOffset * mSprite.getGlobalBounds().height);

sf::Vector2f velocity(0, projectile->getMaxSpeed());

float sign = isAllied() ? -1.f : +1.f; projectile->setPosition(getWorldPosition() + offset * sign); projectile->setVelocity(velocity * sign); node.attachChild(std::move(projectile));

}

[ 166 ]

www.it-ebooks.info

Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]