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

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

Step 1: Generate Positions with NumPy

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

Step 2: Create a Material

A single shared material for all entities. You can also create multiple materials and assign them per-entity after batch spawning.

Step 3: Batch Spawn

Transform.from_numpy() converts the NumPy array into a batch of transforms. spawn_batch() creates all entities in a single operation — this is the key performance win. All components listed here are shared across the batch.

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:
    cube_mesh = meshes.add(Cuboid.from_length(0.15))
 
    # Step 1: Generate positions with NumPy
    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 a shared material
    mat = materials.add(Color.srgb(0.4, 0.6, 0.9))
 
    # Step 3: Batch spawn
    batch = Transform.from_numpy(translation=positions)
    commands.spawn_batch(
        batch,
        Cube(),
        Mesh3d(cube_mesh),
        MeshMaterial3d(mat),
    )
 
    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()