Simple Roaming Ralph modifications

This modification changes player controls (gives walk-run option, for example; although animations are the same at the moment, but it can be easily changed), camera controls, and some other tweaks.

# This code was created with Crimsonland-like shooter concept in my head.
# This is why I placed camera so high and removed camera collisions. They
# can be easily restored, having the original Roaming Ralph sample
# and this code.
# Thanks to all who helped to make this code! I have just started
# to learn Panda, so, all working code belongs to them,
# all bugs are my own :) 
# Version 0.1 

import direct.directbase.DirectStart

from direct.actor.Actor import Actor
from direct.showbase.DirectObject import DirectObject
from direct.task.Task import Task
from pandac.PandaModules import WindowProperties
from pandac.PandaModules import PandaNode,NodePath,Camera,Vec3,Filename

from pandac.PandaModules import CollisionTraverser,CollisionNode,BitMask32
from pandac.PandaModules import CollisionHandlerQueue,CollisionRay

import sys

# Relative path to Roaming Ralph's models directory
modelsDir = "models/"

# Ralph's speed, units per second
speed = 2

# Set floater and camera position (relative to self.actorPlace).
# The camera will look at the floater, and not at Ralph himself.
# Right(-) or left(+), forward (-) or backward (+), down(-) or up(+)
setFloaterPos = (0, -2, 0.5)
setCameraPos = (0, 15, 8)

