Add Distributed Network samples

1: Simple client-server connection
2: Client-server connection with timeManager
3: Create distributed objects, sending and receiving messages
4: Distributed model file
5: Simple text chat
6: Simple smooth moving actor
This commit is contained in:
Fireclaw 2020-06-08 22:10:28 +02:00 committed by rdb
parent a4ea476cce
commit 3d8f824081
39 changed files with 2077 additions and 0 deletions

View File

@ -0,0 +1,123 @@
# all imports needed by the engine itself
from direct.showbase.ShowBase import ShowBase
# initialize the engine
base = ShowBase()
# initialize the client
from direct.distributed.ClientRepository import ClientRepository
from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import TextNode
base.accept("escape", exit)
# Function to put instructions on the screen.
def addInstructions(pos, msg):
return OnscreenText(text=msg, style=1, fg=(0, 0, 0, 1), shadow=(1, 1, 1, 1),
parent=base.a2dTopLeft, align=TextNode.ALeft,
pos=(0.08, -pos - 0.04), scale=.06)
# Function to put title on the screen.
def addTitle(text):
return OnscreenText(text=text, style=1, pos=(-0.1, 0.09), scale=.08,
parent=base.a2dBottomRight, align=TextNode.ARight,
fg=(1, 1, 1, 1), shadow=(0, 0, 0, 1))
title = addTitle("Panda3D: Tutorial - Distributed Network (NOT CONNECTED)")
inst2 = addInstructions(0.06, "esc: Close the client")
inst2 = addInstructions(0.12, "See console output")
def setConnectedMessage():
title["text"] = "Panda3D: Tutorial - Distributed Network (CONNECTED)"
base.accept("client-ready", setConnectedMessage)
#
# CLIENT
#
class GameClientRepository(ClientRepository):
def __init__(self):
dcFileNames = ['../direct.dc']
# a distributed object of our game.
self.distributedObject = None
self.aiDGameObect = None
ClientRepository.__init__(
self,
dcFileNames = dcFileNames,
threadedNet = True)
# Set the same port as configured on the server to be able to connect
# to it
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# Set the IP or hostname of the server we want to connect to
hostname = ConfigVariableString('server-host', '127.0.0.1').getValue()
# Build the URL from the server hostname and port. If your server
# uses another protocol then http you should change it accordingly.
# Make sure to pass the connectMethod to the ClientRepository.__init__
# call too. Available connection methods are:
# self.CM_HTTP, self.CM_NET and self.CM_NATIVE
self.url = URLSpec('http://{}:{}'.format(hostname, tcpPort))
# Attempt a connection to the server
self.connect([self.url],
successCallback = self.connectSuccess,
failureCallback = self.connectFailure)
def lostConnection(self):
""" This should be overridden by a derived class to handle an
unexpectedly lost connection to the gameserver. """
# Handle the disconnection from the server. This can be a reconnect,
# simply exiting the application or anything else.
exit()
def connectFailure(self, statusCode, statusString):
""" Something went wrong """
# here we could create a reconnect task to try and connect again.
exit()
def connectSuccess(self):
""" Successfully connected. But we still can't really do
anything until we've got the doID range. """
# Mark interest for zone 1, 2 and 3. There won't be anything in them,
# it's just to display how it works for this small example.
self.setInterestZones([1, 2, 3])
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if self.haveCreateAuthority():
# we already have one
self.gotCreateReady()
else:
# Not yet, keep waiting a bit longer.
self.accept(self.uniqueName('createReady'), self.gotCreateReady)
def gotCreateReady(self):
""" Ready to enter the world. Expand our interest to include
any other zones """
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if not self.haveCreateAuthority():
# Not ready yet.
return
# we are ready now, so ignore further createReady events
self.ignore(self.uniqueName('createReady'))
print("Client Ready")
base.messenger.send("client-ready")
# Start the client
GameClientRepository()
base.run()

View File

@ -0,0 +1,31 @@
# all imports needed by the engine itself
from direct.showbase.ShowBase import ShowBase
# all imports needed by the server
from direct.distributed.ServerRepository import ServerRepository
from panda3d.core import ConfigVariableInt
# initialize the engine
base = ShowBase(windowType='none')
# the main server class
class GameServerRepository(ServerRepository):
"""The server repository class"""
def __init__(self):
"""initialise the server class"""
# get the port number from the configuration file
# if it doesn't exist, we use 4400 as the default
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# list of all needed .dc files
dcFileNames = ['../direct.dc']
# initialise a threaded server on this machine with
# the port number and the dc filenames
ServerRepository.__init__(self, tcpPort, dcFileNames=dcFileNames, threadedNet=True)
# start the server
GameServerRepository()
base.run()

View File

@ -0,0 +1,81 @@
from direct.distributed.ClientRepository import ClientRepository
from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString
class AIRepository(ClientRepository):
def __init__(self):
""" The AI Repository usually lives on a server and is responsible for
server side logic that will handle game objects """
# List of all dc files that are of interest to this AI Repository
dcFileNames = ['../direct.dc']
# Initialize the repository. We pass it the dc files and as this is an
# AI repository the dcSuffix AI. This will make sure any later calls to
# createDistributedObject will use the correct version.
# The connectMethod
ClientRepository.__init__(
self,
dcFileNames = dcFileNames,
dcSuffix = 'AI',
threadedNet = True)
# Set the same port as configured on the server to be able to connect
# to it
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# Set the IP or hostname of the server we want to connect to
hostname = ConfigVariableString('server-host', '127.0.0.1').getValue()
# Build the URL from the server hostname and port. If your server
# doesn't use http you should change it accordingly. Make sure to pass
# the connectMethod to the ClientRepository.__init__ call too.
# Available connection methods are:
# self.CM_HTTP, self.CM_NET and self.CM_NATIVE
url = URLSpec('http://{}:{}'.format(hostname, tcpPort))
# Attempt a connection to the server
self.connect([url],
successCallback = self.connectSuccess,
failureCallback = self.connectFailure)
def connectFailure(self, statusCode, statusString):
""" something went wrong """
print("Couldn't connect. Make sure to run server.py first!")
raise(StandardError, statusString)
def connectSuccess(self):
""" Successfully connected. But we still can't really do
anything until we've got the doID range. """
# The Client Repository will throw this event as soon as it has a doID
# range and would be able to create distributed objects
self.accept('createReady', self.gotCreateReady)
def lostConnection(self):
""" This should be overridden by a derived class to handle an
unexpectedly lost connection to the gameserver. """
exit()
def gotCreateReady(self):
""" Now we're ready to go! """
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if not self.haveCreateAuthority():
# Not ready yet.
return
# we are ready now, so ignore further createReady events
self.ignore('createReady')
# Create a Distributed Object by name. This will look up the object in
# the dc files passed to the repository earlier
self.timeManager = self.createDistributedObject(
className = 'TimeManagerAI', # The Name of the Class we want to initialize
zoneId = 1) # The Zone this Object will live in
print("AI Repository Ready")
def deallocateChannel(self, doID):
""" This method will be called whenever a client disconnects from the
server. The given doID is the ID of the client who left us. """
print("Client left us: ", doID)

View File

