Concept / structure for server / client space game

I’m working on a client / server space adventure/exploration/trading/resource management game.

I’d like ships to be able to fly around in a star system, but not deep space. If they hyperspace to a new solar system, it should load it like a new level.

But wait, I have no idea how to do that.

If I instance the Sol system, with our star starting at coords 0,0,0, then instance another solar system, … I can’t have them over lap, right? These things are instanced on the server, to handle authoritative collision detection, so I need 'em to be persistent if a player is in the Sol system, and another player is in the Alpha Centari system.

So how should I load the server ‘worlds’ such that they can coexist, but not have to waste server resources rendering a bajillion coords points of emptiness?

Why not treat them as entirely separate “worlds”, rather than part of a single universe? Each would then have its own scene graph, etc., and having them each be centred on (0, 0, 0) should no longer be a problem.

For example, if you were using Bullet to handle physics, just assign each to its own BulletWorld (if I have the appropriate class correct).

As to the clients, it seems to me that they only really call for information on their current “world”: other systems would presumably be outside of their range; any information that you do want to have trickle in would presumably be fairly limited (“Ship destruction detected in Carnax System! Ship class identified as Cat-class freighter.”), rather than the relatively rich information used to present things to the player within their current “world”.

Mkay, I’m giving it a whirl - upgraded from Panda 1.7.2 to 1.8, and am trying to fire up a multiworld sample.

#from pandac.PandaModules import loadPrcFileData
#loadPrcFileData('', 'load-display tinydisplay')

import sys
import direct.directbase.DirectStart

from direct.showbase.DirectObject import DirectObject
from direct.showbase.InputStateGlobal import inputState

from panda3d.core import AmbientLight
from panda3d.core import DirectionalLight
from panda3d.core import Vec3
from panda3d.core import Vec4
from panda3d.core import Point3
from panda3d.core import TransformState
from panda3d.core import BitMask32

from panda3d.bullet import BulletWorld
from panda3d.bullet import BulletPlaneShape
from panda3d.bullet import BulletBoxShape
from panda3d.bullet import BulletSphereShape
from panda3d.bullet import BulletConeShape
from panda3d.bullet import BulletRigidBodyNode
from panda3d.bullet import BulletDebugNode
from panda3d.bullet import ZUp