class World(DirectObject):

    def __init__(self):

        # Disable defaul camera controls
        base.disableMouse()

        # Initial setup
        props = WindowProperties()
        props.setCursorHidden(True)
        base.win.requestProperties(props)
        base.setBackgroundColor(0, 0, 0)

        # Find the center of the screen
        self.screenCenterX = base.win.getProperties().getXSize()/2
        self.screenCenterY = base.win.getProperties().getYSize()/2

        # Move mouse to the middle of the screen
        base.win.movePointer(0, self.screenCenterX, self.screenCenterY)

        # Game state variables
        # Previous recorded time in milliseconds (will be used for movements)
        self.prevtime = 0
        # Is Ralph moving now? (will be used to animate Ralph)
        self.isMoving = False

        # Initial orientation of Ralph, floater, and camera
        # (i.e. self.actorPlace) in degrees
        self.angle = 0

        # Set floater and camera position (relative to self.actorPlace).
        # The camera will look at the floater, and not at Ralph himself.
        # Right(-) or left(+), forward (-) or backward (+), down(-) or up(+)
        self.floaterPos = setFloaterPos
        self.cameraPos = setCameraPos

        # When we scale nodes, their local coordinate system is scaled too.
        # Also, some models use Y as top-down axis, while the engine uses Z
        # for it. Therefore, in order to avoid confusion, it's better to
        # create an empty node (here it's called "actorPlace"), and reparent
        # Ralph to it. In this case Ralph can be scaled as he needs,
        # and have Y as his top-down axis, still, we can move, rotate or use
        # him in any other way with ease

        # Create Actor's placeholder
        self.actorPlace = NodePath(PandaNode("actorPlace"))
        self.actorPlace.reparentTo(render)
        self.actorPlace.setPos(0, 0, 0)
        self.actorPlace.setHpr(self.angle, 0, 0)

        # Create the main character, Ralph, with two animations
        # and reparent him to the self.actorPlace
        self.ralph = Actor(modelsDir+"ralph",
                                 {"run":modelsDir+"ralph-run",
                                  "walk":modelsDir+"ralph-walk"})
        self.ralph.reparentTo(self.actorPlace)
        self.ralph.setScale(.2)
        self.ralph.setPos(0, 0, 0)
        self.ralph.setHpr(0, 0, 0)

        # Create the floater and reparent it to the self.actorPlace
        self.floater = NodePath(PandaNode("floater"))
        self.floater.reparentTo(self.actorPlace)
        self.floater.setPos(self.floaterPos[0], self.floaterPos[1], self.floaterPos[2])

        # Create the camera, reparent it to the self.actorPlace
        # and make it look at the floater
        base.camera.reparentTo(self.actorPlace)
        base.camera.setPos(self.cameraPos[0], self.cameraPos[1], self.cameraPos[2])
        base.camera.lookAt(self.floater)

        # Load world
        self.environ = loader.loadModel(modelsDir+"world")
        self.environ.reparentTo(render)
        self.environ.setPos(0, 0, 0)

        # Create key map
        # (primFire and secFire are not used, but reserved for future use)
        self.keyMap = {"left":0,
                       "right":0,
                       "forward":0,
                       "backward":0,
                       "run":0,
                       "changeView":0,
                       "primFire":0,
                       "secFire":0}

        # Accept the control keys for movement and rotation
        # (primFire and secFire are not used, but reserved for future use)
        self.accept("escape", sys.exit)
        self.accept("w", self.setKey, ["forward", 1])
        self.accept("a", self.setKey, ["left", 1])
        self.accept("s", self.setKey, ["backward", 1])
        self.accept("d", self.setKey, ["right", 1])
        self.accept("w-up", self.setKey, ["forward", 0])
        self.accept("a-up", self.setKey, ["left", 0])
        self.accept("s-up", self.setKey, ["backward", 0])
        self.accept("d-up", self.setKey, ["right", 0])
        self.accept("shift", self.setKey, ["run", 1])
        self.accept("shift-up", self.setKey, ["run", 0])
        self.accept("shift-w", self.setKey, ["forward", 1])
        self.accept("shift-a", self.setKey, ["left", 1])
        self.accept("shift-s", self.setKey, ["backward", 1])
        self.accept("shift-d", self.setKey, ["right", 1])
        self.accept("shift-w-up", self.setKey, ["forward", 0])
        self.accept("shift-a-up", self.setKey, ["left", 0])
        self.accept("shift-s-up", self.setKey, ["backward", 0])
        self.accept("shift-d-up", self.setKey, ["right", 0])
        self.accept("mouse2", self.setKey, ["changeView", 1])
        self.accept("mouse2-up", self.setKey, ["changeView", 0])
        self.accept("mouse1", self.setKey, ["primFire", 1])
        self.accept("mouse1-up", self.setKey, ["primFire", 0])
        self.accept("mouse3", self.setKey, ["secFire", 1])
        self.accept("mouse3-up", self.setKey, ["secFire", 0])

        # We repeat self.move every frame
        taskMgr.add(self.move,"moveTask")

        # Collision detection
        self.ralphGroundRay = CollisionRay()
        self.ralphGroundRay.setOrigin(0, 0, 1000)
        self.ralphGroundRay.setDirection(0, 0, -1)
        self.ralphGroundCol = CollisionNode('ralphRay')
        self.ralphGroundCol.addSolid(self.ralphGroundRay)
        self.ralphGroundCol.setFromCollideMask(BitMask32.bit(0))
        self.ralphGroundCol.setIntoCollideMask(BitMask32.allOff())
        self.ralphGroundColNp = self.ralph.attachNewNode(self.ralphGroundCol)
        self.ralphGroundHandler = CollisionHandlerQueue()

        # base.cTrav doesn't need to be called every frame,
        # it happens automatically.
        base.cTrav = CollisionTraverser()
        base.cTrav.addCollider(self.ralphGroundColNp, self.ralphGroundHandler)

    # Records the state of the arrow keys
    def setKey(self, key, value):
        self.keyMap[key] = value

    def move(self, task):
        # This method is called every frame, since it is attached to taskMgr.

        # The elapsed time is the current time minus the last saved time
        elapsed = task.time - self.prevtime

        # Save Ralph's initial position so that we can restore it,
        # in case he falls off the map or runs into something.
        startpos = self.actorPlace.getPos()

        # If a move key is pressed, turn ralph in the specified direction.
        if (self.keyMap["left"]!=0) or (self.keyMap["right"]!=0) or (self.keyMap["backward"]!=0) or (self.keyMap["forward"]!=0):
            rotDirections = (self.keyMap["left"], self.keyMap["right"], self.keyMap["backward"], self.keyMap["forward"])
            rotValues = (90, 270, 180, (0, 360))
            rotateBy = 0
            divideBy = 0
            for direction in rotDirections:
                divideBy += direction
            for value in range(0, 4):
                if value != 3:
                    rotateBy += rotDirections[value]*rotValues[value]
                if value == 3:
                    if self.keyMap["left"]!=0:
                        rotateBy += rotDirections[value]*rotValues[value][0]
                    else:
                        rotateBy += rotDirections[value]*rotValues[value][1]
            rotateBy = rotateBy / divideBy
            self.ralph.setH(rotateBy)

        # If a move-key is pressed, move ralph in the specified direction.
        if (self.keyMap["left"]!=0):
            if (self.keyMap["run"]!=0):
                self.actorPlace.setPos(self.actorPlace, Vec3(1, 0, 0)*(elapsed*speed*2))
            else:
                self.actorPlace.setPos(self.actorPlace, Vec3(1, 0, 0)*(elapsed*speed))
        if (self.keyMap["right"]!=0):
            if (self.keyMap["run"]!=0):
                self.actorPlace.setPos(self.actorPlace, Vec3(-1, 0, 0)*(elapsed*speed*2))
            else:
                self.actorPlace.setPos(self.actorPlace, Vec3(-1, 0, 0)*(elapsed*speed))
        if (self.keyMap["forward"]!=0):
            if (self.keyMap["run"]!=0):
                self.actorPlace.setPos(self.actorPlace, Vec3(0, -1, 0)*(elapsed*speed*2))
            else:
                self.actorPlace.setPos(self.actorPlace, Vec3(0, -1, 0)*(elapsed*speed))
        if (self.keyMap["backward"]!=0):
            if (self.keyMap["run"]!=0):
                self.actorPlace.setPos(self.actorPlace, Vec3(0, 1, 0)*(elapsed*speed*2))
            else:
                self.actorPlace.setPos(self.actorPlace, Vec3(0, 1, 0)*(elapsed*speed))

        # If ralph is moving, loop "run" animation.
        # If he is standing still, stop the animation.
        if (self.keyMap["forward"]!=0) or (self.keyMap["left"]!=0) or (self.keyMap["right"]!=0) or (self.keyMap["backward"]!=0):
            if not self.isMoving:
                self.ralph.loop("run")
                self.isMoving = True
        else:
            if self.isMoving:
                self.ralph.stop()
                self.ralph.pose("walk",5)
                self.isMoving = False

        # Mouse controls
        if self.keyMap["changeView"]!=0: # If changeView button is pressed, change view when moving mouse.
            mouseX = base.win.getPointer(0).getX()
            # Update ratation
            if base.win.movePointer(0, self.screenCenterX, self.screenCenterY):
                self.angle -= (mouseX - self.screenCenterX)
            if self.angle < 0:
                self.angle += 360
            if self.angle >= 360:
                self.angle -= 360
        # Otherwise, just keep cursor position at the center of the screen
        else:
            base.win.movePointer(0, self.screenCenterX, self.screenCenterY)

        # Adjust ralph's Z coordinate.  If ralph'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.ralphGroundHandler.getNumEntries()):
            entry = self.ralphGroundHandler.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.actorPlace.setZ(entries[0].getSurfacePoint(render).getZ())
        else:
            self.actorPlace.setPos(startpos)

        # Update actorPlace orientation
        self.actorPlace.setH(self.angle)

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

# Create world
World()

#render.analyze()
run()