@ -0,0 +1,136 @@
# all imports needed by the engine itself
from direct.showbase.ShowBase import ShowBase
# initialize the engine
base = ShowBase()
# initialize the client
from direct.distributed.ClientRepository import ClientRepository
from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import TextNode
base.accept("escape", exit)
# Function to put instructions on the screen.
def addInstructions(pos, msg):
return OnscreenText(text=msg, style=1, fg=(0, 0, 0, 1), shadow=(1, 1, 1, 1),
parent=base.a2dTopLeft, align=TextNode.ALeft,
pos=(0.08, -pos - 0.04), scale=.06)
# Function to put title on the screen.
def addTitle(text):
return OnscreenText(text=text, style=1, pos=(-0.1, 0.09), scale=.08,
parent=base.a2dBottomRight, align=TextNode.ARight,
fg=(1, 1, 1, 1), shadow=(0, 0, 0, 1))
title = addTitle("Panda3D: Tutorial - Distributed Network (NOT CONNECTED)")
inst1 = addInstructions(0.06, "esc: Close the client")
inst2 = addInstructions(0.12, "See console output")
def setConnectedMessage():
title["text"] = "Panda3D: Tutorial - Distributed Network (CONNECTED)"
base.accept("client-ready", setConnectedMessage)
#
# CLIENT
#
class GameClientRepository(ClientRepository):
def __init__(self):
dcFileNames = ['../direct.dc']
# a distributed object of our game.
self.distributedObject = None
self.aiDGameObect = None
ClientRepository.__init__(
self,
dcFileNames = dcFileNames,
threadedNet = True)
# Set the same port as configured on the server to be able to connect
# to it
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# Set the IP or hostname of the server we want to connect to
hostname = ConfigVariableString('server-host', '127.0.0.1').getValue()
# Build the URL from the server hostname and port. If your server
# uses another protocol then http you should change it accordingly.
# Make sure to pass the connectMethod to the ClientRepository.__init__
# call too. Available connection methods are:
# self.CM_HTTP, self.CM_NET and self.CM_NATIVE
self.url = URLSpec('http://{}:{}'.format(hostname, tcpPort))
# Attempt a connection to the server
self.connect([self.url],
successCallback = self.connectSuccess,
failureCallback = self.connectFailure)
def lostConnection(self):
""" This should be overridden by a derived class to handle an
unexpectedly lost connection to the gameserver. """
# Handle the disconnection from the server. This can be a reconnect,
# simply exiting the application or anything else.
exit()
def connectFailure(self, statusCode, statusString):
""" Something went wrong """
# we could create a reconnect task to try and connect again.
exit()
def connectSuccess(self):
""" Successfully connected. But we still can't really do
anything until we've got the doID range. """
# Make sure we have interest in the by the AIRepository defined
# TimeManager zone, so we always see it even if we switch to
# another zone.
self.setInterestZones([1])
# We must wait for the TimeManager to be fully created and
# synced before we can enter another zone and wait for the
# game object. The uniqueName is important that we get the
# correct, our sync message from the TimeManager and not
# accidentaly a message from another client
self.acceptOnce(self.uniqueName('gotTimeSync'), self.syncReady)
def syncReady(self):
""" Now we've got the TimeManager manifested, and we're in
sync with the server time. Now we can enter the world. Check
to see if we've received our doIdBase yet. """
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if self.haveCreateAuthority():
# we already have one
self.gotCreateReady()
else:
# Not yet, keep waiting a bit longer.
self.accept(self.uniqueName('createReady'), self.gotCreateReady)
def gotCreateReady(self):
""" Ready to enter the world. Expand our interest to include
any other zones """
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if not self.haveCreateAuthority():
# Not ready yet.
return
# we are ready now, so ignore further createReady events
self.ignore(self.uniqueName('createReady'))
print("Client Ready")
base.messenger.send("client-ready")
# Start the client
client = GameClientRepository()
base.run()

View File

@ -0,0 +1,34 @@
# all imports needed by the engine itself
from direct.showbase.ShowBase import ShowBase
# all imports needed by the server
from direct.distributed.ServerRepository import ServerRepository
from panda3d.core import ConfigVariableInt
from AIRepository import AIRepository
# initialize the engine
base = ShowBase(windowType='none')
# the main server class
class GameServerRepository(ServerRepository):
"""The server repository class"""
def __init__(self):
"""initialise the server class"""
# get the port number from the configuration file
# if it doesn't exist, we use 4400 as the default
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# list of all needed .dc files
dcFileNames = ['../direct.dc']
# initialise a threaded server on this machine with
# the port number and the dc filenames
ServerRepository.__init__(self, tcpPort, dcFileNames=dcFileNames, threadedNet=True)
# start the server
GameServerRepository()
AIRepository()
base.run()

View File

@ -0,0 +1,29 @@
from direct.distributed.DistributedObject import DistributedObject
class AIDGameObject(DistributedObject):
""" This class is a DirectObject which will be created and managed by the
AI Repository. """
def __init__(self, cr):
DistributedObject.__init__(self, cr)
def announceGenerate(self):
""" The AI has created this object, so we send it's distributed object ID
over to the client. That way the client can actually grab the object
and use it to communicate with the AI. Alternatively store it in the
Client Repository in self.cr """
base.messenger.send(self.cr.uniqueName('AIDGameObjectGenerated'), [self.doId])
# call the base class method
DistributedObject.announceGenerate(self)
def d_requestDataFromAI(self):
""" Request some data from the AI and passing it some data from us. """
data = ("Some Data", 1, -1.25)
print("Sending game data:", data)
self.sendUpdate('messageRoundtripToAI', [data])
def messageRoundtripToClient(self, data):
""" Here we expect the answer from the AI from a previous
messageRoundtripToAI call """
print("Got Data:", data)
print("Roundtrip message complete")

View File

@ -0,0 +1,25 @@
from direct.distributed.DistributedObjectAI import DistributedObjectAI
class AIDGameObjectAI(DistributedObjectAI):
def __init__(self, aiRepository):
DistributedObjectAI.__init__(self, aiRepository)
def messageRoundtripToAI(self, data):
""" The client sent us some data to process. So work with it and send
changed data back to the requesting client """
requesterId = self.air.getAvatarIdFromSender()
print("Got client data:", data, "from client with ID", requesterId)
# do something with the data
aiChangedData = (
data[0] + " from the AI",
data[1] + 1,
data[2])
print("Sending modified game data back:", aiChangedData)
self.d_messageRoundtripToClient(aiChangedData, requesterId)
def d_messageRoundtripToClient(self, data, requesterId):
""" Send the given data to the requesting client """
print("Send message to back to:", requesterId)
self.sendUpdateToAvatarId(requesterId, 'messageRoundtripToClient', [data])

View File

@ -0,0 +1,86 @@
from direct.distributed.ClientRepository import ClientRepository
from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString
from AIDGameObjectAI import AIDGameObjectAI
class AIRepository(ClientRepository):
def __init__(self):
""" The AI Repository usually lives on a server and is responsible for
server side logic that will handle game objects """
# List of all dc files that are of interest to this AI Repository
dcFileNames = ['../direct.dc', 'sample.dc']
# Initialize the repository. We pass it the dc files and as this is an
# AI repository the dcSuffix AI. This will make sure any later calls to
# createDistributedObject will use the correct version.
# The connectMethod
ClientRepository.__init__(
self,
dcFileNames = dcFileNames,
dcSuffix = 'AI',
threadedNet = True)
# Set the same port as configured on the server to be able to connect
# to it
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# Set the IP or hostname of the server we want to connect to
hostname = ConfigVariableString('server-host', '127.0.0.1').getValue()
# Build the URL from the server hostname and port. If your server
# doesn't use http you should change it accordingly. Make sure to pass
# the connectMethod to the ClientRepository.__init__ call too.
# Available connection methods are:
# self.CM_HTTP, self.CM_NET and self.CM_NATIVE
url = URLSpec('http://{}:{}'.format(hostname, tcpPort))
# Attempt a connection to the server
self.connect([url],
successCallback = self.connectSuccess,
failureCallback = self.connectFailure)
def connectFailure(self, statusCode, statusString):
""" something went wrong """
print("Couldn't connect. Make sure to run server.py first!")
raise(StandardError, statusString)
def connectSuccess(self):
""" Successfully connected. But we still can't really do
anything until we've got the doID range. """
# The Client Repository will throw this event as soon as it has a doID
# range and would be able to create distributed objects
self.accept('createReady', self.gotCreateReady)
def lostConnection(self):
""" This should be overridden by a derived class to handle an
unexpectedly lost connection to the gameserver. """
exit()
def gotCreateReady(self):
""" Now we're ready to go! """
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if not self.haveCreateAuthority():
# Not ready yet.
return
# we are ready now, so ignore further createReady events
self.ignore('createReady')
# Create a Distributed Object by name. This will look up the object in
# the dc files passed to the repository earlier
self.timeManager = self.createDistributedObject(
className = 'TimeManagerAI', # The Name of the Class we want to initialize
zoneId = 1) # The Zone this Object will live in
self.gameDistObject = self.createDistributedObject(
className = 'AIDGameObjectAI',
zoneId = 2)
print("AI Repository Ready")
def deallocateChannel(self, doID):
""" This method will be called whenever a client disconnects from the
server. The given doID is the ID of the client who left us. """
print("Client left us: ", doID)

