Simple demo: Roaming Player and Non-player characters

Just finished the first version of a simple demo in Panda3D:

  • Game environment
  • Player character roaming around the environment with following 3rd-person camera
  • Non-player characters roaming around also

http://seanh.freeshell.org/Roamers.zip

It is an extension of the Roaming Ralph tutorial that comes with Panda3D. I refactored the whole thing to make it object-oriented and easily extensible. I then re-used the code for the Roaming Ralph character to implement a class for a roaming non-player character. The result is an environment with player and multiple non-player characters.

The NPC’s are very simple – they just run around randomly. Next I’ll begin to add steering and pathfinding behaviours.

Oh, I did introduce one bug while refactoring Roaming Ralph. If you run up a hill and then down again, the camera’s Z-axis follows you up, but not down (see if you can find the mistake! :slight_smile: )

You separated the collision detection for the characters and the camera, right ?
But you don’t check the collision for the camera ? No wonder it stays on the last highest 2 feet above the player.
You should put “self.cTrav.traverse(render)” in Camera.move.

Yup, thanks for the hint.

Hey, just a heads up. The file is password protected :slight_smile:

I can rehost it for you if you’d like, but I’m definitely interested in seeing the code.

Should be fixed now, thanks.

cool beans, appreciate it! Gonna test out your code.

I know this post has been dead for a long time,
but if anyone has the code or a similar code
please direct me to it =)
this link is dead.

I’m uploading the whole thing (code and models) for you here:

homepages.inf.ed.ac.uk/s0094060/Roamers.zip

I just tested it with the latest panda on the latest ubuntu beta, and it seems to still work.

That copy will probably disappear after a while too. It’s a shame I don’t have any sort of permanent place to host it. I’ll paste the source code (it’s all one file) here for reference, to run it requires four models which are all from the models download from the panda3d website.

By the way, I think after this code I moved on to work on something called panda steer (1 and 2), also to be found on these forums.

Roamers.py

"""Roamers is a simple demo consisting of:

   * A 3D environment
   * A keyboard-controlled animated player character (class Character)
   * A 3rd-person camera that follows the player (class Camera)
   * And some non-player characters with their own animated avatars 
     (class Agent)

   The main class that initialises everything is the Game class.
   
   Roamers is based on Panda3D's Tut-Roaming-Ralph.py by Ryan Myers and uses
   models and animations from Panda3D's collection.
   
   Classes:
   Camera -- A 3rd-person floating camera that follows an actor around.
   Character -- An animated, 3D character that moves in response to control
                settings. These control settings can be used by a deriving
                class to create non-playe character behaviours (as in class
                Agent_ or can be hooked up to keyboard keys to create a
                player character (see Game.__init__()).
   Agent -- A computer-controlled extension of Character.
   Game -- The game world: environment, characters, camera, onscreen text and
           keyboard controls.
   """

import direct.directbase.DirectStart
from pandac.PandaModules import CollisionTraverser,CollisionNode
from pandac.PandaModules import CollisionHandlerQueue,CollisionRay
from pandac.PandaModules import Filename
from pandac.PandaModules import PandaNode,NodePath,Camera,TextNode
from pandac.PandaModules import Vec3,Vec4,BitMask32
from direct.gui.OnscreenText import OnscreenText
from direct.actor.Actor import Actor
from direct.task.Task import Task
from direct.showbase.DirectObject import DirectObject
import random, sys, os, math

# Figure out what directory this program is in.
MYDIR=os.path.abspath(sys.path[0])
MYDIR=Filename.fromOsSpecific(MYDIR).getFullpath()

