Panda3D - Actor

Introduction

Awesome! You have a world, so now let’s give it an inhabitant.

Task

To make something move in a video game, you have to think about how games actually work.

Much like a flip book, a game is just a series of still pictures drawn on your screen very quickly — often 60 times every single second. Each picture is called a frame.

To make our panda spin, we can’t just tell the computer “spin the panda.” We have to give the game a repeating rule: “Every time you draw a frame, turn the panda a tiny bit more.”

In Panda3D, this repeating rule is called a Task.

panda.py
from direct.showbase.ShowBase import ShowBase
app = ShowBase()
# --- THE SCENERY ---
scene = app.loader.loadModel("models/environment")
scene.reparentTo(app.render)
scene.setScale(0.25)
scene.setPos(0, 60, 0)
# --- THE PANDA ---
# 1. Load the panda model and put it on the stage
panda = app.loader.loadModel("models/panda-model")
panda.reparentTo(app.render)
# 2. Shrink it down (the built-in panda is HUGE!)
panda.setScale(0.005)
panda.setPos(0, 20, 0)
# --- THE ANIMATION ---
# 3. Create a rule for spinning
def spin_panda(task):
# Calculate a new angle based on the game's built-in stopwatch
angle = task.time * 50.0
# Set the panda's rotation
panda.setHpr(angle, 0, 0)
# Tell the engine to keep repeating this rule
return task.cont
# 4. Give the rule to the game's "Task Manager"
app.taskMgr.add(spin_panda, "SpinPandaTask")
app.run()

Breaking Down the Magic

Run that in your terminal and you should see a giant panda spinning happily in front of your scenery! Here is exactly what is happening in the new code:

  • def spin_panda(task): The word def is Python’s way of saying “I am defining a new rule.” We named this rule spin_panda.

  • angle = task.time * 50.0. Every Task has a built-in stopwatch (task.time) that tracks how many seconds the game has been running. By multiplying the time by 50, we create a number that constantly grows larger as the seconds tick by. We store that growing number in a container we named angle.

  • panda.setHpr(angle, 0, 0). This is how things rotate in 3D space. HPR stands for Heading, Pitch, and Roll:

    • Heading: Turning left and right (like shaking your head “no”).
    • Pitch: Tilting forward and backward (like nodding your head “yes”).
    • Roll: Tilting side-to-side (like doing a cartwheel).

    Because we put our constantly growing angle number into the Heading slot, the panda continuously turns left!

  • return task.cont. This translates to “Task Continue.” It tells the game engine, “Once you finish this rule, don’t stop. Do it again on the next frame.”

  • app.taskMgr.add(spin_panda, "SpinPandaTask"). Writing the rule isn’t enough; we have to hire someone to enforce it. The “Task Manager” is your backstage crew chief. This line hands the rule over to the crew chief so it actually starts running.

The Python class Actor is designed to hold an animatable model and a set of animations.

Actor

Let’s bring our panda to life!

Right now, the panda we loaded is just a static 3D model. You can think of a standard model like a statue—it looks nice, but its joints are frozen solid. To make the legs bend and the arms swing, game developers use a technique called “skeletal animation” or “rigging,” which puts an invisible skeleton inside the model.

In Panda3D, to use a model that has a moving skeleton, we can’t use the standard loadModel command anymore. Instead, we need a special blueprint called an Actor.

Since the Actor class inherits from NodePath, everything that can be done to a NodePath, such as reparentTo() and setPos(), etc., may also be done to an Actor.

In addition to the basic NodePath functionality, Actors have several additional methods to control animation. In order for Actors to animate, their pointer (variable) must be retained in memory.

https://docs.panda3d.org/1.10/python/programming/models-and-actors/actor-animations

Here is the updated code.

