demo_features/
main.rs

1#![allow(clippy::too_many_arguments)]
2
3use bones_bevy_renderer::{bevy::diagnostic::LogDiagnosticsPlugin, BonesBevyRenderer};
4use bones_framework::prelude::*;
5
6/// Create our root asset type.
7///
8/// The path to our root asset file is specified in `assets/pack.yaml`.
9#[derive(HasSchema, Default, Clone)]
10#[repr(C)]
11// Allow asset to be loaded from "game.yaml" assets.
12#[type_data(metadata_asset("game"))]
13struct GameMeta {
14    /// A lua script that will be run every frame on the menu.
15    menu_script: Handle<LuaScript>,
16    /// The image displayed on the menu.
17    menu_image: Handle<Image>,
18    /// The image for the sprite demo
19    sprite_demo: Handle<Image>,
20    /// Character information that will be loaded from a separate asset file.
21    atlas_demo: Handle<AtlasDemoMeta>,
22    /// The tilemap demo metadata.
23    tilemap_demo: Handle<TilemapDemoMeta>,
24    /// Audio track for the audio demo.
25    audio_demo: Handle<AudioSource>,
26    /// The color the debug lines in the debug line demo.
27    path2d_color: Color,
28    /// Localization asset
29    localization: Handle<LocalizationAsset>,
30    /// The font to use for the demo title.
31    title_font: FontMeta,
32    /// The list of font files to load for the UI.
33    fonts: SVec<Handle<Font>>,
34    /// The border to use the for main menu.
35    menu_border: BorderImageMeta,
36    /// The style to use for buttons.
37    button_style: ButtonThemeMeta,
38}
39
40/// Atlas information.
41#[derive(HasSchema, Default, Clone)]
42#[repr(C)]
43#[type_data(metadata_asset("atlas-demo"))]
44struct AtlasDemoMeta {
45    /// The size of the camera.
46    camera_size: CameraSize,
47    /// The sprite atlas for the player.
48    pub atlas: Handle<Atlas>,
49    /// The frames-per-second of the animation.
50    pub fps: f32,
51    /// The frames of the animation.
52    ///
53    /// Note: We use an [`SVec`] here because it implements [`HasSchema`], allowing it to be loaded
54    /// in a metadata asset.
55    pub animation: SVec<u32>,
56}
57
58/// Tilemap info.
59#[derive(HasSchema, Default, Clone)]
60#[repr(C)]
61#[type_data(metadata_asset("tilemap"))]
62struct TilemapDemoMeta {
63    /// The atlas that will be used for the tilemap.
64    pub atlas: Handle<Atlas>,
65    /// The size of the tile map in tiles.
66    pub map_size: UVec2,
67    /// The information about each tile in the tilemap.
68    pub tiles: SVec<TileMeta>,
69}
70
71/// Tile info.
72#[derive(HasSchema, Default, Clone)]
73#[repr(C)]
74struct TileMeta {
75    /// The tile position.
76    pos: UVec2,
77    /// The index of the tile in the atlas.
78    idx: u32,
79}
80
81/// Struct containing data that will be persisted with the storage API.
82#[derive(HasSchema, Default, Clone)]
83#[repr(C)]
84struct PersistedTextData(String);
85
86fn main() {
87    // Setup logging
88    setup_logs!();
89
90    // Register persistent data's schema so that it can be loaded by the storage loader.
91    PersistedTextData::register_schema();
92
93    // Create a bones bevy renderer from our bones game
94    let mut renderer = BonesBevyRenderer::new(create_game());
95    // Set the app namespace which will be used by the renderer to decide where to put
96    // persistent storage files.
97    renderer.app_namespace = (
98        "org".into(),
99        "fishfolk".into(),
100        "bones.demo_features".into(),
101    );
102    // Get a bevy app for running our game
103    renderer
104        .app()
105        // We can add our own bevy plugins now
106        .add_plugins(LogDiagnosticsPlugin::default())
107        // And run the bevy app
108        .run()
109}
110
111// Initialize the game.
112pub fn create_game() -> Game {
113    // Create an empty game
114    let mut game = Game::new();
115
116    // Configure the asset server
117    game.install_plugin(DefaultGamePlugin)
118        .init_shared_resource::<AssetServer>()
119        // Register the default asset types
120        .register_default_assets();
121
122    // Register our custom asset types
123    GameMeta::register_schema();
124    AtlasDemoMeta::register_schema();
125    TilemapDemoMeta::register_schema();
126
127    // Create our menu session
128    game.sessions.create_with("menu", menu_plugin);
129
130    game
131}
132
133/// Resource containing data that we will access from our menu lua script.
134#[derive(HasSchema, Default, Clone)]
135#[repr(C)]
136struct MenuData {
137    /// The index of the frame that we are on.
138    pub frame: u32,
139}
140
141/// Menu plugin
142pub fn menu_plugin(session: &mut SessionBuilder) {
143    // Register our menu system
144    session
145        // Install the bones_framework default plugins for this session
146        .install_plugin(DefaultSessionPlugin)
147        // Initialize our menu data resource
148        .init_resource::<MenuData>();
149
150    // And add our systems.
151    session
152        .add_system_to_stage(Update, menu_system)
153        .add_startup_system(menu_startup);
154}
155
156/// Setup the main menu.
157fn menu_startup(
158    mut egui_settings: ResMutInit<EguiSettings>,
159    mut clear_color: ResMutInit<ClearColor>,
160) {
161    // Set the clear color
162    **clear_color = Color::BLACK;
163    // Set the egui scale
164    egui_settings.scale = 2.0;
165}
166
167/// Our main menu system.
168fn menu_system(
169    meta: Root<GameMeta>,
170    ctx: Res<EguiCtx>,
171    mut sessions: ResMut<Sessions>,
172    mut session_options: ResMut<SessionOptions>,
173    mut exit_bones: Option<ResMut<ExitBones>>,
174    // Get the localization field from our `GameMeta`
175    localization: Localization<GameMeta>,
176    world: &World,
177    lua_engine: Res<LuaEngine>,
178) {
179    // Run our menu script.
180    lua_engine.run_script_system(world, meta.menu_script);
181
182    // Render the menu.
183    egui::CentralPanel::default()
184        .frame(egui::Frame::none())
185        .show(&ctx, |ui| {
186            BorderedFrame::new(&meta.menu_border).show(ui, |ui| {
187                ui.vertical_centered(|ui| {
188                    ui.add_space(20.0);
189                    ui.label(meta.title_font.rich(localization.get("title")));
190                    ui.add_space(20.0);
191
192                    if BorderedButton::themed(&meta.button_style, localization.get("sprite-demo"))
193                        .show(ui)
194                        .clicked()
195                    {
196                        // Delete the menu world
197                        session_options.delete = true;
198
199                        // Create a session for the match
200                        sessions.create_with("sprite_demo", sprite_demo_plugin);
201                    }
202
203                    if BorderedButton::themed(&meta.button_style, localization.get("atlas-demo"))
204                        .show(ui)
205                        .clicked()
206                    {
207                        // Delete the menu world
208                        session_options.delete = true;
209
210                        // Create a session for the match
211                        sessions.create_with("atlas_demo", atlas_demo_plugin);
212                    }
213
214                    if BorderedButton::themed(&meta.button_style, localization.get("tilemap-demo"))
215                        .show(ui)
216                        .clicked()
217                    {
218                        // Delete the menu world
219                        session_options.delete = true;
220
221                        // Create a session for the match
222                        sessions.create_with("tilemap_demo", tilemap_demo_plugin);
223                    }
224
225                    if BorderedButton::themed(&meta.button_style, localization.get("audio-demo"))
226                        .show(ui)
227                        .clicked()
228                    {
229                        // Delete the menu world
230                        session_options.delete = true;
231
232                        // Create a session for the match
233                        sessions.create_with("audio_demo", audio_demo_plugin);
234                    }
235
236                    if BorderedButton::themed(&meta.button_style, localization.get("storage-demo"))
237                        .show(ui)
238                        .clicked()
239                    {
240                        // Delete the menu world
241                        session_options.delete = true;
242
243                        // Create a session for the match
244                        sessions.create_with("storage_demo", storage_demo_plugin);
245                    }
246
247                    if BorderedButton::themed(&meta.button_style, localization.get("path2d-demo"))
248                        .show(ui)
249                        .clicked()
250                    {
251                        // Delete the menu world
252                        session_options.delete = true;
253
254                        // Create a session for the match
255                        sessions.create_with("path2d_demo", path2d_demo_plugin);
256                    }
257
258                    if let Some(exit_bones) = &mut exit_bones {
259                        if BorderedButton::themed(&meta.button_style, localization.get("quit"))
260                            .show(ui)
261                            .clicked()
262                        {
263                            ***exit_bones = true;
264                        }
265                    }
266
267                    ui.add_space(10.0);
268
269                    // We can use the `&World` parameter to access the world and run systems to act
270                    // as egui widgets.
271                    //
272                    // This makes it easier to compose widgets that have differing access to the
273                    // bones world.
274                    world.run_system(demo_widget, ui);
275
276                    ui.add_space(30.0);
277                });
278            })
279        });
280}
281
282/// Plugin for running the sprite demo.
283fn sprite_demo_plugin(session: &mut SessionBuilder) {
284    session
285        .install_plugin(DefaultSessionPlugin)
286        .add_startup_system(sprite_demo_startup)
287        .add_system_to_stage(Update, back_to_menu_ui)
288        .add_system_to_stage(Update, move_sprite);
289}
290
291/// System that spawns the sprite demo.
292fn sprite_demo_startup(
293    mut entities: ResMut<Entities>,
294    mut sprites: CompMut<Sprite>,
295    mut transforms: CompMut<Transform>,
296    mut cameras: CompMut<Camera>,
297    meta: Root<GameMeta>,
298) {
299    spawn_default_camera(&mut entities, &mut transforms, &mut cameras);
300
301    let sprite_ent = entities.create();
302    transforms.insert(sprite_ent, default());
303    sprites.insert(
304        sprite_ent,
305        Sprite {
306            image: meta.sprite_demo,
307            ..default()
308        },
309    );
310}
311
312fn move_sprite(
313    entities: Res<Entities>,
314    sprite: Comp<Sprite>,
315    mut transforms: CompMut<Transform>,
316    input: Res<KeyboardInputs>,
317    ctx: Res<EguiCtx>,
318) {
319    egui::CentralPanel::default()
320        .frame(egui::Frame::none())
321        .show(&ctx, |ui| {
322            ui.label("Press left and right arrow keys to move sprite");
323        });
324
325    let mut left = false;
326    let mut right = false;
327
328    for input in &input.key_events {
329        match input.key_code {
330            Set(KeyCode::Right) => right = true,
331            Set(KeyCode::Left) => left = true,
332            _ => (),
333        }
334    }
335
336    for (_ent, (_sprite, transform)) in entities.iter_with((&sprite, &mut transforms)) {
337        if left {
338            transform.translation.x -= 2.0;
339        }
340        if right {
341            transform.translation.x += 2.0;
342        }
343    }
344}
345
346/// Plugin for running the tilemap demo.
347fn tilemap_demo_plugin(session: &mut SessionBuilder) {
348    session
349        .install_plugin(DefaultSessionPlugin)
350        .add_startup_system(tilemap_startup_system)
351        .add_system_to_stage(Update, back_to_menu_ui);
352}
353
354/// System for starting up the tilemap demo.
355fn tilemap_startup_system(
356    mut entities: ResMut<Entities>,
357    mut transforms: CompMut<Transform>,
358    mut tile_layers: CompMut<TileLayer>,
359    mut cameras: CompMut<Camera>,
360    mut tiles: CompMut<Tile>,
361    meta: Root<GameMeta>,
362    assets: Res<AssetServer>,
363) {
364    spawn_default_camera(&mut entities, &mut transforms, &mut cameras);
365
366    // Load our map and atlas info
367    let map_info = assets.get(meta.tilemap_demo);
368    let atlas = assets.get(map_info.atlas);
369
370    // Create a new map layer
371    let mut layer = TileLayer::new(map_info.map_size, atlas.tile_size, map_info.atlas);
372
373    // Load the layer up with the tiles from our metadata
374    for tile in &map_info.tiles {
375        let tile_ent = entities.create();
376        tiles.insert(
377            tile_ent,
378            Tile {
379                idx: tile.idx,
380                ..default()
381            },
382        );
383        layer.set(tile.pos, Some(tile_ent))
384    }
385
386    // Spawn the layer
387    let layer_ent = entities.create();
388    tile_layers.insert(layer_ent, layer);
389    transforms.insert(layer_ent, default());
390}
391
392/// Plugin for running the atlas demo.
393fn atlas_demo_plugin(session: &mut SessionBuilder) {
394    session
395        .install_plugin(DefaultSessionPlugin)
396        .add_startup_system(atlas_demo_startup)
397        .add_system_to_stage(Update, back_to_menu_ui);
398}
399
400/// System to startup the atlas demo.
401fn atlas_demo_startup(
402    mut entities: ResMut<Entities>,
403    mut transforms: CompMut<Transform>,
404    mut cameras: CompMut<Camera>,
405    mut atlas_sprites: CompMut<AtlasSprite>,
406    mut animated_sprites: CompMut<AnimatedSprite>,
407    mut clear_color: ResMutInit<ClearColor>,
408    meta: Root<GameMeta>,
409    assets: Res<AssetServer>,
410) {
411    // Set the clear color
412    **clear_color = Color::GRAY;
413
414    // Get the atlas metadata
415    let demo = assets.get(meta.atlas_demo);
416
417    // Spawn the camera
418    let camera_ent = spawn_default_camera(&mut entities, &mut transforms, &mut cameras);
419    cameras.get_mut(camera_ent).unwrap().size = demo.camera_size;
420
421    // Spawn the character sprite.
422    let sprite_ent = entities.create();
423    transforms.insert(sprite_ent, default());
424    atlas_sprites.insert(
425        sprite_ent,
426        AtlasSprite {
427            atlas: demo.atlas,
428            ..default()
429        },
430    );
431    animated_sprites.insert(
432        sprite_ent,
433        AnimatedSprite {
434            frames: demo.animation.iter().copied().collect(),
435            fps: demo.fps,
436            ..default()
437        },
438    );
439}
440
441fn audio_demo_plugin(session: &mut SessionBuilder) {
442    session
443        .install_plugin(DefaultSessionPlugin)
444        .add_system_to_stage(Update, back_to_menu_ui)
445        .add_system_to_stage(Update, audio_demo_ui);
446}
447
448fn audio_demo_ui(
449    ctx: Res<EguiCtx>,
450    localization: Localization<GameMeta>,
451    mut audio: ResMut<AudioManager>,
452    meta: Root<GameMeta>,
453    assets: Res<AssetServer>,
454) {
455    egui::CentralPanel::default()
456        .frame(egui::Frame::none())
457        .show(&ctx, |ui| {
458            ui.vertical_centered(|ui| {
459                ui.add_space(50.0);
460                if ui.button(localization.get("play-sound")).clicked() {
461                    audio.play(&*assets.get(meta.audio_demo)).unwrap();
462                }
463            })
464        });
465}
466
467fn storage_demo_plugin(session: &mut SessionBuilder) {
468    session
469        .install_plugin(DefaultSessionPlugin)
470        .add_system_to_stage(Update, storage_demo_ui)
471        .add_system_to_stage(Update, back_to_menu_ui);
472}
473
474fn storage_demo_ui(
475    ctx: Res<EguiCtx>,
476    mut storage: ResMut<Storage>,
477    localization: Localization<GameMeta>,
478) {
479    egui::CentralPanel::default().show(&ctx, |ui| {
480        ui.add_space(20.0);
481
482        ui.vertical_centered(|ui| {
483            ui.set_width(300.0);
484            {
485                let data = storage.get_or_insert_default_mut::<PersistedTextData>();
486                egui::TextEdit::singleline(&mut data.0)
487                    .hint_text(localization.get("persisted-text-box-content"))
488                    .show(ui);
489            }
490            if ui.button(localization.get("save")).clicked() {
491                storage.save()
492            }
493        });
494    });
495}
496
497fn path2d_demo_plugin(session: &mut SessionBuilder) {
498    session
499        .install_plugin(DefaultSessionPlugin)
500        .add_startup_system(path2d_demo_startup)
501        .add_system_to_stage(Update, back_to_menu_ui);
502}
503
504fn path2d_demo_startup(
505    meta: Root<GameMeta>,
506    mut entities: ResMut<Entities>,
507    mut transforms: CompMut<Transform>,
508    mut cameras: CompMut<Camera>,
509    mut path2ds: CompMut<Path2d>,
510) {
511    spawn_default_camera(&mut entities, &mut transforms, &mut cameras);
512
513    let ent = entities.create();
514    transforms.insert(ent, default());
515    const SIZE: f32 = 40.;
516    path2ds.insert(
517        ent,
518        Path2d {
519            color: meta.path2d_color,
520            points: vec![
521                vec2(-SIZE, 0.),
522                vec2(0., SIZE),
523                vec2(SIZE, 0.),
524                vec2(-SIZE, 0.),
525            ],
526            thickness: 2.0,
527            ..default()
528        },
529    );
530}
531
532/// Simple UI system that shows a button at the bottom of the screen to delete the current session
533/// and  go back to the main menu.
534fn back_to_menu_ui(
535    ctx: Res<EguiCtx>,
536    mut sessions: ResMut<Sessions>,
537    mut session_options: ResMut<SessionOptions>,
538    localization: Localization<GameMeta>,
539) {
540    egui::TopBottomPanel::bottom("back-to-menu")
541        .frame(egui::Frame::none())
542        .show_separator_line(false)
543        .show(&ctx, |ui| {
544            ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
545                ui.add_space(20.0);
546                if ui.button(localization.get("back-to-menu")).clicked() {
547                    session_options.delete = true;
548                    sessions.create_with("menu", menu_plugin);
549                }
550            });
551        });
552}
553
554/// This is an example widget system.
555fn demo_widget(
556    // Widget systems must have an `In<&mut egui::Ui>` parameter as their first argument.
557    mut ui: In<&mut egui::Ui>,
558    // They can have any normal bones system parameters
559    meta: Root<GameMeta>,
560    egui_textures: Res<EguiTextures>,
561    // And they may return an `egui::Response` or any other value.
562) -> egui::Response {
563    ui.label("Demo Widget");
564    // When using a bones image in egui, we have to get it's corresponding egui texture
565    // from the egui textures resource.
566    ui.image(egui::load::SizedTexture::new(
567        egui_textures.get(meta.menu_image),
568        [50., 50.],
569    ))
570}