How to use Gravity ODE

I’m quite new to Panda3D and was looking through the manual on how to apply gravity to a character when they jump. However the basic code line

myWorld.setGravity(0, 0, -9.81)

Is coming up with an error saying: descriptor ‘setGravity’ requires a ‘panda3d.ode.OdeWorld’ object but received a ‘int’. What am I supposed to put before the coords?

Well, what is “myWorld”–how is it initialised?

It’s a class I assumed, so in my code I’ve changed it to MyGame and it’s initialised by ‘class MyGame(ShowBase):’

Hi, welcome to the community!

The code sample in question is assuming you have already created an OdeWorld object, and myWorld is meant to refer to the instance of that class.

Thank you ~ The OdeWorld object is just my class right?

No, the OdeWorld is an object you have to create as part of the ODE simulation. This page explains it:
https://docs.panda3d.org/1.10/python/programming/physics/ode/worlds-bodies-masses

Okay, so the OdeWorld is a seperate class? I haven’t been programming using OOP so far so I am not sure if I already have some of that in my main class.

This is my code as of current.

from direct.showbase.ShowBaseGlobal import globalClock
from panda3d.core import loadPrcFileData
from direct.showbase.ShowBase import ShowBase
from direct.actor.Actor import Actor
from panda3d.core import DirectionalLight
from panda3d.core import CollisionTraverser
from panda3d.core import CollisionHandlerPusher
from panda3d.core import CollisionSphere, CollisionNode, CollisionCapsule, CollisionPlane
from panda3d.core import Vec3, Point3
from direct.gui.DirectGui import *
from panda3d.ai import *
from panda3d.ode import OdeBody, OdeMass, OdeWorld

configVars = """
window-title My Game
win-size 880 500
show-frame-rate-meter True
cursor-hidden False
show-scene-graph-analyzer-meter True
sync-video 1
"""
loadPrcFileData("", configVars)

keyMap = {
    "w": False,
    "s": False,
    "a": False,
    "d": False,
    "arrowleft": False,
    "arrowright": False,
    "p": False,
    "space": False
}


# callback function to update the keymap
def updateKeyMap(key, state):
    keyMap[key] = state


MyGame = OdeWorld
MyGame.setGravity(0, 0, -9.81)

