Roguelike Dev Day 6: UI Stuff
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.