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

The ECS in 60 Seconds

A simple explanation of Entities, Components, and Systems.

Introduction

pybevy is built on an architecture called the Entity Component System (ECS), popularized in game development by the Bevy engine that PyBevy is built on. It's a way of organizing your game that is very flexible and fast. While PyBevy feels like standard Python, it's designed to scale — use NumPy and Numba integrations to handle thousands of entities without breaking a sweat.

  • Entity: A unique ID. Think of it as a generic "thing" in your game.
  • Component: A piece of data. You attach components to entities to give them properties.
  • System: A function that runs logic on entities with specific components.

This example is console-only and will not open a window.

from pybevy.prelude import *
from pybevy.app import ScheduleRunnerPlugin

Components

Components are just Python classes that inherit from Component. They hold data, not logic. Systems contain the logic. They never mix — this is what makes your code so easy to test and extend.

Here, we define a Player with a name and a Position.

@component
class Player(Component):
    def __init__(self, name: str):
        self.name = name
 
@component
class Position(Component):
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

Startup System

A Startup system runs once when the app starts. We use it to set up our initial world. Commands is a special parameter that lets us create (spawn) entities and add components to them.

def startup_system(commands: Commands):
    # Spawn an entity with both Player and Position components.
    commands.spawn(Player("Alice"), Position(0.0, 0.0))
    # Spawn another entity with just a Position component.
    commands.spawn(Position(10.0, 5.0))

Update Systems

Update systems run on every "tick" of the app. We use Query to select which entities a system should run on.

  • update_system will run on ALL entities that have a Position component.
  • greet_player_system will only run on entities that have BOTH a Player AND a Position component.

The Query[tuple[Player, Position]] syntax means "give me every entity that has both of these components, and return their data". For filtering without fetching data, there's With[Component] and other filters.

def update_system(query: Query[Position]):
    # The query gives us an iterator over all matching components.
    for position in query:
        print(f"Found a thing at position: ({position.x}, {position.y})")
 
def greet_player_system(query: Query[tuple[Player, Position]]):
    # We can query for multiple components as a tuple.
    for player, position in query:
        print(f"Hello, {player.name}, who is at ({position.x}, {position.y})!")

Running the App

We create an App, add our systems, and run it. We use ScheduleRunnerPlugin.run_once() to make the app run one update and then exit, which is perfect for this console example.

@entrypoint
def main(app: App) -> App:
    return (
        app
        .add_plugins(ScheduleRunnerPlugin.run_once())
        .add_systems(Startup, startup_system)
        .add_systems(Update, (update_system, greet_player_system))
    )
 
if __name__ == "__main__":
    main().run()

When you run this, you'll see update_system finds two entities, but greet_player_system only finds one. This is the power of ECS: systems only care about the data they need.