LessNoise.Sh some signal

Roguelike Dev Day 6: UI Stuff

(2 minutes) programming, rust, game, roguelike

Making a User Interface

I'm starting Chapter 8 tonight.

I'm really disliking all the "magic constants" in this code. Take this code, which renders the message log:

let log = ecs.fetch::<GameLog>();
let mut y = 44;
for s in log.entries.iter().rev() {
    if y < 49 { ctx.print(2, y, s); }
    y += 1;
}

What are the numbers 44, 49, and 2 for?

I believe 44 & 49 tell the program to write the messages between lines 44 and 49 (and those lines are only special because of the chosen dimensions of our game). And reading the documentation for ctx.print, the 2 is the x-coordinate of where to start printing the message.

I'm guessing these magic constants will be cleaned up later...

Also, I've noticed that the monsters won't chase me around corners, and it makes me sad. :'(

Items & Inventory

On to Chapter 9!

...

Epiphany

I'm pretty happy. So yesterday I was frustrated by the explicitly bracketed scopes all over the place in this code, and wanted to know if there is a better pattern. Today I just figured out a better pattern: put the scope in a function!

As an example, I turned this code...

fn random_monster(ecs: &mut World, x: i32, y: i32) {
    let roll :i32;
    {
        let mut rng = ecs.write_resource::<RandomNumberGenerator>();
        roll = rng.roll_dice(1, 2);
    }
    match roll {
        1 => { orc(ecs, x, y) }
        _ => { goblin(ecs, x, y) }
    }
}

Into this...

fn roll(ecs: &mut World, dice_count: i32, dice_sides: i32) -> i32 {
    ecs.write_resource::<RandomNumberGenerator>()
        .roll_dice(dice_count, dice_sides)
}

fn random_monster(ecs: &mut World, x: i32, y: i32) {
    match roll(ecs, 1, 2) {
        1 => orc(ecs, x, y),
        _ => goblin(ecs, x, y),
    }
}

And then you also get all the other benefits that go with smaller functions: reusability, testability, etc.

I also managed to refactor that code that was bugging me yesterday using a function, but it's not as compact of an example:


impl State {
    // Other state stuff here

    fn runstate(&mut self) -> RunState {
        *self.ecs.fetch::<RunState>()
    }
}

impl GameState for State {
    fn tick(&mut self, ctx: &mut Rltk) {
        // Other tick stuff here

        let mut newrunstate = self.runstate();

        match newrunstate {
            RunState::PreRun => {
                self.run_systems();
                self.ecs.maintain();
                newrunstate = RunState::AwaitingInput;
            }
            // More matches here
        }

        // More tick stuff here
    }
}

Event-driven

The ECS stuff takes getting used to. It seems to be an event-driven architecture.

The specific thing that made me realize this is the WantsToPickupItem component in this chapter. This component represents an event, or notice, that any other part of the game (a "system") can subscribe to.

Oh. It's a state machine. The ECS keeps track of the world's state, and the systems run the transitions from state to state.

Why represent the transition as another state? I've found it useful in my own work. As a simple example, I often write web pages that need to fetch data from an API, and model the process with four states: Waiting, Loading, Error, and Ready. Waiting is the initial state without data, Loading is the transition state that represents the app is fetching data, and then Error & Ready are the two final states, depending on the outcome of the API fetch.

The real question is how to decide when to represent a transition as a state (like Loading), and when to implement a transition as a state boundary (ie, if I had no loading state and went straight from Waiting to either Error or Ready). I'm not sure I have a good answer to that right now.

That's it for tonight. I'll have to finish Chapter 9 another time.