class Game(DirectObject):

  def __init__(self):
    base.setBackgroundColor(0.1, 0.1, 0.8, 1)
    base.setFrameRateMeter(True)

    base.cam.setPos(0, -20, 4)
    base.cam.lookAt(0, 0, 0)

    # Light
    alight = AmbientLight('ambientLight')
    alight.setColor(Vec4(0.5, 0.5, 0.5, 1))
    alightNP = render.attachNewNode(alight)

    dlight = DirectionalLight('directionalLight')
    dlight.setDirection(Vec3(1, 1, -1))
    dlight.setColor(Vec4(0.7, 0.7, 0.7, 1))
    dlightNP = render.attachNewNode(dlight)

    render.clearLight()
    render.setLight(alightNP)
    render.setLight(dlightNP)

    # Input
    self.accept('escape', self.doExit)
    self.accept('r', self.doReset)
    self.accept('f1', self.toggleWireframe)
    self.accept('f2', self.toggleTexture)
    self.accept('f3', self.toggleDebug)
    self.accept('f5', self.doScreenshot)
    self.accept('f6', self.shuffleWorlds)

    inputState.watchWithModifiers('forward', 'w')
    inputState.watchWithModifiers('left', 'a')
    inputState.watchWithModifiers('reverse', 's')
    inputState.watchWithModifiers('right', 'd')
    inputState.watchWithModifiers('turnLeft', 'q')
    inputState.watchWithModifiers('turnRight', 'e')

    # Task
    taskMgr.add(self.update, 'updateWorld')

    # Physics
    self.setup()

  # _____HANDLER_____

  def doExit(self):
    self.cleanup()
    sys.exit(1)

  def shuffleWorlds(self):

    return none

  def doReset(self):
    self.cleanup()
    self.setup()

  def toggleWireframe(self):
    base.toggleWireframe()

  def toggleTexture(self):
    base.toggleTexture()

  def toggleDebug(self):
    if self.debugNP.isHidden():
      self.debugNP.show()
    else:
      self.debugNP.hide()

  def doScreenshot(self):
    base.screenshot('Bullet')

  # ____TASK___

  def processInput(self, dt):
    force = Vec3(0, 0, 0)
    torque = Vec3(0, 0, 0)

    if inputState.isSet('forward'): force.setY( 1.0)
    if inputState.isSet('reverse'): force.setY(-1.0)
    if inputState.isSet('left'):    force.setX(-1.0)
    if inputState.isSet('right'):   force.setX( 1.0)
    if inputState.isSet('turnLeft'):  torque.setZ( 1.0)
    if inputState.isSet('turnRight'): torque.setZ(-1.0)

    force *= 30.0
    torque *= 10.0

    self.boxNP.node().setActive(True)
    self.boxNP.node().applyCentralForce(force)
    self.boxNP.node().applyTorque(torque)

  def update(self, task):
    dt = globalClock.getDt()

    self.processInput(dt)
    self.world.doPhysics(dt)

    self.raycast()
    return task.cont

  def raycast(self):
    pFrom = Point3(-4, 0, 0.5)
    pTo = Point3(4, 0, 0.5)
    #pTo = pFrom + Vec3(1, 0, 0) * 99999

    # Raycast for closest hit
    result = self.world.rayTestClosest(pFrom, pTo)
    print result.hasHit(), \
          result.getHitFraction(), \
          result.getNode(), \
          result.getHitPos(), \
          result.getHitNormal()

    # Raycast for all hits
    #result = self.world1.rayTestAll(pFrom, pTo)
    #print result.hasHits(), \
    #      result.getClosestHitFraction(), \
    #      result.getNumHits()
    #print [hit.getHitPos() for hit in result.getHits()]

  def cleanup(self):
    self.world = None
    self.worldNP.removeNode()

  def makeWorlds(self, worldsDict):
    for k, v in worldsDict.items():
        self.worldNP = render.attachNewNode(k)
        # World
        self.debugNP = self.worldNP.attachNewNode(BulletDebugNode('Debug'))
        self.debugNP.show()
        self.world = BulletWorld()
        self.world.setGravity(Vec3(0, 0, -9.81))
        self.world.setDebugNode(self.debugNP.node())
        # Ground
        shape = BulletPlaneShape(Vec3(0, 0, 1), 0)
        np = self.worldNP.attachNewNode(BulletRigidBodyNode('Ground'))
        np.node().addShape(shape)
        np.setPos(0, 0, 0)
        np.setCollideMask(BitMask32(0x0f))
        self.world.attachRigidBody(np.node())
        # Box
        shape = BulletBoxShape(Vec3(0.5, 0.5, 0.5))
        np = self.worldNP.attachNewNode(BulletRigidBodyNode('Box'))
        np.node().setMass(1.0)
        np.node().addShape(shape)
        np.setPos(0, 0, 4)
        np.setCollideMask(BitMask32(0x0f))
        self.world.attachRigidBody(np.node())
        self.boxNP = np
        visualNP = loader.loadModel('models/box.egg')
        visualNP.reparentTo(self.boxNP)
        # Sphere
        shape = BulletSphereShape(0.6)
        np = self.worldNP.attachNewNode(BulletRigidBodyNode('Sphere'))
        np.node().setMass(1.0)
        np.node().addShape(shape)
        np.setPos(3, 0, 4)
        np.setCollideMask(BitMask32(0x0f))
        self.world.attachRigidBody(np.node())
        # Cone
        shape = BulletConeShape(0.6, 1.0)
        np = self.worldNP.attachNewNode(BulletRigidBodyNode('Cone'))
        np.node().setMass(1.0)
        np.node().addShape(shape)
        np.setPos(6, 0, 4)
        np.setCollideMask(BitMask32(0x0f))
        self.world.attachRigidBody(np.node())
  def setup(self):
    worldsDict = {"world": "World1", "world": "World2", "world": "World3"}
    self.makeWorlds(worldsDict)

game = Game()
run()

I’m wanting to change Enn0x’s sample to let the F6 button shuffle through the worlds, just to check it out. How can I get Panda3d / Bullet to list the worlds so I can build that function? I tried adding

    for k,v in self.render.node.items():
        print k
        print v

