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 Session) {
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_startup_resource::<MenuData>();
149
150    // And add our systems.
151    session
152        .add_system_to_stage(Update, menu_system)
153        .add_system_to_stage(CoreStage::Startup, 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 Session) {
284    session
285        .install_plugin(DefaultSessionPlugin)
286        .add_system_to_stage(CoreStage::Startup, 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 Session) {
348    session
349        .install_plugin(DefaultSessionPlugin)
350        .add_system_to_stage(CoreStage::Startup, 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
369    // Create a new map layer
370    let mut layer = TileLayer::new(map_info.map_size, map_info.atlas);
371
372    // Load the layer up with the tiles from our metadata
373    for tile in &map_info.tiles {
374        let tile_ent = entities.create();
375        tiles.insert(
376            tile_ent,
377            Tile {
378                idx: tile.idx,
379                ..default()
380            },
381        );
382        layer.set(tile.pos, Some(tile_ent))
383    }
384
385    // Spawn the layer
386    let layer_ent = entities.create();
387    tile_layers.insert(layer_ent, layer);
388    transforms.insert(layer_ent, default());
389}
390
391/// Plugin for running the atlas demo.
392fn atlas_demo_plugin(session: &mut Session) {
393    session
394        .install_plugin(DefaultSessionPlugin)
395        .add_system_to_stage(CoreStage::Startup, atlas_demo_startup)
396        .add_system_to_stage(Update, back_to_menu_ui);
397}
398
399/// System to startup the atlas demo.
400fn atlas_demo_startup(
401    mut entities: ResMut<Entities>,
402    mut transforms: CompMut<Transform>,
403    mut cameras: CompMut<Camera>,
404    mut atlas_sprites: CompMut<AtlasSprite>,
405    mut animated_sprites: CompMut<AnimatedSprite>,
406    mut clear_color: ResMutInit<ClearColor>,
407    meta: Root<GameMeta>,
408    assets: Res<AssetServer>,
409) {
410    // Set the clear color
411    **clear_color = Color::GRAY;
412
413    // Get the atlas metadata
414    let demo = assets.get(meta.atlas_demo);
415
416    // Spawn the camera
417    let camera_ent = spawn_default_camera(&mut entities, &mut transforms, &mut cameras);
418    cameras.get_mut(camera_ent).unwrap().size = demo.camera_size;
419
420    // Spawn the character sprite.
421    let sprite_ent = entities.create();
422    transforms.insert(sprite_ent, default());
423    atlas_sprites.insert(
424        sprite_ent,
425        AtlasSprite {
426            atlas: demo.atlas,
427            ..default()
428        },
429    );
430    animated_sprites.insert(
431        sprite_ent,
432        AnimatedSprite {
433            frames: demo.animation.iter().copied().collect(),
434            fps: demo.fps,
435            ..default()
436        },
437    );
438}
439
440fn audio_demo_plugin(session: &mut Session) {
441    session
442        .install_plugin(DefaultSessionPlugin)
443        .add_system_to_stage(Update, back_to_menu_ui)
444        .add_system_to_stage(Update, audio_demo_ui);
445}
446
447fn audio_demo_ui(
448    ctx: Res<EguiCtx>,
449    localization: Localization<GameMeta>,
450    mut audio: ResMut<AudioManager>,
451    meta: Root<GameMeta>,
452    assets: Res<AssetServer>,
453) {
454    egui::CentralPanel::default()
455        .frame(egui::Frame::none())
456        .show(&ctx, |ui| {
457            ui.vertical_centered(|ui| {
458                ui.add_space(50.0);
459                if ui.button(localization.get("play-sound")).clicked() {
460                    audio.play(&*assets.get(meta.audio_demo)).unwrap();
461                }
462            })
463        });
464}
465
466fn storage_demo_plugin(session: &mut Session) {
467    session
468        .install_plugin(DefaultSessionPlugin)
469        .add_system_to_stage(Update, storage_demo_ui)
470        .add_system_to_stage(Update, back_to_menu_ui);
471}
472
473fn storage_demo_ui(
474    ctx: Res<EguiCtx>,
475    mut storage: ResMut<Storage>,
476    localization: Localization<GameMeta>,
477) {
478    egui::CentralPanel::default().show(&ctx, |ui| {
479        ui.add_space(20.0);
480
481        ui.vertical_centered(|ui| {
482            ui.set_width(300.0);
483            {
484                let data = storage.get_or_insert_default_mut::<PersistedTextData>();
485                egui::TextEdit::singleline(&mut data.0)
486                    .hint_text(localization.get("persisted-text-box-content"))
487                    .show(ui);
488            }
489            if ui.button(localization.get("save")).clicked() {
490                storage.save()
491            }
492        });
493    });
494}
495
496fn path2d_demo_plugin(session: &mut Session) {
497    session
498        .install_plugin(DefaultSessionPlugin)
499        .add_system_to_stage(CoreStage::Startup, path2d_demo_startup)
500        .add_system_to_stage(Update, back_to_menu_ui);
501}
502
503fn path2d_demo_startup(
504    meta: Root<GameMeta>,
505    mut entities: ResMut<Entities>,
506    mut transforms: CompMut<Transform>,
507    mut cameras: CompMut<Camera>,
508    mut path2ds: CompMut<Path2d>,
509) {
510    spawn_default_camera(&mut entities, &mut transforms, &mut cameras);
511
512    let ent = entities.create();
513    transforms.insert(ent, default());
514    const SIZE: f32 = 40.;
515    path2ds.insert(
516        ent,
517        Path2d {
518            color: meta.path2d_color,
519            points: vec![
520                vec2(-SIZE, 0.),
521                vec2(0., SIZE),
522                vec2(SIZE, 0.),
523                vec2(-SIZE, 0.),
524            ],
525            thickness: 2.0,
526            ..default()
527        },
528    );
529}
530
531/// Simple UI system that shows a button at the bottom of the screen to delete the current session
532/// and  go back to the main menu.
533fn back_to_menu_ui(
534    ctx: Res<EguiCtx>,
535    mut sessions: ResMut<Sessions>,
536    mut session_options: ResMut<SessionOptions>,
537    localization: Localization<GameMeta>,
538) {
539    egui::TopBottomPanel::bottom("back-to-menu")
540        .frame(egui::Frame::none())
541        .show_separator_line(false)
542        .show(&ctx, |ui| {
543            ui.with_layout(egui::Layout::bottom_up(egui::Align::Center), |ui| {
544                ui.add_space(20.0);
545                if ui.button(localization.get("back-to-menu")).clicked() {
546                    session_options.delete = true;
547                    sessions.create_with("menu", menu_plugin);
548                }
549            });
550        });
551}
552
553/// This is an example widget system.
554fn demo_widget(
555    // Widget systems must have an `In<&mut egui::Ui>` parameter as their first argument.
556    mut ui: In<&mut egui::Ui>,
557    // They can have any normal bones system parameters
558    meta: Root<GameMeta>,
559    egui_textures: Res<EguiTextures>,
560    // And they may return an `egui::Response` or any other value.
561) -> egui::Response {
562    ui.label("Demo Widget");
563    // When using a bones image in egui, we have to get it's corresponding egui texture
564    // from the egui textures resource.
565    ui.image(egui::load::SizedTexture::new(
566        egui_textures.get(meta.menu_image),
567        [50., 50.],
568    ))
569}