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 ScheduleRunnerPluginPattern 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):
passPattern 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 = zPattern 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 = 0When 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.8Using 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()