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:The structs need to be deserializable form the empty object
#[derive(Clone, PartialEq, Serialize, Deserialize)]
{}
, 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 aYoleckTypeHandler
to the Bevy app.- Register edit systems on the type handler with
edit_with
. - Register populate systems on the type handler with
populate_with
.
- Register edit systems on the type handler with
- If the application starts in editor mode:
- Add the
EguiPlugin
plugin. - Add the
YoleckPluginForEditor
plugin. - Use
YoleckSyncWithEditorState
to synchronize the game’s state with theYoleckEditorState
(optional but highly recommended)
- Add the
- If the application starts in game mode:
- Add the
YoleckPluginForGame
plugin. - Use the
YoleckLevelIndex
asset to determine the list of available levels (optional) - Use
YoleckLoadingCommand
to load the level.
- Add the
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
YoleckEdit::edit
.YoleckEdit::edit
.YoleckEditorSections
..yoli
file (usually index.yoli
) representing the game’s levels.YoleckPopulate::populate
.YoleckPopulate::populate
.