panda_walk.py
from direct.showbase.ShowBase import ShowBase
from direct.actor.Actor import Actor
app = ShowBase()
# --- THE SCENERY ---
scene = app.loader.loadModel("models/environment")
scene.reparentTo(app.render)
scene.setScale(0.25, 0.25, 0.25)
scene.setPos(0, 60, 0)
# --- THE PANDA ---
# 1. Load the panda as an "Actor" and give it its animation files
panda = Actor("models/panda-model",
{"walk": "models/panda-walk4"})
# 2. Put it on the stage and adjust size/position
panda.reparentTo(app.render)
panda.setScale(0.005)
panda.setPos(0, 20, 0)
# 3. Tell the panda to constantly repeat its walk animation
panda.loop("walk")
# --- THE SPINNING RULE ---
def spin_panda(task):
angle = task.time * 50.0
panda.setHpr(angle, 0, 0)
return task.cont
app.taskMgr.add(spin_panda, "SpinPandaTask")
app.run()

What Changed?

If you run this, you will now see the panda continuously walking in a circle!

Here is the breakdown of the new pieces:

  • from direct.actor.Actor import Actor. Just like we did with the ShowBase blueprint, we are going back into the Panda3D library to grab a new blueprint called Actor. This tells Python how to handle moving skeletons.

  • panda = Actor("models/panda-model", {"walk": "models/panda-walk4"}). This is the most important change. We are combining two different files to create our character:

    • "models/panda-model" is the physical body of the panda (the fur, the shape).
    • "models/panda-walk4" is a hidden file in Panda3D that contains just the math for how a panda’s skeleton should step and sway.
    • By pairing them up, we are saying: “Here is your body, and here is a movement called ‘walk’. Memorize this movement!”
  • panda.loop("walk"). This tells the Actor to play the “walk” movement we just taught it, and when it reaches the end of the animation, loop back to the beginning and do it again forever.

Because we left our spin_panda task in the code, the game is spinning the entire panda at the exact same time the panda is moving its legs.

Events

Taking control of a character is where a program truly starts to feel like a game.

To make this happen, we have to teach our game two fundamental programming concepts: Events (listening for you to press a key) and Logic (deciding what to do based on those key presses).

Here is the updated code.

Notice that we completely removed the old spin_panda task and replaced it with a new keyboard setup and a move_panda task.

panda_move.py
from direct.showbase.ShowBase import ShowBase
from direct.actor.Actor import Actor
app = ShowBase()
# --- THE SCENERY ---
scene = app.loader.loadModel("models/environment")
scene.reparentTo(app.render)
scene.setScale(0.25)
scene.setPos(0, 60, 0)
# --- THE PANDA ---
panda = Actor("models/panda-model", {"walk": "models/panda-walk4"})
panda.reparentTo(app.render)
panda.setScale(0.005)
panda.setPos(0, 20, 0)
panda.loop("walk")
# --- KEYBOARD CONTROLS ---
# 1. Create a "cheat sheet" to track which keys are currently held down
keys = {"turn_left": False, "turn_right": False, "walk_forward": False}
# 2. A rule to update our cheat sheet
def update_key(key_name, is_pressed):
keys[key_name] = is_pressed
# 3. Tell the game engine to listen for specific keyboard events
app.accept("arrow_left", update_key, ["turn_left", True])
app.accept("arrow_left-up", update_key, ["turn_left", False])
app.accept("arrow_right", update_key, ["turn_right", True])
app.accept("arrow_right-up", update_key, ["turn_right", False])
app.accept("arrow_up", update_key, ["walk_forward", True])
app.accept("arrow_up-up", update_key, ["walk_forward", False])
# --- THE MOVEMENT RULE ---
def move_panda(task):
# Turn left or right
if keys["turn_left"]:
panda.setH(panda.getH() + 2)
if keys["turn_right"]:
panda.setH(panda.getH() - 2)
# Walk forward
if keys["walk_forward"]:
# Move the panda forward relative to the direction it is facing
panda.setY(panda, -4)
return task.cont
app.taskMgr.add(move_panda, "MovePandaTask")
app.run()

Breaking Down the Magic

You can now use your up, left, and right arrow keys to walk the panda around the environment!