under the setup def, but it fails. Where else can I go to programmatically get a list of the worlds that are instanced?

Hmm, having looked at your code, I have four points that I’d like to raise:

First, in your worldsDict dictionary, you seem to set the key for all three entries to “world” - I’m honestly not sure of what that would do. What do you expect to get back if you pass in the key “world”? Did you perhaps mean to have the second element of each pair (“World1”, etc.) to be the keys?

Second, why are you storing two strings in that dictionary? What is the intended purpose of the second, in particular?

Third, you seem to discard your handles to your created variables with each new world created. For example, self.worldNP seems, based on a quick glance, to store the root node for the “world”. However, on the second iteration of the loop, the root node for the second “world” is assigned to self.worldNP, meaning that you’ve lost your handle on the root node for the first. Similarly, the third displaces the second, and so on.

Fourth, in answer to your, and addressing my third point above: why not store your “worlds” in a new dictionary - let us call it “self.worlds” for now. I imagine something like this:

def __init__(self, <other parameters that you may want>):
    # Initialisation code here...
    self.worlds = {}
    # Further initialisation, perhaps...

def makeWorlds(self, worldsDict):
    # Please give your variables more useful names;
    #  I'm guessing at what you intend these to be.
    for worldName, worldData in worldsDict.items():
        # I'm not sure that this should be parented to render;
        #  after all, we don't want all of them showing all
        #  the time, so we want to attach them to render as
        #  desired.  I may well be incorrect, however.
        newRoot = NodePath(PandaNode(worldName))
        
        # Continue building world...
        
        # At the end of this world-creation:
        self.worlds[worldName] = newRoot
        # You might want to make the above a list or tuple
        #  containing "newRoot" if you want to store other
        #  data along with it.

def printWorlds(self):
    for worldName, world in self.worlds:
        print "World: " + worldName + " - data: " + str(world)
        # -or whatever printout suits your desires.

I’m struggling to understand this - I’ve been reading the manual’s section on The Scene Graph, Searching The Scene Graph, and looking at the cheat sheets. It’s still not making sense; where does NodePath / PandaNode fit in?

“NameError: global name ‘NodePath’ is not defined”

import sys

from panda3d.core import loadPrcFileData
loadPrcFileData("", "want-directtools #t")
loadPrcFileData("", "want-tk #t")

import direct.directbase.DirectStart

from direct.showbase.DirectObject import DirectObject
from direct.showbase.InputStateGlobal import inputState

from panda3d.core import AmbientLight
from panda3d.core import DirectionalLight
from panda3d.core import Vec3
from panda3d.core import Vec4
from panda3d.core import Point3
from panda3d.core import TransformState
from panda3d.core import BitMask32

from panda3d.bullet import BulletWorld
from panda3d.bullet import BulletPlaneShape
from panda3d.bullet import BulletBoxShape
from panda3d.bullet import BulletSphereShape
from panda3d.bullet import BulletConeShape
from panda3d.bullet import BulletRigidBodyNode
from panda3d.bullet import BulletDebugNode
from panda3d.bullet import ZUp

class Game(DirectObject):

  def __init__(self):
    self.worlds = {}
    base.setBackgroundColor(0.1, 0.1, 0.8, 1)
    base.setFrameRateMeter(True)

    base.cam.setPos(0, -20, 4)
    base.cam.lookAt(0, 0, 0)

    # Light
    alight = AmbientLight('ambientLight')
    alight.setColor(Vec4(0.5, 0.5, 0.5, 1))
    alightNP = render.attachNewNode(alight)

    dlight = DirectionalLight('directionalLight')
    dlight.setDirection(Vec3(1, 1, -1))
    dlight.setColor(Vec4(0.7, 0.7, 0.7, 1))
    dlightNP = render.attachNewNode(dlight)

    render.clearLight()
    render.setLight(alightNP)
    render.setLight(dlightNP)

    # Input
    self.accept('escape', self.doExit)
    self.accept('r', self.doReset)
    self.accept('f1', self.toggleWireframe)
    self.accept('f2', self.toggleTexture)
    self.accept('f3', self.toggleDebug)
    self.accept('f5', self.doScreenshot)
    self.accept('f6', self.shuffleWorlds)

    inputState.watchWithModifiers('forward', 'w')
    inputState.watchWithModifiers('left', 'a')
    inputState.watchWithModifiers('reverse', 's')
    inputState.watchWithModifiers('right', 'd')
    inputState.watchWithModifiers('turnLeft', 'q')
    inputState.watchWithModifiers('turnRight', 'e')

    # Task
    taskMgr.add(self.update, 'updateWorld')

    # Physics
    self.setup()

  def doExit(self):
    self.cleanup()
    sys.exit(1)

  def shuffleWorlds(self):
    return none

  def doReset(self):
    self.cleanup()
    self.setup()

  def toggleWireframe(self):
    base.toggleWireframe()

  def toggleTexture(self):
    base.toggleTexture()

  def toggleDebug(self):
    if self.debugNP.isHidden():
      self.debugNP.show()
    else:
      self.debugNP.hide()

  def doScreenshot(self):
    base.screenshot('Bullet')

  def processInput(self, dt):
    force = Vec3(0, 0, 0)
    torque = Vec3(0, 0, 0)

    if inputState.isSet('forward'): force.setY( 1.0)
    if inputState.isSet('reverse'): force.setY(-1.0)
    if inputState.isSet('left'):    force.setX(-1.0)
    if inputState.isSet('right'):   force.setX( 1.0)
    if inputState.isSet('turnLeft'):  torque.setZ( 1.0)
    if inputState.isSet('turnRight'): torque.setZ(-1.0)

    force *= 30.0
    torque *= 10.0

    self.boxNP.node().setActive(True)
    self.boxNP.node().applyCentralForce(force)
    self.boxNP.node().applyTorque(torque)

  def update(self, task):
    dt = globalClock.getDt()

    self.processInput(dt)
    self.world.doPhysics(dt)

    self.raycast()
    return task.cont

  def raycast(self):
    pFrom = Point3(-4, 0, 0.5)
    pTo = Point3(4, 0, 0.5)
    #pTo = pFrom + Vec3(1, 0, 0) * 99999

    # Raycast for closest hit
    result = self.world.rayTestClosest(pFrom, pTo)
    print result.hasHit(), \
          result.getHitFraction(), \
          result.getNode(), \
          result.getHitPos(), \
          result.getHitNormal()

  def cleanup(self):
    self.world = None
    self.worldNP.removeNode()

  def makeWorlds(self, worldsDict):
    # Hmm, does this give me any useful output?
    # render.ls()
    for worldName, worldData in worldsDict.items():
      newroot = NodePath(PandaNode(worldName))
      self.worlds[worldName] = newRoot

  def printWorlds(self):
    for worldName, world in self.worlds:
      print "World: " + worldName + " - data: " + str(world)

  def setup(self):
    self.worldNP = render.attachNewNode("World")
    # World
    self.debugNP = self.worldNP.attachNewNode(BulletDebugNode('Debug'))
    self.debugNP.show()
    self.world = BulletWorld()
    self.world.setGravity(Vec3(0, 0, -9.81))
    self.world.setDebugNode(self.debugNP.node())
    # Ground
    shape = BulletPlaneShape(Vec3(0, 0, 1), 0)
    np = self.worldNP.attachNewNode(BulletRigidBodyNode('Ground'))
    np.node().addShape(shape)
    np.setPos(0, 0, 0)
    np.setCollideMask(BitMask32(0x0f))
    self.world.attachRigidBody(np.node())
    # Box
    shape = BulletBoxShape(Vec3(0.5, 0.5, 0.5))
    np = self.worldNP.attachNewNode(BulletRigidBodyNode('Box'))
    np.node().setMass(1.0)
    np.node().addShape(shape)
    np.setPos(0, 0, 4)
    np.setCollideMask(BitMask32(0x0f))
    self.world.attachRigidBody(np.node())
    self.boxNP = np
    visualNP = loader.loadModel('models/box.egg')
    visualNP.reparentTo(self.boxNP)
    # Sphere
    shape = BulletSphereShape(0.6)
    np = self.worldNP.attachNewNode(BulletRigidBodyNode('Sphere'))
    np.node().setMass(1.0)
    np.node().addShape(shape)
    np.setPos(3, 0, 4)
    np.setCollideMask(BitMask32(0x0f))
    self.world.attachRigidBody(np.node())
    # Cone
    shape = BulletConeShape(0.6, 1.0)
    np = self.worldNP.attachNewNode(BulletRigidBodyNode('Cone'))
    np.node().setMass(1.0)
    np.node().addShape(shape)
    np.setPos(6, 0, 4)
    np.setCollideMask(BitMask32(0x0f))
    self.world.attachRigidBody(np.node())

    worldsDict = {"sol": "data", "alpha": "data", "beta": "data"}
    self.makeWorlds(worldsDict)