View File

@ -0,0 +1,130 @@
from direct.distributed.ClientRepository import ClientRepository
from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString
from DGameObject import DGameObject
class GameClientRepository(ClientRepository):
def __init__(self):
dcFileNames = ['../direct.dc', 'sample.dc']
# a distributed object of our game.
self.distributedObject = None
self.aiDGameObect = None
ClientRepository.__init__(
self,
dcFileNames = dcFileNames,
threadedNet = True)
# Set the same port as configured on the server to be able to connect
# to it
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# Set the IP or hostname of the server we want to connect to
hostname = ConfigVariableString('server-host', '127.0.0.1').getValue()
# Build the URL from the server hostname and port. If your server
# uses another protocol then http you should change it accordingly.
# Make sure to pass the connectMethod to the ClientRepository.__init__
# call too. Available connection methods are:
# self.CM_HTTP, self.CM_NET and self.CM_NATIVE
self.url = URLSpec('http://{}:{}'.format(hostname, tcpPort))
# Attempt a connection to the server
self.connect([self.url],
successCallback = self.connectSuccess,
failureCallback = self.connectFailure)
def lostConnection(self):
""" This should be overridden by a derived class to handle an
unexpectedly lost connection to the gameserver. """
# Handle the disconnection from the server. This can be a reconnect,
# simply exiting the application or anything else.
exit()
def connectFailure(self, statusCode, statusString):
""" Something went wrong """
exit()
def connectSuccess(self):
""" Successfully connected. But we still can't really do
anything until we've got the doID range. """
# Make sure we have interest in the by the AIRepository defined
# TimeManager zone, so we always see it even if we switch to
# another zone.
self.setInterestZones([1])
# We must wait for the TimeManager to be fully created and
# synced before we can enter another zone and wait for the
# game object. The uniqueName is important that we get the
# correct, our sync message from the TimeManager and not
# accidentaly a message from another client
self.acceptOnce(self.uniqueName('gotTimeSync'), self.syncReady)
def syncReady(self):
""" Now we've got the TimeManager manifested, and we're in
sync with the server time. Now we can enter the world. Check
to see if we've received our doIdBase yet. """
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if self.haveCreateAuthority():
# we already have one
self.gotCreateReady()
else:
# Not yet, keep waiting a bit longer.
self.accept(self.uniqueName('createReady'), self.gotCreateReady)
def gotCreateReady(self):
""" Ready to enter the world. Expand our interest to include
any other zones """
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if not self.haveCreateAuthority():
# Not ready yet.
return
# we are ready now, so ignore further createReady events
self.ignore(self.uniqueName('createReady'))
self.join()
print("Client Ready")
def join(self):
""" Join a game/room/whatever """
self.accept(self.uniqueName('AIDGameObjectGenerated'), self.aiDGameObectGenerated)
# set our intersted zones to let the client see all distributed obects
# in those zones
self.setInterestZones([1, 2])
# Manifest a object on the server. The object will have our "base" doId.
self.distributedObject = DGameObject(self)
self.createDistributedObject(
distObj = self.distributedObject,
zoneId = 2)
base.messenger.send("client-joined")
print("Joined")
def aiDGameObectGenerated(self, doId):
print("AIDGameObect was generated")
self.aiDGameObect = self.doId2do[doId]
def sendGameData(self):
if not self.distributedObject: return
print("send game data")
# send a message to the server
self.distributedObject.d_sendGameData()
def sendRoundtripToAI(self):
if not self.aiDGameObect: return
print("Initiate roundtrip message to AI Server")
self.aiDGameObect.d_requestDataFromAI()

View File

@ -0,0 +1,15 @@
from direct.distributed.DistributedObject import DistributedObject
class DGameObject(DistributedObject):
def __init__(self, cr):
DistributedObject.__init__(self, cr)
def sendGameData(self, data):
""" Method that can be called from the clients with an sendUpdate call """
print(data)
def d_sendGameData(self):
""" A method to send an update message to the server. The d_ stands
for distributed """
# send the message to the server
self.sendUpdate('sendGameData', [('ValueA', 123, 1.25)])

View File

@ -0,0 +1,20 @@
# all imports needed by the server
from direct.distributed.ServerRepository import ServerRepository
from panda3d.core import ConfigVariableInt
# the main server class
class GameServerRepository(ServerRepository):
"""The server repository class"""
def __init__(self):
"""initialise the server class"""
# get the port number from the configuration file
# if it doesn't exist, we use 4400 as the default
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# list of all needed .dc files
dcFileNames = ['../direct.dc', 'sample.dc']
# initialise a threaded server on this machine with
# the port number and the dc filenames
ServerRepository.__init__(self, tcpPort, dcFileNames=dcFileNames, threadedNet=True)

View File

@ -0,0 +1,136 @@
# all imports needed by the engine itself
from direct.showbase.ShowBase import ShowBase
# initialize the engine
base = ShowBase()
# initialize the client
from direct.distributed.ClientRepository import ClientRepository
from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import TextNode
base.accept("escape", exit)
# Function to put instructions on the screen.
def addInstructions(pos, msg):
return OnscreenText(text=msg, style=1, fg=(0, 0, 0, 1), shadow=(1, 1, 1, 1),
parent=base.a2dTopLeft, align=TextNode.ALeft,
pos=(0.08, -pos - 0.04), scale=.06)
# Function to put title on the screen.
def addTitle(text):
return OnscreenText(text=text, style=1, pos=(-0.1, 0.09), scale=.08,
parent=base.a2dBottomRight, align=TextNode.ARight,
fg=(1, 1, 1, 1), shadow=(0, 0, 0, 1))
title = addTitle("Panda3D: Tutorial - Distributed Network (NOT CONNECTED)")
inst1 = addInstructions(0.06, "esc: Close the client")
inst2 = addInstructions(0.12, "See console output")
def setConnectedMessage():
title["text"] = "Panda3D: Tutorial - Distributed Network (CONNECTED)"
base.accept("client-ready", setConnectedMessage)
#
# CLIENT
#
class GameClientRepository(ClientRepository):
def __init__(self):
dcFileNames = ['../direct.dc']
# a distributed object of our game.
self.distributedObject = None
self.aiDGameObect = None
ClientRepository.__init__(
self,
dcFileNames = dcFileNames,
threadedNet = True)
# Set the same port as configured on the server to be able to connect
# to it
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# Set the IP or hostname of the server we want to connect to
hostname = ConfigVariableString('server-host', '127.0.0.1').getValue()
# Build the URL from the server hostname and port. If your server
# uses another protocol then http you should change it accordingly.
# Make sure to pass the connectMethod to the ClientRepository.__init__
# call too. Available connection methods are:
# self.CM_HTTP, self.CM_NET and self.CM_NATIVE
self.url = URLSpec('http://{}:{}'.format(hostname, tcpPort))
# Attempt a connection to the server
self.connect([self.url],
successCallback = self.connectSuccess,
failureCallback = self.connectFailure)
def lostConnection(self):
""" This should be overridden by a derived class to handle an
unexpectedly lost connection to the gameserver. """
# Handle the disconnection from the server. This can be a reconnect,
# simply exiting the application or anything else.
exit()
def connectFailure(self, statusCode, statusString):
""" Something went wrong """
# we could create a reconnect task to try and connect again.
exit()
def connectSuccess(self):
""" Successfully connected. But we still can't really do
anything until we've got the doID range. """
# Make sure we have interest in the by the AIRepository defined
# TimeManager zone, so we always see it even if we switch to
# another zone.
self.setInterestZones([1])
# We must wait for the TimeManager to be fully created and
# synced before we can enter another zone and wait for the
# game object. The uniqueName is important that we get the
# correct, our sync message from the TimeManager and not
# accidentaly a message from another client
self.acceptOnce(self.uniqueName('gotTimeSync'), self.syncReady)
def syncReady(self):
""" Now we've got the TimeManager manifested, and we're in
sync with the server time. Now we can enter the world. Check
to see if we've received our doIdBase yet. """
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if self.haveCreateAuthority():
# we already have one
self.gotCreateReady()
else:
# Not yet, keep waiting a bit longer.
self.accept(self.uniqueName('createReady'), self.gotCreateReady)
def gotCreateReady(self):
""" Ready to enter the world. Expand our interest to include
any other zones """
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if not self.haveCreateAuthority():
# Not ready yet.
return
# we are ready now, so ignore further createReady events
self.ignore(self.uniqueName('createReady'))
print("Client Ready")
base.messenger.send("client-ready")
# Start the client
client = GameClientRepository()
base.run()