Here is how the new pieces work:

  • The “Cheat Sheet” (keys = {...}). Computers have terrible short-term memory unless you explicitly tell them to remember something. We created a checklist called a Dictionary. By default, we set turn_left, turn_right, and walk_forward to False (meaning the keys are not being pressed).

  • The Listeners (app.accept). These lines tell the game to keep an ear out for specific events.

    • When you press the left arrow ("arrow_left"), it updates the cheat sheet, turning turn_left to True.
    • When you let go of the left arrow ("arrow_left-up" — the -up means the key popped back up), it changes it back to False.
  • _The Logic (if statements). Inside our move_panda task, we use if statements. This is exactly how humans make decisions. The computer asks: “If 'turn_left' is True on the cheat sheet, what do I do?” It then reads the indented code underneath it and rotates the panda slightly.

  • Relative Movement (panda.setY(panda, -4)). Normally, setting a position in 3D space moves an object based on the “World” coordinates (North, South, East, West). But if the panda turns, we want it to walk in the direction it is facing, not the direction the world is facing.

    By putting panda inside the parentheses before the number -4, we are telling the engine: “Move the panda along its own personal Y-axis.” (Note: The built-in Panda3D model actually faces backwards down its Y-axis, which is why we have to use a negative number to move it forward!)

Camera

The camera is just another object in your 3D world. You can move it, rotate it, and attach it to other things exactly like we did with the panda.

To create a true “third-person” game, we want the camera to constantly hover behind our hero and follow them wherever they go. We can achieve this by updating the camera’s position every single frame right inside our move_panda task!

Here is the final, completed code.

panda_camera.py
from direct.showbase.ShowBase import ShowBase
from direct.actor.Actor import Actor
app = ShowBase()
# 1. Disable the engine's default mouse controls so we can control the camera manually
app.disableMouse()
# --- THE SCENERY ---
scene = app.loader.loadModel("models/environment")
scene.reparentTo(app.render)
scene.setScale(0.25)
scene.setPos(0, 60, 0)
# --- THE PANDA ---
panda = Actor("models/panda-model", {"walk": "models/panda-walk4"})
panda.reparentTo(app.render)
panda.setScale(0.005)
panda.setPos(0, 20, 0)
panda.loop("walk")
# --- KEYBOARD CONTROLS ---
keys = {"turn_left": False, "turn_right": False, "walk_forward": False}
def update_key(key_name, is_pressed):
keys[key_name] = is_pressed
app.accept("arrow_left", update_key, ["turn_left", True])
app.accept("arrow_left-up", update_key, ["turn_left", False])
app.accept("arrow_right", update_key, ["turn_right", True])
app.accept("arrow_right-up", update_key, ["turn_right", False])
app.accept("arrow_up", update_key, ["walk_forward", True])
app.accept("arrow_up-up", update_key, ["walk_forward", False])
# --- THE MOVEMENT & CAMERA RULE ---
def move_panda(task):
# Turn left or right
if keys["turn_left"]:
panda.setH(panda.getH() + 2)
if keys["turn_right"]:
panda.setH(panda.getH() - 2)
# Walk forward
if keys["walk_forward"]:
panda.setY(panda, -4)
# 2. Make the camera follow the panda
# Set the camera's position behind and slightly above the panda
app.camera.setPos(panda, 0, 3000, 1200)
# Force the camera to point directly at the panda's center
app.camera.lookAt(panda)
return task.cont
app.taskMgr.add(move_panda, "MovePandaTask")
app.run()

Breaking Down the Magic

Run this code, and you finally have a fully controllable, walking character with a trailing camera!

Here is how the camera magic works:

  • app.disableMouse(): By default, Panda3D lets you move the camera around by clicking and dragging your mouse (which is great for inspecting a static scene). But because we want to write our own code to move the camera, we have to tell the game to turn off those default mouse controls.

  • app.camera.setPos(panda, 0, 3000, 1200): Just like we did with the panda’s movement, putting panda as the first argument means we are placing the camera in relative space. We are telling the game, “Put the camera 3000 units behind the panda’s personal Y-axis, and 1200 units up on its Z-axis.” Because it sits inside the move_panda task, this placement is recalculated 60 times a second, perfectly mirroring the panda’s movements! (Note: The numbers are so large because they scale down to match our 0.005 panda scale).

  • app.camera.lookAt(panda): The camera is basically a giant eyeball. lookAt() is a super helpful built-in function that automatically calculates the math required to rotate that eyeball so it is staring dead center at whatever object you pass into the parentheses.