Here is new modification of the Roaming Ralph.
I prepare to develop a Crimsonland-like shooter, with hundreds of enemies on the screen. For this reason I temporary removed collision detection (I am just learning Panda, and methods I know are slow, and therefore unacceptable). Since there is no collision detection, I included small flat terrain in the archive at the end of the post.
Also, reticle has been added. In this version Ralph always looks at the reticle, doesn’t matter where he goes.
In this code all collision detection has been removed, since methods I know are slow, while I need fast ones.

Update: this is updated version (v0.2a). The code in archive is still old (v0.2). Don’t forget to replace the old code with this one, if you downloaded archive.

# This code was created with Crimsonland-like shooter concept in my head.
# This is why I placed camera so high and removed camera collisions. They
# can be easily restored, having the original Roaming Ralph sample
# and this code.
# Thanks to all who helped to make this code! Special thanks to mavasher
# for his "Drag Selecting multiple nodes" snippet.
# I have just started to learn Panda, so, all working code belongs to them,
# all bugs are my own :)
# Version 0.2a

import direct.directbase.DirectStart

from direct.actor.Actor import Actor
from direct.showbase.DirectObject import DirectObject
from direct.task.Task import Task
from pandac.PandaModules import *
from direct.gui.OnscreenImage import OnscreenImage

import sys,math

# Path to Roaming Ralph's models directory
modelsDir = "Roaming-Ralph/models/"

# Ralph's speed, units per second
runSpeed = 5
walkSpeed = 2

# Find screen properties
screenWidth = base.win.getProperties().getXSize()
screenHeight = base.win.getProperties().getYSize()
screenCenterX = screenWidth/2
screenCenterY = screenHeight/2
screenAspect = float(screenWidth)/screenHeight

# Set floater and camera position (relative to self.actorPlace).
# The camera will look at the floater, and not at Ralph himself.
# Right(-) or left(+), forward (-) or backward (+), down(-) or up(+)
floaterPos = (0, -2, 0.5)
cameraPos = (0, 15, 15)

