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

Company Atop the Clouds – Co-op Multiplayer

The first step is to send to all clients the current snapshot of the server's state, which consists of the current scrolling of the world (mBattleFieldRect.top + mBattleFieldRect.height) and the positions of all aircraft.

About the aircraft positioning, it is important to notice that the server is not an authority over the movement of aircraft, but rather an agent in their synchronization. When you control your aircraft with the keys, the server will obey and register

your newly obtained positions and the client won't overwrite its own local plane locations with the incoming server data. Therefore, we can assume that each client is responsible for the positions of its own aircraft. The server will however dispatch each client's positions to all others!

Then, checkMissionEnd() corresponds to the code that will check if all aircraft are near enough to the end of the level for the Server::MissionSuccess packet to be delivered, effectively showing a message in the client and quitting to the menu. This check is performed by checking if all the aircraft positions are between the effective end of the level and a given offset, provided in the endLevel constant.

After that, both spawnEnemies() and spawnPickups() functions will be responsible for making enemies and pickups appear at random intervals and at random locations, by using the randomInt() utility function.

Synchronization issues

If you test this chapter's sample extensively enough, you will notice clear synchronization problems, where some things do not happen the exact same way for all clients. This is intended and accounted for. We sacrificed a bit on the final polish level of the networked simulation, so it could remain simple. We understand networking is a very complex topic which might confuse even the brightest minds at first. We could never learn everything about it in one book, let alone in one chapter.

Therefore, we went with an approach as simple as possible in this chapter. We would rather have you focused in learning the concepts we directly teach so you can extend them later into a fully-polished game than to have a way bigger codebase to look and get lost in.

Taking a peek in the other end – the client

We have looked in the server extensively and have hopefully clarified all systems and learned how they come together to form a single object that services a lot of clients at once, and potentially even more aircraft! Now let's look at the other end, the client, and see how we took a jump from a single-player-only game into a fully-networked game.

[ 258 ]

www.it-ebooks.info

Chapter 10

Let's examine the MultiplayerGameState constructor first:

sf::IpAddress ip; if (isHost)

{

mGameServer.reset(new GameServer()); ip = "127.0.0.1";

}

else

{

ip = getAddressFromFile();

}

if (mSocket.connect(ip, ServerPort, sf::seconds(5.f)) == sf::TcpSocket::Done)

mConnected = true;

else

mFailedConnectionClock.restart();

mSocket.setBlocking(false);

...

We need to deduce which IP to communicate with, in order to successfully join a game. If we are the host, we just connect to the loopback address 127.0.0.1, otherwise, we need to connect to a pseudo-remote server. This means that in

practice, the server could still be running in the same machine if the user is testing two clients in the same computer. However, if we are joining a server on another computer, we actually need a valid IP address. We get it from a file conveniently named ip.txt, which is created and saved in the same directory as the executable in case it doesn't exist, already containing the loopback address. Changing this file is the way to go if you want to pick an arbitrary IP to connect to.

The port used is 5000 and it is hardcoded both in the server and the client. If you try the application, make sure you don't have other games or programs conflicting with this port.

The loopback address we referred previously is simply a widely adopted IPv4 address that points to the local host or the machine itself where it is being used.

After attempting to connect with a timeout of five seconds, we either set the client to a valid connected state, or we restart a clock that will timeout after another 5 seconds, in the meantime showing the error message stating that connection was not possible.

[ 259 ]

www.it-ebooks.info

Company Atop the Clouds – Co-op Multiplayer

Most things in MultiplayerGameState are a direct copy of how GameState used to work. Though there are some changes and additions we would like to mention. In the update() function, besides what was already there, we now check for incoming packets from the server:

sf::Packet packet;

if (mSocket.receive(packet) == sf::Socket::Done)

{

sf::Int32 packetType; packet >> packetType;

handlePacket(packetType, packet);

}

The handlePacket() function is very alike to the server's handleIncomingPacket() function.

Then we perform some logic to update the broadcast queue that shows the messages from the server on the screen and the text that blinks prompting a second player to join in by pressing the Return or Enter key:

updateBroadcastMessage(dt);

mPlayerInvitationTime += dt;

