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

Command and Control – Input Handling

The default constructor initializes the category to Category::None. By assigning a different value to it, we can specify exactly who receives the command. If we want a command to be executed for all airplanes except the player's one, the category can be set accordingly:

Command command; command.action = ...;

command.category = Category::AlliedAircraft | Category::EnemyAircraft;

Command execution

We have discussed how to construct commands with a function and a receiver category. In order to execute them, the function must be invoked on the receivers.

In our world, commands are passed to the scene graph, inside which they are distributed to all scene nodes with the corresponding game objects. Each scene node is responsible for forwarding a command to its children.

We write a non-virtual method SceneNode::onCommand() which is called every time a command is passed to the scene graph. First, we check if the current scene node is a receiver of the command, that is, if it is listed in the command's receiver category. The check is performed using the bitwise AND operator. If a bit is set in both the command's and the current node's category, then we know that the node receives the command. In this case, we can execute the command by invoking the action member of type std::function on the current node, and with the current frame time. The second part of onCommand() forwards the command to all the child nodes:

void SceneNode::onCommand(const Command& command, sf::Time dt)

{

if (command.category & getCategory()) command.action(*this, dt);

FOREACH(Ptr& child, mChildren) child->onCommand(command, dt);

}

[ 100 ]

www.it-ebooks.info

Chapter 4

Command queues

Now that the interface to distribute a command inside the scene graph is ready, we need a way to transport commands to the world and the scene graph. For this purpose, we write a new class CommandQueue. This class is a very thin wrapper around a queue of commands. A queue is a FIFO (first in, first out) data structure that ensures that elements, which are inserted first, are also removed first. Only the front element can be accessed. The standard library already provides the container adapter std::queue, which implements a queue interface on top of a full-featured STL container such as std::deque.

Our class looks similar to the following:

class CommandQueue

 

{

 

public:

 

void

push(const Command& command);

Command

pop();

bool

isEmpty() const;

private:

std::queue<Command> mQueue;

};

It only provides three methods, which directly forward their calls to the underlying std::queue. Their definitions are straightforward, and hence they are omitted here.

The World class holds an instance of CommandQueue. In the World::update() function, all commands that have been triggered since the last frame are forwarded to the scene graph:

void World::update(sf::Time dt)

{

...

//Forward commands to the scene graph while (!mCommandQueue.isEmpty())

mSceneGraph.onCommand(mCommandQueue.pop(), dt);

//Regular update step

mSceneGraph.update(dt);

}

[ 101 ]

www.it-ebooks.info

Command and Control – Input Handling

As explained earlier, SceneNode::onCommand() distributes a command across all scene nodes. We also provide a getter function to access the command queue from outside the world:

CommandQueue& World::getCommandQueue()

{

return mCommandQueue;

}

Handling player input

Since this chapter is about input, it would be interesting to see how the commands can be exploited to react to the SFML events and real-time input. Up to now, player input has been handled in the Game class. But it deserves an own class, we call it

Player.

The Player class contains two methods to react to the SFML events and real-time input, respectively:

class Player

 

{

 

public:

 

void

handleEvent(const sf::Event& event,

 

CommandQueue& commands);

void

handleRealtimeInput(CommandQueue& commands);

};

 

These methods are invoked from the Game class, inside the processInput() member function. Only the sf::Event::Closed event is still handled inside Game, all other events are delegated to the Player class:

void Game::processInput()

{

CommandQueue& commands = mWorld.getCommandQueue();

sf::Event event;

while (mWindow.pollEvent(event))

{

mPlayer.handleEvent(event, commands);

if (event.type == sf::Event::Closed) mWindow.close();

}

mPlayer.handleRealtimeInput(commands);

}

[ 102 ]

www.it-ebooks.info

Chapter 4

Now let's see how input is handled inside the Player class. We treat the example of the arrow keys and real-time input with sf::Keyboard. What we want to do is change the aircraft's velocity if an arrow key is pressed. For our command, we need

