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

Organize your code into reusable plugins.

Introduction

As your project grows, putting everything into one file becomes messy. Plugins let you group related systems, resources, and components into reusable modules.

A plugin is a class that implements build(self, app) and registers its systems and resources on the app.

from pybevy.prelude import *

Defining a Plugin

Use the @plugin decorator AND inherit from Plugin. In the build method, register everything this plugin needs.

@resource
class Score(Resource):
    def __init__(self):
        self.value = 0
 
@component
class ScoreDisplay(Component):
    pass
 
def setup_score_ui(commands: Commands):
    commands.spawn(
        Text("Score: 0"),
        ScoreDisplay(),
    )
 
def update_score_display(
    score: Res[Score],
    query: Query[Mut[Text], With[ScoreDisplay]],
):
    for text in query:
        text.content = f"Score: {score.value}"
 
# ScorePlugin: Manages score tracking and display.
@plugin
class ScorePlugin(Plugin):
    def build(self, app: App):
        app.insert_resource(Score())
        app.add_systems(Startup, setup_score_ui)
        app.add_systems(Update, update_score_display)

Another Plugin: Enemy Spawning

Each plugin is self-contained with its own components, resources, and systems.

@component
class Enemy(Component):
    pass
 
@resource
class EnemySpawnTimer(Resource):
    def __init__(self):
        from pybevy.time import Timer, TimerMode
        self.timer = Timer.from_seconds(3.0, TimerMode.REPEATING)
 
def spawn_enemies(
    commands: Commands,
    time: Res[Time],
    timer: ResMut[EnemySpawnTimer],
    meshes: ResMut[Assets[Mesh]],
    materials: ResMut[Assets[StandardMaterial]],
):
    timer.timer.tick(time.delta())
    if timer.timer.just_finished():
        import random
        x = random.uniform(-5.0, 5.0)
        commands.spawn(
            Enemy(),
            Mesh3d(meshes.add(Sphere(0.5))),
            MeshMaterial3d(materials.add(Color.srgb(0.8, 0.2, 0.2))),
            Transform.from_xyz(x, 0.5, -10.0),
        )
 
def move_enemies(
    commands: Commands,
    time: Res[Time],
    score: ResMut[Score],
    query: Query[tuple[Entity, Mut[Transform]], With[Enemy]],
):
    for entity, transform in query:
        transform.translation.z += 3.0 * time.delta_secs()
        # Despawn when past camera, add score
        if transform.translation.z > 5.0:
            commands.despawn(entity)
            score.value += 10
 
# EnemyPlugin: Manages enemy spawning and movement.
@plugin
class EnemyPlugin(Plugin):
    def build(self, app: App):
        app.insert_resource(EnemySpawnTimer())
        app.add_systems(Update, (spawn_enemies, move_enemies))

A Scene Setup Plugin

Plugins can also handle scene setup: cameras, lights, and environment.

def setup_scene(commands: Commands):
    commands.spawn(Camera3d(), Transform.from_xyz(0.0, 5.0, 10.0).looking_at(Vec3.ZERO, Vec3.Y))
    commands.spawn(DirectionalLight(illuminance=5000.0), Transform.IDENTITY.looking_at(Vec3(-1.0, -1.0, -1.0), Vec3.Y))
 
@plugin
class ScenePlugin(Plugin):
    def build(self, app: App):
        app.add_systems(Startup, setup_scene)

Composing Plugins

Now the main app is clean: just add your plugins.

@entrypoint
def main(app: App) -> App:
    return (
        app
        .add_plugins(DefaultPlugins)
        .add_plugins(ScenePlugin())
        .add_plugins(ScorePlugin())
        .add_plugins(EnemyPlugin())
    )
 
if __name__ == "__main__":
    main().run()

Each plugin can live in its own Python file and be imported. This keeps your codebase organized and makes it easy to reuse game systems across projects.