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.
Three Ways to Update Entities
Compare Query, View API, and Numba JIT side by side.
Introduction
PyBevy offers three syntaxes for modifying entity components. This recipe shows the
same operation — a sine-wave animation on translation.y — implemented in all three
styles, so you can see the tradeoffs directly.
from pybevy.prelude import *Marker Components
Each cube group gets a different marker so we can animate them with separate systems.
@component
class QueryCube(Component):
pass
@component
class ViewCube(Component):
pass
@component
class JitCube(Component):
passSyntax 1: Query Iteration
The simplest syntax. Iterate over entities and modify each one directly. Best for small entity counts or complex per-entity logic.
def update_query(time: Res[Time], query: Query[Mut[Transform], With[QueryCube]]) -> None:
for transform in query:
transform.rotate_y(time.delta_secs())This is clean and readable, but each iteration crosses the Python/Rust boundary. At 10k+ entities, the loop becomes the bottleneck.
Syntax 2: View API Expressions
Write a single expression that applies to ALL matching entities in parallel. No Python loop — the expression is compiled to bytecode and executed in batch.
def update_view(time: Res[Time], view: View[Mut[Transform], With[ViewCube]]) -> None:
t = time.elapsed_secs()
transform = view.column_mut(Transform)
transform.translation.y = (t * 2.0).sin() * 2.0~200x faster than Query at 1M entities. The tradeoff: you can only use arithmetic, math functions, and conditionals — no Python objects or control flow.
Syntax 3: ViewColumn + Numba JIT
Write a compiled kernel function with direct pointer access and multi-core parallelization. Maximum performance for complex math.
import math
import numba
@numba.jit(nopython=True)
def jit_kernel(pos_x, pos_y, time):
for i in range(len(pos_x)):
pos_y[i] = math.sin(time + pos_x[i]) * 1.5
def update_jit(time: Res[Time], view: View[Mut[Transform], With[JitCube]]) -> None:
t = time.elapsed_secs()
for batch in view.iter_batches():
transform = batch.column_mut(Transform)
jit_kernel(transform.translation.x, transform.translation.y, t)~500x faster than Query. The tradeoff: requires Numba installation and the kernel can only use Numba-compatible operations (no Python objects, no Bevy API calls).
Comparison Summary
| Syntax | Speed at 1M | Ease of Use | Best For | |--------|-------------|-------------|----------| | Query iteration | 1x | Easiest | <1k entities, complex logic | | View API | 200x | Easy | 1k-1M entities, arithmetic | | ViewColumn + Numba | 500x | Moderate | 100k+ entities, heavy math |
Choose based on your entity count and the complexity of your per-entity logic.
Setup
def setup(
commands: Commands,
meshes: ResMut[Assets[Mesh]],
materials: ResMut[Assets[StandardMaterial]],
) -> None:
cube_mesh = meshes.add(Cuboid.from_length(1.0))
# Red — Query (left)
commands.spawn(
QueryCube(),
Mesh3d(cube_mesh),
MeshMaterial3d(materials.add(Color.srgb(1.0, 0.3, 0.3))),
Transform.from_xyz(-3.0, 0.0, 0.0),
)
# Green — View API (center)
commands.spawn(
ViewCube(),
Mesh3d(cube_mesh),
MeshMaterial3d(materials.add(Color.srgb(0.3, 1.0, 0.3))),
Transform.from_xyz(0.0, 0.0, 0.0),
)
# Blue — Numba JIT (right)
commands.spawn(
JitCube(),
Mesh3d(cube_mesh),
MeshMaterial3d(materials.add(Color.srgb(0.3, 0.3, 1.0))),
Transform.from_xyz(3.0, 0.0, 0.0),
)
commands.spawn(
DirectionalLight(illuminance=5000.0),
Transform.IDENTITY.looking_at(Vec3(-1.0, -1.0, -1.0), Vec3.Y),
)
commands.spawn(
Camera3d(),
Transform.from_xyz(0.0, 5.0, 10.0).looking_at(Vec3.ZERO, Vec3.Y),
)Running the Example
Red cube (left) rotates via Query. Green cube (center) bounces via View API. Blue cube (right) would animate via Numba JIT when the kernel is enabled.
@entrypoint
def main(app: App) -> App:
return (
app
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, (update_query, update_view))
)
if __name__ == "__main__":
main().run()