an action function, we design it as a function object (functor) similar to the following:

struct AircraftMover

{

AircraftMover(float vx, float vy) : velocity(vx, vy)

{

}

void operator() (SceneNode& node, sf::Time) const

{

Aircraft& aircraft = static_cast<Aircraft&>(node); aircraft.accelerate(velocity);

}

sf::Vector2f velocity;

};

When the functor is invoked, operator() is called, which adds (vx, vy) to the current aircraft velocity. aircraft.accelerate(velocity) is a utility function that acts equivalently to aircraft.setVelocity(aircraft.getVelocity() + velocity). In other words, the variable velocity is added to the aircraft's current velocity. The downcast is required because the command stores a function which is invoked on SceneNode&, but we need Aircraft&. It is safe as long as we guarantee with the receiver category that only correct types receive the command. We can now construct a command as follows:

Command moveLeft;

moveLeft.category = Category::PlayerAircraft; moveLeft.action = AircraftMover(-playerSpeed, 0.f);

[ 103 ]

www.it-ebooks.info

Command and Control – Input Handling

Since we often work on entities that are classes derived from SceneNode, the constant need for downcasts is annoying. It would be much more user friendly if we could directly create a function with the signature void(Aircraft& aircraft, sf::Time dt) instead. This is possible, if we provide a small adapter derivedAction() that takes a function on a derived class such as Aircraft and converts it to a function on the SceneNode base class. We create a lambda expression, inside which we invoke the original function fn on the derived class, passing a downcast argument to it. An additional assertion checks in the debug mode that the conversion is safe, which

is extremely helpful to avoid bugs. The lambda expression uses a [=] capture list, meaning that variables referenced from its body (such as the variable fn) are copied from the surrounding scope:

template <typename GameObject, typename Function> std::function<void(SceneNode&, sf::Time)>

derivedAction(Function fn)

{

return [=] (SceneNode& node, sf::Time dt)

{

// Check if cast is safe assert(dynamic_cast<GameObject*>(&node) != nullptr);

// Downcast node and invoke function on it fn(static_cast<GameObject&>(node), dt);

};

}

Given this adapter, we can change our AircraftMover to take Aircraft& instead of

SceneNode&:

struct AircraftMover

{

...

void operator() (Aircraft& aircraft, sf::Time) const

{

aircraft.accelerate(velocity);

}

};

A command would then be constructed as follows:

Command moveLeft;

moveLeft.category = Category::PlayerAircraft; moveLeft.action

= derivedAction<Aircraft>(AircraftMover(-playerSpeed, 0));

[ 104 ]

www.it-ebooks.info

Chapter 4

To be honest, our adapter does not have the simplest implementation, but it should be worth the advantage that after writing it once, we can create actions in a much cleaner way, without the need to downcast again and again.

Let's get back to the interesting part. Let's finally define

Player::handleRealtimeInput(), which creates a command every frame an arrow key is held down:

void Player::handleRealtimeInput(CommandQueue& commands)

{

const float playerSpeed = 30.f;

if (sf::Keyboard::isKeyPressed(sf::Keyboard::Up))

{

Command moveLeft;

moveLeft.category = Category::PlayerAircraft; moveLeft.action = derivedAction<Aircraft>(

AircraftMover(-playerSpeed, 0.f)); commands.push(moveLeft);

}

}

For one-time events, the handling is quite similar. As a simple example, we write a lambda expression that outputs the position of the player's aircraft every time the user presses the P key:

void Player::handleEvent(const sf::Event& event, CommandQueue& commands)

{

if (event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::P)

{

Command output;

output.category = Category::PlayerAircraft; output.action = [] (SceneNode& s, sf::Time)

{

std::cout << s.getPosition().x << "," << s.getPosition().y << "\n";

};

commands.push(output);

}

}

[ 105 ]

www.it-ebooks.info

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