View File

@ -0,0 +1,17 @@
import DGameObject
import AIDGameObject/AI
struct gameDataModel {
string value_a;
uint8 value_b;
int8 value_c/100;
}
dclass DGameObject: DistributedObject {
sendGameData(gameDataModel data) broadcast;
};
dclass AIDGameObject: DistributedObject {
messageRoundtripToAI(gameDataModel data) p2p;
messageRoundtripToClient(gameDataModel data) p2p;
}

View File

@ -0,0 +1,18 @@
# all imports needed by the engine itself
from direct.showbase.ShowBase import ShowBase
# import our own repositories
from ServerRepository import GameServerRepository
from AIRepository import AIRepository
# initialize the engine
base = ShowBase(windowType='none')
# instantiate the server
GameServerRepository()
# The AI Repository to manage server side (AI) clients
AIRepository()
# start the server
base.run()

View File

@ -0,0 +1,81 @@
from direct.distributed.ClientRepository import ClientRepository
from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString
class AIRepository(ClientRepository):
def __init__(self):
""" The AI Repository usually lives on a server and is responsible for
server side logic that will handle game objects """
# List of all dc files that are of interest to this AI Repository
dcFileNames = ['../direct.dc', 'sample.dc']
# Initialize the repository. We pass it the dc files and as this is an
# AI repository the dcSuffix AI. This will make sure any later calls to
# createDistributedObject will use the correct version.
# The connectMethod
ClientRepository.__init__(
self,
dcFileNames = dcFileNames,
dcSuffix = 'AI',
threadedNet = True)
# Set the same port as configured on the server to be able to connect
# to it
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# Set the IP or hostname of the server we want to connect to
hostname = ConfigVariableString('server-host', '127.0.0.1').getValue()
# Build the URL from the server hostname and port. If your server
# doesn't use http you should change it accordingly. Make sure to pass
# the connectMethod to the ClientRepository.__init__ call too.
# Available connection methods are:
# self.CM_HTTP, self.CM_NET and self.CM_NATIVE
url = URLSpec('http://{}:{}'.format(hostname, tcpPort))
# Attempt a connection to the server
self.connect([url],
successCallback = self.connectSuccess,
failureCallback = self.connectFailure)
def connectFailure(self, statusCode, statusString):
""" something went wrong """
print("Couldn't connect. Make sure to run server.py first!")
raise(StandardError, statusString)
def connectSuccess(self):
""" Successfully connected. But we still can't really do
anything until we've got the doID range. """
# The Client Repository will throw this event as soon as it has a doID
# range and would be able to create distributed objects
self.accept('createReady', self.gotCreateReady)
def lostConnection(self):
""" This should be overridden by a derived class to handle an
unexpectedly lost connection to the gameserver. """
exit()
def gotCreateReady(self):
""" Now we're ready to go! """
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if not self.haveCreateAuthority():
# Not ready yet.
return
# we are ready now, so ignore further createReady events
self.ignore('createReady')
# Create a Distributed Object by name. This will look up the object in
# the dc files passed to the repository earlier
self.timeManager = self.createDistributedObject(
className = 'TimeManagerAI', # The Name of the Class we want to initialize
zoneId = 1) # The Zone this Object will live in
print("AI Repository Ready")
def deallocateChannel(self, doID):
""" This method will be called whenever a client disconnects from the
server. The given doID is the ID of the client who left us. """
print("Client left us: ", doID)

View File

@ -0,0 +1,116 @@
from direct.distributed.ClientRepository import ClientRepository
from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString
from random import random
class GameClientRepository(ClientRepository):
def __init__(self):
dcFileNames = ['../direct.dc', 'sample.dc']
ClientRepository.__init__(
self,
dcFileNames = dcFileNames,
threadedNet = True)
# Set the same port as configured on the server to be able to connect
# to it
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# Set the IP or hostname of the server we want to connect to
hostname = ConfigVariableString('server-host', '127.0.0.1').getValue()
# Build the URL from the server hostname and port. If your server
# uses another protocol then http you should change it accordingly.
# Make sure to pass the connectMethod to the ClientRepository.__init__
# call too. Available connection methods are:
# self.CM_HTTP, self.CM_NET and self.CM_NATIVE
self.url = URLSpec('http://{}:{}'.format(hostname, tcpPort))
# Attempt a connection to the server
self.connect([self.url],
successCallback = self.connectSuccess,
failureCallback = self.connectFailure)
def lostConnection(self):
""" This should be overridden by a derived class to handle an
unexpectedly lost connection to the gameserver. """
# Handle the disconnection from the server. This can be a reconnect,
# simply exiting the application or anything else.
exit()
def connectFailure(self, statusCode, statusString):
""" Something went wrong """
exit()
def connectSuccess(self):
""" Successfully connected. But we still can't really do
anything until we've got the doID range. """
# Make sure we have interest in the by the AIRepository defined
# TimeManager zone, so we always see it even if we switch to
# another zone.
self.setInterestZones([1])
# We must wait for the TimeManager to be fully created and
# synced before we can enter another zone and wait for the
# game object. The uniqueName is important that we get the
# correct, our sync message from the TimeManager and not
# accidentaly a message from another client
self.acceptOnce(self.uniqueName('gotTimeSync'), self.syncReady)
def syncReady(self):
""" Now we've got the TimeManager manifested, and we're in
sync with the server time. Now we can enter the world. Check
to see if we've received our doIdBase yet. """
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if self.haveCreateAuthority():
# we already have one
self.gotCreateReady()
else:
# Not yet, keep waiting a bit longer.
self.accept(self.uniqueName('createReady'), self.gotCreateReady)
def gotCreateReady(self):
""" Ready to enter the world. Expand our interest to include
any other zones """
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if not self.haveCreateAuthority():
# Not ready yet.
return
# we are ready now, so ignore further createReady events
self.ignore(self.uniqueName('createReady'))
self.join()
print("Client Ready")
def join(self):
""" Join a game/room/whatever """
# set our intersted zones to let the client see all distributed obects
# in those zones
self.setInterestZones([1, 2])
# Manifest a object on the server. The object will have our "base" doId.
self.myDistributedModel = self.createDistributedObject(
className = "DModel",
zoneId = 2)
x = random()
z = random()
# set position for this local client
self.myDistributedModel.setPos(x, 10, z)
# make sure already connected clients will get notified of the
# position by calling the distributred (d_*) version of the method
self.myDistributedModel.d_setPos(x, 10, z)
base.messenger.send("client-joined")
print("Joined")
def modelReady(self, doId):
print("AIDGameObect was generated")
self.aiDGameObect = self.doId2do[doId]

