 # Need help with ODE and character movement

Hello everyone,

I recently started to learn how to use ODE with Panda3D. One of my first attempts now was to implement the Roaming Ralph example with ODE for collision detection and physic handling. I got so far, that collisions got caught and Ralph gets moved by applied forces. But now comes the part where I need some help. At the moment I have three problems in that example.

1. If Ralph moves forward and I look at him from the side, I can sometimes see how he jitters. This I think comes from the applied force, the friction of the ground and the repositioning in the move task. But i have no idea, how i can set the force, friction or the reposition correctly, that he doesn’t jitter anymore.
2. When Ralph moves down a hill, the speed how he moves forward is much to high. Does someone knows how i can set the force correct that he doesn’t move that fast, when he moves downwards?
3. Directly at the start of the sample, Ralph falls below the map. I worked around this to test the other things by reset it’s position when hitting the ‘r’ key. This will reset his position to the start position. After that he doesn’t fall off the map anymore.
Wolf

Here is my edited Roaming Ralph example:

``````# Author: Ryan Myers
# ODE Edit: Wolf
# Models: Jeff Styers, Reagan Heller

# Last Updated: 6/13/2012
#
# This tutorial provides an example of creating a character
# and having it walk around on uneven terrain, as well
# as implementing a fully rotatable camera.

import direct.directbase.DirectStart
from panda3d.core import CollisionTraverser,CollisionNode
from panda3d.core import CollisionHandlerQueue, CollisionRay
from panda3d.core import AmbientLight, DirectionalLight
from panda3d.core import PandaNode, NodePath, TextNode
from panda3d.core import Vec3, Vec4, BitMask32, Mat3, Quat
from panda3d.ode import OdeWorld, OdeSimpleSpace, OdeJointGroup
from panda3d.ode import OdeBody, OdeMass, OdeCappedCylinderGeom, OdeRayGeom
from panda3d.ode import OdeTriMeshData, OdeTriMeshGeom
from direct.gui.OnscreenText import OnscreenText
from direct.actor.Actor import Actor
from direct.showbase.DirectObject import DirectObject
import sys
from OdeHelper import wireGeom

SPEED = 0.5

# Function to put instructions on the screen.
return OnscreenText(text=msg, style=1, fg=(1,1,1,1),
pos=(-1.3, pos), align=TextNode.ALeft, scale = .05)

# Function to put title on the screen.
return OnscreenText(text=text, style=1, fg=(1,1,1,1),
pos=(1.3,-0.95), align=TextNode.ARight, scale = .07)

class World(DirectObject):

def __init__(self):

self.keyMap = {"left":0, "right":0, "forward":0, "cam-left":0, "cam-right":0, "reset":0}
base.win.setClearColor(Vec4(0,0,0,1))

# Post the instructions

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

# Set up the environment
#
# This environment model contains collision meshes.  If you look
# in the egg file, you will see the following:
#
#    <Collide> { Polyset keep descend }
#
# This tag causes the following mesh to be converted to a collision
# mesh -- a mesh which is optimized for collision, not rendering.
# It also keeps the original mesh, so there are now two copies ---
# one optimized for rendering, one for collisions.

self.environ.reparentTo(render)
self.environ.setPos(0,0,0)

# Create the main character, Ralph

self.ralphStartPos = self.environ.find("**/start_point").getPos()
self.ralphStartPos.setZ(1)
self.ralph = Actor("models/ralph",
{"run":"models/ralph-run",
"walk":"models/ralph-walk"})
self.ralph.reparentTo(render)
self.ralph.setScale(.2)
self.ralph.setPos(self.ralphStartPos)

# Create a floater object.  We use the "floater" as a temporary
# variable in a variety of calculations.

self.floater = NodePath(PandaNode("floater"))
self.floater.reparentTo(render)

# Accept the control keys for movement and rotation

self.accept("escape", sys.exit)
self.accept("arrow_left", self.setKey, ["left",1])
self.accept("arrow_right", self.setKey, ["right",1])
self.accept("arrow_up", self.setKey, ["forward",1])
self.accept("a", self.setKey, ["cam-left",1])
self.accept("s", self.setKey, ["cam-right",1])
self.accept("arrow_left-up", self.setKey, ["left",0])
self.accept("arrow_right-up", self.setKey, ["right",0])
self.accept("arrow_up-up", self.setKey, ["forward",0])
self.accept("a-up", self.setKey, ["cam-left",0])
self.accept("s-up", self.setKey, ["cam-right",0])