if (mPlayerInvitationTime > sf::seconds(1.f)) mPlayerInvitationTime = sf::Time::Zero;

Finally, we tick the client in the same way and rate we tick in the server. Instead of sending a snapshot of all the local states, the client sends only the positions of its local aircraft:

if (mTickClock.getElapsedTime() > sf::seconds(1.f / 20.f))

{

sf::Packet positionUpdatePacket; positionUpdatePacket << static_cast<sf::Int32>(

Client::PositionUpdate); positionUpdatePacket << static_cast<sf::Int32>(

mLocalPlayerIdentifiers.size());

FOREACH(sf::Int32 identifier, mLocalPlayerIdentifiers)

{

if (Aircraft* aircraft = mWorld.getAircraft(identifier)) positionUpdatePacket << identifier

<< aircraft->getPosition().x << aircraft->getPosition().y;

}

mSocket.send(positionUpdatePacket);

mTickClock.restart();

}

[ 260 ]

www.it-ebooks.info

Chapter 10

Client packets

Here's the protocol explanation for the client. The Client::PacketType enum contains the following enumerators:

PlayerEvent: This takes two sf::Int32 variables, an aircraft identifier, and the event to be triggered as defined in the Player class. It is used to request the server to trigger an event on the requested aircraft.

Quit: This takes no parameters. It simply informs the server that the game state is closing, so it can remove its aircraft immediately.

PlayerRealtimeChange: This is the same as PlayerEvent, but additionally takes a Boolean variable to state whether the ongoing action is active or not.

RequestCoopPartner: This takes no parameters. It is sent when the user presses the Return key to request the server a local partner. Its counterpart AcceptCoopPartner will contain all information to actually do the spawn of the friendly unit.

PositionUpdate: This is what we saw in the client's tick code. It takes a sf::Int32 variable with the number of local aircraft, and for each aircraft,

it packs another sf::Int32 variable for the identifier and two float values for the position.

GameEvent: This packet informs the server of a specific happening in the client's game logic, such as enemy explosions.

Transmitting game actions via network nodes

Now, we will take a closer look at the GameEvent packet, which is sent when certain actions in the game occur. We use it to notify about explosions of enemies in a

way that pick-up dropping is synchronized among different clients (either, a pickup drops at every client or not at all). However, our implementation allows you to extend it for any game action. First, we have a GameActions namespace which

contains an enum to differ between the game actions, and a struct to store an action:

namespace GameActions

{

enum Type { EnemyExplode };

struct Action

{

Action();

Action(Type type, sf::Vector2f position);

Type type; sf::Vector2f position;

};

}

[ 261 ]

www.it-ebooks.info

Company Atop the Clouds – Co-op Multiplayer

In Chapter 9, Cranking Up the Bass – Music and Sound Effects, you saw that we used a dedicated scene node class named SoundNode to build an interface between

command-based game events and another game component, in that case, the sound player. Here, we are repeating this approach: We create a NetworkNode class that lets objects in the scene directly send events over the network:

class NetworkNode : public SceneNode

{

public:

 

NetworkNode();

void

notifyGameAction(GameActions::Type type,

 

sf::Vector2f position);

bool

pollGameAction(GameActions::Info& out);

...

 

private:

std::queue<GameActions::Action> mPendingActions;

};

This class holds a queue of game actions that are going to be transmitted. The notifyGameAction() method inserts a new game action into the queue, while pollGameAction() checks if an action is pending. If so, it pops the action from the queue and stores it in the output parameter—just as you know it from SFML's pollEvent() function.

Now, how does this look in practice? In the Aircraft::updateCurrent() method, we have a check if the current airplane has just exploded and if it's an enemy. In this case, we issue a command. The Category::Network category is the receiver category

of NetworkNode:

Command command;

command.category = Category::Network; command.action = derivedAction<NetworkNode>(

[position] (NetworkNode& node, sf::Time)

{

node.notifyGameAction(GameActions::EnemyExplode, position);

});

The network node itself is placed in the World class. A World::pollGameAction() member function acts as a pure forwarder and can be used in other parts of the game where we only have access to the world, but not its scene and entities.

[ 262 ]

www.it-ebooks.info

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