class MyGame(ShowBase):
    def __init__(self):
        super().__init__()
        # Only disables the default camera control
        self.disableMouse()
        self.render.setShaderAuto()

        # setting some basic constants
        self.x = 0
        self.z = 0
        self.y = 0
        self.speed = 20

        # creating a new nodepath for the light, rendering it and positioning it
        self.mainlight = DirectionalLight("mainlight")
        self.mainlight.setColor((10, 10, 10, 10))
        self.mainLightNodePath = self.render.attachNewNode(self.mainlight)
        # Turn it around by 45 degrees, and tilt it down by 45 degrees
        self.mainLightNodePath.setHpr(-55, -65, 30)
        self.render.setLight(self.mainLightNodePath)
        self.set_background_color(0.6, 0.6, 1.0, 1.0)

        # loads the wolf model, sets it's position and scales it down.
        self.wolf = Actor("my-models/wolf")
        self.wolf.setScale(0.8, 0.8, 0.8)
        self.wolf.setPos(0, 0, 0)
        self.wolf.setHpr(0, 0, 0)
        self.wolf.reparentTo(self.render)

        self.player = Actor("my-models/sphere")
        self.player.setScale(0.8, 0.8, 0.8)
        self.player.setPos(0, -30, -1.2)
        self.player.setHpr(0, 0, 0)
        self.player.reparentTo(self.render)

        # set camera back on the y axis
        #self.cam.setPos(0, -30, 0)
        #self.cam.setHpr(0, -3, 0)

        # the keyboard controls for when the buttons are pressed and released
        self.accept("a", updateKeyMap, ["a", True])
        self.accept("a-up", updateKeyMap, ["a", False])

        self.accept("d", updateKeyMap, ["d", True])
        self.accept("d-up", updateKeyMap, ["d", False])

        self.accept("w", updateKeyMap, ["w", True])
        self.accept("w-up", updateKeyMap, ["w", False])

        self.accept("s", updateKeyMap, ["s", True])
        self.accept("s-up", updateKeyMap, ["s", False])

        self.accept("p", updateKeyMap, ["p", True])
        self.accept("p-up", updateKeyMap, ["p", True])

        self.accept("arrowright", updateKeyMap, ["arrowright", True])
        self.accept("arrowright-up", updateKeyMap, ["arrowright", False])

        self.accept("arrowleft", updateKeyMap, ["arrowleft", True])
        self.accept("arrowleft-up", updateKeyMap, ["arrowleft", False])

        self.accept("space", updateKeyMap, ["space", True])
        self.accept("space-up", updateKeyMap, ["space", False])

        # initialising the basic collisions
        self.cTrav = CollisionTraverser()
        self.pusher = CollisionHandlerPusher()

        # creating a node for an active collision for the player
        playerNode = CollisionNode("player")
        # Add a collision-sphere centred on (0, 0, 0), and with a radius of 0.3
        playerNode.addSolid(CollisionCapsule(0, 0, 0, 0, 0, 40, 3.5))
        player = self.cam.attachNewNode(playerNode)
        self.pusher.addCollider(player, self.player)
        self.cTrav.addCollider(player, self.pusher)
        player.show()

        # creating a node for an active collision for the wolf
        wolfNode = CollisionNode("wolf")
        # Add a collision-sphere centred on (0, 0, 0), and with a radius of 0.3
        wolfNode.addSolid(CollisionCapsule(0, 0, 0, 0, 0, 40, 3.5))
        wolf = self.wolf.attachNewNode(wolfNode)
        self.pusher.addCollider(wolf, self.wolf)
        self.cTrav.addCollider(wolf, self.pusher)

        # if this is unhashed, it shows the collision boundaries around each active collision
        # wolf.show()

        # The GUI
        # The title menu is initialised with the size of its box
        self.titleMenu = DirectDialog(frameSize=(-0.7, 0.7, -0.7, 0.7),
                                      fadeScreen=0.4,
                                      relief=DGG.GROOVE)
        # The game name is given properties
        title = DirectLabel(text="My Game",
                            scale=0.25,
                            pos=(0, 0, 0.4),
                            parent=self.titleMenu)
        # A button is initialised for starting the game, if pressed it will run the method 'startGame'
        btn = DirectButton(text="Start Game",
                           # command=self.startGame,
                           pos=(0, 0, 0),
                           parent=self.titleMenu,
                           scale=0.1)
        # A button is initialised for quitting the game, if pressed it will run the method for 'quit'
        btn = DirectButton(text="Quit",
                           # command=self.quit,
                           pos=(0, 0, -0.3),
                           parent=self.titleMenu,
                           scale=0.1)

        # The paused menu is initialised with the size of its box
        self.paused = DirectDialog(frameSize=(-0.9, 0.9, -0.7, 0.7),
                                   fadeScreen=0.4,
                                   relief=DGG.GROOVE)
        # The title of the paused screen is given its properties
        title = DirectLabel(text="Paused",
                            scale=0.2,
                            pos=(0, 0, 0.4),
                            parent=self.paused)
        # A button is initialised for continuing the game, if pressed it will run the method 'continue'
        btn = DirectButton(text="Continue",
                           # command=self.continue,
                           pos=(0, 0, 0.1),
                           parent=self.paused,
                           scale=0.1)
        # A button is initialised for quitting the game, if pressed it will run the method 'quit'
        btn = DirectButton(text="Quit",
                           # command=self.quit,
                           pos=(0, 0, -0.15),
                           parent=self.paused,
                           scale=0.1)
        # A title for inputting the volume percentage
        title = DirectLabel(text="Volume (0-100)%",
                            scale=0.07,
                            pos=(-0.2, 0, -0.4),
                            parent=self.paused)
        # A form is initialised to input the percentage audio volume
        entry = DirectEntry(text="",
                            scale=0.07,
                            numLines=1,
                            parent=self.paused,
                            pos=(0.15, 0, -0.4),
                            frameColor=(0, 0, 0, 0))

        # The game over menu is initialised with the size of its box
        self.gameOverScreen = DirectDialog(frameSize=(-0.7, 0.7, -0.7, 0.7),
                                           fadeScreen=0.4,
                                           relief=DGG.GROOVE)
        # The title is given its properties
        label = DirectLabel(text="Game Over!",
                            parent=self.gameOverScreen,
                            scale=0.2,
                            pos=(0, 0, 0.3))
        # ?
        self.finalScoreLabel = DirectLabel(text="",
                                           parent=self.gameOverScreen,
                                           scale=0.09,
                                           pos=(0, 0, 0))
        # A button is initialised to start the game, if pressed it will run the method 'startGame'
        btn = DirectButton(text="Play again!",
                           # command=self.startGame,
                           pos=(-0.3, 0, -0.3),
                           parent=self.gameOverScreen,
                           scale=0.09)
        # A button is initialised to quit the game, if pressed it will run the method 'quit'
        btn = DirectButton(text="Quit",
                           # command=self.quit,
                           pos=(0.3, 0, -0.31),
                           parent=self.gameOverScreen,
                           scale=0.1)

        self.AIworld = AIWorld(self.render)

        self.AIchar = AICharacter("pursuer", self.wolf, 100, 0.05, 5)
        self.AIworld.addAiChar(self.AIchar)
        self.AIbehaviors = self.AIchar.getAiBehaviors()
        self.AIbehaviors.pursue(self.player)

        # AI World update
        self.taskMgr.add(self.AIUpdate, "AIUpdate")

        self.taskMgr.add(self.update, "update")

        # to update the AIWorld

    def AIUpdate(self, task):
        self.AIworld.update()
        return task.cont

    def update(self, task):
        dt = globalClock.getDt()
        print(dt)
        startPos = self.player.getPos()
        posCam = self.cam.getPos()
        self.angle = 0
        # controlling the camera using key bindings

        pos = self.cam.getPos()
        if keyMap["a"]:
            startPos.x -= self.speed * dt
            posCam.x -= self.speed * dt
            self.player.setHpr(-90, 0, 0)

        if keyMap["d"]:
            startPos.x += self.speed * dt
            posCam.x += self.speed * dt
            self.player.setHpr(90, 0, 0)

        if keyMap["w"]:
            startPos.y += self.speed * dt
            posCam.y += self.speed * dt
            self.player.setHpr(0, 0, 0)

        if keyMap["s"]:
            startPos.y -= self.speed * dt
            posCam.y -= self.speed * dt
            self.player.setHpr(180, 0, 0)

        if keyMap["space"]:
            startPos.z += self.speed * dt
            posCam.z += self.speed * dt
            self.player.setHpr(0, 0, 0)

        self.player.setPos(startPos)
        self.cam.setPos(posCam)

        # prints the mouse pointer coords every second
        md1 = self.win.getPointer(0)
        print(md1.getX(), md1.getY())
        mouseX = md1.getX() * dt
        mouseY = md1.getY() * dt

        self.player.setHpr(mouseX, 0, 0)

        # md = self.win.getPointer(0)
        # heading = 0
        # pitch = 0
        # x = md.getX()
        # y = md.getY()

        # heading += y
        # pitch += x

        # self.cam.setHpr(heading, pitch, 0)

        # Hides all of the screens to begin with
        self.paused.hide()
        self.titleMenu.hide()
        self.gameOverScreen.hide()

        # If the user presses space key, it will bring the paused interface up
        if keyMap["p"]:
            self.paused.show()

        self.gameOverScreen.hide()
        self.finalScoreLabel["text"] = "Achievements: "
        self.finalScoreLabel.setText()

        return task.cont