self.accept("r", self.setKey, ["reset", 1])
self.accept("r-up", self.setKey, ["reset", 0])

# Game state variables
self.isMoving = False

# Set up the camera

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

# Create the ODE physic world

# init the Worlds physics

self.odeWorld = OdeWorld()

# set normal (Earth) gravity

self.odeWorld.setGravity((0, 0, -9.81))

# initialise 1 Surfaces for the environment

self.odeWorld.initSurfaceTable(1)

# set the surface values

self.odeWorld.setSurfaceEntry(
#id1 id2 mu bounce bounce_vel erp cfm slip dampen
0, 0, 50, 0.0, 9.1, 0.9, 0.00001, 0.0, 0.002)

# set a space for collision detection

self.odeSpace = OdeSimpleSpace()

# and apply it to our physic world

self.odeSpace.setAutoCollideWorld(self.odeWorld)
self.contactgroup = OdeJointGroup()
self.odeSpace.setAutoCollideJointGroup(self.contactgroup)
self.odeSpace.setCollisionEvent("physCollision")

# Create an accumulator to track the time since the sim
# has been running

self.deltaTimeAccumulator = 0.0

# This stepSize makes the simulation run at 90 frames per second

self.stepSize = 1.0 / 90.0

# Create the collision geometry for the environment

self.collideModelTrimesh = OdeTriMeshData(self.environ, True)
self.collisionGeom = OdeTriMeshGeom(self.odeSpace, self.collideModelTrimesh)
self.odeSpace.setSurfaceType(self.collisionGeom, 0)

# Here we set up the collision geometry for Ralph. Therfor we will
# use a ray geom, which will be set from little below ralphs feet
# to its center for the ground collision. For the wall collision
# detection we will use a capsule with the height of Ralph.

ralphSize = 0.2
heightMultiplier = 2
footRayLength = 1

# setup the physical body of ralph. This will tell ODE how big
# Ralph is and how much density he has.

self.ralphPhysicBody = OdeBody(self.odeWorld)
ralphMass = OdeMass()
ralphMass.setCapsule(998.1, 1, ralphSize, ralphSize * heightMultiplier)
self.ralphPhysicBody.setMass(ralphMass)
self.ralphPhysicBody.setPosition(self.ralph.getPos(render))
self.ralphPhysicBody.setQuaternion(self.ralph.getQuat(render))

# setup the body capsule. This capsule represents the body
# of Ralph in the ode physics world

self.bodyGeom = OdeCappedCylinderGeom(self.odeSpace,
ralphSize,
ralphSize * heightMultiplier)
pos = Vec3(self.ralph.getPos(render))
pos.setZ(pos.getZ() - 3)
self.bodyGeom.setPosition(pos)
self.bodyGeom.setBody(self.ralphPhysicBody)

# now setup the foot ray, which reach from little below ralphs feet
# upwards to his center

self.footGeom = OdeRayGeom(self.odeSpace, footRayLength)
pos = Vec3(self.ralph.getPos(render))
pos.setZ(pos.getZ() - 1)
direction = Vec3(0, 0, -1)
self.footGeom.set(pos, direction)
self.footGeom.setBody(self.ralphPhysicBody)

# now setup a body and collision geometry for the camera

self.camPhysicBody = OdeBody(self.odeWorld)
camMass = OdeMass()
camMass.setZero()
self.camPhysicBody.setMass(camMass)
self.camPhysicBody.setPosition(base.camera.getPos(render))
self.camPhysicBody.setQuaternion(base.camera.getQuat(render))

self.camRayGeom = OdeRayGeom(self.odeSpace, 2)
pos = Vec3(base.camera.getPos(render))
pos.setZ(pos.getZ() - 0.5)
direction = Vec3(0, 0, 1)
self.camRayGeom.set(pos, direction)
self.camRayGeom.setBody(self.camPhysicBody)

# setup some debug geometry for the body capsule
self.capsule = wireGeom().generate(
'capsule',
length=ralphSize * heightMultiplier)
self.capsule.setPos(self.bodyGeom.getPosition())
self.capsule.setHpr(0, 0, 0)
self.capsule.reparentTo(render)