game = Game()
run()

You have to import this and PandaNode (just like any other Panda object), add the following line:

from panda3d.core import NodePath, PandaNode

But to be honest I’m not sure how you could have read the manual’s section on the Scene Graph and not understand where node paths come into play (unless I’m completely misunderstanding you), from the manual:

My assumption was that PandaNode & NodePath were implicitly a part of Panda3d, that they wouldn’t need to be called because they were always there.

But PandaNode is a class, right? So I still need to invoke an instance of it if I want to do anything useful around it. Object oriented concepts are a little abstract for me, but I think I’m getting it.

I know how to create multiple models in a scene, and place them, and hide / remove them. When I see a BulletWorld() getting created, I don’t see it parented to a node like I would with a model like model.reparentTo(render) or somesuch.

So if I wanted to have a def setup two worlds, using Thaumaturge’s structure:

    def setup(self):
        worldsDict = {"Earth": "data", "Mars": "data"}
        for worldName, worldData in worldsDict.items():
            self.worldNP = render.attachNewNode(worldName)
            #World
            newRoot = NodePath(PandaNode(worldName))
            # Continue building world ... umm, how?
            # something like self.world = BulletWorld()   ?
            self.worlds[worldName] = newRoot

I don’t get where I could instance a new world and still have a handle on it. How does newRoot -> instancing a BulletWorld()? BulletWorld is a world class, no?

Thank you all very much for your time. I am grateful for the knowledge you all share here, and your attention is muchly apprecitated :slight_smile:

Ok, I have only ever programmed OO so I am bound to make some assumptions about other people’s coding styles that are not warranted. In Panda3d you have to import every class that you need (Vec3, Point3, BulletWorld, etc.) If you don’t import any panda3d classes you would essentially just be running a raw python script that has no relation to panda3d. The following line automatically imports certain pre-initialized objects (render, base, taskMgr) for convenience,

import direct.directbase.DirectStart

but nothing about panda3d is implicitly part of your python script to start with. Before you can create a new node you need add that type of node (PandaNode, CollisionNode, GeomNode, etc.) to your import statements at the top of your script and then create a new instance of that node “new_node = PandaNode(‘any_name_here’)” whenever you need one. In this case you need to create a new node for each world so you need access to the class “PandaNode” (as the most basic kind of node).

But I have to reiterate here a point that Thaumaturge raises, there are some issues with your code that need to be cleared up before you’ll be able to accomplish what you’re trying to do.

First of all:

for worldName, worldData in worldsDict.items():
	self.worldNP = render.attachNewNode(worldName)

in this loop you are replacing the previous world node you created with each iteration through it. Every time the loop executes the statement “self.worldNP = …” you are destroying the world that was created the pass before (i.e. Mars will replace Earth and so on). (Not only that but you can’t pass the string worldName directly to “attachNewNode” you have to give it a node object, see my examples below.)

In Python when you see the term “self.” before a variable you are referring to an attribute of an object. In this case the object is “game” as an instance of the class “Game”, so the result of your loop would be identical to the following code:

game = Game()
game.worldNP = render.attachNewNode('Earth')
game.worldNP = render.attachNewNode('Mars')
game.worldNP = render.attachNewNode('Jupiter')

Whenenver you try to access “game.worldNP” you will only be able to get the last one created. What you need to do is create a dictionary into which you place each world’s nodepath using its name as the key:

self.world_NP_Dict = {}  # dict to hold nodepaths.
self.worldsDict = {"Earth": "data", "Mars": "data"}  # attach this to the game object as well.
for worldName, worldData in self.worldsDict.items():
	new_world_node = PandaNode(worldName)  # Init the node to pass to attachNewNode.
	new_world_np = render.attachNewNode(new_world_node)
	self.world_NP_Dict[worldName] = new_world_np