game = MyGame()
game.run()

Hmm… It’s a problem, I think, to have the same name (with the same capitalisation) for both the variable “MyWorld” (the OdeWorld) and the class “MyGame” (which inherits from ShowBase). If I’m not much mistaken, since the latter is located after the former, it will replace the former.

Also, when you create your OdeWorld, you’re not actually initialising it: you’re just assigning a reference to the class itself to the variable “MyGame”. Instead, you should have a pair of brackets after “OdeWorld”, thus calling its initialisation function which returns an instance of the class.

Adding the brackets stopped the error from showing and ran the program smoothly. I’ve also changed the OdeWorld variable name to just ‘World’ as you said. Although, how do I link that to the main class? I don’t currently have a collision plane beneath my player model so it should just fall once I have it working?

1 Like

Well done. :slight_smile:

That depends on what you mean by “link it to the main class”.

If you simply want it to be a variable held by the main class, then it should be as simple as moving the current variable and its initialisation into the __init__ method of the main class, and adding “self.” before it. Essentially, treat it just as you did the “speed” variable of the main class, for example.

If you mean something else, could you clarify, please?

As to the floor, I’m afraid that I don’t use ODE myself–I use either Bullet or Panda’s internal collision system–so I’m not in a position to help you there. :/

The end goal is to have some gravity so that when I press the space and it applies a force vertically upwards to the player, the gravity will bring it back down.

