While the recipes preceding this one have shown off some neat networking features of the Panda3D engine using standard communication protocols, none of them have touched upon the topic of implementing the lower-level custom network protocols needed for synchronizing game objects across players connected to a game server. Starting with this recipe though, we change that situation. The rest of this chapter will be dedicated to how to open a connection and exchange custom crafted data between hosts.
Why games might require special network handling, such that you need to know about lower-level custom networking protocols?
The problems game developers have to solve when developing online multiplayer games are plenty. First of all, each game is a unique case on its own: First, there are different types of games, like shooters, racing games, or online role-playing games. Every game out of any category offers a different set of game modes, gameplay, game mechanics, objects, and ways of interaction. Apart from offering non-standardized experiences, each online game has a different set of requirements for its multiplayer functionality: An MMO has to be able to let thousands of players share the same world at the same time, while a fast-paced shooter has to minimize the communication lag between hosts to allow precise and accurate player movement and hit detection.
These are just a few simple samples, but ultimately it's up to us game developers to find solutions for all of these problems. This means that we need to be in control over how, when and which data is sent to meet our games' requirements.
This recipe marks the beginning of a three-part series. First we will shed some light on the components that are involved in getting Panda3D to talk over a network and how to establish a connection. Second, we will implement a tiny custom protocol for learning how to build and send custom datagrams across a network. Finally, in the third part of the series, we will implement a basic sample for synchronizing the state of a game object as a start for implementing a custom network protocol for a game.
We will build this sample on the foundation created in Setting up the game structure found in Chapter 1. Take a step back to read that recipe before continuing if you're unsure.
Unlike the other recipes in this chapter, we will be building this sample from our basic project setup without using any additional libraries or frameworks.
Let's implement a basic client and server and open a connection between them:
import
statements and the NetCommon
class to Application.py:
from direct.showbase.ShowBase import ShowBase from panda3d.core import * class NetCommon: def __init__(self, protocol): self.manager = ConnectionManager() self.reader = QueuedConnectionReader(self.manager, 0) self.writer = ConnectionWriter(self.manager, 0) self.protocol = protocol taskMgr.add(self.updateReader, "updateReader") def updateReader(self, task): if self.reader.dataAvailable(): data = NetDatagram() self.reader.getData(data) reply = self.protocol.process(data) if reply != None: self.writer.send(reply, data.getConnection()) return task.cont
NetCommon
, implement the Server
class:class Server(NetCommon): def __init__(self, protocol, port): NetCommon.__init__(self, protocol) self.listener = QueuedConnectionListener(self.manager, 0) socket = self.manager.openTCPServerRendezvous(port, 100) self.listener.addConnection(socket) self.connections = [] taskMgr.add(self.updateListener, "updateListener") def updateListener(self, task): if self.listener.newConnectionAvailable(): connection = PointerToConnection() if self.listener.getNewConnection(connection): connection = connection.p() self.connections.append(connection) self.reader.addConnection(connection) print "Server: New connection established." return task.cont
Client
and Protocol
classes:class Client(NetCommon): def __init__(self, protocol): NetCommon.__init__(self, protocol) def connect(self, host, port, timeout): self.connection = self.manager.openTCPClientConnection(host, port, timeout) if self.connection: self.reader.addConnection(self.connection) print "Client: Connected to server." def send(self, datagram): if self.connection: self.writer.send(datagram, self.connection) class Protocol: def process(self, data): return None
Application
class to look like this:class Application(ShowBase): def __init__(self): ShowBase.__init__(self) server = Server(Protocol(), 9999) client = Client(Protocol()) client.connect("localhost", 9999, 3000)
Client: Connected to server. Server: New connection established.
To avoid duplication of code, we begin this recipe by adding the NetCommon
class to Application.py
. In the constructor we can already see some of the main components needed for implementing custom network functionality in Panda3D.
The ConnectionManager
class handles opening ports, initiating connections to remote hosts and encapsulates all the low level IO operations involved when communicating over a network. Additionally, we need to create QueuedConnectionReader
and a ConnectionWriter
, responsible for reading and writing data respectively. QueuedConnectionReader
is a subclass of ConnectionReader
that buffers all incoming datagrams so they can be processed one after another in the updateReader()
task. This task periodically polls the reader object for newly available data. If anything has been received, it is handed to the process()
method of a Protocol
object, which decides how to react to the received data and which reply to send.
Currently, we only have a Protocol
class that doesn't do anything. This will change in the following recipes, where we will use different protocol implementations to define the behaviors of client and server.
The NetCommon
class already contains a good part of what's necessary for sending and receiving data over a network, but to create a server we still need to derive a new class and add some additional features. We need a QueuedConnectionListener
to listen for new connections on the network port that is opened using the call to openTCPServerRendezvous()
. The first parameter defines the port number our server will listen on for connections. The second parameter sets the maximum amount of simultaneous connection attempts. If the number of requests exceeds this value, new connection attempts are simply ignored.
Similar to the QueuedConnectionReader
class, QueuedConnectionListener
buffers requests for new connections and needs to be polled in a task where new connections are put into a list and registered to the QueuedConnectionReader
owned by the class so incoming data is received and processed.
All we need to add to the Client
class, on the other hand, are the connect()
and send()
methods. The former opens a new connection to a server. For this we need to specify the target host and port as well as the maximum time to wait for a reply before considering the connection to be terminated. The latter method is just a wrapper for sending a datagram.
In the constructor of Application we finally create a new Server and Client object and connect them using the internal loopback connection. Both client and server are using our stub protocol that does nothing yet. If you want this to change, go on to the next recipe!