View File

@ -0,0 +1,61 @@
from direct.distributed.DistributedNode import DistributedNode
class DModel(DistributedNode):
def __init__(self, cr):
DistributedNode.__init__(self, cr)
# Load up the visible representation of this avatar.
self.model = loader.loadModel('smiley.egg')
self.model.reparentTo(self)
def announceGenerate(self):
""" This method is called after generate(), after all of the
required fields have been filled in. At the time of this call,
the distributed object is ready for use. """
DistributedNode.announceGenerate(self)
# Now that the object has been fully manifested, we can parent
# it into the scene.
self.reparentTo(render)
def disable(self):
""" This method is called when the object is removed from the
scene, for instance because it left the zone. It is balanced
against generate(): for each generate(), there will be a
corresponding disable(). Everything that was done in
generate() or announceGenerate() should be undone in disable().
After a disable(), the object might be cached in memory in case
it will eventually reappear. The DistributedObject should be
prepared to receive another generate() for an object that has
already received disable().
Note that the above is only strictly true for *cacheable*
objects. Most objects are, by default, non-cacheable; you
have to call obj.setCacheable(True) (usually in the
constructor) to make it cacheable. Until you do this, your
non-cacheable object will always receive a delete() whenever
it receives a disable(), and it will never be stored in a
cache.
"""
# Take it out of the scene graph.
self.detachNode()
DistributedNode.disable(self)
def delete(self):
""" This method is called after disable() when the object is to
be completely removed, for instance because the other user
logged off. We will not expect to see this object again; it
will not be cached. This is stronger than disable(), and the
object may remove any structures it needs to in order to allow
it to be completely deleted from memory. This balances against
__init__(): every DistributedObject that is created will
eventually get delete() called for it exactly once. """
# Clean out self.model, so we don't have a circular reference.
self.model = None
DistributedNode.delete(self)

View File

@ -0,0 +1,20 @@
# all imports needed by the server
from direct.distributed.ServerRepository import ServerRepository
from panda3d.core import ConfigVariableInt
# the main server class
class GameServerRepository(ServerRepository):
"""The server repository class"""
def __init__(self):
"""initialise the server class"""
# get the port number from the configuration file
# if it doesn't exist, we use 4400 as the default
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# list of all needed .dc files
dcFileNames = ['../direct.dc', 'sample.dc']
# initialise a threaded server on this machine with
# the port number and the dc filenames
ServerRepository.__init__(self, tcpPort, dcFileNames=dcFileNames, threadedNet=True)

View File

@ -0,0 +1,43 @@
# all imports needed by the engine itself
from direct.showbase.ShowBase import ShowBase
# import our own repositories
from ClientRepository import GameClientRepository
# initialize the engine
base = ShowBase()
# initialize the client
client = GameClientRepository()
base.accept("escape", exit)
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import TextNode
# Function to put instructions on the screen.
def addInstructions(pos, msg):
return OnscreenText(text=msg, style=1, fg=(0, 0, 0, 1), shadow=(1, 1, 1, 1),
parent=base.a2dTopLeft, align=TextNode.ALeft,
pos=(0.08, -pos - 0.04), scale=.06)
# Function to put title on the screen.
def addTitle(text):
return OnscreenText(text=text, style=1, pos=(-0.1, 0.09), scale=.08,
parent=base.a2dBottomRight, align=TextNode.ARight,
fg=(1, 1, 1, 1), shadow=(0, 0, 0, 1))
title = addTitle("Panda3D: Tutorial - Distributed Network (NOT CONNECTED)")
inst1 = addInstructions(0.06, "esc: Close the client")
inst2 = addInstructions(0.12, "See console output")
def setConnectedMessage():
title["text"] = "Panda3D: Tutorial - Distributed Network (CONNECTED)"
base.accept("client-joined", setConnectedMessage)
# start the client
base.run()

View File

@ -0,0 +1,3 @@
import DModel
dclass DModel: DistributedNode { }

View File

@ -0,0 +1,18 @@
# all imports needed by the engine itself
from direct.showbase.ShowBase import ShowBase
# import our own repositories
from ServerRepository import GameServerRepository
from AIRepository import AIRepository
# initialize the engine
base = ShowBase(windowType='none')
# instantiate the server
GameServerRepository()
# The AI Repository to manage server side (AI) clients
AIRepository()
# start the server
base.run()

View File

@ -0,0 +1,81 @@
from direct.distributed.ClientRepository import ClientRepository
from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString
class AIRepository(ClientRepository):
def __init__(self):
""" The AI Repository usually lives on a server and is responsible for
server side logic that will handle game objects """
# List of all dc files that are of interest to this AI Repository
dcFileNames = ['../direct.dc', 'sample.dc']
# Initialize the repository. We pass it the dc files and as this is an
# AI repository the dcSuffix AI. This will make sure any later calls to
# createDistributedObject will use the correct version.
# The connectMethod
ClientRepository.__init__(
self,
dcFileNames = dcFileNames,
dcSuffix = 'AI',
threadedNet = True)
# Set the same port as configured on the server to be able to connect
# to it
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# Set the IP or hostname of the server we want to connect to
hostname = ConfigVariableString('server-host', '127.0.0.1').getValue()
# Build the URL from the server hostname and port. If your server
# doesn't use http you should change it accordingly. Make sure to pass
# the connectMethod to the ClientRepository.__init__ call too.
# Available connection methods are:
# self.CM_HTTP, self.CM_NET and self.CM_NATIVE
url = URLSpec('http://{}:{}'.format(hostname, tcpPort))
# Attempt a connection to the server
self.connect([url],
successCallback = self.connectSuccess,
failureCallback = self.connectFailure)
def connectFailure(self, statusCode, statusString):
""" something went wrong """
print("Couldn't connect. Make sure to run server.py first!")
raise(StandardError, statusString)
def connectSuccess(self):
""" Successfully connected. But we still can't really do
anything until we've got the doID range. """
# The Client Repository will throw this event as soon as it has a doID
# range and would be able to create distributed objects
self.accept('createReady', self.gotCreateReady)
def lostConnection(self):
""" This should be overridden by a derived class to handle an
unexpectedly lost connection to the gameserver. """
exit()
def gotCreateReady(self):
""" Now we're ready to go! """
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if not self.haveCreateAuthority():
# Not ready yet.
return
# we are ready now, so ignore further createReady events
self.ignore('createReady')
# Create a Distributed Object by name. This will look up the object in
# the dc files passed to the repository earlier
self.timeManager = self.createDistributedObject(
className = 'TimeManagerAI', # The Name of the Class we want to initialize
zoneId = 1) # The Zone this Object will live in
print("AI Repository Ready")
def deallocateChannel(self, doID):
""" This method will be called whenever a client disconnects from the
server. The given doID is the ID of the client who left us. """
print("Client left us: ", doID)

View File