Camera (TODO move to a dedicated page)

By default, Panda3D includes a built-in mouse camera control (where you click and drag), but it’s meant for debugging, not for actual gameplay.

We will build a classic “First-Person” style mouse look. We will hide the mouse cursor, track its movement, and use that movement to look around the 3D world.

The Core Concepts

Before we write the code, here is a quick rundown of how we tackle this in Panda3D:

  1. Disable the Default Mouse: We have to tell Panda3D to turn off its default debugger camera so we can take over.

  2. Hide and Lock the Cursor: We will hide the mouse cursor and force it back to the exact center of the screen every single frame.

  3. Measure the Difference (Delta): Because we constantly force the mouse back to the center, any time you move your physical mouse, the cursor briefly leaves the center. We measure that distance, rotate the camera, and snap the cursor back to the middle.

  4. HPR (Heading, Pitch, Roll): This is how Panda3D handles rotation.

    • Heading: Looking left and right (Yaw).
    • Pitch: Looking up and down.
    • Roll: Tilting your head side to side.

from direct.showbase.ShowBase import ShowBase
from panda3d.core import WindowProperties
class MouseCameraGame(ShowBase):
def __init__(self):
super().__init__()
# 1. Disable the default Panda3D mouse camera controls
self.disableMouse()
# Load a default environment model so we have something to look at
self.scene = self.loader.loadModel("models/environment")
self.scene.reparentTo(self.render)
self.scene.setScale(0.25)
self.scene.setPos(-8, 42, 0)
# 2. Set up our custom camera variables
self.heading = 0.0 # Left/Right rotation
self.pitch = 0.0 # Up/Down rotation
self.mouse_sensitivity = 0.1 # How fast the camera moves
# 3. Hide the mouse cursor
properties = WindowProperties()
properties.setCursorHidden(True)
self.win.requestProperties(properties)
# 4. Add our custom function to Panda3D's Task Manager
# The Task Manager runs this function automatically every single frame
self.taskMgr.add(self.mouse_look_task, "MouseLookTask")
def mouse_look_task(self, task):
# Check if the application actually has the mouse inside its window
if self.mouseWatcherNode.hasMouse():
# Get the size of the window and find the center coordinates
center_x = self.win.getXSize() // 2
center_y = self.win.getYSize() // 2
# Get the current position of the mouse pointer
pointer = self.win.getPointer(0)
mouse_x = pointer.getX()
mouse_y = pointer.getY()
# If we successfully snap the mouse back to the center of the screen...
if self.win.movePointer(0, center_x, center_y):
# Calculate how far the mouse moved from the center
delta_x = mouse_x - center_x
delta_y = mouse_y - center_y
# Update our heading and pitch based on that movement
self.heading -= delta_x * self.mouse_sensitivity
self.pitch -= delta_y * self.mouse_sensitivity
# Optional: Clamp the pitch so you can't break your neck looking too far up/down
if self.pitch < -90: self.pitch = -90
if self.pitch > 90: self.pitch = 90
# Apply the calculated rotation to the actual camera
# We leave Roll at 0 because people don't usually walk with tilted heads!
self.camera.setHpr(self.heading, self.pitch, 0)
# Tell the Task Manager to keep running this task on the next frame
return task.cont
# Start the game
game = MouseCameraGame()
game.run()

Run your Python script. You should see a simple grassy environment.

Move your mouse to look around.

You won’t be able to move forward or backward yet (that requires keyboard inputs), but you should have a perfectly smooth, first-person camera look system!

To exit the game, you can press Alt + F4 (Windows/Linux) or Cmd + Q (Mac), or simply close your terminal.

How to adjust the “feel”. If the camera feels too fast or too slow, simply change the self.mouse_sensitivity = 0.1 variable. A lower number makes it slower, and a higher number makes it faster.