⚠️ Beta State

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()