class World(DirectObject):

    def __init__(self):

        # Disable default camera controls
        base.disableMouse()

        # Initial setup
        props = WindowProperties()
        props.setCursorHidden(True)
        base.win.requestProperties(props)
        base.setBackgroundColor(0, 0, 0)

        # Move mouse to the middle of the screen
        base.win.movePointer(0, screenCenterX, screenCenterY)

        # Game state variables
        # Previous recorded time in milliseconds (will be used for movement)
        self.prevtime = 0
        # Is Ralph moving now? (will be used to animate Ralph)
        self.isMoving = False

        # Initial orientation of Ralph, floater, and camera
        # (i.e. self.actorPlace) in degrees
        self.keyPointRot = 0

        # Create reticle
        self.reticlePosition = NodePath(PandaNode("reticlePosition"))
        self.reticlePosition.reparentTo(aspect2d)

        self.reticle = OnscreenImage(image = modelsDir+"reticle.png",
                                        parent = self.reticlePosition,
                                        scale = 0.07)
        # If you want your image to have transparency, add this line
        # (otherwise, its transparent parts will be black):
        self.reticle.setTransparency(TransparencyAttrib.MAlpha)

        # Create a point Ralph will look at. We will use it as
        # reference point to calculate Ralph's rotation
        self.actorPosIn2d = NodePath(PandaNode("actorPosIn2d"))
        self.actorPosIn2d.reparentTo(aspect2d)

        # Create key point
        self.keyPoint = NodePath(PandaNode("keyPoint"))
        self.keyPoint.reparentTo(render)

        # When we scale nodes, their local coordinate system is scaled too.
        # Also, some models use Y as top-down axis, while the engine uses Z
        # for it. Therefore, in order to avoid confusion, it's better to
        # create an empty node and reparent Ralph to it. In this case
        # Ralph can be scaled as he needs, and have Y as his top-down axis,
        # still, we can move, rotate or use him in any other way with ease

        # Create empty node as actor placeholder
        self.actorPoint = NodePath(PandaNode("actorPoint"))
        self.actorPoint.reparentTo(self.keyPoint)

        # Create Ralph
        self.ralph = Actor(modelsDir+"ralph",
                                     {"run" : modelsDir+"ralph-run",
                                      "walk" : modelsDir+"ralph-walk"})
        self.ralph.reparentTo(self.actorPoint)
        self.ralph.setScale(.2)

        # Create the floater and reparent it to the self.actorPoint
        self.floater = NodePath(PandaNode("floater"))
        self.floater.reparentTo(self.keyPoint)
        self.floater.setPos(floaterPos[0],
                            floaterPos[1],
                            floaterPos[2])

        # Create the camera, reparent it to the self.actorPoint
        # and make it look at the floater
        base.camera.reparentTo(self.keyPoint)
        base.camera.setPos(cameraPos[0],
                            cameraPos[1],
                            cameraPos[2])
        base.camera.lookAt(self.floater)

        # Load world
        self.environ = loader.loadModel(modelsDir+"small-flat-terrain")
        self.environ.reparentTo(render)
        self.environ.setPos(0, 0, 0)

        '''# Create lights
        ambientLight = AmbientLight('ambientLight')
        ambientLight.setColor(Vec4(0.8, 0.7, 0.7, 1))
        render.setLight(render.attachNewNode(ambientLight))'''

        # Create key map
        # (primFire and secFire are not used, but reserved for future use)
        self.keyMap = {"left":0,
                       "right":0,
                       "forward":0,
                       "backward":0,
                       "run":0,
                       "changeView":0,
                       "primFire":0,
                       "secFire":0}

        # Accept the control keys for movement and rotation
        # (primFire and secFire are not used, but reserved for future use)
        self.accept("escape", sys.exit)
        self.accept("w", self.setKey, ["forward", 1])
        self.accept("a", self.setKey, ["left", 1])
        self.accept("s", self.setKey, ["backward", 1])
        self.accept("d", self.setKey, ["right", 1])
        self.accept("w-up", self.setKey, ["forward", 0])
        self.accept("a-up", self.setKey, ["left", 0])
        self.accept("s-up", self.setKey, ["backward", 0])
        self.accept("d-up", self.setKey, ["right", 0])

        self.accept("shift", self.setKey, ["run", 1])
        self.accept("shift-up", self.setKey, ["run", 0])

        self.accept("shift-w", self.setKey, ["forward", 1])
        self.accept("shift-a", self.setKey, ["left", 1])
        self.accept("shift-s", self.setKey, ["backward", 1])
        self.accept("shift-d", self.setKey, ["right", 1])
        self.accept("shift-w-up", self.setKey, ["forward", 0])
        self.accept("shift-a-up", self.setKey, ["left", 0])
        self.accept("shift-s-up", self.setKey, ["backward", 0])
        self.accept("shift-d-up", self.setKey, ["right", 0])

        self.accept("mouse2", self.setKey, ["changeView", 1])
        self.accept("mouse2-up", self.setKey, ["changeView", 0])
        self.accept("shift-mouse2", self.setKey, ["changeView", 1])
        self.accept("shift-mouse2-up", self.setKey, ["changeView", 0])

        self.accept("mouse1", self.setKey, ["primFire", 1])
        self.accept("mouse1-up", self.setKey, ["primFire", 0])
        self.accept("shift-mouse1", self.setKey, ["primFire", 1])
        self.accept("shift-mouse1-up", self.setKey, ["primFire", 0])

        self.accept("mouse3", self.setKey, ["secFire", 1])
        self.accept("mouse3-up", self.setKey, ["secFire", 0])
        self.accept("shift-mouse3", self.setKey, ["secFire", 1])
        self.accept("shift-mouse3-up", self.setKey, ["secFire", 0])

        # We repeat self.move every frame
        taskMgr.add(self.move,"moveTask")

    def setKey(self, key, value):

        # Records the state of the arrow keys
        self.keyMap[key] = value

    def rot(self, x1, x2, y1, y2):

        # Since lookAt() method doesn't work in 2D space of the camera
        # (from my own experience, maybe I am wrong), we have to calculate
        # rotation using tangent formula
        oppositeSide = x1 - x2
        adjacentLeg = y1 - y2
        rot = math.degrees(math.atan2(oppositeSide, adjacentLeg))
        return rot

    def actorPos(self):

        # Now we are going to find point in 2D space of the camera
        # where actor is displayed. We are going to convert the point
        # in 3D space into the point in 2D space of the camera
        p3 = base.cam.getRelativePoint(render, self.keyPoint.getPos())
        # Convert it through the lens to render2d coordinates
        p2 = Point2()
        base.camLens.project(p3, p2)
        r2d = Point3(p2[0], 0, p2[1])
        # And then convert it to aspect2d coordinates
        a2d = aspect2d.getRelativePoint(render2d, r2d)
        return a2d

    def keysPressed(self):

        # We will now calculate how many movement keys are pressed.
        # This information will be used to determine
        # when Ralph moves diagonally
        directions = (self.keyMap["left"],
                        self.keyMap["right"],
                        self.keyMap["backward"],
                        self.keyMap["forward"])
        keysPressed = 0
        for direction in directions:
            keysPressed += direction
        return keysPressed

    def actualMove(self, elapsed):

        # We have to calculate how much we shall move our actor.
        # This is a bit more tricky then you can think.
        # Learn about Pythagorean theorem ;)
        # If you don't do it, Ralph will run diagonally faster
        # then forward-backward-left-right

        # If shift key is pressed, Ralph moves with different speed
        if self.keyMap["run"] != 0:
            speed = float(runSpeed)
        else:
            speed = float(walkSpeed)

        # If more then one button is pressed, Ralph moves diagonally,
        # i.e. along hypotenuse. Then we should calculate the legs of
        # the triangle.
        if self.keysPressed() > 1:
            movePerSecond = math.sqrt(((speed)**2)/2)
        else:
            movePerSecond = speed

        actualMove = elapsed*movePerSecond
        return actualMove

    def changeView(self, mouseX):

        # How much should we rotate self.keyPoint:
        if base.win.movePointer(0, screenCenterX, screenCenterY):
            mouseXchange = mouseX - screenCenterX
            self.keyPointRot -= mouseXchange
        if self.keyPointRot < 0:
            self.keyPointRot += 360
        if self.keyPointRot >= 360:
            self.keyPointRot -= 360
        # Update self.keyPoint orientation
        self.keyPoint.setH(self.keyPointRot)

    def rotRalph(self, mouseX, mouseY):

        if base.win.movePointer(0, screenCenterX, screenCenterY):
            mouseXchange = float(mouseX - screenCenterX) / screenCenterX
            mouseYchange = float(mouseY - screenCenterY) / screenCenterY
            retPos = [self.reticlePosition.getX(),
                        self.reticlePosition.getY(),
                        self.reticlePosition.getZ()]
            retPos[0] += mouseXchange
            retPos[2] -= mouseYchange
            if retPos[0] < -screenAspect:
                retPos[0] = -screenAspect
            if retPos[0] > screenAspect:
                retPos[0] = screenAspect
            if retPos[2] < -1:
                retPos[2] = -1
            if retPos[2] > 1:
                retPos[2] = 1
        # Update reticle position
        self.reticlePosition.setPos(retPos[0], 0, retPos[2])
        # Rotate Ralph so that he always looks at the reticle
        self.actorPoint.setH(-self.rot(retPos[0],
                                        self.actorPos()[0],
                                        retPos[2],
                                        self.actorPos()[2]))

    def move(self, task):

        # This method is called every frame, since it is attached to taskMgr

        # Save self.keyPoint position so that we can restore it,
        # in case Ralph falls off the map or runs into something
        prevPos = self.keyPoint.getPos()
        # Save X and Y coordinates in order to check if Ralph is moving or not
        prevXY = (self.keyPoint.getX(), self.keyPoint.getY())

        # Update self.actorPosIn2d position
        self.actorPosIn2d.setPos(self.actorPos())

        # The elapsed time is the current time minus the last saved time
        elapsed = task.time - self.prevtime

        # If a move-key is pressed, move Ralph in the specified direction
        if self.keyMap["left"]:
            self.keyPoint.setPos(self.keyPoint,
                                Vec3(1, 0, 0)*self.actualMove(elapsed))
        if self.keyMap["right"]:
            self.keyPoint.setPos(self.keyPoint,
                                Vec3(-1, 0, 0)*self.actualMove(elapsed))
        if self.keyMap["forward"]:
            self.keyPoint.setPos(self.keyPoint,
                                Vec3(0, -1, 0)*self.actualMove(elapsed))
        if self.keyMap["backward"]:
            self.keyPoint.setPos(self.keyPoint,
                                Vec3(0, 1, 0)*self.actualMove(elapsed))

        # Mouse position
        mouseX = base.win.getPointer(0).getX()
        mouseY = base.win.getPointer(0).getY()

        # If changeView button is pressed, rotate camera when moving mouse
        if self.keyMap["changeView"]:
            self.changeView(mouseX)
        # Otherwise, move reticle and rotate Ralph himself
        # so that he looks at reticle
        else:
            self.rotRalph(mouseX, mouseY)

        # Check if Ralph is moving. If he is, loop "run" animation.
        # If he is standing still, stop the animation
        if (self.keyPoint.getX(), self.keyPoint.getY()) != prevXY:
            if not self.isMoving:
                self.ralph.loop("run")
                self.isMoving = True
        else:
            if self.isMoving:
                self.ralph.stop()
                self.ralph.pose("walk",5)
                self.isMoving = False

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