@ -0,0 +1,104 @@
from direct.distributed.ClientRepository import ClientRepository
from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString
from message import Message
class GameClientRepository(ClientRepository):
def __init__(self):
dcFileNames = ['../direct.dc', 'sample.dc']
ClientRepository.__init__(
self,
dcFileNames = dcFileNames,
threadedNet = True)
# Set the same port as configured on the server to be able to connect
# to it
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# Set the IP or hostname of the server we want to connect to
hostname = ConfigVariableString('server-host', '127.0.0.1').getValue()
# Build the URL from the server hostname and port. If your server
# uses another protocol then http you should change it accordingly.
# Make sure to pass the connectMethod to the ClientRepository.__init__
# call too. Available connection methods are:
# self.CM_HTTP, self.CM_NET and self.CM_NATIVE
self.url = URLSpec('http://{}:{}'.format(hostname, tcpPort))
# Attempt a connection to the server
self.connect([self.url],
successCallback = self.connectSuccess,
failureCallback = self.connectFailure)
def lostConnection(self):
''' This should be overridden by a derived class to handle an
unexpectedly lost connection to the gameserver. '''
# Handle the disconnection from the server. This can be a reconnect,
# simply exiting the application or anything else.
exit()
def connectFailure(self, statusCode, statusString):
''' Something went wrong '''
# we could create a reconnect task to try and connect again.
exit()
def connectSuccess(self):
''' Successfully connected. But we still can't really do
anything until we've got the doID range. '''
# Make sure we have interest in the by the AIRepository defined
# TimeManager zone, so we always see it even if we switch to
# another zone.
self.setInterestZones([1])
# We must wait for the TimeManager to be fully created and
# synced before we can enter another zone and wait for the
# game object. The uniqueName is important that we get the
# correct, our sync message from the TimeManager and not
# accidentaly a message from another client
self.acceptOnce(self.uniqueName('gotTimeSync'), self.syncReady)
def syncReady(self):
''' Now we've got the TimeManager manifested, and we're in
sync with the server time. Now we can enter the world. Check
to see if we've received our doIdBase yet. '''
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if self.haveCreateAuthority():
# we already have one
self.gotCreateReady()
else:
# Not yet, keep waiting a bit longer.
self.accept(self.uniqueName('createReady'), self.gotCreateReady)
def gotCreateReady(self):
''' Ready to enter the world. Expand our interest to include
any other zones '''
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if not self.haveCreateAuthority():
# Not ready yet.
return
# we are ready now, so ignore further createReady events
self.ignore(self.uniqueName('createReady'))
# create a instance of the message class
msg = Message(self)
# and create the distributed Object with it
self.createDistributedObject(
distObj = msg, zoneId = 1)
# save the created Distributed Object
# in self.msg for later usage
self.msg = msg
def sendMessage(self, msgText):
'''Function to call the send function of the message class,
which sends the Message over the Network to the other users'''
sentText = "{}: {}".format(self.doIdBase, msgText)
self.msg.b_sendText(sentText)

View File

@ -0,0 +1,85 @@
from direct.showbase.ShowBase import ShowBase
from direct.distributed.ClientRepository import ClientRepository
from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString
from message import Message
from ClientRepository import GameClientRepository
from direct.gui.DirectGui import DirectButton, DirectEntry, DirectFrame
from direct.gui import DirectGuiGlobals as DGG
# initialize the engine
base = ShowBase()
client = GameClientRepository()
# Setup the GUI
# this frame will contain all our GUI elements
frameMain = DirectFrame(frameColor = (0, 0, 0, 1))
def clearText():
''' Write an empty string in the textbox '''
txt_msg.enterText('')
def setDefaultText():
''' Write the default message in the textbox '''
txt_msg.enterText('Your Message')
def send(args=None):
''' Send the text written in the message textbox to the clients '''
client.sendMessage(txt_msg.get())
txt_msg.enterText('')
# the Textbox where we write our Messages, which we
# want to send over to the other users
txt_msg = DirectEntry(
initialText = 'Your Message',
cursorKeys = True,
pos = (-0.9,0,-0.9),
scale = 0.1,
width = 14,
focusInCommand = clearText,
focusOutCommand = setDefaultText,
command = send,
parent = frameMain)
# a button to initiate the sending of the message
btn_send = DirectButton(
text = 'Send',
pos = (0.75,0,-0.9),
scale = 0.15,
command = send,
parent = frameMain)
# This will show all sent messages
txt_messages = DirectEntry(
cursorKeys = False,
pos = (-0.8,0,0.5),
scale = 0.1,
width = 14,
numLines = 10,
state = DGG.DISABLED,
parent = frameMain)
# Function which will write the given text in the
# textbox, where we store all send and recieved messages.
# This Function will only write the last 10 messages and
# cut of the erlier messages
def setText(messageText):
# get all messages from the textbox
parts = txt_messages.get().split('\n')
if len(parts) >= 10:
cutParts = ''
# as the textbox can only hold 10 lines cut out the first entry
for i in range(1,len(parts)):
cutParts += parts[i] + '\n'
txt_messages.enterText(cutParts + messageText)
else:
txt_messages.enterText(txt_messages.get() + '\n' + messageText)
# create a DirectObject instance, which will then catch the events and
# handle them with the given functions
base.accept('setText', setText)
base.accept('escape', exit)
# start the application
base.run()

View File

@ -0,0 +1,25 @@
from direct.distributed.DistributedObject import DistributedObject
from direct.showbase.MessengerGlobal import messenger
class Message(DistributedObject):
def __init__(self, clientRepo):
DistributedObject.__init__(self, clientRepo)
def sendText(self, messageText):
"""Function which is caled for local changes only"""
# send an event, which will set the text on the
#print "got a message"
messenger.send("setText", [messageText])
def d_sendText(self, messageText):
"""Function which is caled to send the message over the network
therfore the d_ suffix stands for distributed"""
#print "send message %s" % messageText
self.sendUpdate("sendText", [messageText])
def b_sendText(self, messageText):
"""Function which combines the local and distributed functionality,
so the sendText and d_sendText functions are called.
The b_ suffix stands for both"""
self.sendText(messageText)
self.d_sendText(messageText)

View File

@ -0,0 +1,5 @@
from message import Message
dclass Message: DistributedObject {
sendText(string messageText) broadcast;
}

View File

@ -0,0 +1,34 @@
# all imports needed by the engine itself
from direct.showbase.ShowBase import ShowBase
# all imports needed by the server
from direct.distributed.ServerRepository import ServerRepository
from panda3d.core import ConfigVariableInt
from AIRepository import AIRepository
# initialize the engine
base = ShowBase(windowType='none')
# the main server class
class GameServerRepository(ServerRepository):
"""The server repository class"""
def __init__(self):
"""initialise the server class"""
# get the port number from the configuration file
# if it doesn't exist, we use 4400 as the default
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# list of all needed .dc files
dcFileNames = ['../direct.dc', 'sample.dc']
# initialise a threaded server on this machine with
# the port number and the dc filenames
ServerRepository.__init__(self, tcpPort, dcFileNames=dcFileNames, threadedNet=True)
# start the server
GameServerRepository()
AIRepository()
base.run()

View File

@ -0,0 +1,81 @@
from direct.distributed.ClientRepository import ClientRepository
from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString
class AIRepository(ClientRepository):
def __init__(self):
""" The AI Repository usually lives on a server and is responsible for
server side logic that will handle game objects """
# List of all dc files that are of interest to this AI Repository
dcFileNames = ['../direct.dc', 'sample.dc']
# Initialize the repository. We pass it the dc files and as this is an
# AI repository the dcSuffix AI. This will make sure any later calls to
# createDistributedObject will use the correct version.
# The connectMethod
ClientRepository.__init__(
self,
dcFileNames = dcFileNames,
dcSuffix = 'AI',
threadedNet = True)
# Set the same port as configured on the server to be able to connect
# to it
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# Set the IP or hostname of the server we want to connect to
hostname = ConfigVariableString('server-host', '127.0.0.1').getValue()
# Build the URL from the server hostname and port. If your server
# doesn't use http you should change it accordingly. Make sure to pass
# the connectMethod to the ClientRepository.__init__ call too.
# Available connection methods are:
# self.CM_HTTP, self.CM_NET and self.CM_NATIVE
url = URLSpec('http://{}:{}'.format(hostname, tcpPort))
# Attempt a connection to the server
self.connect([url],
successCallback = self.connectSuccess,
failureCallback = self.connectFailure)
def connectFailure(self, statusCode, statusString):
""" something went wrong """
print("Couldn't connect. Make sure to run server.py first!")
raise(StandardError, statusString)
def connectSuccess(self):
""" Successfully connected. But we still can't really do
anything until we've got the doID range. """
# The Client Repository will throw this event as soon as it has a doID
# range and would be able to create distributed objects
self.accept('createReady', self.gotCreateReady)
def lostConnection(self):
""" This should be overridden by a derived class to handle an
unexpectedly lost connection to the gameserver. """
exit()
def gotCreateReady(self):
""" Now we're ready to go! """
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if not self.haveCreateAuthority():
# Not ready yet.
return
# we are ready now, so ignore further createReady events
self.ignore('createReady')
# Create a Distributed Object by name. This will look up the object in
# the dc files passed to the repository earlier
self.timeManager = self.createDistributedObject(
className = 'TimeManagerAI', # The Name of the Class we want to initialize
zoneId = 1) # The Zone this Object will live in
print("AI Repository Ready")
def deallocateChannel(self, doID):
""" This method will be called whenever a client disconnects from the
server. The given doID is the ID of the client who left us. """
print("Client left us: ", doID)

