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

Custom Components and Resources

The three patterns for defining your own components and resources.

Introduction

Custom components are how you add game-specific data to entities. There are three patterns, each suited to a different use case. Choosing the right one avoids common pitfalls like the dreaded @dataclass error.

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

Pattern 1: Marker Components (No Data)

The simplest kind. A marker component tags an entity so you can query for it. No data, no __init__, no @dataclass needed.

@component
class Player(Component):
    pass
 
@component
class Enemy(Component):
    pass

Pattern 2: __init__ Components (Dynamic Data)

When your component holds data, define an __init__ method. This is the most common pattern for components with complex initialization or default values.

@component
class Health(Component):
    def __init__(self, current: int, maximum: int = 100):
        self.current = current
        self.maximum = maximum
 
@component
class Velocity(Component):
    def __init__(self, x: float = 0.0, y: float = 0.0, z: float = 0.0):
        self.x = x
        self.y = y
        self.z = z

Pattern 3: @dataclass Components (Annotated Fields)

If you use type annotations on fields (not in __init__), you must add @dataclass below @component. Without it, PyBevy sees the annotations but no dataclass machinery, and raises a TypeError.

# This FAILS:
@component
class Broken(Component):
    x: float = 0.0    # <-- type annotation without @dataclass
 
# TypeError: Broken has data fields but is not a dataclass
@component
@dataclass
class Position(Component):
    x: float = 0.0
    y: float = 0.0
    z: float = 0.0
 
@component
@dataclass
class Stats(Component):
    speed: float = 5.0
    damage: int = 10
    armor: int = 0

When to Use Which?

Pattern When Example
Marker (pass) Tag entities for queries Player, Enemy, Collider
__init__ Complex defaults, validation Health(50, 100), Timer(...)
@dataclass Simple data bags with annotations Position(x=1.0), Stats(speed=3)

The __init__ and @dataclass patterns are functionally similar — choose whichever reads better for your use case.

Custom Resources

Resources follow the same patterns. They are global singletons — only one instance exists in the entire app.

@resource
class Score(Resource):
    def __init__(self):
        self.value = 0
        self.combo = 1
 
@resource
@dataclass
class GameSettings(Resource):
    difficulty: int = 1
    sound_volume: float = 0.8

Using Custom Components

Here's a complete example that uses all three patterns together.

def setup(commands: Commands):
    commands.insert_resource(Score())
    commands.insert_resource(GameSettings(difficulty=2))
 
    # Spawn a player with multiple custom components
    commands.spawn(
        Player(),
        Health(100),
        Velocity(0.0, 0.0, 1.0),
        Position(x=0.0, y=0.0, z=0.0),
        Stats(speed=8.0, damage=15),
    )
 
    # Spawn some enemies
    for i in range(3):
        commands.spawn(
            Enemy(),
            Health(50),
            Position(x=float(i) * 3.0, y=0.0, z=-10.0),
        )
 
def game_system(
    score: ResMut[Score],
    settings: Res[GameSettings],
    players: Query[tuple[Mut[Position], Velocity, Stats], With[Player]],
    enemies: Query[tuple[Mut[Health], Position], With[Enemy]],
):
    for pos, vel, stats in players:
        pos.x += vel.x * stats.speed
        pos.z += vel.z * stats.speed
        print(f"Player at ({pos.x:.1f}, {pos.z:.1f}), speed={stats.speed}")
 
    for health, pos in enemies:
        health.current -= settings.difficulty
        if health.current <= 0:
            score.value += 10 * score.combo
            print(f"Enemy at z={pos.z:.1f} defeated! Score: {score.value}")

Running the Example

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