The variable and it’s initialisation are now within the main class. Although I am unsure on how to apply apply this to the world. Whether I need to write some code to update it within the update method. I have since added this:

self.World = OdeWorld()
self.World.setGravity(0, 0, -9.81)
self.body = OdeBody(self.World)
self.M = OdeMass()
self.M.setSphere(7874, 1.0)
self.body.setMass(self.M)
self.body.setPosition(self.player.getPos(self.render))
self.body.setQuaternion(self.player.getQuat(self.render))

And that’s perfectly fine, you’ve been an amazing help so far, I’m thankful for this forum. As you are familiar with the other two, are you aware of any easier way to accomplish my end goal using Bullet or Panda’s own collision system?

Oh yes–it’s fairly easy with both!

I haven’t used Panda’s physics system, but with the collision system I’d likely just apply gravity “manually”–that is, I’d have a task that updates a “velocity” variable according to an acceleration of 9.8 units-per-second-per-second downwards, and then update the object’s position according to that velocity-variable.

The collision system updates automatically (when properly set up), meaning that there would be little else to do in the most basic case, I think.

In Bullet, you set up a “BulletWorld” (much as in your ODE example, above), set the force of gravity, and call a Bullet update-function each frame. I think that there are details in the manual.

That said, whether using Panda’s internal collision system or Bullet, I’d likely use a ray-cast to detect the floor, myself–for one thing, I suspect that it might be easier. However, it can be done with normal collision, I daresay.

I’m reading up on the Bullet system now and have written some code for it.

self.world = BulletWorld()
self.world.setGravity(Vec3(0, 0, -9.81))
self.shape = BulletPlaneShape(Vec3(0, 0, 1), 1)
planenode = BulletRigidBodyNode('Ground')
planenode.addShape(self.shape)
np = self.render.attachNewNode(planenode)
np.setPos(0, 0, -2)
self.world.attachRigidBody(planenode)

With the doPhysics() in the update method

self.world.doPhysics(dt)

I’ve also made it so that my code outputs the coordinates of my player model every time it updates, however, the Z coordinate is not changing. Have I missed something?

1 Like

Hmm… I don’t see the code for your player-model here. Do I take it correctly that you’re using a BulletRigidBody for it (i.e. not an OdeBody)? If so, have you set its mass (which makes it an “active” physics body, if I recall correctly)?

I do not recommend using a physics engine if all you need is to handle gravity. That pulls in a lot of unnecessary complexity. The built-in collision system will suffice for this.

1 Like

That’s true: Unless you have actual physics stuff to do–more than just “things should fall downwards, and not go through the floor and walls”–then a physics system might be not only overkill but over-complicated.

That’s okay, I do plan to take the physics further later on but for now it’s probably best if I take some time to understand the basics. Thank you although.

