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│◄────────────┃Component┃◄═══════════►│Widgets│
└────────┘             ┃Structs  ┃             └───────┘
                       ┗━━━━━━━━━┛
                           ▲
                           ║
                           ║ Serde
                           ║
                           ▼
                         ┌─────┐
                         │.yol │
                         │Files│
                         └─────┘

To support integrate Yoleck, a game needs to:

To support picking and moving entities in the viewport with the mouse, check out the vpeol_2d and vpeol_3d modules. After adding the appropriate feature flag (vpeol_2d/vpeol_3d), import their types from bevy_yoleck::vpeol::prelude::*.

§Example

use bevy::prelude::*;
use bevy_yoleck::bevy_egui::EguiPlugin;
use bevy_yoleck::prelude::*;
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 {
        // 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_plugins(EguiPlugin {
            enable_multipass_for_primary_context: true,
        });
        app.add_plugins(YoleckSyncWithEditorState {
            when_editor: GameState::Editor,
            when_game: GameState::Game,
        });
        app.add_plugins(YoleckPluginForEditor);
    } else {
        app.add_plugins(YoleckPluginForGame);
        app.init_state::<GameState>();
        // 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_systems(Update, load_first_level.run_if(in_state(GameState::Loading)));
    }
    app.add_systems(Startup, setup_camera);

    app.add_yoleck_entity_type({
        YoleckEntityType::new("Rectangle")
            .with::<Rectangle>()
    });
    app.add_yoleck_edit_system(edit_rectangle);
    app.add_systems(YoleckSchedule::Populate, populate_rectangle);

    app.run();
}

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

fn setup_camera(mut commands: Commands) {
    commands.spawn(Camera2d::default());
}

#[derive(Clone, PartialEq, Serialize, Deserialize, Component, YoleckComponent)]
struct Rectangle {
    width: f32,
    height: f32,
}

impl Default for Rectangle {
    fn default() -> Self {
        Self {
            width: 50.0,
            height: 50.0,
        }
    }
}

fn populate_rectangle(mut populate: YoleckPopulate<&Rectangle>) {
    populate.populate(|_ctx, mut cmd, rectangle| {
        cmd.insert(Sprite {
            color: bevy::color::palettes::css::RED.into(),
            custom_size: Some(Vec2::new(rectangle.width, rectangle.height)),
            ..Default::default()
        });
    });
}

fn edit_rectangle(mut ui: ResMut<YoleckUi>, mut edit: YoleckEdit<&mut Rectangle>) {
    let Ok(mut rectangle) = edit.single_mut() else { return };
    ui.add(egui::Slider::new(&mut rectangle.width, 50.0..=500.0).prefix("Width: "));
    ui.add(egui::Slider::new(&mut rectangle.height, 50.0..=500.0).prefix("Height: "));
}

fn load_first_level(
    mut level_index_handle: Local<Option<Handle<YoleckLevelIndex>>>,
    asset_server: Res<AssetServer>,
    level_index_assets: Res<Assets<YoleckLevelIndex>>,
    mut commands: Commands,
    mut game_state: ResMut<NextState<GameState>>,
) {
    // Keep the handle in local resource, so that Bevy will not unload the level index asset
    // between frames.
    let level_index_handle = level_index_handle
        .get_or_insert_with(|| asset_server.load("levels/index.yoli"))
        .clone();
    let Some(level_index) = level_index_assets.get(&level_index_handle) else {
        // During the first invocation of this system, the level index asset is not going to be
        // loaded just yet. Since this system is going to run on every frame during the Loading
        // state, it just has to keep trying until it starts in a frame where it is loaded.
        return;
    };
    // 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));
    commands.spawn(YoleckLoadLevel(level_handle));
    game_state.set(GameState::Game);
}

Re-exports§

pub use bevy_egui;
pub use bevy_egui::egui;

Modules§

exclusive_systems
knobs
level_files_upgrading
prelude
vpeol
Viewport Editing Overlay - utilities for editing entities from a viewport.
vpeol_2d
Viewport Editing Overlay for 2D games.
vpeol_3d
Viewport Editing Overlay for 3D games.

Structs§

YoleckBelongsToLevel
A marker for entities that belongs to the Yoleck level and should be despawned with it.
YoleckDirective
Event that can be sent to control Yoleck’s editor.
YoleckEditMarker
Marks which entities are currently being edited in the level editor.
YoleckEditableLevels
Accessible only to edit systems - provides information about available levels.
YoleckEditorLevelsDirectoryPath
The path for the levels directory.
YoleckEditorSection
A single section of the UI. See YoleckEditorSections.
YoleckEditorSections
Sections for the Yoleck editor window.
YoleckLevelInEditor
Automatically added to level entities that are being edited in the level editor.
YoleckLevelInPlaytest
Automatically added to level entities that are being play-tested in the level editor.
YoleckLevelJustLoaded
During the YoleckSchedule::LevelLoaded schedule, this component marks the level entities that were just loaded and triggered that schedule.
YoleckManaged
A component that describes how Yoleck manages an entity under its control.
YoleckPluginForEditor
YoleckPluginForGame
YoleckPopulateContext
A context for YoleckPopulate::populate.
YoleckSystemMarker
See YoleckMarking.

Enums§

YoleckEditorEvent
Events emitted by the Yoleck editor.
YoleckEntityLifecycleStatus
YoleckSchedule
Schedules for user code to do the actual entity/level population after Yoleck spawns the level “skeleton”.

Traits§

YoleckExtForApp

Functions§

yoleck_exclusive_system_cancellable
Pipe an exclusive system into this system to make it cancellable by either pressing the Escape key or clicking on a button in the UI.
yoleck_map_entity_to_uuid
Transforms an entity to its UUID. Meant to be used with Yoleck’s exclusive edit systems and with Bevy’s system piping.