class Camera:
    
    """A floating 3rd person camera that follows an actor around, and can be
    turned left or right around the actor.

    Public fields:
    self.controlMap -- The camera's movement controls.
    actor -- The Actor object that the camera will follow.
    
    Public functions:
    init(actor) -- Initialise the camera.
    move(task) -- Move the camera each frame, following the assigned actor.
                  This task is called every frame to update the camera.
    setControl -- Set the camera's turn left or turn right control on or off.
    
    """

    def __init__(self,actor):
        """Initialise the camera, setting it to follow 'actor'.
        
        Arguments:
        actor -- The Actor that the camera will initially follow.
        
        """
        
        self.actor = actor
        self.prevtime = 0

        # The camera's controls:
        # "left" = move the camera left, 0 = off, 1 = on
        # "right" = move the camera right, 0 = off, 1 = on
        self.controlMap = {"left":0, "right":0}

        taskMgr.add(self.move,"cameraMoveTask")

        # Create a "floater" object. It is used to orient the camera above the
        # target actor's head.
        
        self.floater = NodePath(PandaNode("floater"))
        self.floater.reparentTo(render)        

        # Set up the camera.

        base.disableMouse()
        base.camera.setPos(self.actor.getX(),self.actor.getY()+10,2)

        # A CollisionRay beginning above the camera and going down toward the
        # ground is used to detect camera collisions and the height of the
        # camera above the ground. A ray may hit the terrain, or it may hit a
        # rock or a tree.  If it hits the terrain, we detect the camera's
        # height.  If it hits anything else, the camera is in an illegal
        # position.

        self.cTrav = CollisionTraverser()
        self.groundRay = CollisionRay()
        self.groundRay.setOrigin(0,0,1000)
        self.groundRay.setDirection(0,0,-1)
        self.groundCol = CollisionNode('camRay')
        self.groundCol.addSolid(self.groundRay)
        self.groundCol.setFromCollideMask(BitMask32.bit(1))
        self.groundCol.setIntoCollideMask(BitMask32.allOff())
        self.groundColNp = base.camera.attachNewNode(self.groundCol)
        self.groundHandler = CollisionHandlerQueue()
        self.cTrav.addCollider(self.groundColNp, self.groundHandler)

        # Uncomment this line to see the collision rays
        #self.groundColNp.show()
      
    def move(self,task):
        """Update the camera's position before rendering the next frame.
        
        This is a task function and is called each frame by Panda3D. The
        camera follows self.actor, and tries to remain above the actor and
        above the ground (whichever is highest) while looking at a point
        slightly above the actor's head.
        
        Arguments:
        task -- A direct.task.Task object passed to this function by Panda3D.
        
        Return:
        Task.cont -- To tell Panda3D to call this task function again next
                     frame.
        
        """

        # FIXME: There is a bug with the camera -- if the actor runs up a
        # hill and then down again, the camera's Z position follows the actor
        # up the hill but does not come down again when the actor goes down
        # the hill.

        elapsed = task.time - self.prevtime

        # If the camera-left key is pressed, move camera left.
        # If the camera-right key is pressed, move camera right.
         
        base.camera.lookAt(self.actor)
        camright = base.camera.getNetTransform().getMat().getRow3(0)
        camright.normalize()
        if (self.controlMap["left"]!=0):
            base.camera.setPos(base.camera.getPos() - camright*(elapsed*20))
        if (self.controlMap["right"]!=0):
            base.camera.setPos(base.camera.getPos() + camright*(elapsed*20))

        # If the camera is too far from the actor, move it closer.
        # If the camera is too close to the actor, move it farther.

        camvec = self.actor.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

        # Now check for collisions.

        self.cTrav.traverse(render)

        # Keep the camera at one foot above the terrain,
        # or two feet above the actor, whichever is greater.
        
        entries = []
        for i in range(self.groundHandler.getNumEntries()):
            entry = self.groundHandler.getEntry(i)
            entries.append(entry)
        entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
                                     x.getSurfacePoint(render).getZ()))
        if (len(entries)>0) and (entries[0].getIntoNode().getName() == "terrain"):
            base.camera.setZ(entries[0].getSurfacePoint(render).getZ()+1.0)
        if (base.camera.getZ() < self.actor.getZ() + 2.0):
            base.camera.setZ(self.actor.getZ() + 2.0)
            
        # The camera should look in the player's direction,
        # but it should also try to stay horizontal, so look at
        # a floater which hovers above the player's head.
        
        self.floater.setPos(self.actor.getPos())
        self.floater.setZ(self.actor.getZ() + 2.0)
        base.camera.lookAt(self.floater)

        # Store the task time and continue.
        self.prevtime = task.time
        return Task.cont

    def setControl(self, control, value):
        """Set the state of one of the camera's movement controls.
        
        Arguments:
        See self.controlMap in __init__.
        control -- The control to be set, must be a string matching one of
                   the strings in self.controlMap.
        value -- The value to set the control to.
        
        """

        # FIXME: this function is duplicated in Camera and Character, and
        # keyboard control settings are spread throughout the code. Maybe
        # add a Controllable class?
        
        self.controlMap[control] = value

