Python Ursina 3D Engine — Deep Dive
Under the Hood: Panda3D
Ursina is a convenience layer over Panda3D, which has been developed since 2002 (originally by Disney VR Studio, now maintained by Carnegie Mellon University’s Entertainment Technology Center). Understanding this relationship matters because any Panda3D feature is accessible from Ursina — you can drop down to the lower-level API whenever you need something Ursina does not directly expose.
Panda3D uses a scene graph — a tree of nodes where each node has a transform (position, rotation, scale) relative to its parent. Ursina’s Entity class wraps a Panda3D NodePath. When you set entity.position = (1, 2, 3), Ursina calls node_path.setPos(1, 2, 3) internally.
Entity Lifecycle and Custom Components
Creating Custom Entity Classes
from ursina import *
class Bullet(Entity):
def __init__(self, direction, **kwargs):
super().__init__(
model='sphere',
color=color.yellow,
scale=0.2,
collider='sphere',
**kwargs
)
self.direction = direction
self.speed = 40
def update(self):
self.position += self.direction * self.speed * time.dt
if abs(self.x) > 50 or abs(self.y) > 50 or abs(self.z) > 50:
destroy(self)
Each entity with an update method is automatically called each frame. The destroy() function removes the entity from the scene graph and frees its resources.
Component-Like Patterns
While Ursina does not have a formal ECS (Entity-Component-System), you can attach behaviors by subclassing or by assigning functions dynamically:
def wobble(self):
self.rotation_y += 50 * time.dt
cube = Entity(model='cube')
cube.update = wobble.__get__(cube)
Procedural Mesh Generation
Ursina allows runtime mesh creation for terrain, voxels, or custom shapes:
from ursina import Mesh, Entity, Vec3
vertices = [
Vec3(0, 0, 0), Vec3(1, 0, 0), Vec3(1, 1, 0),
Vec3(0, 0, 0), Vec3(1, 1, 0), Vec3(0, 1, 0),
]
normals = [Vec3(0, 0, -1)] * 6
uvs = [(0,0), (1,0), (1,1), (0,0), (1,1), (0,1)]
custom_mesh = Mesh(vertices=vertices, normals=normals, uvs=uvs, mode='triangle')
Entity(model=custom_mesh, texture='brick', double_sided=True)
For voxel games, developers generate chunk meshes by iterating a 3D array and only emitting faces between solid and air blocks — a technique called greedy meshing. Ursina’s mesh system is flexible enough to handle this, and because it sits on Panda3D’s geometry nodes, the GPU handles rendering efficiently.
Custom Shaders
Ursina entities can use custom GLSL shaders:
lit_shader = Shader(
vertex='''
#version 140
uniform mat4 p3d_ModelViewProjectionMatrix;
in vec4 p3d_Vertex;
in vec2 p3d_MultiTexCoord0;
out vec2 uv;
void main() {
gl_Position = p3d_ModelViewProjectionMatrix * p3d_Vertex;
uv = p3d_MultiTexCoord0;
}
''',
fragment='''
#version 140
uniform sampler2D p3d_Texture0;
in vec2 uv;
out vec4 fragColor;
void main() {
vec4 tex = texture(p3d_Texture0, uv);
fragColor = tex * vec4(1.0, 0.8, 0.8, 1.0); // warm tint
}
'''
)
Entity(model='cube', shader=lit_shader, texture='white_cube')
Panda3D’s shader system supports vertex, fragment, and geometry shaders. You can pass custom uniforms via entity.set_shader_input('time_val', time.time()) for animated effects like water ripples or dissolve transitions.
Collision System Details
Ursina supports three collider types:
| Collider | Use Case | Cost |
|---|---|---|
'box' | Rectangular objects, walls | Cheapest |
'sphere' | Balls, projectiles, proximity triggers | Cheap |
'mesh' | Complex terrain, irregular shapes | Expensive — use sparingly |
Collision detection uses Panda3D’s CollisionTraverser. For performance-critical scenarios, keep mesh colliders on static geometry only and use box or sphere colliders for moving entities.
Raycasting for Game Mechanics
from ursina import raycast
hit_info = raycast(
origin=player.position,
direction=player.forward,
distance=100,
ignore=[player]
)
if hit_info.hit:
print(f"Hit {hit_info.entity.name} at distance {hit_info.distance}")
hit_info.entity.color = color.red
Raycasting is essential for shooting mechanics, line-of-sight AI, and terrain interaction (placing blocks, mining).
Networking and Multiplayer
Ursina includes an experimental UrsinaNetworking module (and there are community libraries like ursina-networking). The pattern uses a client-server model:
- Server maintains authoritative game state.
- Clients send input events, receive state updates.
- Entity synchronization maps server-side IDs to client-side entities.
For production multiplayer, most developers use external libraries like enet (low-level UDP) or WebSockets, using Ursina only for rendering. The engine’s single-threaded nature means network I/O should be async or on a separate thread to avoid frame drops.
Performance Optimization
Batching and Instancing
When rendering many identical objects (trees, grass, particles), combine them into a single mesh or use Panda3D’s instancing:
# Combine static entities into one mesh
from ursina import combine
trees = [Entity(model='tree', position=(x, 0, z))
for x in range(-50, 50, 5) for z in range(-50, 50, 5)]
combined = combine(trees) # single draw call
Level of Detail (LOD)
Swap high-poly models for low-poly versions at distance. Ursina does not have built-in LOD, but you can implement it:
def update():
dist = distance(player, tree)
if dist > 100 and tree.model != 'tree_low':
tree.model = 'tree_low'
elif dist <= 100 and tree.model != 'tree_high':
tree.model = 'tree_high'
Profiling
Panda3D includes PStats, a real-time profiling tool that shows frame time broken down by render, collision, animation, and Python logic. Enable it with:
from panda3d.core import loadPrcFileData
loadPrcFileData('', 'want-pstats 1')
Common performance pitfalls:
| Issue | Cause | Solution |
|---|---|---|
| Low FPS with many entities | Too many draw calls | Combine static meshes, use instancing |
| Slow collisions | Mesh colliders on moving entities | Switch to box/sphere colliders |
| Memory spikes | Loading full-res textures | Compress textures, use mipmaps |
| Lag spikes every few seconds | Python garbage collection | Tune gc thresholds or pool objects |
Building and Distributing
Ursina games can be packaged with:
ursina.build()— the engine’s own packaging tool that bundles assets and creates an executable.- PyInstaller — works with Panda3D; requires adding data files to the spec.
- Panda3D’s deploy-ng — the official Panda3D deployment system that produces standalone builds for Windows, macOS, and Linux.
For web deployment, Panda3D is experimenting with WebGL/WebAssembly builds, but this remains in early stages.
Real-World Project Structure
my_game/
├── main.py # App setup, view switching
├── settings.py # Window size, controls, constants
├── entities/
│ ├── player.py # Player entity + controller
│ ├── enemy.py # Enemy AI entities
│ └── projectile.py # Bullets, arrows
├── world/
│ ├── terrain.py # Procedural or loaded terrain
│ ├── chunk_manager.py # Voxel chunk loading/unloading
│ └── lighting.py # Sun, ambient, point lights
├── ui/
│ ├── hud.py # Health bar, ammo counter
│ └── menus.py # Main menu, settings
├── shaders/
│ ├── water.glsl
│ └── fog.glsl
└── assets/
├── models/
├── textures/
└── sounds/
When to Choose Ursina
| Scenario | Ursina | Alternative |
|---|---|---|
| Quick 3D prototype | Excellent | — |
| Game jam (48-72 hours) | Great | Godot, Unity |
| Teaching 3D programming | Best in Python ecosystem | — |
| AAA-quality production game | Not suitable | Unreal, Unity, Godot |
| VR application | Limited | Panda3D directly, Godot |
The one thing to remember: Ursina gives you Panda3D’s full 3D pipeline — shaders, scene graph, collision, networking — wrapped in an API so concise that a complete game prototype fits in a single Python file.
See Also
- Python Arcade Library Think of a magical art table that draws your game characters, listens when you press buttons, and cleans up the mess — that's Python Arcade.
- Python Audio Fingerprinting Ever wonder how Shazam identifies a song from just a few seconds of noisy audio? Audio fingerprinting is the magic behind it, and Python can do it too.
- Python Barcode Generation Picture the stripy labels on grocery items to understand how Python can create those machine-readable barcodes from numbers.
- Python Cellular Automata Imagine a checkerboard where each square follows simple rules to turn on or off — and suddenly complex patterns emerge like magic.
- Python Godot Gdscript Bridge Imagine speaking English to a friend who speaks French, with a translator in the middle — that's how Python talks to the Godot game engine.