# Create world
World()

#render.analyze()
run()

You can get the whole archive (including the above code) from FileFactory:
RRmod-0.2.zip

Here is the new update for the modification. Now you can move camera closer to Ralph (or away from him) using mouse wheel or Home-End-PageUp-PageDown buttons. Still no collisions.

From time to time I used tangent/sine/cosine formulas because other ways to compute certain changes lowered fps a lot.

The updated archive (with this code, a little bit changed grass texture and the rest of the files) can be downloaded from FileFactory (link at the end of the post).

I hope this sample will be useful for newbies like me in future.

# This code was created with Crimsonland-like shooter concept in my head.
# Thanks to all who helped to make this code! Special thanks to mavasher
# for his "Drag Selecting multiple nodes" snippet.
# I have just started to learn Panda, so, all working code belongs to them,
# all bugs are my own :)
# Version 0.2b

import direct.directbase.DirectStart

from direct.actor.Actor import Actor
from direct.showbase.DirectObject import DirectObject
from direct.task.Task import Task
from pandac.PandaModules import *
from direct.gui.OnscreenImage import OnscreenImage

import sys,math

###############################################
# Play with these variables to see their effect
###############################################

# Path to Roaming Ralph's models directory.
modelsDir = "Roaming-Ralph/models/"

# Ralph's speed, units per second.
runSpeed = 5
walkSpeed = 2

# Set floater and camera position (relative to their parents).
# The camera will look at the floater, and not at Ralph himself.
# Right(-) or left(+), forward (-) or backward (+), down(-) or up(+).
floaterPos = [0, -2, 1]
cameraPos = [0, 15, 10]

# Set minimal and maximal allowed distance between the camera
# and its parent
minCamDist = 0
maxCamDist = 20

# Set minimal allowed Y and Z coordinates for the camera
minCamY = 0
minCamZ = 0

###############################################
###############################################
###############################################

# Determine screen properties.
screenWidth = base.win.getProperties().getXSize()
screenHeight = base.win.getProperties().getYSize()
screenCenterX = screenWidth/2
screenCenterY = screenHeight/2
screenAspect = float(screenWidth)/screenHeight

def nodeCoordIn2d(nodePath):
    # Converts the node coordinates in 3D space into
    # the same node coordinates in 2D space of the camera.

    # Find node coordinates in space relative to the camera.
    coord3d = base.cam.getRelativePoint(render,
                                    nodePath.getPos())
    # Convert this point into 2d coordinates on screen
    # (in other words, project 3D point on the screen).
    coord2d = Point2()
    base.camLens.project(coord3d, coord2d)
    # And then convert it to aspect2d coordinates. Read the manual
    # to learn more about coordinates in render2d or aspect2d.
    coordInRender2d = Point3(coord2d[0], 0, coord2d[1])
    coordInAspect2d = aspect2d.getRelativePoint(render2d,
                                                coordInRender2d)
    return coordInAspect2d

