Seven Lessons from 7DRL 2024
Another year, another Seven Day Roguelike challenge is upon us! This year was the 20th time it happened, and I submitted my first finished game (yes, it has a boss level and everything), and I'm really tired and really happy. There are many things that could have been, but this is the game it is, and that makes it infinitely better. Now, let's go over some lessons I've written down during these seven days. Some of these are general game development, some are specific to Rust and bevy, and some -- some might just be me! Enjoy.
Not every gameplay aspect is born equal and accepting this yields speed
Your player character isn't just anyone! Some things can be specific. Enemies can outwardly do the same things the player is doing, but internally different! In my case, the throwing mechanics were very hard to transpose to enemies - the player can easily throw stuff, but it would have to dramatically change for enemies to do the same. This annoyed me a lot, but then it occurred to me that I could make a carbon copy for them that skipped over the player-specific behaviours and just, well...
There were crashes and all of them were for STUPID reasons
One of the main mechanics in HELLTH is tied to the hitpoints one has, not only their number, but their characteristics. In short, your health points can carry stat boosts - a stat and the value by which it affects it. You also have focus, which can be used to target a specific health point, but it starts at the right. Because it was easy the first time around, and because it was day 6, late, and I was tired, there's a lot of copy/paste code around that part. A part of that copy paste is something along the lines of...
for (index, (stat, val)) in character.hp.iter().enumerate() {
let pos = index - 1 - focus as usize; // <---- BUG!
// ... do something cool with pos here
}
index
in the case above is usize
, and focus
can grow as much as you like. A line like this goes below zero, and this causes the game to just crash. It's not even what I'd call an obvious bug, it's an obvious misuse of usize
!
Lesson is: always work with isize
if possible! If you don't expect to have Yugioh numbers, it's the way to go, and even then - it might work!
Another thing that caused people to ragequit is a freeze that happens sometimes after either an ascension or death. I think it has to do with the turn order, which is a priority queue. I think some actor survives from the previous game, comes to the top of the order, but isn't... evaluated? So it takes 100% CPU power and lags into infinity. It's annoying to debug because I didn't setup everything from the get-go. Next time, I'll be doing tracking first.
Passing Res
objects to functions
A rather Rust/bevy thing that happened over and over and I realized too late. If you need a resource from a system (so, a Res<T>
) passed into a function, pass .as_ref()
instead of passing the Res
object itself! The same works for ResMut
and .as_mut()
. I remembered this late and laughed at my own silliness... Strive to always use the most general form of whatever you're passing in, to have it work in as many situations as possible.
It's a short one, but it would have saved me A LOT of trouble in writing and thinking about complex things.
DearImgui
is great if you have fewer than seven days to make UI for your game
Having UI be a function comes very natural, and is only missing a few features in bevy to make it super cool! I'd sum up the missing parts as:
including and using sprites within the UI needs to be easy
text alignment needs to be simpler to do than manual calculus
changing font would be cool
Imgui is a good fit with Run Conditions, to make sure some parts of the UI are drawn only when needed, guided by the state of the game. Here's a personal, subjective, opinion, but don't you think UI is more logical as function-like, instead of object-like? I find myself burdened by thinking about declarational things when I don't need a piece of UI; I kinda want it there for me to call when I do instead. In that sense, I find bevy's UI framework hard to use, and will be looking at potential solutions.
One of the surprising things is that people not only didn't mind the look and feel of Imgui, but rather called it very readable, unique, and even the factor that makes this game truly shine. I have to reconsider my opinions of simple UI design!
Data needs to be recognized as data early
I left many things in code because it was easier to draft them like that -- or so it seemed, but this lead to working on cool shortcuts that more-or-less tied me down. Soon, the data was no longer data -- it was interspersed with callbacks, and tied to the lifetimes of variables -- and I lost the ability to quickly iterate.
Next time, I want to have ready the framework that will enable me to toss enemies, items, and other things into data easily. This might be in the form of pure data (maybe even TOML) extended in Rust with semantic attachments that change data into behaviour.
We need to talk about roguelikes and carpal tunnel due to repetitive inputs
Walking is a chore. Autoexplore is cool, using vim keys and gamepads is cool, but maybe the problem is deeper: the unit step is at the same time very important and very small. Mouse input is cool but robs me, for some reason, of the feeling of total control that I have using a keyboard. I can kinda feel some good experience floating in this thought space there, but I'm not entirely sure what it is yet.
Would a grid-based game work with controls that allow more flow? As in, imagine if you hold the key, the character moves between the tiles seamlessly, but if you let go, it comes to land on the nearest grid tile. I don't know...
Internal mechanics are internal for a reason
No enemies in HELLTH have pathfinding, and no one believes me when I just say it. People want to believe. If you can get away with something cheap to do and cheap to execute, try it out. In my case, it goes like this: I don't want super-intelligent enemies who hunt you down from the corners of the map, I want them to be quite fair and not do much if you don't see them coming. This is why I needed them to act when you see them, which in turn lead me to only keep the player's field of view and use it to inspect whether enemies see you too. The logic wasn't that they have the same sight as you do (as that, too, can be affected via your build), but rather that they always have a larger FOV than the enemies. It's not about realistic simulation, it's about information control. If you want short-sighted enemies, use your FOV and then check against the distance you want.
I can hear people going "yes, but what about moving them outside your view, then they can't follow into your view", and to you I say: yes they do! Queues, pending actions, and thinking ahead: instead of having enemies think about you only when you see them, when they see you, they plan ahead. For example, they might want to prepare the following actions...
if distance > 5.0 {
vec![
a_track(mob, player), a_track(mob, player),
a_random_walk(mob), a_track(mob, player)),
}
The a_track
(I prepended a_
to all my actions to make the calls very visible) action moves towards the player in this case -- the player's position isn't taken into account, so that if the player moves, they'll use the current position even if out of sight. This makes them seem like they're actually searching if they walk into your FOV, but then the random walk makes you second guess. And then they a_track
again and seem smart and dangerous!
The AI scripts are fun and simple to do, and I added a_behave
that allows for them to change behaviours on a whim, allowing for others to influence them. Even though I didn't have this in the game, I'm planning to have orcs yell at goblins to tell them where you are, even if they don't see you. They'd yell, cause the goblins to put several a_track
actions into their bar, and then go back into chaos.
Final Thoughts and Announcements
One of the things people similarly don't see is the pressure we put on ourselves. I fell apart as soon as the jam was over. My voice is lost and yet to recover. Choose wisely on how to spend those moments! Don't do new developments for #7drl. I had the luck to have thought about this week for the other 51 weeks of the previous year, and still I've barely made it. That said, that's the main point of this new blog and the topic it's about: Svarog.
Besides it being the dreaming pagan deity of my fellow Slavs in whose dreams the world happens to be in, Svarog is my bevy-powered attempt at a one-stop shop library for roguelikes in Rust. It's not meant to satisfy everything and everyone but rather be modular enough to allow fast work within some strict definition of "roguelike". It won't even consider roguelite development, for example. It's going to be traditional, modular, and has already been used to develop HELLTH. It's not yet battle-ready, but it is battle-tested. It almost failed, but we pulled through. I'll be posting weekly updates on progress and integration here, and hopefully, next year, there can be many more roguelikes with more soul and flare, and fewer days lost on the boring topics that we just want done.
Subscribe to my newsletter
Read articles from Miroslav directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Miroslav
Miroslav
I'm a developer from Serbia, and I love roguelikes and Rust! I've worked in AAA game development, been indie, done software engineering, web development; at different times did functional languages, embedded, as well as working at UCSB as a researcher and educator. Came back to making games!