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:
- Define the component structs, and make sure they implement:
#[derive(Default, Clone, PartialEq, Component, Serialize, Deserialize, YoleckComponent)]
- For each entity type that can be created in the level editor, use
add_yoleck_entity_type
to add aYoleckEntityType
. UseYoleckEntityType::with
to register theYoleckComponent
s for that entity type. - Register edit systems with
add_yoleck_edit_system
. - Register populate systems on
YoleckSchedule::Populate
- 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) - Spawn an entity with the
YoleckLoadLevel
component to load the level. Note that the level can be unloaded by despawning that entity or by removing theYoleckKeepLevel
component that will automatically be added to it.
- Add the
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§
- Yoleck
Belongs ToLevel - A marker for entities that belongs to the Yoleck level and should be despawned with it.
- Yoleck
Directive - Event that can be sent to control Yoleck’s editor.
- Yoleck
Edit Marker - Marks which entities are currently being edited in the level editor.
- Yoleck
Editable Levels - Accessible only to edit systems - provides information about available levels.
- Yoleck
Editor Levels Directory Path - The path for the levels directory.
- Yoleck
Editor Section - A single section of the UI. See
YoleckEditorSections
. - Yoleck
Editor Sections - Sections for the Yoleck editor window.
- Yoleck
Level InEditor - Automatically added to level entities that are being edited in the level editor.
- Yoleck
Level InPlaytest - Automatically added to level entities that are being play-tested in the level editor.
- Yoleck
Level Just Loaded - During the
YoleckSchedule::LevelLoaded
schedule, this component marks the level entities that were just loaded and triggered that schedule. - Yoleck
Managed - A component that describes how Yoleck manages an entity under its control.
- Yoleck
Plugin ForEditor - Yoleck
Plugin ForGame - Yoleck
Populate Context - A context for
YoleckPopulate::populate
. - Yoleck
System Marker - See
YoleckMarking
.
Enums§
- Yoleck
Editor Event - Events emitted by the Yoleck editor.
- Yoleck
Entity Lifecycle Status - Yoleck
Schedule - Schedules for user code to do the actual entity/level population after Yoleck spawns the level “skeleton”.
Traits§
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.