class World(DirectObject):

    def __init__(self):

        # Disable default camera controls.
        base.disableMouse()

        # Initial setup.
        props = WindowProperties()
        props.setCursorHidden(True)
        base.win.requestProperties(props)
        base.setBackgroundColor(0, 0, 0)

        # Move mouse to the middle of the screen.
        base.win.movePointer(0, screenCenterX, screenCenterY)

        # Let's define a few world-related variables.

        # Set variable to store recorded time
        # (will be used for movements).
        self.prevtime = 0
        # Is Ralph moving (will be used to animate Ralph)?
        self.isMoving = False
        # Floater position.
        self.floaterPos = floaterPos
        # Camera position.
        self.cameraPos = cameraPos
        # Minimal and maximal allowed distance between the camera
        # and its parent
        self.minCamDist = minCamDist
        self.maxCamDist = maxCamDist
        # Minimal allowed Y and Z coordinates for the camera
        self.minCamY = minCamY
        self.minCamZ = minCamZ

        # Let's create different objects in our world.

        # Initial orientation of Ralph and camera in degrees.
        self.keyPointR = 0

        # Create reticle.
        self.reticle = OnscreenImage(image = modelsDir+"reticle.png",
                                        parent = aspect2d,
                                        scale = 0.07)
        # If you want your image to have transparency, add this line
        # (otherwise, its transparent parts will be black):
        self.reticle.setTransparency(TransparencyAttrib.MAlpha)

        # Create an empty node in aspect2d. It's position will be
        # equal to actor's position projected on the camera.
        self.actorPosIn2d = NodePath(PandaNode("actorPosIn2d"))
        self.actorPosIn2d.reparentTo(aspect2d)

        # Create key point. It will be used as a parent for
        # the camParent, the actor placeholder and the floater.
        self.keyPoint = NodePath(PandaNode("keyPoint"))
        self.keyPoint.reparentTo(render)

        # When we scale nodes, their own coordinate systems are scaled
        # too. Also, some models use Y as top-down axis, while the
        # engine uses Z for it. Therefore, in order to avoid confusion,
        # it's better to create an empty node and reparent the model to it.
        # In this example Ralph can be scaled as needed, and can have
        # Y as his top-down axis, still, we can move or rotate with ease.

        # Create an empty node as actor placeholder.
        self.actorPlaceholder = NodePath(PandaNode("actorPlaceholder"))
        self.actorPlaceholder.reparentTo(self.keyPoint)

        # Create Ralph.
        self.ralph = Actor(modelsDir+"ralph",
                                     {"run" : modelsDir+"ralph-run",
                                      "walk" : modelsDir+"ralph-walk"})
        self.ralph.reparentTo(self.actorPlaceholder)
        self.ralph.setScale(.2)

        # Create an empty node - the floater - the camera will look at.
        self.floater = NodePath(PandaNode("floater"))
        self.floater.reparentTo(self.keyPoint)
        self.floater.setPos(self.floaterPos[0],
                            self.floaterPos[1],
                            self.floaterPos[2])

        # Create an empty node - camera parent. Set it at the same
        # height as the floater. When we move the camera,
        # it will move further or closer to the camParent.
        self.camParent = NodePath(PandaNode("camParent"))
        self.camParent.reparentTo(self.keyPoint)
        self.camParent.setPos(0, self.floaterPos[2], self.floaterPos[2])

        # Create the camera and reparent it to the self.camParent.
        base.camera.reparentTo(self.camParent)
        base.camera.setPos(self.cameraPos[0],
                            self.cameraPos[1],
                            self.cameraPos[2])
        base.camera.lookAt(self.floater)

        # Load environment.
        self.environ = loader.loadModel(modelsDir+"small-flat-terrain")
        self.environ.reparentTo(render)
        self.environ.setPos(0, 0, 0)

        # Create key map (primFire and secFire are not used now,
        # but reserved for future use).
        self.keyMap = {"left":0,
                       "right":0,
                       "forward":0,
                       "backward":0,
                       "run":0,
                       "camLower":0,
                       "camHigher":0,
                       "camCloser":0,
                       "camFurther":0,
                       "changeView":0,
                       "primFire":0,
                       "secFire":0}

        # Set the control keys.
        self.accept("escape", sys.exit)
        self.accept("w", self.setKey, ["forward", 1])
        self.accept("a", self.setKey, ["left", 1])
        self.accept("s", self.setKey, ["backward", 1])
        self.accept("d", self.setKey, ["right", 1])
        self.accept("w-up", self.setKey, ["forward", 0])
        self.accept("a-up", self.setKey, ["left", 0])
        self.accept("s-up", self.setKey, ["backward", 0])
        self.accept("d-up", self.setKey, ["right", 0])

        self.accept("shift", self.setKey, ["run", 1])
        self.accept("shift-up", self.setKey, ["run", 0])

        self.accept("page_down", self.setKey, ["camLower", 1])
        self.accept("page_down-up", self.setKey, ["camLower", 0])
        self.accept("page_up", self.setKey, ["camHigher", 1])
        self.accept("page_up-up", self.setKey, ["camHigher", 0])
        self.accept("home", self.setKey, ["camCloser", 1])
        self.accept("home-up", self.setKey, ["camCloser", 0])
        self.accept("end", self.setKey, ["camFurther", 1])
        self.accept("end-up", self.setKey, ["camFurther", 0])

        self.accept("mouse2", self.setKey, ["changeView", 1])
        self.accept("mouse2-up", self.setKey, ["changeView", 0])

        self.accept("mouse1", self.setKey, ["primFire", 1])
        self.accept("mouse1-up", self.setKey, ["primFire", 0])
        self.accept("mouse3", self.setKey, ["secFire", 1])
        self.accept("mouse3-up", self.setKey, ["secFire", 0])

        # There is no need in key map to accept mouse wheel rotation.
        self.accept("wheel_up", self.moveCam, [1])
        self.accept("wheel_down", self.moveCam, [-1])

        # Duplicates of the above keys with "shift-" in front.
        # Otherwise, shift key acts as so-called "modifier" button,
        # and will make some troubles (read this thread:
        # https://discourse.panda3d.org/viewtopic.php?t=1313).
        self.accept("shift-w", self.setKey, ["forward", 1])
        self.accept("shift-a", self.setKey, ["left", 1])
        self.accept("shift-s", self.setKey, ["backward", 1])
        self.accept("shift-d", self.setKey, ["right", 1])
        self.accept("shift-w-up", self.setKey, ["forward", 0])
        self.accept("shift-a-up", self.setKey, ["left", 0])
        self.accept("shift-s-up", self.setKey, ["backward", 0])
        self.accept("shift-d-up", self.setKey, ["right", 0])
        self.accept("shift-page_down", self.setKey, ["camLower", 1])
        self.accept("shift-page_down-up", self.setKey, ["camLower", 0])
        self.accept("shift-page_up", self.setKey, ["camHigher", 1])
        self.accept("shift-page_up-up", self.setKey, ["camHigher", 0])
        self.accept("shift-home", self.setKey, ["camCloser", 1])
        self.accept("shift-home-up", self.setKey, ["camCloser", 0])
        self.accept("shift-end", self.setKey, ["camFurther", 1])
        self.accept("shift-end-up", self.setKey, ["camFurther", 0])
        self.accept("shift-mouse2", self.setKey, ["changeView", 1])
        self.accept("shift-mouse2-up", self.setKey, ["changeView", 0])
        self.accept("shift-mouse1", self.setKey, ["primFire", 1])
        self.accept("shift-mouse1-up", self.setKey, ["primFire", 0])
        self.accept("shift-mouse3", self.setKey, ["secFire", 1])
        self.accept("shift-mouse3-up", self.setKey, ["secFire", 0])
        self.accept("shift-wheel_up", self.moveCam, [1])
        self.accept("shift-wheel_down", self.moveCam, [-1])

        # Update world on every frame.
        taskMgr.add(self.updateWorld, "updateWorldTask")

    def setKey(self, key, value):
        # Records the state of the arrow keys.

        self.keyMap[key] = value

    def rot(self, x1, x2, y1, y2):
        # Since lookAt() method doesn't work in 2D space of the camera
        # (from my own experience, maybe I am wrong), we have to calculate
        # rotation using tangent formula.

        oppositeSide = x1 - x2
        adjacentLeg = y1 - y2
        rotation = math.degrees(math.atan2(oppositeSide, adjacentLeg))
        return rotation

    def keysPressed(self):
        # We will now calculate how many movement keys are pressed.
        # This information will be used to determine
        # when Ralph moves diagonally.

        directions = (self.keyMap["left"],
                        self.keyMap["right"],
                        self.keyMap["backward"],
                        self.keyMap["forward"])
        keysPressed = 0
        for direction in directions:
            keysPressed += direction
        return keysPressed

    def moveSize(self, timeElapsed):
        # Calculates movement size.

        # We have to calculate how much we shall move our actor
        # on every frame. This is a little bit trickier
        # then you could think. If you don't do it, Ralph will run
        # diagonally faster then forward-backward-left-right.
        # Learn about Pythagorean theorem ;)

        # If shift key is pressed, Ralph moves with different speed.
        if self.keyMap["run"]:
            speed = float(runSpeed)
        else:
            speed = float(walkSpeed)

        # If more then one button is pressed, Ralph moves diagonally,
        # i.e. along hypotenuse. Then we should calculate the legs of
        # the triangle.
        if self.keysPressed() > 1:
            movePerSecond = math.sqrt((speed**2) / 2)
        else:
            movePerSecond = speed

        moveSize = timeElapsed * movePerSecond
        return moveSize

    def changeView(self, mouseX):
        # Rotates the key point (and its children).

        # Determine how much to rotate the key point.
        if base.win.movePointer(0, screenCenterX, screenCenterY):
            mouseXchange = mouseX - screenCenterX
            self.keyPointR -= mouseXchange
        if self.keyPointR < 0:
            self.keyPointR += 360
        if self.keyPointR >= 360:
            self.keyPointR -= 360
        # Update the key point orientation.
        self.keyPoint.setH(self.keyPointR)

    def moveReticle(self, mouseX, mouseY):
        # Updates reticle position.

        if base.win.movePointer(0, screenCenterX, screenCenterY):
            mouseXchange = float(mouseX - screenCenterX) / screenCenterX
            mouseYchange = float(mouseY - screenCenterY) / screenCenterY
            retPos = [self.reticle.getX(),
                        self.reticle.getY(),
                        self.reticle.getZ()]
            retPos[0] += mouseXchange
            retPos[2] -= mouseYchange
        # Keep reticle within the range of possible values in aspect2d.
        if retPos[0] < -screenAspect:
            retPos[0] = -screenAspect
        if retPos[0] > screenAspect:
            retPos[0] = screenAspect
        if retPos[2] < -1:
            retPos[2] = -1
        if retPos[2] > 1:
            retPos[2] = 1
        # Update reticle position.
        self.reticle.setPos(retPos[0], 0, retPos[2])

    def rotateRalph(self):
        # Rotates Ralph so that he always looks at the reticle.

        self.actorPlaceholder.setH(-self.rot(self.reticle.getX(),
                                            self.actorCoordinatesIn2d[0],
                                            self.reticle.getZ(),
                                            self.actorCoordinatesIn2d[2]))

    def moveCam(self, value):
        # Updates camera position when mouse wheel is rolled.

        # Set a few variables.
        camPos = [base.camera.getX(),
                    base.camera.getY(),
                    base.camera.getZ()]
        camDist = math.sqrt(camPos[1]**2 + camPos[2]**2)
        cos = camPos[1] / camDist
        sin = camPos[2] / camDist

        # Change distance between the camera and its parent.
        camDist -= value * 0.5
        # Calculate new Y and Z coordinates.
        camPos[1] = cos * camDist
        camPos[2] = sin * camDist

        # Move camera only if its calculated position is
        # inside of the allowed range.
        if self.moveCamAllowed(camPos, camDist):
            self.cameraPos = camPos

    # The next four methods update camera position
    # when Home-End-PageUp-PageDown buttons are pressed.
    def camLower(self, timeElapsed):
        # Moves camera down along Z axis (PageDown button).

        camPos = [base.camera.getX(),
                    base.camera.getY(),
                    base.camera.getZ()]

        camPos[2] -= timeElapsed * 5
        camDist = math.sqrt(camPos[1]**2 + camPos[2]**2)

        if self.moveCamAllowed(camPos, camDist):
            self.cameraPos = camPos

    def camHigher(self, timeElapsed):
        # Moves camera up along Z axis (PageUp button).

        camPos = [base.camera.getX(),
                    base.camera.getY(),
                    base.camera.getZ()]

        camPos[2] += timeElapsed * 5
        camDist = math.sqrt(camPos[1]**2 + camPos[2]**2)

        if self.moveCamAllowed(camPos, camDist):
            self.cameraPos = camPos

    def camCloser(self, timeElapsed):
        # Moves camera along Y axis closer to Ralph (Home button).

        camPos = [base.camera.getX(),
                    base.camera.getY(),
                    base.camera.getZ()]

        camPos[1] -= timeElapsed * 5
        camDist = math.sqrt(camPos[1]**2 + camPos[2]**2)

        if self.moveCamAllowed(camPos, camDist):
            self.cameraPos = camPos

    def camFurther(self, timeElapsed):
        # Moves camera along Y axis further from Ralph (End button).

        camPos = [base.camera.getX(),
                    base.camera.getY(),
                    base.camera.getZ()]

        camPos[1] += timeElapsed * 5
        camDist = math.sqrt(camPos[1]**2 + camPos[2]**2)

        if self.moveCamAllowed(camPos, camDist):
            self.cameraPos = camPos

    def moveCamAllowed(self, camPos, camDist):
        # Checks whether moving the camera is allowed.

        # If the given position and distance are within
        # allowed range, moving is possible.
        y = 0
        if self.minCamY < camPos[1]:
            y = 1
        z = 0
        if self.minCamZ < camPos[2]:
            z = 1
        dist = 0
        if self.minCamDist < camDist < self.maxCamDist:
            dist = 1

        if y and z and dist: # i.e. if they all are equal to 1
            return True
        else:
            return False

    def setCamMode(self):
        # Changes camera mode into the third person view (or back)
        # when camera is close to Ralph.
        # In this sample changing camera mode doesn't change controls.
        # Generally, it is possible to create several sets of controls
        # and switch between them, thus having few gameplay modes:
        # strategy style with top-down view, adventure style with
        # third person view, and shooter style with first person view.

        camDist = math.sqrt(self.cameraPos[1]**2 + self.cameraPos[2]**2)
        if camDist <= 0.5:
            self.cameraPos[0] = -0.7
        else:
            self.cameraPos[0] = 0

    def updateWorld(self, task):
        # This method is called every frame,
        # since it is attached to taskMgr.

        # Save X and Y coordinates in order to check
        # whether Ralph is moving or not.
        prevXY = (self.keyPoint.getX(), self.keyPoint.getY())

        # Calculate Ralph's coordinates in 2D space of the camera.
        self.actorCoordinatesIn2d = nodeCoordIn2d(self.keyPoint)
        # Update actorPosIn2d position (as you remember this node
        # represents actor's position on 2d screen, aspect2d).
        self.actorPosIn2d.setPos(self.actorCoordinatesIn2d)

        # The timeElapsed is the current time minus the last saved time.
        timeElapsed = task.time - self.prevtime

        # If a movement key is pressed then calculate movement size
        # and move Ralph in the specified direction.
        if self.keyMap["left"]:
            self.keyPoint.setPos(self.keyPoint,
                                Vec3(1, 0, 0) * self.moveSize(timeElapsed))
        if self.keyMap["right"]:
            self.keyPoint.setPos(self.keyPoint,
                                Vec3(-1, 0, 0) * self.moveSize(timeElapsed))
        if self.keyMap["forward"]:
            self.keyPoint.setPos(self.keyPoint,
                                Vec3(0, -1, 0) * self.moveSize(timeElapsed))
        if self.keyMap["backward"]:
            self.keyPoint.setPos(self.keyPoint,
                                Vec3(0, 1, 0) * self.moveSize(timeElapsed))

        # Record mouse position.
        mouseX = base.win.getPointer(0).getX()
        mouseY = base.win.getPointer(0).getY()

        # If changeView button is pressed,
        # rotate camera when moving mouse.
        if self.keyMap["changeView"]:
            self.changeView(mouseX)
        # Otherwise, move reticle and then rotate Ralph
        # so that he looks at reticle.
        else:
            self.moveReticle(mouseX, mouseY)
            self.rotateRalph()

        # If camLower button is pressed,
        # move the camera down along Z axis.
        if self.keyMap["camLower"]:
            self.camLower(timeElapsed)
        # If camHigher button is pressed,
        # move the camera up along Z axis.
        if self.keyMap["camHigher"]:
            self.camHigher(timeElapsed)
        # If camCloser button is pressed,
        # move the camera closer along Y axis.
        if self.keyMap["camCloser"]:
            self.camCloser(timeElapsed)
        # If camFurther button is pressed,
        # move the camera further along Y axis.
        if self.keyMap["camFurther"]:
            self.camFurther(timeElapsed)

        # Change camera mode when the camera is close to Ralph.
        self.setCamMode()

        # Move camera and make it look at the floater.
        base.camera.setPos(self.cameraPos[0],
                                self.cameraPos[1],
                                self.cameraPos[2])
        base.camera.lookAt(self.floater)

        # Check if Ralph is moving. If he is, loop "run" animation.
        # If he is standing still, stop the animation.
        if (self.keyPoint.getX(), self.keyPoint.getY()) != prevXY:
            if not self.isMoving:
                self.ralph.loop("run")
                self.isMoving = True
        else:
            if self.isMoving:
                self.ralph.stop()
                self.ralph.pose("walk", 5)
                self.isMoving = False

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

# Create world
world = World()

#render.analyze()
run()

Archive: RRmod-0.2b.zip[/url]

Thanks a lot for posting this. I discovered a method of finding the diagonal speed, although it is not the same as yours since our code is so different. I never would have thought that the character is running on a triangle so I would never have thought to find the hypotenuse :smiley: