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

States and Run Conditions

Manage game states (menu, playing, paused) and conditionally run systems.

Introduction

Most games have distinct phases: a main menu, gameplay, a pause screen. pybevy provides States to model these phases and run conditions to control which systems execute in each phase.

  • @state decorator + Enum: Define your game states.
  • OnEnter(state) / OnExit(state): Run systems on state transitions.
  • run_if(): Only run a system when a condition is true.
  • in_state(): A built-in condition that checks the current state.
from enum import Enum, auto
from pybevy.prelude import *
from pybevy.ecs import state, in_state, NextState, State, run_if, OnEnter, OnExit

Defining States

Use the @state decorator on an Enum. The first variant is the default when you use app.init_state().

@state
class GameState(Enum):
    MENU = auto()
    PLAYING = auto()
    PAUSED = auto()

Setup Systems for Each State

OnEnter(GameState.MENU) systems run once when the state changes TO MENU. OnExit(GameState.MENU) systems run once when the state changes AWAY FROM MENU.

def setup_menu(commands: Commands):
    print(">> Entering MENU: Press Enter to start")
 
def cleanup_menu(commands: Commands):
    print(">> Leaving MENU")
 
def setup_game(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))
    print(">> Entering PLAYING: Press P to pause, Escape to quit to menu")
 
def setup_pause(commands: Commands):
    print(">> PAUSED: Press P to resume")

Conditional Systems

run_if() wraps a system so it only executes when a condition returns True. in_state(value) is a built-in condition that checks the current game state.

def menu_input(
    keyboard: Res[ButtonInput],
    next_state: ResMut[NextState],
):
    if keyboard.just_pressed(KeyCode.Enter):
        next_state.set(GameState.PLAYING)
 
def game_input(
    keyboard: Res[ButtonInput],
    next_state: ResMut[NextState],
):
    if keyboard.just_pressed(KeyCode.KeyP):
        next_state.set(GameState.PAUSED)
    if keyboard.just_pressed(KeyCode.Escape):
        next_state.set(GameState.MENU)
 
def pause_input(
    keyboard: Res[ButtonInput],
    next_state: ResMut[NextState],
):
    if keyboard.just_pressed(KeyCode.KeyP):
        next_state.set(GameState.PLAYING)

Custom Run Conditions

You can write any function that returns bool as a run condition. Condition functions can take system parameters just like regular systems.

def is_debug_mode() -> bool:
    return True  # Toggle this for debugging
 
def debug_system():
    pass  # Only runs when is_debug_mode() returns True

Running the App

We register state-specific systems with OnEnter/OnExit, and use run_if with in_state to gate update systems to the correct state.

@entrypoint
def main(app: App) -> App:
    return (
        app
        .add_plugins(DefaultPlugins)
        .init_state(GameState)
        # Menu state
        .add_systems(OnEnter(GameState.MENU), setup_menu)
        .add_systems(OnExit(GameState.MENU), cleanup_menu)
        .add_systems(Update, run_if(menu_input, in_state(GameState.MENU)))
        # Playing state
        .add_systems(OnEnter(GameState.PLAYING), setup_game)
        .add_systems(Update, run_if(game_input, in_state(GameState.PLAYING)))
        # Paused state
        .add_systems(OnEnter(GameState.PAUSED), setup_pause)
        .add_systems(Update, run_if(pause_input, in_state(GameState.PAUSED)))
        # Debug system with custom condition
        .add_systems(Update, run_if(debug_system, is_debug_mode))
    )
 
if __name__ == "__main__":
    main().run()

When you run this, the app starts in MENU state. Press Enter to transition to PLAYING, then P to toggle PAUSED. Each transition triggers the matching OnEnter/OnExit systems, and only the systems guarded by the correct in_state() condition will run each frame.