class Character:
    
    """A character with an animated avatar that moves left, right or forward
       according to the controls turned on or off in self.controlMap.
    
    Public fields:
    self.controlMap -- The character's movement controls
    self.actor -- The character's Actor (3D animated model)
    
    
    Public functions:
    __init__ -- Initialise the character
    move -- Move and animate the character for one frame. This is a task
            function that is called every frame by Panda3D.
    setControl -- Set one of the character's controls on or off.
    
    """

    def __init__(self, model, run, walk, startPos, scale):        
        """Initialise the character.
        
        Arguments:
        model -- The path to the character's model file (string)
           run : The path to the model's run animation (string)
           walk : The path to the model's walk animation (string)
           startPos : Where in the world the character will begin (pos)
           scale : The amount by which the size of the model will be scaled 
                   (float)
                   
           """

        self.controlMap = {"left":0, "right":0, "forward":0}

        self.actor = Actor(MYDIR+model,
                                 {"run":MYDIR+run,
                                  "walk":MYDIR+walk})        
        self.actor.reparentTo(render)
        self.actor.setScale(scale)
        self.actor.setPos(startPos)

        taskMgr.add(self.move,"moveTask") # Note: deriving classes DO NOT need
                                          # to add their own move tasks to the
                                          # task manager. If they override
                                          # self.move, then their own self.move
                                          # function will get called by the
                                          # task manager (they must then
                                          # explicitly call Character.move in
                                          # that function if they want it).
        self.prevtime = 0
        self.isMoving = False

        # We will detect the height of the terrain by creating a collision
        # ray and casting it downward toward the terrain.  One ray will
        # start above ralph's head, and the other will start above the camera.
        # A ray may hit the terrain, or it may hit a rock or a tree.  If it
        # hits the terrain, we can detect the height.  If it hits anything
        # else, we rule that the move is illegal.

        self.cTrav = CollisionTraverser()

        self.groundRay = CollisionRay()
        self.groundRay.setOrigin(0,0,1000)
        self.groundRay.setDirection(0,0,-1)
        self.groundCol = CollisionNode('ralphRay')
        self.groundCol.addSolid(self.groundRay)
        self.groundCol.setFromCollideMask(BitMask32.bit(1))
        self.groundCol.setIntoCollideMask(BitMask32.allOff())
        self.groundColNp = self.actor.attachNewNode(self.groundCol)
        self.groundHandler = CollisionHandlerQueue()
        self.cTrav.addCollider(self.groundColNp, self.groundHandler)

        # Uncomment this line to see the collision rays
        # self.groundColNp.show()

        #Uncomment this line to show a visual representation of the 
        #collisions occuring
        # self.cTrav.showCollisions(render)

    def move(self, task):
        """Move and animate the character for one frame.
        
        This is a task function that is called every frame by Panda3D.
        The character is moved according to which of it's movement controls
        are set, and the function keeps the character's feet on the ground
        and stops the character from moving if a collision is detected.
        This function also handles playing the characters movement
        animations.

        Arguments:
        task -- A direct.task.Task object passed to this function by Panda3D.
        
        Return:
        Task.cont -- To tell Panda3D to call this task function again next
                     frame.
        """
        
        elapsed = task.time - self.prevtime

        # save the character's initial position so that we can restore it,
        # in case he falls off the map or runs into something.

        startpos = self.actor.getPos()

        # move the character if any of the move controls are activated.

        if (self.controlMap["left"]!=0):
            self.actor.setH(self.actor.getH() + elapsed*300)
        if (self.controlMap["right"]!=0):
            self.actor.setH(self.actor.getH() - elapsed*300)
        if (self.controlMap["forward"]!=0):
            backward = self.actor.getNetTransform().getMat().getRow3(1)
            backward.setZ(0)
            backward.normalize()
            self.actor.setPos(self.actor.getPos() - backward*(elapsed*5))

        # If the character is moving, loop the run animation.
        # If he is standing still, stop the animation.

        if (self.controlMap["forward"]!=0) or (self.controlMap["left"]!=0) or (self.controlMap["right"]!=0):
           
            if self.isMoving is False:
                self.actor.loop("run")
                self.isMoving = True
        else:
            if self.isMoving:
                self.actor.stop()
                self.actor.pose("walk",5)
                self.isMoving = False

        # Now check for collisions.

        self.cTrav.traverse(render)

        # Adjust the character's Z coordinate.  If the character's ray hit terrain,
        # update his Z. If it hit anything else, or didn't hit anything, put
        # him back where he was last frame.

        entries = []
        for i in range(self.groundHandler.getNumEntries()):
            entry = self.groundHandler.getEntry(i)
            entries.append(entry)
        entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
                                     x.getSurfacePoint(render).getZ()))
        if (len(entries)>0) and (entries[0].getIntoNode().getName() == "terrain"):
            self.actor.setZ(entries[0].getSurfacePoint(render).getZ())
        else:
            self.actor.setPos(startpos)

        # Store the task time and continue.
        self.prevtime = task.time
        return Task.cont

    def setControl(self, control, value):
        """Set the state of one of the character's movement controls.
        
        Arguments:
        See self.controlMap in __init__.
        control -- The control to be set, must be a string matching one of
                   the strings in self.controlMap.
        value -- The value to set the control to.
        
        """

        # FIXME: this function is duplicated in Camera and Character, and
        # keyboard control settings are spread throughout the code. Maybe
        # add a Controllable class?
        
        self.controlMap[control] = value