# setup some debug geometry for the foot ray
self.ray = wireGeom().generate('ray', length=footRayLength)
self.ray.setPos(self.footGeom.getPosition())
self.ray.setHpr(0, 0, 0)
self.ray.reparentTo(render)

# Create some lighting
ambientLight = AmbientLight("ambientLight")
ambientLight.setColor(Vec4(.3, .3, .3, 1))
directionalLight = DirectionalLight("directionalLight")
directionalLight.setDirection(Vec3(-5, -5, -5))
directionalLight.setColor(Vec4(1, 1, 1, 1))
directionalLight.setSpecularColor(Vec4(1, 1, 1, 1))
render.setLight(render.attachNewNode(ambientLight))
render.setLight(render.attachNewNode(directionalLight))

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

'''This task will handle the actualisation of
the physic calculations each frame'''

dt = globalClock.getDt()

# Setup the contact joints
self.odeSpace.autoCollide()

self.deltaTimeAccumulator += dt
while self.deltaTimeAccumulator > self.stepSize:

# Remove a stepSize from the accumulator until
# the accumulated time is less than the stepsize

self.deltaTimeAccumulator -= self.stepSize

# Step the simulation

self.odeWorld.quickStep(self.stepSize)

# Clear the contact joints

self.contactgroup.empty()

# Accepts arrow keys to move either the player or the menu cursor,
# Also deals with grid checking and collision detection

# If the camera-left key is pressed, move camera left.
# If the camera-right key is pressed, move camera right.

base.camera.lookAt(self.ralph)
if (self.keyMap["cam-left"]!=0):
base.camera.setX(base.camera, -20 * globalClock.getDt())
if (self.keyMap["cam-right"]!=0):
base.camera.setX(base.camera, +20 * globalClock.getDt())

# actualise the actor model and quaternion with the pos
# and quat of the physic body

pos = self.ralphPhysicBody.getPosition()
pos.setZ(pos.getZ() - 0.5)
self.ralph.setPosQuat(
render,
pos,
Quat(self.ralphPhysicBody.getQuaternion()))

# keep the foot ray below ralph, so set it's offset at the
# size of the rays length

offFoot = Vec3(0, 0, -0.5)
self.footGeom.setOffsetPosition(offFoot)

# set the rotation of ralph to the physic body, so it won't rotate
# ralph back to 0.

charRot = -self.ralph.getH()
rotMat = Mat3.rotateMatNormaxis(charRot, Vec3.up())
self.ralphPhysicBody.setRotation(rotMat)

# set the angular Velocity, so ralph is not
# rotating in a direction we don't want him to rotate
self.ralphPhysicBody.setAngularVel(0, 0, 0)

# If a move-key is pressed, move ralph in the specified direction.

if (self.keyMap["left"]!=0):
turnLeft = -(self.ralph.getH() - globalClock.getDt() * -300)
rotMat = Mat3.rotateMatNormaxis(turnLeft, Vec3.up())
self.ralphPhysicBody.setRotation(rotMat)
if (self.keyMap["right"]!=0):
turnRight = -(self.ralph.getH() - globalClock.getDt() * 300)
rotMat = Mat3.rotateMatNormaxis(turnRight, Vec3.up())
self.ralphPhysicBody.setRotation(rotMat)
if (self.keyMap["forward"]!=0):

# If ralph is moving, loop the 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):
if self.isMoving is False:
#self.ralph.loop("run")
self.isMoving = True
else:
if self.isMoving:
self.ralph.stop()
self.ralph.pose("walk",5)
self.isMoving = False

# reset ralphs position to his startposition if the reset key is pressed
if self.keyMap["reset"]!=0:
self.ralphPhysicBody.setPosition(self.ralphStartPos)

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

camvec = self.ralph.getPos() - base.camera.getPos()
camvec.setZ(0)
camdist = camvec.length()
camvec.normalize()
if (camdist > 10.0):
base.camera.setPos(base.camera.getPos() + camvec*(camdist-10))
camdist = 10.0
if (camdist < 5.0):
base.camera.setPos(base.camera.getPos() - camvec*(5-camdist))
camdist = 5.0

# Keep the camera at one foot above the terrain,
# or two feet above ralph, whichever is greater.

if (base.camera.getZ() < self.ralph.getZ() + 2.0):
base.camera.setZ(self.ralph.getZ() + 2.0)

# The camera should look in ralph's direction,
# but it should also try to stay horizontal, so look at
# a floater which hovers above ralph's head.

self.floater.setPos(self.ralph.getPos())
self.floater.setZ(self.ralph.getZ() + 2.0)
base.camera.lookAt(self.floater)

# actualise the debug models for the ode geoms
self.ray.setPosQuat(
render,
self.footGeom.getPosition(),
self.footGeom.getQuaternion())
self.capsule.setPosQuat(
render,
self.bodyGeom.getPosition(),
self.bodyGeom.getQuaternion())

w = World()
run()``````

And here the OdeHelper.py for the debug help of ODE geometry:

``````#
# Code by FenrirWolf
# https://discourse.panda3d.org/t/representing-ode-geoms-collisions-as-visible-geom/6201/1
#

from pandac.PandaModules import Point3

from pandac.PandaModules import GeomVertexFormat, GeomVertexData, GeomVertexWriter
from pandac.PandaModules import Geom, GeomNode, NodePath, GeomLinestrips

import math, random

"""
Note that wireprims are wire-like representations of geom, in the same manner as Ogre's debug mode.  I find this the most useful way to represent
ODE geom structures visually, as you can clearly see the orientation versus a more generic wireframe mesh.

These wireprims are rendered as linestrips.  Therefore, only vertices are required and texturing is not supported.  You can use standard render attribute changes such
as setColor in order to change the line's color.  By default it is green.

This class merely returns a NodePath to a GeomNode that is a representation of what is requested.  You can use this outside of ODE geom visualizations, obviously.

Supported are sphere, box, cylinder, capsule (aka capped cylinder), ray, and plane

to use:

boxNodepath = wireGeom().generate ('box', extents=(1, 1, 1))
cylinderNodepath = wireGeom().generate ('cylinder', radius=1.0, length=3.0)
rayNodepath = wireGeom().generate ('ray', length=3.0)
planeNodepath = wireGeom().generate ('plane')

"""
class wireGeom:
def __init__(self):
# GeomNode to hold our individual geoms
self.gnode = GeomNode ('wirePrim')

# How many times to subdivide our spheres/cylinders resulting vertices.  Keep low
# because this is supposed to be an approximate representation
self.subdiv = 12

def line(self, start, end):

# since we're doing line segments, just vertices in our geom
format = GeomVertexFormat.getV3()

# build our data structure and get a handle to the vertex column
vdata = GeomVertexData('', format, Geom.UHStatic)
vertices = GeomVertexWriter(vdata, 'vertex')

# build a linestrip vertex buffer
lines = GeomLinestrips(Geom.UHStatic)

lines.closePrimitive()

geom = Geom(vdata)
# Add our primitive to the geomnode

# since we're doing line segments, just vertices in our geom
format = GeomVertexFormat.getV3()

# build our data structure and get a handle to the vertex column
vdata = GeomVertexData('', format, Geom.UHStatic)
vertices = GeomVertexWriter(vdata, 'vertex')

# build a linestrip vertex buffer
lines = GeomLinestrips (Geom.UHStatic)

for i in range(0, self.subdiv):
angle = i / float(self.subdiv) * 2.0 * math.pi
ca = math.cos(angle)
sa = math.sin(angle)
if axis == "x":
if axis == "y":
if axis == "z":

for i in range(1, self.subdiv):

lines.closePrimitive()

geom = Geom(vdata)
# Add our primitive to the geomnode

# since we're doing line segments, just vertices in our geom
format = GeomVertexFormat.getV3()

# build our data structure and get a handle to the vertex column
vdata = GeomVertexData('', format, Geom.UHStatic)
vertices = GeomVertexWriter(vdata, 'vertex')

# build a linestrip vertex buffer
lines = GeomLinestrips(Geom.UHStatic)

# draw upper dome
for i in range (0, self.subdiv / 2 + 1):
angle = i / float(self.subdiv) * 2.0 * math.pi
ca = math.cos (angle)
sa = math.sin (angle)
if axis == "x":
if axis == "y":

# draw lower dome
for i in range(0, self.subdiv / 2 + 1):
angle = -math.pi + i / float(self.subdiv) * 2.0 * math.pi
ca = math.cos(angle)
sa = math.sin(angle)
if axis == "x":
if axis == "y":

for i in range(1, self.subdiv + 1):

lines.closePrimitive()

geom = Geom(vdata)
# Add our primitive to the geomnode

def rect(self, width, height, axis):

# since we're doing line segments, just vertices in our geom
format = GeomVertexFormat.getV3()

# build our data structure and get a handle to the vertex column
vdata = GeomVertexData('', format, Geom.UHStatic)
vertices = GeomVertexWriter(vdata, 'vertex')

# build a linestrip vertex buffer
lines = GeomLinestrips(Geom.UHStatic)

# draw a box
if axis == "x":
if axis == "y":
if axis == "z":

for i in range(1, 3):

lines.closePrimitive()

geom = Geom(vdata)
# Add our primitive to the geomnode

def generate(self, type, radius=1.0, length=1.0, extents=Point3(1, 1, 1), R=-1, G=-1, B=-1):
if R==-1:
R=random.uniform(0,1)
if G==-1:
G=random.uniform(0,1)
if B==-1:
B=random.uniform(0,1)

if type == 'sphere':
# generate a simple sphere

if type == 'capsule':
# generate a simple capsule

if type == 'box':
# generate a simple box
self.rect(extents/2, extents/2, "x")
self.rect(extents/2, extents/2, "y")
self.rect(extents/2, extents/2, "z")

if type == 'cylinder':
# generate a simple cylinder

if type == 'ray':
# generate a ray
self.circle(length / 10, "x", 0)
self.circle(length / 10, "z", 0)
self.line((0, 0, 0), (0, 0, length))
self.line((0, 0, length), (0, -length/10, length*0.9))
self.line((0, 0, length), (0, length/10, length*0.9))

if type == 'plane':
# generate a plane
length = 3.0
self.rect(1.0, 1.0, "z")
self.line((0, 0, 0), (0, 0, length))
self.line((0, 0, length), (0, -length/10, length*0.9))
self.line((0, 0, length), (0, length/10, length*0.9))

# rename ourselves to wirePrimBox, etc.
name = self.gnode.getName()
self.gnode.setName(name + type.capitalize())

NP = NodePath(self.gnode)  # Finally, make a nodepath to our geom
NP.setColor(R, G, B)   # Set default color

return NP``````
1 Like

First and foremost, you should never try to build a dynamic character controller (DCC), unless you know the physics engine very well and have a very good reason (game design) for it. A DCC is a character controller that is pushed around by forces and has a dynamic body, like the one you tried to build.

A KCC (kinematic character controller), on the other hand, is a controller that is built with collision detection and custom code only, without any physics being involved.

It’s a common misconception that building a DCC is easier than build a KCC, because it looks like KCC is gonna require a lot of code, while with DCC forces, friction and the rest of the physics engine will do everything for you. This is not true. A character (like a human or animal) is not an inert body and it cannot be handled as such.

This is why, when building a DCC, you spend most of your time dealing with problems like the ones you described, which originate in trying to use the physics engine where it doesn’t apply. Thus, to regain control and simulate conscious behavior, you need to write lots and lots of code which only purpose is to prevent the physics engine from affecting your character. Which obviously defeats the purpose of using dynamics in the first place.

This is why PhysX uses a KCC.

KCC is a much better choice, but don’t expect to write one for Panda’s ODE using AutoCollide – it can’t be done for too many reasons to list.

[ODE Middleware) – this is my ODE KCC, if you wanted to take a look at how it’s done.

Most importantly, tho, it’s unreasonable to get into ODE now that Bullet is integrated. Bullet is a much better physics engine for games. It was built with games in mind, it can collide everything with everything (unlike ODE, which is very limited in this regard), it has continuous collision detection, it’s generally more robust and so on.

The only limitation of Bullet is that the character controller that comes with it is… well, not very good. That’s why I’ve ported the one I wrote for ODE to Bullet and I will be releasing it later this week.

OK, thank you for this information coppertop. So then I will take a look at the Bullet engine instead of ODE. Do you maybe know some good sources to read more about those KCCs and Bullet? And could you please send me a message when you release your rewritten character controller as I really like to take a look at it.