Day 2 – Menu creation
The basics of UI – a main menu – has been created.
(I'll use this blog to work through my issues and thoughts while learning how to use the Amethyst engine.)
Creating a menu forced me to learn two aspects of game implementation: switching from one state to another (here main menu -> game play) and drawing a user interface.
Drawing the menu
Drawing an interface is conceptually simple but a bit more complicated to get right, as probably anyone who has made a web site can attest to. There's the matter of creating the visual elements, grouping them in desired blocks and positioning them correctly.
Amethyst has a UI crate whose documentation immediately throws a couple dozen structs in your face to sort through. From an earlier tutorial I knew that UiText and UiTransform are the main building blocks for my simple text based menu. Alas, no helper functions in sight for easily chaining elements.
Or so I thought, until I looked at some examples of UiWidget which uses Rust`s powerful enum variants system to create a hierarchy of containers and different items, like:
Container( transform, /* Size and positioning of main div */ children: [ Label( /* A text widget */ transform, /* Positioning of this is relative to the main div */ text, ), Container( /* Another grouped block */ transform, /* Every level in the recursion makes positions relative to the closest parent */ children: [Label(...), Image(...), etc.], ), ... ], )
Great! Unfortunately as far as I can tell this is currently only for creating these hierarchies by reading it from disk using UiCreator instead of coding the hierarchy. This makes it easy to modify the structure but 1) as far as I can tell all values have to be entered manually, so if I want to change a padding value it has to be changed at all entries instead of once in a definition file, 2) there's a lot of boilerplate text for every entry, and 3) I think I'd like to determine certain values at run time depending on the rendering resolution, although I haven't come that far yet.
For now though it's more than sufficient for my needs – and since Amethyst is still under development I figure the API will improve a lot over time.
Starting the game – changing states
Switching states is reasonably well documented in the official book although it's shown at a few different places. It's also slightly confusing to keep all of the different variants of EmptyState and EmptyTrans versus SimpleState and SimpleTrans or something custom made in mind for now. Basically, a group of methods (update, fixed_update and handle_event) are run on the active state on every iteration of the main loop. These are modified to detect and perform state changes.
From the main menu I want the game to either enter the game if "p" is pressed or exit to desktop if "q" is. Either will be an event, signaled by the engine. So we detect them and answer with the corresponding signal, which the engine will parse.
fn handle_event(&mut self, _data: StateData<_>, event: StateEvent) -> SimpleTrans { if let StateEvent::Window(event) = event { if is_key_down(&event, VirtualKeyCode::P) { // Here the main state `Regular` is created // and returned through a signal return Trans::Push(Box::new(Regular::default())); } else if is_key_down(&event, VirtualKeyCode::Q) { return Trans::Quit; // Signal the engine to exit } } Trans::None // Carry on, then }
If the Trans::Push signal is returned to the engine it will push the contained state as the new active and execute its on_start method. Easy! I also went through the steps to make the game play systems execute only when the game play state is active instead of at all times, as described here. Understanding states a bit better I can easily imagine extensions for eg. dialogue or really anything that moves away from the main game loop.
Undrawing the menu
But oh no, as I tested this out I noticed that the main menu was still drawn over the game play even after the switch – the drawn entities are still active in the world even if the state that created them is inactive! (which isn't a surprise given how the ECS system that Amethyst uses works: there is by default no hierarchy between entities, they all just exist)
The solution is simple. When a state is paused it's `on_pause` method is called. This is easily modified to remove the UI entities (or rather, hide them from the rendering step).
fn on_pause(&mut self, data: StateData<'_, GameData<'_, '_>>) { if let Some(ui) = self.ui_entity { // self.ui_entity is the root of all // the main menu's UI components let world = data.world; hide_entity_and_children(ui, world); } }
Unfortunately, UiCreator which loads my main menu only returns a handle to its root container. I need to trace all of its children.
Now, the current state of the Amethyst documentation is that relationships between some components and how the engine uses them tend to be understated or even implicit rather than written out. UiWidget creates relative positioning to containers by each children getting a `Parent { entity }` component, from which the parent's position (if available) is extracted and so on. There is no reverse link from a parent to their child. However, we can build a chain by going through all relevant entities and checking whether they have the needle entity as parent.
fn find_children(current_entity: Entity, world: &World) -> Vec<Entity> { let entities = world.read_resource::<EntitiesRes>(); let parents = world.read_storage::<Parent>(); let ui_transforms = world.read_storage::<UiTransform>(); (&*entities, &parents, &ui_transforms) .join() .filter(|(_, Parent { entity: parent }, _)| parent == ¤t_entity) .map(|(child, _, _)| child) .collect::<Vec<_>>() }
I'm not sure if this is what would be called an anti-pattern but it works well enough for now (better would be to create and store this list after the menu is initiated, but all things in due time). I also know there is some sort of more formal hierarchy system available but I haven't dived into that yet.
Finally, hiding is done by adding the Hidden component to desired entities (took me a while to find it in the core module). I could remove the entities fully and recreate them if the main menu is accessed again but at this time there's little point to that.
fn hide_entity_and_children(current_entity: Entity, world: &mut World) { // Recursively find and hide all children for entity in find_children(current_entity, world) { hide_entity_and_children(entity, world); } let mut hidden_store = world.write_storage::<Hidden>(); hidden_store .insert(current_entity, Hidden) .expect("could not access Hidden entity storage"); }
That's it for now.
Files
Windy City Politics
More posts
- Day 11 – A mostly finished inklingJul 11, 2019
- Day 7 – Gronking dialogue graphsJul 07, 2019
- Day 5 – Dialogue systemsJul 04, 2019
- Day 4 – Revenge on prefabsJul 03, 2019
- Day 3 – Crying with prefabsJul 02, 2019
- Day 1 – Joining a jamJun 30, 2019
Leave a comment
Log in with itch.io to leave a comment.