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

Chapter 4

A command-based communication system

Up to now, you have seen how events and real-time inputs are handled. In our game, we might handle them as follows (the aircraft methods are fictional to show the principle):

// One-time events sf::Event event;

while (window.pollEvent(event))

{

if (event.type == sf::Event::KeyPressed

&&event.key.code == sf::Keyboard::X) mPlayerAircraft->launchMissile();

}

// Real-time input

if (sf::Keyboard::isKeyPressed(sf::Keyboard::Left)) mPlayerAircraft->moveLeft();

else if (sf::Keyboard::isKeyPressed(sf::Keyboard::Right)) mPlayerAircraft->moveRight();

There are several problems with this approach. On one side, we have hardcoded keys, so a user cannot choose WASD instead of arrow keys for movement. On the other side, we gather input and logic code in one place. The game logic code grows as we implement more features, and makes the originally simple code complicated.

For example, imagine that we want to keep the airplane inside the screen bounds. So we could only move the airplanes to the left if it is currently inside the screen, which can be verified using an if condition. In case of the missiles, we want to check whether enough ammunition is available before firing, leading to anotherif statement. Maybe, we want to limit the fire interval, so that the player must wait at least one second between two launched missiles. For this, we need a timer and further logic, and we already have plenty of game-logic code mixed with the input-handling code. Sure, we could outsource the code into functions, but there is still a problem.

Sometimes, we do not know whom to affect when the user input occurs. Imagine a guided missile which is steered by the player. The missile itself may be stored somewhere in the depths of the world's scene graph, we do not have access to it at

the time we handle input. To gain access, a pointer would have to be passed from the scene graph via the World to the Game class. The same thoughts apply to many other entities in the game, eventually we would duplicate the information about the world at the place where input is handled.

[ 95 ]

www.it-ebooks.info

Command and Control – Input Handling

This is of course not what we want. Instead, we aim to separate input handling and game logics. A possible approach to achieve this goal is discussed in the following sections.

Introducing commands

Here after, commands denote messages that are sent to various game objects.

A command is able to alter the object and to issue orders such as moving an entity, firing a weapon, and triggering an explosion.

We design a structure Command that contains a function object, which can be called on any game object represented by a scene node:

struct Command

{

std::function<void(SceneNode&, sf::Time)>

action;

};

std::function is a C++11 class template to implements callback mechanisms. It treats functions as objects and makes it possible to copy functions or to store them in containers. The std::function class is compatible with function pointers, member function pointers, functors, and lambda expressions.

The template parameter represents the signature of the function being stored.

The following example shows std::function in action. First, we assign a function pointer to it, then we assign a lambda expression:

int add(int a, int b) { return a + b }; std::function<int(int, int)> adder1 = &add; std::function<int(int, int)> adder2

= [] (int a, int b) { return a + b; };

The std::function object can be called using its overloaded operator():

int sum = adder1(3, 5);

// same as add(3, 5)

Using the standard function template std::bind(), the function arguments can be bound to the given values. The std::bind() function returns a new function object that can be stored inside std::function.

[ 96 ]

www.it-ebooks.info

Chapter 4

In the earlier example, we can fix the second argument to 1 and thus have a new function that takes only one argument. The placeholder _1 expresses that the first argument is forwarded, while the second argument is bound to the constant 1:

std::function<int(int)> increaser = std::bind(&add, _1, 1);

int increased = increaser(5);

// same as add(5, 1)

For more detailed information, consult a standard library documentation such as www.cppreference.com. Coming back to our game, the Command structure's member variable action contains the function that implements the order issued to an object.

The first parameter is a reference to a scene node, which is affected by the command.

The second parameter denotes the delta time of the current frame. For example, we can instantiate a command as follows:

void moveLeft(SceneNode& node, sf::Time dt)

{

node.move(-30.f * dt.asSeconds(), 0.f);

}

Command c;

c.action = &moveLeft;

Using the lambda expressions, the equivalent functionality can be written as follows:

c.action = [] (SceneNode& node, sf::Time dt)

{

node.move(-30.f * dt.asSeconds(), 0.f);

};

In short, we can now define any operation on a scene node inside a Command object. The advantage of a command over a direct function call is abstraction: We do not need to know on which scene node to invoke the function; we only specify the action that is performed on it. The message passing system is responsible to deliver the commands to the correct recipients.

[ 97 ]

www.it-ebooks.info

Command and Control – Input Handling

Receiver categories

In order to ensure the correct delivery, we divide our game objects into different categories. Each category is a group of one or multiple game objects, which are likely to receive similar commands. For example, the player's aircraft is an own category, the enemies are one, the projectiles launched by the enemies are one, and so on.

We define an enum to refer to the different categories. Each category except None is initialized with an integer that has one bit set to 1, and the rest are set to 0:

namespace Category

 

 

{

 

 

enum Type

 

 

{

 

 

None

= 0,

Scene

= 1

<< 0,

PlayerAircraft

= 1

<< 1,

AlliedAircraft

= 1

<< 2,

EnemyAircraft

= 1

<< 3,

};

 

 

}

 

 

This makes it possible to combine different categories with the bitwise OR operator. When we want to send a command to all available airplanes, we can create a combined category as follows:

unsigned int anyAircraft = Category::PlayerAircraft | Category::AlliedAircraft | Category::EnemyAircraft;

The SceneNode class gets a new virtual method that returns the category of the game object. In the base class, we return Category::Scene by default:

unsigned int SceneNode::getCategory() const

{

return Category::Scene;

}

[ 98 ]

www.it-ebooks.info

Chapter 4

In a derived class, we can override getCategory() to return a specific category. For example, we can say that an aircraft belongs to the player if it is of type Eagle, and that it is an enemy otherwise:

unsigned int Aircraft::getCategory() const

{

switch (mType)

{

case Eagle:

return Category::PlayerAircraft;

default:

return Category::EnemyAircraft;

}

}

Of course, this is just a simple demonstration of how categories work. You can implement a much more complex logic to determine a game object's category. An object may also be part of multiple categories, where operator| is used to combine categories. This is the reason why the return type of getCategory() is unsigned

int and not Category::Type.

In our game, categories are stored as unsigned int variables. This implies a small issue that you should be aware of: Integer flags are not type-safe. Meaningless numbers can be assigned to category variables, not only enumerators in the Category namespace. A bigger problem is however the confusion between different enum types:

Command command;

command.category = Aircraft::Eagle; // only Eagles receive command

Accidentally, the enums Category::Type and Aircraft::Type were mistaken, but the code still compiles, leading to nasty bugs. Type safety could be achieved by

overloading bitwise operators for the enum, a dedicated class template Flags<Enum> could store flag combinations.

Getting back to commands, we give our Command class another member variable that stores the recipients of the command in a category:

struct Command

 

{

 

 

Command();

std::function<void(SceneNode&, sf::Time)>

action;

unsigned int

category;

};

 

 

 

 

 

 

 

 

[ 99 ]

 

 

 

 

 

www.it-ebooks.info

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