View File

@ -0,0 +1,103 @@
from direct.distributed.ClientRepository import ClientRepository
from panda3d.core import URLSpec, ConfigVariableInt, ConfigVariableString
from DistributedSmoothActor import DistributedSmoothActor
class GameClientRepository(ClientRepository):
def __init__(self):
dcFileNames = ['../direct.dc', 'sample.dc']
# a distributed object of our game.
self.distributedObject = None
self.aiDGameObect = None
ClientRepository.__init__(
self,
dcFileNames = dcFileNames,
threadedNet = True)
# Set the same port as configured on the server to be able to connect
# to it
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# Set the IP or hostname of the server we want to connect to
hostname = ConfigVariableString('server-host', '127.0.0.1').getValue()
# Build the URL from the server hostname and port. If your server
# uses another protocol then http you should change it accordingly.
# Make sure to pass the connectMethod to the ClientRepository.__init__
# call too. Available connection methods are:
# self.CM_HTTP, self.CM_NET and self.CM_NATIVE
self.url = URLSpec('http://{}:{}'.format(hostname, tcpPort))
# Attempt a connection to the server
self.connect([self.url],
successCallback = self.connectSuccess,
failureCallback = self.connectFailure)
def lostConnection(self):
""" This should be overridden by a derived class to handle an
unexpectedly lost connection to the gameserver. """
# Handle the disconnection from the server. This can be a reconnect,
# simply exiting the application or anything else.
exit()
def connectFailure(self, statusCode, statusString):
""" Something went wrong """
exit()
def connectSuccess(self):
""" Successfully connected. But we still can't really do
anything until we've got the doID range. """
# Make sure we have interest in the by the AIRepository defined
# TimeManager zone, so we always see it even if we switch to
# another zone.
self.setInterestZones([1])
# We must wait for the TimeManager to be fully created and
# synced before we can enter another zone and wait for the
# game object. The uniqueName is important that we get the
# correct, our sync message from the TimeManager and not
# accidentaly a message from another client
self.acceptOnce(self.uniqueName('gotTimeSync'), self.syncReady)
def syncReady(self):
""" Now we've got the TimeManager manifested, and we're in
sync with the server time. Now we can enter the world. Check
to see if we've received our doIdBase yet. """
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if self.haveCreateAuthority():
# we already have one
self.gotCreateReady()
else:
# Not yet, keep waiting a bit longer.
self.accept(self.uniqueName('createReady'), self.gotCreateReady)
def gotCreateReady(self):
""" Ready to enter the world. Expand our interest to include
any other zones """
# This method checks whether we actually have a valid doID range
# to create distributed objects yet
if not self.haveCreateAuthority():
# Not ready yet.
return
# we are ready now, so ignore further createReady events
self.ignore(self.uniqueName('createReady'))
self.join()
print("Client Ready")
def join(self):
""" Join a game/room/whatever """
# set our intersted zones to let the client see all distributed obects
# in those zones
self.setInterestZones([1, 2])
base.messenger.send('client-joined')
print("Joined")

View File

@ -0,0 +1,50 @@
from direct.distributed.DistributedSmoothNode import DistributedSmoothNode
from panda3d.core import NodePath
from direct.actor.Actor import Actor
class DistributedSmoothActor(DistributedSmoothNode, Actor):
def __init__(self, cr):
Actor.__init__(self, "models/ralph",
{"run": "models/ralph-run",
"walk": "models/ralph-walk"})
DistributedSmoothNode.__init__(self, cr)
self.setCacheable(1)
self.setScale(.2)
def generate(self):
DistributedSmoothNode.generate(self)
self.activateSmoothing(True, False)
self.startSmooth()
def announceGenerate(self):
DistributedSmoothNode.announceGenerate(self)
self.reparentTo(render)
def disable(self):
# remove all anims, on all parts and all lods
self.stopSmooth()
if (not self.isEmpty()):
Actor.unloadAnims(self, None, None, None)
DistributedSmoothNode.disable(self)
def delete(self):
try:
self.DistributedActor_deleted
except:
self.DistributedActor_deleted = 1
DistributedSmoothNode.delete(self)
Actor.delete(self)
def start(self):
# Let the DistributedSmoothNode take care of broadcasting the
# position updates several times a second.
self.startPosHprBroadcast()
def loop(self, animName):
self.sendUpdate("loop", [animName])
return Actor.loop(self, animName)
def pose(self, animName, frame):
self.sendUpdate("pose", [animName, frame])
return Actor.pose(self, animName, frame)

View File

@ -0,0 +1,20 @@
# all imports needed by the server
from direct.distributed.ServerRepository import ServerRepository
from panda3d.core import ConfigVariableInt
# the main server class
class GameServerRepository(ServerRepository):
"""The server repository class"""
def __init__(self):
"""initialise the server class"""
# get the port number from the configuration file
# if it doesn't exist, we use 4400 as the default
tcpPort = ConfigVariableInt('server-port', 4400).getValue()
# list of all needed .dc files
dcFileNames = ['../direct.dc', 'sample.dc']
# initialise a threaded server on this machine with
# the port number and the dc filenames
ServerRepository.__init__(self, tcpPort, dcFileNames=dcFileNames, threadedNet=True)

View File

