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.0Running 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()