Crate bevy_yoleck

source ·
Expand description

Your Own Level Editor Creation Kit

Yoleck is a crate for having a game built with the Bevy game engine act as its own level editor.

Yoleck uses Plain Old Rust Structs to store the data, and uses Serde to store them in files. The user code defines populate systems for creating Bevy entities (populating their components) from these structs and edit systems to edit these structs with egui.

The synchronization between the structs and the files is bidirectional, and so is the synchronization between the structs and the egui widgets, but the synchronization from the structs to the entities is unidirectional - changes in the entities are not reflected in the structs:

┌────────┐  Populate   ┏━━━━━━┓   Edit      ┌───────┐
│Bevy    │  Systems    ┃Yoleck┃   Systems   │egui   │
│Entities│◄────────────┃Struct┃◄═══════════►│Widgets│
└────────┘             ┗━━━━━━┛             └───────┘
                         ▲
                         ║
                         ║ Serde
                         ║
                         ▼
                       ┌─────┐
                       │.yol │
                       │Files│
                       └─────┘

To support integrate Yoleck, a game needs to:

  • Define the entity structs, and make sure they implement:
    #[derive(Clone, PartialEq, Serialize, Deserialize)]
    The structs need to be deserializable form the empty object {}, because that’s how they’ll be initially created when the editor clicks on Add New Entity. Just slap #[serde(default)] on all the fields.
  • For each struct, use add_yoleck_handler to add a YoleckTypeHandler to the Bevy app.
    • Register edit systems on the type handler with edit_with.
    • Register populate systems on the type handler with populate_with.
  • If the application starts in editor mode:
  • If the application starts in game mode:

To support picking and moving entities in the viewport with the mouse, check out the vpeol_2d module. Helpers that can be used in vpeol_2d can be found in vpeol.

Minimal Working Example

use bevy::prelude::*;
use bevy_yoleck::bevy_egui::EguiPlugin;
use bevy_yoleck::{
    egui, YoleckEdit, YoleckExtForApp, YoleckLevelIndex, YoleckLoadingCommand,
    YoleckPluginForEditor, YoleckPluginForGame, YoleckPopulate, YoleckRawLevel,
    YoleckSyncWithEditorState, YoleckTypeHandler,
};
use serde::{Deserialize, Serialize};

fn main() {
    let is_editor = std::env::args().any(|arg| arg == "--editor");

    let mut app = App::new();
    app.add_plugins(DefaultPlugins);
    if is_editor {
        app.add_plugin(EguiPlugin);
        app.add_plugin(YoleckPluginForEditor);
        // Doesn't matter in this example, but a proper game would have systems that can work
        // on the entity in `GameState::Game`, so while the level is edited we want to be in
        // `GameState::Editor` - which can be treated as a pause state. When the editor wants
        // to playtest the level we want to move to `GameState::Game` so that they can play it.
        app.add_plugin(YoleckSyncWithEditorState {
            when_editor: GameState::Editor,
            when_game: GameState::Game,
        });
    } else {
        app.add_plugin(YoleckPluginForGame);
        app.add_state(GameState::Loading);
        // In editor mode Yoleck takes care of level loading. In game mode the game needs to
        // tell yoleck which levels to load and when.
        app.add_system_set(SystemSet::on_update(GameState::Loading).with_system(load_first_level));
    }
    app.add_startup_system(setup_camera);

    app.add_yoleck_handler({
        YoleckTypeHandler::<Rectangle>::new("Rectangle")
            .populate_with(populate_rectangle)
            .edit_with(edit_rectangle)
    });

    app.run();
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum GameState {
    Loading,
    Game,
    Editor,
}

fn setup_camera(mut commands: Commands) {
    commands.spawn_bundle(Camera2dBundle::default());
}

#[derive(Clone, PartialEq, Serialize, Deserialize)]
struct Rectangle {
    #[serde(default = "default_rectangle_side")]
    width: f32,
    #[serde(default = "default_rectangle_side")]
    height: f32,
}

fn default_rectangle_side() -> f32 {
    50.0
}

fn populate_rectangle(mut populate: YoleckPopulate<Rectangle>) {
    populate.populate(|_ctx, data, mut cmd| {
        cmd.insert_bundle(SpriteBundle {
            sprite: Sprite {
                color: Color::RED,
                custom_size: Some(Vec2::new(data.width, data.height)),
                ..Default::default()
            },
            ..Default::default()
        });
    });
}

fn edit_rectangle(mut edit: YoleckEdit<Rectangle>) {
    edit.edit(|_ctx, data, ui| {
        ui.add(egui::Slider::new(&mut data.width, 50.0..=500.0).prefix("Width: "));
        ui.add(egui::Slider::new(&mut data.height, 50.0..=500.0).prefix("Height: "));
    });
}

fn load_first_level(
    asset_server: Res<AssetServer>,
    level_index_assets: Res<Assets<YoleckLevelIndex>>,
    mut loading_command: ResMut<YoleckLoadingCommand>,
    mut game_state: ResMut<State<GameState>>,
) {
    let level_index_handle: Handle<YoleckLevelIndex> = asset_server.load("levels/index.yoli");
    if let Some(level_index) = level_index_assets.get(&level_index_handle) {
        // A proper game would have a proper level progression system, but here we are just
        // taking the first level and loading it.
        let level_handle: Handle<YoleckRawLevel> =
            asset_server.load(&format!("levels/{}", level_index[0].filename));
        *loading_command = YoleckLoadingCommand::FromAsset(level_handle);
        game_state.set(GameState::Game).unwrap();
    }
}

Re-exports

pub use bevy_egui;
pub use bevy_egui::egui;

Structs

Event that can be sent to control Yoleck’s editor.
Parameter for systems that edit entities. See YoleckEdit::edit.
The path for the levels directory.
A single section of the UI. See YoleckEditorSections.
Sections for the Yoleck editor window.
Used by Yoleck to determine how to handle the entity.
Marks an entity as a knob - a temporary helper entity used for editing another entity.
An handle for intearcing with a knob from an edit system.
An asset loaded from a .yoli file (usually index.yoli) representing the game’s levels.
Describes a level in the index.
A component that describes how Yoleck manages an entity under its control.
Parameter for systems that populate entities. See YoleckPopulate::populate.
An entry for a Yoleck entity, as it appears in level files.
Represents a level file.
Fields of the Yoleck editor.
Sync the game’s state back and forth when the level editor enters and exits playtest mode.
Descriptor for how Yoleck handles an entity type.

Enums

Events emitted by the Yoleck editor.
Whether or not the Yoleck editor is active.
Command Yoleck to load a level, represented as an asset to an handle.

Traits