⚠️ 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.

Batch Spawning with NumPy

Spawn hundreds of thousands of entities efficiently with NumPy arrays.

Introduction

Spawning entities one at a time in a loop is slow — each commands.spawn() call has overhead from crossing the Python/Rust boundary and allocating ECS storage.

PyBevy provides spawn_batch_fast() which takes a NumPy array of positions and creates all entities in a single batch operation. This is ~100x faster than loop spawning for large entity counts.

The Problem: Loop Spawning

Spawning 262,144 cubes in a loop takes several seconds:

# Slow: one spawn call per entity
for i in range(262_144):
    x = (i % grid_size - half) * 0.25
    z = (i // grid_size - half) * 0.25
    commands.spawn(Cube(), Mesh3d(mesh), Transform.from_xyz(x, 0.0, z))

The Solution: Transform.from_numpy() + spawn_batch_fast()

Generate all positions at once with NumPy, then batch-spawn:

import numpy as np
from pybevy.prelude import *
 
 
@component
class Cube(Component):
    pass
 
 
def setup(
    commands: Commands,
    meshes: ResMut[Assets[Mesh]],
    materials: ResMut[Assets[StandardMaterial]],
) -> None:

Step 1: Generate Positions with NumPy

NumPy vectorized operations create all positions in one shot — no Python loop for position calculation.

cube_mesh = meshes.add(Cuboid.from_length(0.15))
 
    cube_count = 1024 * 256  # 262,144 cubes
    grid_size = int(cube_count**0.5)
    half = grid_size // 2
 
    i = np.arange(cube_count, dtype=np.float32)
    x = ((i % grid_size) - half) * 0.25
    z = ((i // grid_size) - half) * 0.25
    y = np.full(cube_count, 0.1, dtype=np.float32)
 
    positions = np.stack([x, y, z], axis=1)

Step 2: Create Shared Materials

Instead of one material per entity, create a pool of shared materials. This dramatically reduces GPU memory usage.

num_materials = 2048
    material_handles = []
    for idx in range(num_materials):
        hue = idx / num_materials
        r = int(100 + hue * 155)
        g = int(100 + (1.0 - hue) * 155)
        b = int(150 + (0.5 - abs(hue - 0.5)) * 105)
        material_handles.append(materials.add(Color.srgb_u8(r, g, b)))

Step 3: Batch Spawn

Transform.from_numpy() converts the NumPy array into a batch of transforms. spawn_batch_fast() creates all entities in a single operation.

batch = Transform.from_numpy(positions=positions)
    entities = commands.spawn_batch_fast(
        batch,
        Cube(),
        Mesh3d(cube_mesh),
    )

Step 4: Add Materials

After batch spawning, add materials to each entity. This is done in a loop but only inserts one component per entity — much cheaper than full spawning.

for idx, entity in enumerate(entities):
        material_idx = idx % num_materials
        commands.entity(entity).insert(MeshMaterial3d(material_handles[material_idx]))
 
    commands.spawn(DirectionalLight(illuminance=2000))
    commands.spawn(
        Camera3d(),
        Transform.from_xyz(-15.0, 50.0, 60.0).looking_at(Vec3(0.0, 15.0, 0.0), Vec3.Y),
    )

Animating the Batch

With 262,144 entities spawned efficiently, we can animate them all using the View API:

def move_cubes(view: View[Mut[Transform], With[Cube]], time: Res[Time]) -> None:
    t = time.elapsed_secs()
    transform = view.column_mut(Transform)
 
    x_wave = (transform.translation.x * 0.5 + t * 3.0).sin()
    z_wave = (transform.translation.z * 0.4 - t * 2.5).sin()
    transform.translation.y = (x_wave + z_wave) * 3.0

Running the Example

@entrypoint
def main(app: App) -> App:
    return (
        app
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .add_systems(Update, move_cubes)
    )
 
if __name__ == "__main__":
    main().run()