PyBevy is in an early and experimental stage. The API is incomplete, subject to breaking changes without notice, and you should expect bugs. Many features are still under development.
Ultra-Fast Animation with Numba JIT
Animate 100k+ entities at near-native speed with compiled kernels.
Introduction
This recipe demonstrates PyBevy's fastest performance tier: ViewColumn with Numba JIT compilation. By writing a kernel function that Numba compiles to native machine code, you get direct pointer access to ECS storage with multi-core parallelization.
This achieves ~500x the speed of Query iteration — fast enough to animate 100k+ cubes at 60+ FPS.
Prerequisites
Install Numba alongside PyBevy with pip install pybevy[jit].
import math
try:
import numba # type: ignore
except ImportError:
numba = None
from pybevy.prelude import *The Numba Kernel
The kernel is a pure function decorated with @numba.jit. It receives ViewColumn handles
as arguments and accesses entity data with array indexing. numba.prange() distributes
work across all CPU cores.
if numba is not None:
@numba.jit(nopython=True, parallel=True)
def wave_kernel(pos_x, pos_y, pos_z, time, speed, amplitude):Sine wave radiating from the center of the grid.""" for i in numba.prange(len(pos_x)): x, z = pos_x[i], pos_z[i] dist = math.sqrt(x * x + z * z) pos_y[i] = math.sin(dist * 0.5 - time * speed) * amplitude
### Key Points
- `nopython=True` — Numba compiles everything to native LLVM IR; no Python fallback
- `parallel=True` — Enables automatic multi-core parallelization
- `numba.prange()` — Like `range()`, but distributes iterations across CPU threads
- `pos_x[i]` — Direct read from ECS memory (zero copy)
- `pos_y[i] = ...` — Direct write to ECS memory (zero copy)## Marker Component and Setup@component class Cube(Component): pass
def setup( commands: Commands, meshes: ResMut[Assets[Mesh]], materials: ResMut[Assets[StandardMaterial]], ) -> None: cube_mesh = meshes.add(Cuboid.from_length(0.8)) cube_material = materials.add(Color.srgb(0.3, 0.7, 0.9))
grid_size = 100
half = grid_size // 2
spacing = 1.5
for row in range(grid_size):
for col in range(grid_size):
x = (col - half) * spacing
z = (row - half) * spacing
commands.spawn(
Cube(),
Mesh3d(cube_mesh),
MeshMaterial3d(cube_material),
Transform.from_xyz(x, 0.0, z),
)
commands.spawn(
DirectionalLight(illuminance=10000.0),
Transform.IDENTITY.looking_at(Vec3(-1.0, -2.5, -1.0), Vec3.Y),
)
commands.spawn(
Camera3d(),
Transform.from_xyz(0.0, 80.0, 120.0).looking_at(Vec3.ZERO, Vec3.Y),
)## The Animation System
The system iterates over archetype batches, extracts ViewColumn handles, and passes
them to the Numba kernel. Each batch corresponds to a group of entities with the
same set of components (an "archetype").def animate(view: View[Mut[Transform], With[Cube]], time: Res[Time]) -> None: t = time.elapsed_secs() for batch in view.iter_batches(): transform = batch.column_mut(Transform) wave_kernel( transform.translation.x, transform.translation.y, transform.translation.z, t, 2.0, 3.0, )
### Performance
At 10,000 cubes, the Numba kernel runs in under 0.2ms per frame — leaving plenty
of headroom for rendering. Scale to 100k or even 1M cubes and the kernel still
completes in single-digit milliseconds.## Running the Example
"""
@entrypoint
def main(app: App) -> App:
return (
app
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, animate)
)
if __name__ == "__main__":
main().run()