I have written this much

        self.world = BulletWorld()
        self.world.setGravity(Vec3(0, 0, -9.81))

        self.shape = BulletPlaneShape(Vec3(0, 0, 1), 1)
        planenode = BulletRigidBodyNode('Ground')
        planenode.addShape(self.shape)
        np = self.render.attachNewNode(planenode)
        np.setPos(0, 0, 0)
        self.world.attachRigidBody(planenode)

        shape = BulletBoxShape(Vec3(0.5, 0.5, 0.5))
        node = BulletRigidBodyNode('Box')
        node.setMass(1.0)
        node.addShape(shape)
        np = self.render.attachNewNode(node)
        np.setPos(0, 0, 2)
      
        self.world.attachRigidBody(node)
        self.player = self.loader.loadModel('my-models/sphere')
        self.player.flattenLight()
        self.player.reparentTo(np)
        self.player.setScale(0.8, 0.8, 0.8)
        self.player.setPos(0, -30, -1.2)
        self.player.setHpr(0, 0, 0)

However, once again, the player’s coords for Z aren’t changing.

When you say that the player’s z-coordinate isn’t changing, do you mean that you’re calling “self.player.getPos()” or “self.player.getZ()”?

If so, then that might be the problem: You see, it’s not the “self.player” node that’s moving; it’s the node that holds your BulletRigidBodyNode, underneath which “self.player” is attached.

Now, naturally this means that “self.player” should move with the BulletRigidBodyNode, and it may well be doing so. However, a call to “getPos()” gives the node’s position relative to its parent-node. (And similar is true of “getHpr()”, or “getY()”, etc., naturally.) And since “self.player” isn’t moving relative to the BulletRigidBodyNode, its position relative to its parent isn’t changing.

You can work around this be specifying a node relative to which “getPos()” (or “getHpr()”, etc.) should report its values. So, if you were to call “self.player.getPos(render)” you would get the position of “self.player” relative to “render”–which might well be what you want.

Of course, you can also deal with this by calling “getPos()” on the BulletRigidBodyNode’s NodePath, instead of on “self.player”.

That said, if you’re already passing in “render” to your call to “getPos()”, or if you’re already calling “getPos()” on the BulletRigidBodyNode, or if you’re seeing your object sitting still in mid-space, then the problem might be elsewhere…

Ah! I see the problem. What you are doing is that you are making MyGame with:

MyGame = OdeWorld

Not:

MyGame = OdeWorld()

I am not saying that it is wrong.What happens when you are using the former is that, instead of making the MyGame variable an instance of the class OdeWorld, you are making the MyGame class an alias of OdeWorld.

So if you had done the latter, you could easily do:

MyGame.setGravity(0, 0, -9.81) # or MyGame.setGravity(Vec3(0, 0, -9.81))

Where the method setGravity’s code is:

# It actually is C++, but I am giving a Python visualization
# In class 'OdeWorld'
def setGravity(self, xGravity, yGravity, zGravity): # or def setGravity(self, gravityVec3):
    # The code for setGravity
    pass

And because using the latter method makes an instance, Python parses the code:

MyGame.setGravity(0, 0, -9.81)

as

OdeWorld.setGravity(MyGame, 0, 0, -9.81) # MyGame is parameter 'self'

But because instead of making an instance, you are making a class. So Python doesn’t know what the parameter ‘self’ should be. That is why we manually have to give the first parameter doing:

MyGame.setGravity(MyGame(), 0, 0, -9.81) # You can also use OdeWorld() for self, as long as an instance is created.

Finally, the main code is either:

MyGame = OdeWorld()
MyGame.setGravity(0, 0, -9.81)

or

MyGame = OdeWorld
MyGame.setGravity(MyGame(), # or OdeWorld()
                     0, 0, -9.81)

You’re largely correct in what you say. One minor correction, if I may:

In this case, I think that “setGravity” is expecting an instance of class “OdeWorld”. As you pointed out, “MyGame” is a reference to the class itself, not an instance of the class, and thus I’m not sure that just passing in “MyGame” would work.

You could do it by creating an instance and passing that in I do think, however.