In this recipe you will learn how to remotely control an actor by sending commands over a network. This is a very common usage scenario in AI simulations, for example. In many of these simulations one central server program is used for visualizing the current simulation state. Each of the simulated entities is processed on a dedicated computer that connects to the server to send commands for altering the state of the simulation.
The following instructions will teach you how to build such a setup using Panda3D. The client part of this application will send text strings to the server side. The server will then interpret these strings as movement commands for an actor placed in a simple scene.
To be able to complete this recipe, you need to set up a new project according to the steps presented in the Chapter 1 article Setting up the game structure. Additionally, you need the FollowCam
class from the recipe Making the camera smoothly follow an object found in Chapter 2 as well as the InputHandler
class described in Implementing an abstraction layer for supporting multiple input methods, which can be found in this chapter. Copy the files FollowCam.py
and InputHandler.py
to the src
subdirectory of your project.
This recipe also assumes that you have worked your way through Sending and receiving custom datagrams in Chapter 9, Networking. Some of the code presented in the article will be reused and altered slightly in the following steps. If you haven't yet read said recipe, this might be the right time to do so.
This recipe consists of the following tasks:
NetClasses.py:
from panda3d.core import * from direct.distributed.PyDatagram import PyDatagram from direct.distributed.PyDatagramIterator import PyDatagramIterator from random import choice 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 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): input handling, Panda3Dinput data, reading from networkif 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 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) def start(self): data = PyDatagram() data.addUint8(0) data.addString("hi") self.send(data) class Protocol: def printMessage(self, title, msg): print "%s %s" % (title, msg) def buildReply(self, msgid, data): reply = PyDatagram() reply.addUint8(msgid) reply.addString(data) return reply def process(self, data): input handling, Panda3Dinput data, reading from networkreturn None class ServerProtocol(Protocol): def process(self, data): it = PyDatagramIterator(data) msgid = it.getUint8() if msgid == 0: pass elif msgid == 1: command = it.getString() self.printMessage("new command:", command) messenger.send(command) return self.buildReply(0, "ok") class ClientProtocol(Protocol): def __init__(self): self.lastCommand = globalClock.getFrameTime() self.commands = ["net-walk-start", "net-walk-stop", "net-left-start", "net-left-stop", "net-right-start", "net-right-stop"] def process(self, data): time = globalClock.getFrameTime() if time - self.lastCommand > 0.5: self.lastCommand = time return self.buildReply(1, choice(self.commands)) else: return self.buildReply(0, "nop")
NetworkHandler.py
. Implement the network input handler in the newly created file:from InputHandler import InputHandler from panda3d.core import * class NetworkHandler(InputHandler): def __init__(self): InputHandler.__init__(self) self.accept("net-walk-start", self.beginWalk) self.accept("net-walk-stop", self.endWalk) self.accept("net-left-start", self.beginTurnLeft) self.accept("net-left-stop", self.endTurnLeft) self.accept("net-right-start", self.beginTurnRight) self.accept("net-right-stop", self.endTurnRight) taskMgr.add(self.updateInput, "update network input") def updateInput(self, task): self.dispatchMessages() return task.cont
Application.py
and extend the Application
class implementation:from direct.showbase.ShowBase import ShowBase from direct.actor.Actor import Actor from panda3d.core import * from FollowCam import FollowCam from NetworkHandler import NetworkHandler from NetClasses import Server, Client, ServerProtocol, ClientProtocol class Application(ShowBase): def __init__(self): ShowBase.__init__(self) self.setupScene() self.setupInput() self.setupNetwork() def setupScene(self): self.world = loader.loadModel("environment") self.world.reparentTo(render) self.world.setScale(0.5) self.world.setPos(-8, 80, 0) self.panda = Actor("panda", {"walk": "panda-walk"}) self.panda.reparentTo(render) self.followCam = FollowCam(self.cam, self.panda) def setupInput(self): self.netInput = NetworkHandler() self.accept("walk-start", self.beginWalk) self.accept("walk-stop", self.endWalk) self.accept("reverse-start", self.beginReverse) self.accept("reverse-stop", self.endReverse) self.accept("walk", self.walk) self.accept("reverse", self.reverse) self.accept("turn", self.turn) def setupNetwork(self): server = Server(ServerProtocol(), 9999) client = Client(ClientProtocol()) client.connect("localhost", 9999, 3000) client.start() def beginWalk(self): self.panda.setPlayRate(1.0, "walk") self.panda.loop("walk") def endWalk(self): input handling, Panda3Dinput data, reading from networkself.panda.stop() def beginReverse(self): self.panda.setPlayRate(-1.0, "walk") self.panda.loop("walk") def endReverse(self): self.panda.stop() def walk(self, rate): self.panda.setY(self.panda, rate) def reverse(self, rate): self.panda.setY(self.panda, rate) def turn(self, rate): self.panda.setH(self.panda, rate)
We start this recipe by implementing our networking layer. This is mostly taken from Chapter 9, but not without a few notable alterations to the communication protocol.
In our custom network protocol, we distinguish between two general message types, indicated by a numerical ID sent along with every command string. A message ID of 0 indicates an internal command, while an ID of 1 stands for a movement command.
After the client establishes a connection, it sends the internal command"hi"
to start the conversation between the two hosts. The server then sends the reply"hi"
to signal it has successfully received a command. In fact, the server acknowledges every command it receives by sending this reply to request further data.
Every 0.5 seconds, the client sends a random command string out of the possible movement commands stored in self.commands
. When the server receives such a command with message ID 1, it uses Panda3D's messaging system to create a new event named after the command. This is where the NetworkHandler
class we implemented in step 2 comes into play.
NetworkHandler
is derived from the InputHandler
class to create a new input handling implementation for network commands. We implement this class to listen for the messages the server side protocol dispatches when it receives a new command from the client. Whenever a new movement command arrives, the NetworkHandler
class translates it to the common input message format implemented previously in the recipe Implementing an abstraction layer for supporting multiple input methods.
This leaves us with the Application
class. Here we set up the scene and the networking layer. Additionally, we implement methods for handling incoming