class Agent(Character, DirectObject):
    """A computer-controlled non-player character.
    
    This class derives from Character.
    
    New public fields:
    None.
    
    New public functions:
    None.
    
    Functions extended from Character:
    __init__ -- Initialise some private fields.
    move -- Make the character run around randomly.
    
    """
    
    def __init__(self,model,run,walk,startPoint,scale):
        """Initialise the character.
        
        Initialises private fields used to control the character's behaviour.
        Also see Character.__init__().
        
        Arguments:
        See Character.__init__().
        
        """
        
        Character.__init__(self, model, run, walk, startPoint, scale)
        self.prevTurnTime = 0
        self.setControl('forward',1)
    
    def move(self,task):
        """Update the character for one frame.
        
        Pick a new direction for the character to turn in every second.
        Also see Character.move().
        
        Arguments:
        See Character.move().
        
        Return:
        See Character.move().
        
        """
        
        if task.time - self.prevTurnTime >= 1:
            import random
            direction = random.randint(1,3)
            if direction == 1:
                self.setControl('left',1)
                self.setControl('right',0)
            elif direction == 2:
                self.setControl('left',0)
                self.setControl('right',1)
            elif direction == 3:
                self.setControl('left',0)
                self.setControl('right',0)
            self.prevTurnTime = task.time
        return Character.move(self,task)