@ -0,0 +1,151 @@
# all imports needed by the engine itself
from direct.showbase.ShowBase import ShowBase
from panda3d.core import KeyboardButton, NodePath, PandaNode
# import our own repositories
from ClientRepository import GameClientRepository
from DistributedSmoothActor import DistributedSmoothActor
# initialize the engine
base = ShowBase()
base.disableMouse()
class Avatar:
def __init__(self, cr):
self.cr = cr
self.ralph = DistributedSmoothActor(self.cr)
self.cr.createDistributedObject(
distObj = self.ralph,
zoneId = 2)
# Create a floater object, which floats 2 units above ralph. We
# use this as a target for the camera to look at.
self.floater = NodePath(PandaNode("floater"))
self.floater.reparentTo(self.ralph)
self.floater.setZ(2.0)
# We will use this for checking if keyboard keys are pressed
self.isDown = base.mouseWatcherNode.isButtonDown
taskMgr.add(self.move, "moveTask")
# Set up the camera
base.camera.setPos(self.ralph.getX(), self.ralph.getY() + 10, 2)
# start the avatar
self.ralph.start()
# Accepts arrow keys to move either the player or the menu cursor,
# Also deals with grid checking and collision detection
def move(self, task):
# Get the time that elapsed since last frame. We multiply this with
# the desired speed in order to find out with which distance to move
# in order to achieve that desired speed.
dt = globalClock.getDt()
# If the camera-left key is pressed, move camera left.
# If the camera-right key is pressed, move camera right.
if self.isDown(KeyboardButton.asciiKey(b"j")):
base.camera.setX(base.camera, -20 * dt)
if self.isDown(KeyboardButton.asciiKey(b"k")):
base.camera.setX(base.camera, +20 * dt)
# If a move-key is pressed, move ralph in the specified direction.
if self.isDown(KeyboardButton.asciiKey(b"a")):
self.ralph.setH(self.ralph.getH() + 300 * dt)
if self.isDown(KeyboardButton.asciiKey(b"d")):
self.ralph.setH(self.ralph.getH() - 300 * dt)
if self.isDown(KeyboardButton.asciiKey(b"w")):
self.ralph.setY(self.ralph, -20 * dt)
if self.isDown(KeyboardButton.asciiKey(b"s")):
self.ralph.setY(self.ralph, +10 * dt)
# update distributed position and rotation
#self.ralph.setDistPos(self.ralph.getX(), self.ralph.getY(), self.ralph.getZ())
#self.ralph.setDistHpr(self.ralph.getH(), self.ralph.getP(), self.ralph.getR())
# If ralph is moving, loop the run animation.
# If he is standing still, stop the animation.
currentAnim = self.ralph.getCurrentAnim()
if self.isDown(KeyboardButton.asciiKey(b"w")):
if currentAnim != "run":
self.ralph.loop("run")
elif self.isDown(KeyboardButton.asciiKey(b"s")):
# Play the walk animation backwards.
if currentAnim != "walk":
self.ralph.loop("walk")
self.ralph.setPlayRate(-1.0, "walk")
elif self.isDown(KeyboardButton.asciiKey(b"a")) or self.isDown(KeyboardButton.asciiKey(b"d")):
if currentAnim != "walk":
self.ralph.loop("walk")
self.ralph.setPlayRate(1.0, "walk")
else:
if currentAnim is not None:
self.ralph.stop()
self.ralph.pose("walk", 5)
self.isMoving = False
# If the camera is too far from ralph, move it closer.
# If the camera is too close to ralph, move it farther.
camvec = self.ralph.getPos() - base.camera.getPos()
camvec.setZ(0)
camdist = camvec.length()
camvec.normalize()
if camdist > 10.0:
base.camera.setPos(base.camera.getPos() + camvec * (camdist - 10))
camdist = 10.0
if camdist < 5.0:
base.camera.setPos(base.camera.getPos() - camvec * (5 - camdist))
camdist = 5.0
# The camera should look in ralph's direction,
# but it should also try to stay horizontal, so look at
# a floater which hovers above ralph's head.
base.camera.lookAt(self.floater)
return task.cont
base.accept("escape", exit)
# initialize the client
client = GameClientRepository()
from direct.gui.OnscreenText import OnscreenText
from panda3d.core import TextNode
# Function to put instructions on the screen.
def addInstructions(pos, msg):
return OnscreenText(text=msg, style=1, fg=(0, 0, 0, 1), shadow=(1, 1, 1, 1),
parent=base.a2dTopLeft, align=TextNode.ALeft,
pos=(0.08, -pos - 0.04), scale=.06)
# Function to put title on the screen.
def addTitle(text):
return OnscreenText(text=text, style=1, pos=(-0.1, 0.09), scale=.08,
parent=base.a2dBottomRight, align=TextNode.ARight,
fg=(1, 1, 1, 1), shadow=(0, 0, 0, 1))
title = addTitle("Panda3D: Tutorial - Distributed Network (NOT CONNECTED)")
inst1 = addInstructions(0.06, "W|A|S|D: Move avatar)")
inst2 = addInstructions(0.12, "esc: Close the client")
inst3 = addInstructions(0.24, "See console output")
def clientJoined():
title["text"] = "Panda3D: Tutorial - Distributed Network (CONNECTED)"
# Setup our avatar
Avatar(client)
base.accept("client-joined", clientJoined)
# start the client
base.run()

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

View File

@ -0,0 +1,6 @@
import DistributedSmoothActor
dclass DistributedSmoothActor: DistributedSmoothNode {
loop(string animName) broadcast;
pose(string animName, int16 frame) broadcast;
}

View File

@ -0,0 +1,18 @@
# all imports needed by the engine itself
from direct.showbase.ShowBase import ShowBase
# import our own repositories
from ServerRepository import GameServerRepository
from AIRepository import AIRepository
# initialize the engine
base = ShowBase(windowType='none')
# instantiate the server
GameServerRepository()
# The AI Repository to manage server side (AI) clients
AIRepository()
# start the server
base.run()

91
samples/networking/direct.dc Executable file
View File

@ -0,0 +1,91 @@
// This is a sample dc file for some of the classes defined within the
// direct source tree. It is suggested that you copy this file into
// your own project (or load it from the direct source tree) and build
// on it with your own dc file for your own classes.
keyword broadcast;
keyword ram;
keyword p2p;
from direct.distributed import DistributedObject/AI
from direct.distributed import TimeManager/AI
from direct.distributed import DistributedNode/AI
from direct.distributed import DistributedSmoothNode/AI
struct BarrierData {
uint16 context;
string name;
uint32 avIds[];
};
// The most fundamental class
dclass DistributedObject {
// These are used to support DistributedObjectAI.beginBarrier() and
// the matching DistributedObject.doneBarrier(). If you don't call
// these functions, you don't care about these distributed methods.
// (Actually, you probably don't care anyway.)
setBarrierData(BarrierData data[]) broadcast ram;
setBarrierReady(uint16 context);
setLocation(uint32 parentId, uint32 zoneId) broadcast ram;
};
dclass TimeManager: DistributedObject {
requestServerTime(uint8 context) p2p;
serverTime(uint8 context, int32 timestamp);
};
dclass DistributedNode: DistributedObject {
setX(int16 / 10) broadcast ram;
setY(int16 / 10) broadcast ram;
setZ(int16 / 10) broadcast ram;
setH(int16 % 360 / 10) broadcast ram;
setP(int16 % 360 / 10) broadcast ram;
setR(int16 % 360 / 10) broadcast ram;
setPos: setX, setY, setZ;
setHpr: setH, setP, setR;
setPosHpr: setX, setY, setZ, setH, setP, setR;
setXY: setX, setY;
setXZ: setX, setZ;
setXYH: setX, setY, setH;
setXYZH: setX, setY, setZ, setH;
};
dclass DistributedSmoothNode: DistributedNode {
// Component set pos and hpr functions.
setComponentL(uint64) broadcast ram;
setComponentX(int16 / 10) broadcast ram;
setComponentY(int16 / 10) broadcast ram;
setComponentZ(int16 / 10) broadcast ram;
setComponentH(int16 % 360 / 10) broadcast ram;
setComponentP(int16 % 360 / 10) broadcast ram;
setComponentR(int16 % 360 / 10) broadcast ram;
setComponentT(int16 timestamp) broadcast ram;
// Composite set pos and hpr functions. These map to combinations
// of one or more of the above components. They all include
// setComponentT(), which must be called last.
setSmStop: setComponentT;
setSmH: setComponentH, setComponentT;
setSmZ: setComponentZ, setComponentT;
setSmXY: setComponentX, setComponentY, setComponentT;
setSmXZ: setComponentX, setComponentZ, setComponentT;
setSmPos: setComponentX, setComponentY, setComponentZ, setComponentT;
setSmHpr: setComponentH, setComponentP, setComponentR, setComponentT;
setSmXYH: setComponentX, setComponentY, setComponentH, setComponentT;
setSmXYZH: setComponentX, setComponentY, setComponentZ, setComponentH, setComponentT;
setSmPosHpr: setComponentX, setComponentY, setComponentZ, setComponentH, setComponentP, setComponentR, setComponentT;
// special update if L (being location, such as zoneId) changes, send everything, intended to
// keep position and 'location' in sync
setSmPosHprL: setComponentL, setComponentX, setComponentY, setComponentZ, setComponentH, setComponentP, setComponentR, setComponentT;
clearSmoothing(int8 bogus) broadcast;
suggestResync(uint32 avId, int16 timestampA, int16 timestampB,
int32 serverTimeSec, uint16 serverTimeUSec,
uint16 / 100 uncertainty);
returnResync(uint32 avId, int16 timestampB,
int32 serverTimeSec, uint16 serverTimeUSec,
uint16 / 100 uncertainty);
};