bevy_fact_rule_event — A generic, data-driven Fact-Rule-Event (FRE) system for Bevy engine.
| English | Simplified Chinese |
|---|---|
| English | 简体中文 |
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:
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.
Like any architectural pattern, the FRE system is a trade-off.
- 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.
- 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.
"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
- 🗂️ 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 |
bevy_fact_rule_event |
|---|---|
| 0.18 | 0.3.0 |
| 0.17 | < 0.3.0 |
┌─────────────────────────────────────────────────────────────┐
│ 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) │ └──────────────────┘
└───────────────┘
-
Install Rust (if not already installed):
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
-
Add to Cargo.toml:
[dependencies] bevy_fact_rule_event = "0.3.0"
-
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); }
-
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(); }
-
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"], ), ], ) -
Emit events in your game code:
fn player_collision_system( mut events: ResMut<PendingFactEvents>, ) { // Trigger rule evaluation events.emit("player_hit"); }
This project uses the following crates:
| Crate | Version | Description |
|---|---|---|
| bevy | 0.18 | Game engine |
| serde | 1.0 | Serialization framework |
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.
This project is licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.