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

Company Atop the Clouds – Co-op Multiplayer

std::map<int, PlayerPtr>

mPlayers;

std::vector<sf::Int32>

mLocalPlayerIdentifiers;

sf::TcpSocket

mSocket;

bool

mConnected;

std::unique_ptr<GameServer>

mGameServer;

sf::Clock

mTickClock;

std::vector<std::string>

mBroadcasts;

sf::Text

mBroadcastText;

sf::Time

mBroadcastElapsedTime;

...

 

}

 

We changed the main menu appropriately to accommodate the newly created Host and Join options. To avoid creating one state for each of those options, we created a custom constructor for MultiplayerGameState, which takes a parameter clearly

stating whether this state will be hosting or just joining. You can see those changes in StateStack.cpp. Because both modes are almost equal, it wouldn't even make sense to have another state. The only difference is that if the state is the host, it will launch a background server in its constructor and shut it down again in its destructor!

By this logic, we can say that MultiplayerGameState is the client-side of our application and GameServer is the server-side.

Working with the Server

Now it is time to put our hands on the actual server! We will be dissecting the GameServer class to understand properly what is going on.

Server thread

To begin with, we decided to put the server in a separate thread. This is very useful in many ways. For once, server processing can be intensive and could hurt the frame rate on the client. On the other hand, if the server is running on a parallel thread, aside from improving its performance, it also allows it to perform blocking calls whenever

necessary, and the game keeps running smoothly. Another plus in this approach is that our server does not communicate programmatically with the client, it communicates only via network; therefore, we don't even need to care about synchronization between the two. It is just like as if server is running on a different application!

As we have already introduced sf::Thread before in Chapter 5, Diverting the Game Flow – State Stack, we will skip that topic here. It is important to notice that GameServer::executionThread() is the thread function and that it starts running when GameServer gets constructed, and is stopped before it gets destructed.

[ 246 ]

www.it-ebooks.info

Chapter 10

Now, inside GameServer::executionThread() is where all the magic happens. First rule is that while executionThread() doesn't return a value, the parallel thread is alive. There are three main logical steps in the server thread: initialization, the server loop, and termination. Take a look at this pseudo-code:

void GameServer::executionThread()

{

initialize(); while(!timeToStop) loop(); shutdown();

}

Not unlike the client-side of the game, we must do the appropriate things in the proper places. In the initialization phase, just when the server thread starts, we are going to tell the sf::TcpListener socket to start accepting new connections. This is what allows the players to join our server. Also, we initialize some timing variables here to be used in the server loop later. As you may notice, there isn't that much to initialize at this point as the world is still empty without any ally or enemy aircraft, and the basic variables are initialized by the constructor.

Then, we enter the server loop, which will simply keep executing until the while loop ends, either by a forced termination of the thread, which is not recommended, or by setting the timeToStop Boolean variable to true, effectively ending the while loop at the end of the next step.

Server loop

Without a doubt, this part is the most important, and while many programmers take different paths in the process of creating a server loop, we did it in a way that is not unusual to see across programs. The following is the simplified anatomy of each step in the loop:

handleIncomingPackets();

handleIncomingConnections();

while (stepTime >= stepInterval)

{

updateLogic();

stepTime -= stepInterval;

}

while (tickTime >= tickInterval)

{

tick();

tickTime -= tickInterval;

}

sf::sleep(sf::milliseconds(50));

[ 247 ]

www.it-ebooks.info

Company Atop the Clouds – Co-op Multiplayer

The first two functions are going to respectively handle all incoming traffic from the connected peers and accept new connections, if there are any. Then, updateLogic() will be very similar to the client's update() functions; it will simply perform the evolution of data over time, to keep the server's state always up-to-date. The next function, tick(), is very similar to the previous update step, but usually executes fewer times and is used to send a snapshot of the server's state to the clients. In our case, we send updates to the clients 20 times per second. We may call this frequency the tick rate.

Just to be clear, the main reason tick() and updateLogic() functions are not merged is because they run at different rates. This is to save processing time and network bandwidth, as sending too many packets too often would just put a heavy load on the network with no benefits. Ideally, we want to send as little data as possible but making sure it is enough to accomplish the gameplay demands. This way, updateLogic() runs a lot faster to always keep the data as refreshed as possible, while tick() only performs as few times as necessary to make sure the client has an accurate version of the server state locally at all times.

Finally, the call to sf::sleep() is entirely optional; however, it is not a bad idea to tell the thread to sleep a bit and let the client's thread take the processor for itself for a little while. The bigger time you pass to sf::sleep(), the less time will be spent on server's tasks. It will be fine just until the server has too many tasks to perform and too little time to do them.

Before heading to the depths of these functions, let's rest a bit by looking at the data structures we will use, and how they are laid out.

Peers and aircraft

The multiplayer version of our game is a little different than its single-player counterpart. Now, each client can have one or two local aircraft objects, but many clients can be connected simultaneously. Though, one client is always one peer in the eyes of the server. A peer by itself can contain multiple aircraft. Therefore the server always knows how many clients are connected, how many aircraft are in the game and which peers they belong to:

struct RemotePeer

{

 

RemotePeer();

sf::TcpSocket

socket;

sf::Time

lastPacketTime;

std::vector<sf::Int32>

aircraftIdentifiers;

bool

ready;

bool

timedOut;

};

 

 

 

 

 

 

 

 

 

[ 248 ]

 

 

 

 

www.it-ebooks.info

Chapter 10

The preceding code snippet shows the structure of RemotePeer, which is declared inside GameServer. The constructor merely initializes the peer's data to an invalid state, which by itself means the peer is instanced, but not yet pointing to an actual client. The socket variable is the TCP socket we mentioned earlier in the chapter that we will use to communicate exclusively with a specific peer. The lastPacketTime variable always contains the timestamp of the last packet received from that peer. This is used to deduct disconnections and timeouts by a simple rule: If the peer did not send any data after n seconds, kick it out because something is wrong, as there are packets that the client has to compromise to send regularly.

The aircraftIdentifiers variable is an interesting one. It holds a list of IDs of all the planes that belong to a specific peer. There is a good reason there is only an

integer here: All the aircraft data is centralized in GameServer, and is easily referred to in there by using this integer ID, if needed.

The ready Boolean variable refers exactly to the valid or invalid state of the peer connection. It only becomes true after a successful connection and sending the world state to the newly connected socket.

The timedOut variable is just a flag that is set in the server logic to tell the handleDisconnections() function that this peer needs to be erased.

std::size_t

mAircraftCount;

std::map<sf::Int32, AircraftInfo>

mAircraftInfo;

std::vector<PeerPtr>

mPeers;

The preceding code snippet shows where all the peers are stored, as well as the mentioned aircraft data. mAircraftCount will always contain the total of

human-controlled aircraft in the world at a time. Their data can be queried using mAircraftInfo, through the struct declared as follows:

// Structure to store information about current aircraft state struct AircraftInfo

{

sf::Vector2f

position;

sf::Int32

hitpoints;

sf::Int32

missileAmmo;

std::map<sf::Int32, bool>

realtimeActions;

};

Yes, it is very simple. It only holds the position of the aircraft and the set of real-time actions the player is currently performing, such as moving and shooting.

[ 249 ]

www.it-ebooks.info

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