Whenever you need to get a handle on, say, Earth’s nodepath you would just use “earth_np = self.world_NP_Dict[‘Earth’]”.

Next, I’m fairly certain that you are misunderstanding the Bullet physics module: a bullet world is not a planet or world like your code seems to imply, it is simply the module’s analogy to panda’s scene graph. That is you should only ever need one instance of a bullet world which works with the scene graph to simulate physics for it. Your worlds or planets would be objects in the bullet world, not bullet worlds themselves.

That being said having looked into the Bullet physics module myself for similar purposes it is not useful for simulating the physics of a solar system. For example, its “gravity” only pulls in one direction meaning that you can’t use it to create gravity for spherical worlds, only for completely horizontal terrains like most video games. Though the module may still be useful for collisions and such in your game.

The native panda3d physics module provides the LinearSinkForce object, but again after experimentation it seems inadequate for simulating gravity (creating a bowl shaped attraction rather than a gravity well.) So you have to simulate gravity yourself and apply it to your bodies (planets, ships, etc.)

Some final thoughts on objects and design: your program layout is really just a functional program stuck inside a class object. This is fine if you are more comfortable with functional programming but you would be handicapping yourself not to take advantage of some of Python’s basic OO features. Why not separate your major conceptual objects into different classes? Your worlds or planets will be large and complex objects in the end (think of all their physical and orbital paramaters and behaviors), you should definitely create a separate class called “World” where you “encapsulate” all this information and behavior. A rough outline with some demo behavior would be:

class Game(DirectObject):

	def __init__(self):
		self.world_Dict = {}   # note we lose the "NP" from the name.
		self.world_init_Dict = {"Earth": "data", "Mars": "data"}  # change name to avoid confusion.
		## more game init code ##
	
	def setup(self):
		# Instantiate each world and add it to self.world_Dict.
		for worldName, worldData in worldsDict.items():
			new_world = World(worldName, worldData)
			self.world_Dict[worldName] = new_world

	# Example behavior.
	def hide_all_worlds(self):
		for world in self.world_Dict.values():
			world.hide_world()


class World:

	def __init__(self, worldName, worldData):
		self.node = PandaNode(worldName)
		self.NP = render.attachNewNode(self.node)
		self.name = worldName
		self.moon_list = []  # add planet moons.
		self.worldData = worldData
		## more init code ##
	
	# Example behavior.
	def show_world(self):
		print("Now showing world {}".format(self.name))
		self.NP.show()

	def hide_world(self);
		print("Now hiding world {}".format(self.name))
		self.NP.hide()

You could then easily access anything about your worlds (including their nodepaths) simply by taking them from the “game.world_Dict” (or “self.world_Dict” if you’re working inside the “Game” class code). For example:

# Get Mars.
mars = self.world_Dict['Mars']

# Access the planet's nodepath.
mars_np = mars.NP

# Attach another node to the planet (i.e. a moon).
phobos_data = {...some data...}
phobos = World("Phobos", phobos_data)
mars.attachNewNode(phobos.NP)
mars.moon_list.append(phobos)

# Print info about mars.
print(mars.worldData)

# Or hide all planets.
self.hide_all_worlds()

Hmm, I can see then how Bullet wouldn’t be advantageous. I’m not looking for super real physics at this point, just wanna make a fun game.

But I do want to have multiple scene graphs, don’t I? As per Thaumaturge’s second post, using a new world() instance for each solar system would:

  • keep each solar system separate, which I want
  • eliminate having extremely messy coords if all solar systems shared the same scene graph

OT: I do understand the class inheritance stuff - I think I do, anyway. My OO deficit comes into play when I need to instance things, and forget I can do this and store them all in a dict. I’ve done straight script style programming for network admin stuff for years; changing to a more dynamic feeling OO is pretty dope, but not intuitive (yet).

Thanks again, for all the insights. Currently watching the PyOhio demo on networked gaming, and the Uint16 stuff in Pydatagrams looks like just the ticket to upgrade my current networking code.

youtube.com/watch?v=Qou-6d5qEw4