- •Credits
- •Foreword
- •About the Authors
- •About the Reviewers
- •www.PacktPub.com
- •Table of Contents
- •Preface
- •Introducing SFML
- •Downloading and installation
- •A minimal example
- •A few notes on C++
- •Developing the first game
- •The Game class
- •Game loops and frames
- •Input over several frames
- •Vector algebra
- •Frame-independent movement
- •Fixed time steps
- •Other techniques related to frame rates
- •Displaying sprites on the screen
- •File paths and working directories
- •Real-time rendering
- •Adapting the code
- •Summary
- •Defining resources
- •Resources in SFML
- •Textures
- •Images
- •Fonts
- •Shaders
- •Sound buffers
- •Music
- •A typical use case
- •Graphics
- •Audio
- •Acquiring, releasing, and accessing resources
- •An automated approach
- •Finding an appropriate container
- •Loading from files
- •Accessing the textures
- •Error handling
- •Boolean return values
- •Throwing exceptions
- •Assertions
- •Generalizing the approach
- •Compatibility with sf::Music
- •A special case – sf::Shader
- •Summary
- •Entities
- •Aircraft
- •Alternative entity designs
- •Rendering the scene
- •Relative coordinates
- •SFML and transforms
- •Scene graphs
- •Scene nodes
- •Node insertion and removal
- •Making scene nodes drawable
- •Drawing entities
- •Connecting entities with resources
- •Aligning the origin
- •Scene layers
- •Updating the scene
- •One step back – absolute transforms
- •The view
- •Viewport
- •View optimizations
- •Resolution and aspect ratio
- •View scrolling
- •Zoom and rotation
- •Landscape rendering
- •SpriteNode
- •Landscape texture
- •Texture repeating
- •Composing our world
- •World initialization
- •Loading the textures
- •Building the scene
- •Update and draw
- •Integrating the Game class
- •Summary
- •Polling events
- •Window events
- •Joystick events
- •Keyboard events
- •Mouse events
- •Getting the input state in real time
- •Events and real-time input – when to use which
- •Delta movement from the mouse
- •Playing nice with your application neighborhood
- •A command-based communication system
- •Introducing commands
- •Receiver categories
- •Command execution
- •Command queues
- •Handling player input
- •Commands in a nutshell
- •Implementing the game logic
- •A general-purpose communication mechanism
- •Customizing key bindings
- •Why a player is not an entity
- •Summary
- •Defining a state
- •The state stack
- •Adding states to StateStack
- •Handling updates, input, and drawing
- •Input
- •Update
- •Draw
- •Delayed pop/push operations
- •The state context
- •Integrating the stack in the Application class
- •Navigating between states
- •Creating the game state
- •The title screen
- •Main menu
- •Pausing the game
- •The loading screen – sample
- •Progress bar
- •ParallelTask
- •Thread
- •Concurrency
- •Task implementation
- •Summary
- •The GUI hierarchy, the Java way
- •Updating the menu
- •The promised key bindings
- •Summary
- •Equipping the entities
- •Introducing hitpoints
- •Storing entity attributes in data tables
- •Displaying text
- •Creating enemies
- •Movement patterns
- •Spawning enemies
- •Adding projectiles
- •Firing bullets and missiles
- •Homing missiles
- •Picking up some goodies
- •Collision detection and response
- •Finding the collision pairs
- •Reacting to collisions
- •An outlook on optimizations
- •An interacting world
- •Cleaning everything up
- •Out of view, out of the world
- •The final update
- •Victory and defeat
- •Summary
- •Defining texture atlases
- •Adapting the game code
- •Low-level rendering
- •OpenGL and graphics cards
- •Understanding render targets
- •Texture mapping
- •Vertex arrays
- •Particle systems
- •Particles and particle types
- •Particle nodes
- •Emitter nodes
- •Affectors
- •Embedding particles in the world
- •Animated sprites
- •The Eagle has rolled!
- •Post effects and shaders
- •Fullscreen post effects
- •Shaders
- •The bloom effect
- •Summary
- •Music themes
- •Loading and playing
- •Use case – In-game themes
- •Sound effects
- •Loading, inserting, and playing
- •Removing sounds
- •Use case – GUI sounds
- •Sounds in 3D space
- •The listener
- •Attenuation factor and minimum distance
- •Positioning the listener
- •Playing spatial sounds
- •Use case – In-game sound effects
- •Summary
- •Playing multiplayer games
- •Interacting with sockets
- •Socket selectors
- •Custom protocols
- •Data transport
- •Network architectures
- •Peer-to-peer
- •Client-server architecture
- •Authoritative servers
- •Creating the structure for multiplayer
- •Working with the Server
- •Server thread
- •Server loop
- •Peers and aircraft
- •Hot Seat
- •Accepting new clients
- •Handling disconnections
- •Incoming packets
- •Studying our protocol
- •Understanding the ticks and updates
- •Synchronization issues
- •Taking a peek in the other end – the client
- •Client packets
- •Transmitting game actions via network nodes
- •The new pause state
- •Settings
- •The new Player class
- •Latency
- •Latency versus bandwidth
- •View scrolling compensation
- •Aircraft interpolation
- •Cheating prevention
- •Summary
- •Index
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