class Game(DirectObject):
    """The game world -- environment, characters, camera, onscreen  text and
    keyboard controls.
    
    Public functions:
    _init__ -- Initialise the game environment, characters, camera, onscreen
               text and keyboard controls.
    
    """
    
    def __init__(self):
        """Initialise the game environment and characters."""
    
        # Post some onscreen instructions.

        title = addTitle("Panda3D Tutorial: Roaming Ralph (Walking on Uneven Terrain)")
        inst1 = addInstructions(0.95, "[ESC]: Quit")
        inst2 = addInstructions(0.90, "[Left Arrow]: Rotate Ralph Left")
        inst3 = addInstructions(0.85, "[Right Arrow]: Rotate Ralph Right")
        inst4 = addInstructions(0.80, "[Up Arrow]: Run Ralph Forward")
        inst6 = addInstructions(0.70, "[A]: Rotate Camera Left")
        inst7 = addInstructions(0.65, "[S]: Rotate Camera Right")

        # Initialise the environment.

        base.win.setClearColor(Vec4(0,0,0,1))
        environ = loader.loadModel(MYDIR+"/models/world/world")      
        environ.reparentTo(render)
        environ.setPos(0,0,0)    
        environ.setCollideMask(BitMask32.bit(1))

        # Create a character for the player.

        player = Character("/models/ralph/ralph",
                        "/models/ralph/ralph-run",
                        "/models/ralph/ralph-walk",
                        environ.find("**/start_point").getPos(),
                        .2)
                        
        # Hook up some control keys to the character
        
        self.accept("arrow_left", player.setControl, ["left",1])
        self.accept("arrow_right", player.setControl, ["right",1])
        self.accept("arrow_up", player.setControl, ["forward",1])
        self.accept("arrow_left-up", player.setControl, ["left",0])
        self.accept("arrow_right-up", player.setControl, ["right",0])
        self.accept("arrow_up-up", player.setControl, ["forward",0])

        # Create a camera to follow the player.

        camera = Camera(player.actor)
    
        # Accept some keys to move the camera.

        self.accept("a-up", camera.setControl, ["left",0])
        self.accept("s-up", camera.setControl, ["right",0])
        self.accept("a", camera.setControl, ["left",1])
        self.accept("s", camera.setControl, ["right",1])
                        
        # Create some non-player characters.
        
        rex = Agent("/models/trex/trex",
                        "/models/trex/trex-run",
                        "/models/trex/trex-run",
                        environ.find("**/start_point").getPos(),
                        .2)

        eve = Agent("/models/eve/eve",
                        "/models/eve/eve-walk",
                        "/models/eve/eve-run",
                        environ.find("**/start_point").getPos(),
                        .2)    
    
        # Accept the Esc key to quit the game.
    
        self.accept("escape", sys.exit)

def addInstructions(pos, msg):
    """Put 'msg' on the screen at position 'pos'."""
    return OnscreenText(text=msg, style=1, fg=(1,1,1,1),
            pos=(-1.3, pos), align=TextNode.ALeft, scale = .05)

def addTitle(text):
    """Put 'text' on the screen as the title."""
    return OnscreenText(text=text, style=1, fg=(1,1,1,1),
                    pos=(1.3,-0.95), align=TextNode.ARight, scale = .07)
        
if __name__ == "__main__":        
    
    game = Game()
    run()

Ooh, this is much appreciated. I just tried to make my version of roaming ralph object oriented myself, and I wasn’t sure if I did it well enough to post on the forums. Now I can compare my code to yours, woohoo. There’s no need to post the code now, either. :smiley: Kind of relieved.

Btw, pandasteer really helped me out, so thanks for that as well.

Why thankyou very much chombee, i appreciate it :slight_smile:
downloading it now :stuck_out_tongue:

chombee, what about panda3dprojects?
go to p3dp.com/ login/register and click Request Webspace, and you will be granted free webspace to host your stuff permanently.

The whole goal of p3dp is actually to avoid this crappy stuff with broken downloads etc.

Yeah I think I had some stuff on there at one point. I am too busy to sort it out but please feel free to host any code I’ve posted on there if you find it useful.

Uhmm… I can’t until you click that darn “Request Webspace” button :slight_smile:

Ok account created and webspace requested.

You’ve got mail :slight_smile:

I know this thread doesn’t seem to have any new post in a while but could someone repost a link to chombee’s updated roamer source zip that includes the models required to run, people new to panda3d and python can use as many functioning examples to learn from, i would like to see this source running. :cry:

It’s old, and IIRC not much thing added to it.
BTW, the python source is also available couple posts below the 1st.

Try this too : discourse.panda3d.org/viewtopic.php?t=2431

Thanks for the link ynjh_jo. :smiley:

github.com/seanh/ this demo was pretty simplistic, check out PandaSteer it’s much better.

Thanks chombee for the home sites, i have tried PandaSteer and it works. I also wanted to know if you and the others are going to continue working on the source or have you found something else to delve into, i know how i am when it comes to programming always something else to divide my interest :open_mouth: