Skip to content

Bli-AIk/bevy_fact_rule_event

Repository files navigation

bevy_fact_rule_event

license

bevy_fact_rule_event — A generic, data-driven Fact-Rule-Event (FRE) system for Bevy engine.

English Simplified Chinese
English 简体中文

Acknowledgements

This FRE system is deeply inspired by aarthificial's brilliant work on data-driven game logic. We express our sincere gratitude and admiration for these foundational ideas:

Introduction

Hardcoding complex game logic like "if player HP < 10 and has the magic sword, trigger the secret dialogue" often leads to a tangled web of if/else statements that are difficult to maintain and require constant recompilation for every tiny tweak.

bevy_fact_rule_event offers a data-driven alternative. It is a declarative rule engine that separates game logic from your Rust code, allowing you to define behavior in external RON files.

However, it's important to remember that this isn't a magic bullet for every project. It's an architectural choice designed for those who value flexibility and designer-led iteration over direct, hardcoded control.

Pros and Cons

Like any architectural pattern, the FRE system is a trade-off.

Pros ✅

  • Decoupling: Game logic is completely separated from the engine and system code.
  • Iteration Speed: Change game behavior by editing RON files without waiting for recompilation.
  • Designer Friendly: Empowers non-programmers to tweak quests, dialogue, and balance on the fly.
  • Centralized State: The Fact database provides a single source of truth for the entire game state.

Cons ❌

  • Indirection: It can be harder to trace the exact cause of a behavior compared to traditional code debugging.
  • Performance Overhead: Evaluating rules has a cost; while negligible for dialogue, it's not suited for high-frequency physics logic.
  • Complexity: For very simple games, managing external rules might introduce more overhead than it saves.

Core Philosophy

"Events don't contain logic, data doesn't contain behavior, logic only exists in rules."

The FRE system enforces clean separation of concerns:

  • Facts (F): Centralized key-value database for game state
  • Rules (R): Declarative logic that transforms state based on conditions
  • Events (E): Signal broadcasts that trigger rule evaluation

Features

  • 🗂️ Data-Driven Rules: Define game logic in RON files without code changes
  • 🥞 Layered Fact Database: Hierarchical state management with Global and Local layers
    • Global layer: Persistent facts that survive scene changes (e.g., player stats)
    • Local layer: Temporary facts scoped to current context (e.g., battle state)
  • 🗄️ Centralized State Management: All game facts stored in a queryable database
  • 🔀 Conditional Logic: Complex condition evaluation with nested logic operators
  • 📥 Automatic Asset Loading: Seamless integration with Bevy's asset system
  • 📡 Event Broadcasting: Decoupled communication between game systems
  • 🛡️ Type-Safe Values: Support for Int, Float, Bool, and String fact types
  • 🔄 Bidirectional Sync: Facts can sync with ECS components for reactive UI updates
  • 👁️ (Planned) Visual Rule Editor
  • 🔥 (Planned) Hot-Reloading Support

Bevy Version Support

bevy bevy_fact_rule_event
0.18 0.3.0
0.17 < 0.3.0

Architecture Overview

┌─────────────────────────────────────────────────────────────┐
│                   LayeredFactDatabase                        │
├─────────────────────────────────────────────────────────────┤
│  Global Layer (persistent)     │  Local Layer (temporary)   │
│  ─────────────────────────     │  ──────────────────────    │
│  • player_hp, player_lv        │  • battle_turn_count       │
│  • player_gold, player_name    │  • current_enemy_hp        │
│  • inventory_items             │  • dialogue_state          │
│                                │  (cleared on context exit) │
├─────────────────────────────────────────────────────────────┤
│              ↓ Read (local overrides global) ↓               │
│              ↑ Write (choose target layer)   ↑               │
└─────────────────────────────────────────────────────────────┘
            ↕                                      ↕
    ┌───────────────┐                    ┌──────────────────┐
    │  Rule Engine  │←── triggers ──────→│   Game Events    │
    │ (evaluates    │                    │ (FactEvent)      │
    │  conditions)  │                    └──────────────────┘
    └───────────────┘

How to Use

  1. Install Rust (if not already installed):

    curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
  2. Add to Cargo.toml:

    [dependencies]
    bevy_fact_rule_event = "0.3.0"
  3. Basic usage:

    use bevy::prelude::*;
    use bevy_fact_rule_event::prelude::*;
    
    fn main() {
        App::new()
            .add_plugins(DefaultPlugins)
            .add_plugins(FREPlugin)  // Add FRE plugin
            .add_systems(Startup, setup_rules)
            .run();
    }
    
    fn setup_rules(
        asset_server: Res<AssetServer>,
        mut commands: Commands,
    ) {
        // Load rules from file
        let rules_handle: Handle<FreAsset> = asset_server.load("rules/game_rules.fre.ron");
        commands.spawn(rules_handle);
    }
  4. Using the Layered Database:

    fn update_player_stats(
        mut layered_db: ResMut<LayeredFactDatabase>,
    ) {
        // Write to global layer (persistent)
        layered_db.set_global("player_hp", 100i64);
        layered_db.set_global("player_name", "Chara".to_string());
        
        // Write to local layer (temporary, for current context)
        layered_db.set("battle_turn", 1i64);
        
        // Read (local layer takes priority over global)
        let hp = layered_db.get_int("player_hp").unwrap_or(20);
        let name = layered_db.get_string("player_name").unwrap_or("???");
        
        // Clear local layer when leaving context
        layered_db.clear_local();
    }
  5. Create a rule file (assets/rules/game_rules.fre.ron):

    (
        facts: {
            "player_health": Int(100),
            "score": Int(0),
        },
        rules: [
            (
                id: "damage_player",
                event: Event("player_hit"),
                condition: GreaterThan(key: "player_health", value: Int(0)),
                modifications: [
                    Decrement(key: "player_health", amount: 10),
                ],
                outputs: ["health_changed"],
            ),
            (
                id: "game_over",
                event: Event("health_changed"),
                condition: LessEqual(key: "player_health", value: Int(0)),
                actions: ["GameOver"],
                outputs: ["game_ended"],
            ),
        ],
    )
  6. Emit events in your game code:

    fn player_collision_system(
        mut events: ResMut<PendingFactEvents>,
    ) {
        // Trigger rule evaluation
        events.emit("player_hit");
    }

Dependencies

This project uses the following crates:

Crate Version Description
bevy 0.18 Game engine
serde 1.0 Serialization framework

Contributing

Contributions are welcome! Whether you want to fix a bug, add a feature, or improve documentation:

  • Submit an Issue or Pull Request.
  • Share ideas and discuss design or architecture.

License

This project is licensed under either of

at your option.

About

A generic, data-driven Fact-Rule-Event (FRE) system for Bevy engine.

Topics

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Packages

 
 
 

Contributors