Tag: bevy

  • building an in-game menu system from scratch (in Bevy)

    I’ve veered lightly away from building my virtual grocery store this week in favour of an equally important aspect of this game project: the menu system.

    Yikes! Yeah, I know, me too. I’d rather be painting sprites and building an interesting world, but oh that user interface is going to be important.

    As with almost everything I’ve approached this particular problem—that is, how to build a menu over top of a game world—incrementally.  And as I’ve been lightly glazing over the coding progress in most posts on this blog, I wanted to do something a little different and deep dive into how I approached this particular problem, if only because it took me two solid days to work through and get it functioning in the barest-of-bare minimums (that is, as of another three hour coding session this morning.)

    So, here goes.

    You may have picked up on the vibe that story is very important to what I’m building here. I am a novelist in my other crazy project life, so having a coherent story with lore is key to this whole deal.  To that effect one of the earlier things that I built (a week or so back in phase 0.2.3) was a kind of simple story engine that runs through the whole game.

    Stories in Pleck’s Mart are comprised of a few simple database tables: One keeps track of all the words, and one keeps track of all the statuses. Just to complicate things, a story can be comprised of multiple “beats” or pages. This is for both flow and because my font choice at the moment really only allows for about 250 characters on the screen at any given time, so that’s like 20 words. Any substantial story needs to graced with a bit more expositional text than a tweet, you know? In other words, pagination.

    #[derive(Component, Debug)]
    pub struct Storybeat {
        pub storybeat_id: i32,
        pub storybeat_code: String,
        pub storybeat_name: String,
        pub storybeat_nextcode: String,
        pub storybeat_complete: i32,
        pub storybeat_built: i32,
        pub storybeat_replayable: i32,
    }

    (There was a deliberate choice not to use bools for some of these status flags as I’ve got some ideas about non-binary status states and using the system for books, notes, etc, but more on that in a future post.)

    The story engine works pretty well, if I’m being honest. I’m happy with it so far and I haven’t run into any big flaws short of it could use a nicer UI design.

    All the chapters indexes are loaded into the status database on a new game initiation, and if a story has an unread status then a collision trigger coded into the game map can fling it’s unique code into the story queue which is constantly checking for active stories in the queue and bobs-yer-uncle it reads it all (text from the data file) into the database, spawns a story UI up on the screen and you can either NEXT your way through each beat or EXIT out and move along to just play the game. Meanwhile things churn in the background to change the status to read so that particular part of story never spawns again—

    —which leads to a little sad face from Brad as I’m building this game.

    See, unless you play the game over you can’t (couldn’t) go back and read the story chapters that you already triggered.  They were complete=1 and would never spawn again on any given save file. Thus, one of my first “this needs to be in the menu” features was the addition of a story history so a player could revisit and review and reread—and thinking about how to accomplish this was where I was sitting two-plus days ago, staring blankly into a computer screen full of five thousand lines of game code that barely even had a menu system, let alone something useful for a story history navigator.

    I started to address this problem by tucking in and refactoring the whole menu scheme that I’d started to build last phase. (It was okay, but it had some limitations.)

    First, I divided the in-game menu into three distinct, heirarchecal panels: there is now a TOP menu which is a kind of icon-based ribbon of menu categories. Next there is the LEFT menu which is a scrollable list of sub-categories. And finally there is the RIGHT menu which is for now a kind of panel that displays the results of whatever you selected in the left menu.

    To break that down with an example, when the STORY icon is selected in the TOP menu, then a scrollable list of STORY CHAPTERS appears in the LEFT menu which subsequently results in the selected story chapter’s TEXT BODY to display in the RIGHT panel.

    Like so:

    If all this sounds fairly straightforward, well, it is—but it took me a couple days of debugging and coding effort to not only get to that architecture straight in my head and in place, but also to make it actually work.

    how does it work? 

    For starters it’s all based on a collection of menu state components that are tracked and manipulated by player input.  This is the root and core of how this all functions. 

    That is to say, when a player moves into the GAME MENU App() state by pressing the M key while in-game, the App() moves out of the GAME state and into the overall GAME MENU state. (And vice versa on closing.)

    This action spawns all three menu panels:

    TOP menu has a GameMenuState of 0;

    LEFT menu has a ScrollMenuState of 0 with a bundled Length value;

    RIGHT menu is a blank canvas;

    A player now looking at the menu on the screen can either move Left or Right which cycles the GameMenuState from 0 to 1 to 2 to 3 and back to 0.

    #[derive(Bundle)]
    pub struct GameMenuBundle {
    	pub menu: Menu,
    	pub gamemenustate: GameMenuState,
    }

    Or, a player can move up and down which cycles the left menus’ ScrollMenuState from 0 to 1 all the way up to a variable length value stored in that state.  (Because of the variable length this also does some fancy math in there to adjust the scroll_y value so that the active item is within the view area of the left menu’s draw area. Math, math, always lots of math. And my daughter writing her Math final in her last year of high school in just two days from me penning these words wonders when she’ll ever need math again! Bah!

    #[derive(Bundle)]
    pub struct LeftMenuBundle {
    	pub menu: Menu,
    	pub scrollmenustate: ScrollMenuState,
    	pub scrolldelta: ScrollDelta,
    }

    If all this seems pretty basic, it is. The tricks all come when we need to the GAME MENU systems in the App() to react to all these variables.

    For starters the contents of the LeftMenu are uniform in design but simultaneously conditional on the GameMenuState. That is to say, it’s always a scrolling list with an icon and a piece of text, but what icons and what text depends on the number in that GameMenuState.

    For example, right now my STORY menu is the fourth icon on the top menu ribbon. This means that the App() listens for a GameMenuState of 3. If that happens to become true a lot of things then are conditionally allowed to happen:

    • the fourth icon sprite in the TOP menu “remaps” to the “highlighted” version of itself on a texture map so that it appears in colour instead of in greyscale. In other words, it highlights itself and de-highlights all the others.
    • a query extracts all the “complete” story codes (things the player has seen) from the database as a vector of data and iterates through them to build out an n=rows number of icon and story title pairs inside the LEFT menu canvas. In the case of a player who has just started the game and played into the first room, this means the function will spawn two item pairs in the list: (1) an icon an a piece of  text with the title of chapter 1 (the introductory story text) and (2) an icon an a title of chapter 2 (the story that activates when the player first enters the store).  Later, as more story beats are unlocked this list will grow longer.
    • the LeftMenuStatus resets to 0. (Top item in the scrollable list.)
    • the first item in the list of things in the left menu goes into it’s highlighted state in the same fashion as the top menu does when it is highlighted (ie. the sprite shifts its texture map and de-highlights everything else)
    • the right panel function checks for valid content based on a hierarchical check though GameMenuStatus of 3 and a LeftMenuStatus of 0, which in this case populates the canvas with the first story block’s text.
    • the right panel (still checking through this conditional mode) waits for a keypress and if the player clicks the NEXT key it does some more mathy-texty-database stuff to pull the next beat from the story chapter and then replaces the content of the right panel if that all happens.

    Of course, there are some extra conditionals and “something happened” checks tucked in there somewhere to balance things out and prevent these conditionals just going into processor eating loops of infinity, but those are details that don’t materially impact the overall deal here.

    The net result of all this is that if, while playing the game, the player presses the M key a menu appears.

    If the player then Arrow Key Left/Right’s over to the story menu, a list of her story history items will appear in the left panel. 

    And as they player Arrow Key Up/Down’s through that list of story history, the first beat of each story will appear in the right panel.

    And if more than one beat exists for a story, the player can press the N key to cycle endlessly through the various beats that exist in that chapter:

    let new_text: &str = this_story_text.as_str();
    								for mut this_text in rightcontentpanel.iter_mut() {									           
    	this_text.0.clear();
    	this_text.0.push_str(&new_text.clone());															}

    Voila. A menu with story history.

    Check.

    side note: If enough people are interested in such a thing, I could pull this menu code out of my main project, clean it up for general use, and release it as a Git repository.

    Comment if that’s of interest.

    looking back

    My biggest hangups after all this got working as I expected were making sure that the canvas contents were responsive to the conditional changes: mutable text fields, scrollable nodes, etc, etc — which you would think in retrospect should be pretty obvious but designing this all in raw code with overlapping coordinate systems across three spawned objects just seemed to be keeping track of things and naming stuff in meaningful ways.

    And having built it this way, I think—right now, I think—all this work lays the foundations for most every other menu in the game:

    • The items in any the left menu can be dynamically defined by the contents of a database or hand coded to be very specific items.
    • The contents of the right panel can be adapted to display just about anything, text, images, or inputs.
    • And, not that I have much to fill them at the moment but I left room for up to eight menu categories, which is very probably overkill.

    Now, back to building that virtual grocery store, huh?

  • after more seriously cold coding, progress abounds

    I hunkered. It helped that it’s been twenty-five degrees below zero outside and I can’t do much besides sit in the warm house and look out through the frosty windows. No dog walks. No running adventures. No casual outdoor excursions. Instead, I’ve spent two full days coding.

    I wrote just a couple days ago about my intention to work on this current phase of development (Space Carrot) and barely forty-eight hours later I’m posting again to say that I’m moving my efforts to a new sub-sub-version. I updated the milestone log with the results of 0.2.1 and I’m moving on to 0.2.2 as of my next coding session.

    What goes into writing a game like this?

    I’m not an expert, I’ll remind you. I’m winging this and hoping something comes out of it.   I’ve been working on some game story lore, fleshing out the mapping system on (digital) paper (ie sketching out things on my tablet) and sitting down to think about the mechanics of creating a game space. 

    After all, the whole point of Space Carrot (I realize that some people might read that as the name of the game and not just a development phase) is to create a navigable world for the game-proper to exist in.

    First, I worked through the nuances of creating a collision detection system that doesn’t weigh down the processor checking everything constantly. Now you can’t walk through walls. And since I have now forced every movement to happen inside the boundary of virtual walls, my player sprite can navigate around without falling out into the void. Testing will continue on this, but so far it works.

    Second, I had to figure out the strangenes of door-to-door inter-room navigation. This, also a foundational aspect of the game, seems simple at first but in reality it’s a kind of interplay of a player “colliding” with door, waiting for an action to be triggered, and then figuring out what door she collided with is (specifically) and where it leads. Since this whole thing will exist as a series of  virtual rooms, any given door maps to a corresponding door on a different room map, all of it encoded into a few lines of data and “match” statements in the little door collision detection function. In other words, a door collision is detected and if that happens a trigger action can be accepted and validated, and if that happens the player’s location can be updated, and if that happens the code goes uh-oh, I’m displaying the wrong room because the player isn’t here anymore and then the code despawns wrong room and then spawns the new room where the player is now located and also standing at the cooresponding door she just walked through.  And all that happens in a fraction of a second so it seems to the player (maybe you) that your little dude just walked through a door into a new room. Sketch that out in a math equation, huh?

    Finally, I wanted to add a bit of explanatory text to the scene. Specifically,  this comes in the form of a little corner notice room label, when a new room was entered (it’s going to get confusing otherwise, believe me) and any other little notices that might help the player poke around the game.  This was relatively the most simple part, but I did need to figure out Bevy’s new (0.15) Text system, which has not been updated in almost every 3rd party tutorial and code sample. Every explanation of how to use this leaned into the TextBundle constructor which has been deprecated in the latest version. Good thing this whole langauge is starting to make more sense to me with each new function I create.

    So. That’s that. Two solid days in front of Visual Studio and a compiler—and countless times typing the run command and debugging my buggy code until I incrementally added all this stuff and it seems to work.

    But then, this is just a few more basic building blocks of a much bigger plan.

  • the obligatory “Hello world!”

    As of late 2024 I’ve been incrementally teaching myself how to do this Rust Language coding thing and it has somehow started to emerge as a game. My first love is and will always be words, tho, so here begins many, many words on the topic of building a game from scratch.

    So.

    Thing is, as of this week my efforts have come full circle.

    Kinda.

    An early attempt to build a game tick time loop was a factor of trying to create a series of nested loops that managed random things in a database. It would count off the seconds and if sufficient time had passed it would tell the database to +1 to the age of a game object —the game is about running a little [redacted] and [redacted] exists [redacted] but age consistently across all [redacted] blah blah—I’ll explain it more as we go, later. Needless to say I was leaning pretty heavily into the database design as an active tool to manage all my game variables all the time, including during the current screen gameplay.

    That was silly.

    So I started learning about this game engine called Bevy.

    Bevy handles all sorts of things. Or, essentially, it wraps all the regular pieces you would use—should use—for a game into objects that are then managed by the game engine. This means it can spawn a player and a board and objects and they can exist as virtual game pieces, but also as things that can display on the screen or whatever. It’s all neat and tidy.

    I wrote a bunch of code to use a bunch of array data, essentially an x and y grid of tiles that maps out a player area in the game. I call these simply, obviously rooms. A room can be something like the back store room where you keep and get new products. Or there are storefront rooms which are basically all the same across dimensions but may look slightly different or have different texture files, that type of thing. But the core here is that as much as a player can move around inside each room, there will be doors that take them to other rooms.

    Hense, the full circle.

    Aaaaand—now I am back to the database.

    I started encoding a bunch of database functions just this morning that are updated at various times during play. When a room is exited, I am thinking, the database will slurp up all the room data, store the state of all the objects in that room and essentially create a kind of mapped overlay of the base map for each room, but at coordinates there may be products or tools or other things that despawn out of the active play but can be respawned when the player re-enters that particular play area.

    I lean heavily into database driven design online and I am very comfortable thinking about things that way, but in many ways it this is because databases are virtually a necessity for web based stuff if only because you need preserve a lot of data between pages and across sessions. Each page is essentially a little app that builds itself dynamically from the data in the database and doesn’t rely on a system loading the entirety of the site into memory and keeping track of everything across every session then passing all of it from page to page. Now that would be silly,

    I figure, too, that this approach may really work to my advantage in the roguelite game plan that I am contemplating. Certain data can always be persisistent across runs of the game and just doesn’t get purged from the databse on initiation.

    I sat down here to write something about the abstractions of learning this language but now that my ideas are actually starting to take shape and form I’m noticing that I can write in more than abstractions. And that’s a good thing, right? Maybe silly. Maybe more.