I’d usually be surprised by squeezing another blog post on a year, but this year the bar for “surprises” has been moved a bit for everyone, I think (except for virologists).
Fun anecdote: at the end of last year, my wife and I were discussing moving to a flat that was considerably more comfortable, which obviously came with a price. My exact words were “well, we could always take the hit for a year and then see if it’s worth it. It’s not like the world is about to go into a major crisis or anything“. We were planning the move to be before me finally attending GDC for the first time after so many years lurking the vault.
So I should start by saying I’m sorry. I have clearly jinxed 2020 for the entirety of humankind.
Now, deep down, I might be a nice person, but I’m mostly a cynical pessimist, so as soon as we got word around the office that our trip to GDC wasn’t going to happen, I just called home, asked if we needed any groceries and started mentally preparing to be cooped up for quite some time. No, we did not hoard toilet paper, or anything else – we actually had to ration some stuff at some point because you couldn’t really get supermarket delivery slots around March/April, and we wanted to avoid exposing anyone because we were likely unwilling early-adopters of the newest trend. I say “likely” because best diagnosis you could back then was a doctor on the phone saying “oh, she’s having difficulty breathing again, after 2 weeks? Yeah, it’s probably not a common flu then“. Fun times!
One of the worst parts of London is definitely housing, and if you take out everything else… you’re mostly left with overpriced bad flats, really. We cancelled our move, as the “major global crisis” safety got triggered, but ended up having to move anyway a few months later because our neighbors were driving us crazy. And I mean that literally: their pattern of loud music and drug fueled parties (which got us sleeping with earplugs for weeks) was entirely random, which got us anxious at any sign of loud, muffled noises. My wife had anxiety attacks. I started having night terrors.
Now, if there’s one thing I learnt about living in London is that it’s not a matter of if you’ll have an issue in your flat, it’s a matter of when you’ll find it out. The new flat was pretty good, if you ignore the flooding balcony (hey, last time it was a flooding kitchen) and the cold water jet every 4 minutes in the shower (hey, last time there wasn’t enough space to dodge the water until it got warm again). When the oven broke, we only had to wait for about a month to get it replaced!
This is obviously nitpicking: I’m on a pretty comfortable position, working from home, living with someone that I can happily have the entirety of my social contact with for months, and that can stand me 24/7/how-long-has-this-been-again?
Guess what I’m saying is: there’s nothing as comforting as being a cynical pessimist. You know you can’t trust large groups of people to do the right thing. You know things will break. Sure, things are random, but there’s an underlying determinism that you can use to prepare for the worst case scenario, and that determinism is a great tool. Oh, yeah, I forgot to mention, this is a long-ass intro to talking about my latest #procjam thing, Pestis Apotheca, which you can play here!
I was out of ideas for #procjam this year, but I had some holidays that coincided with the jam, and if there’s one thing I needed was something to fill up my time during the days off (before you look at me like that, it was a tough, tough year and I needed to do something fun).
Turns out that living in a mild post-truth dystopia is all the inspiration you need, especially spending 98% of your time on a London flat. Being a pair of introverts who got relatively comfortable with the lockdown hermit life, our first glance at social contact in months was with some plumbers fixing our cold shower issue. Oh yeah, the plumber seemed to have a mild cold. Also, it was on the same week in which we got a new oven delivered, by a nice gentleman with a cough.
A lot of people would immediately get angry. However, one must r/AmItheAsshole with oneself every now and then: I’m comfortably working from home and have tons of support from the company if I need time out. Do these guys have that option?
But it’s kind of comedic really: you might spend months sanitizing your groceries and quarantining your mail, and then one random encounter might get you sick. So how do you balance out your pyramid of needs between “having warm showers”, “being able to cook” and “avoiding getting sick”? How does the social element fit into all of this?
That scenario just clicked in my head: it would be my #procjam game. You’re fighting an unknown plague, trying not to get infected yourself. Originally, the game was going to be way more of a “social scenario” simulation: you’re an apothecary who has to deal with the customers and equipment breaking. Can you afford to have a a customer giving you a bad review because you sent them out for not wearing a mask? If your equipment breaks and the person doing the repairs has a symptom, do you send them away and risk not being able to craft medicine? That guy who just showed up with the latest rumors about how to treat the plague… can you really trust that information? But most of all, how do you deal with being sick with no one but yourself to rely on?
It’s funny how none of those mechanics ended up in the final game, but they’re essentially the root of it all. Speaking of roots, the name, Pestis Apotheca isn’t supposed to mean “plague pharmacy” or something like that; it’s supposed to be “plague repository“: there was going to be an underlying mechanic in which people would get infected by visiting your shop at the same time as someone else who had the plague.
(Yes, this is me insidiously inoculating the original idea into your mind without actually having to implement it. Take that, @PeterMolydeux.)
Last year, one of the best things about Vortex was that I was essentially just polishing an incomplete prototype, with lots of UI eye candy provided by thia9uera. Since that worked out well and this game would need quite a bit of UI, I asked if he was interested in another collab, and he was. Huzzah!
But as you can tell by the paragraphs above, my process on game jams tends to be very open ended: there’s a general idea, then I just start trying to throw parts of it to the wall and seeing what has enough time to stick before starting with the next coat of Jackson Pollocking the code. To attempt shaping things a bit more so I wouldn’t make Thiago do throwaway work, the first thing I did was a simple conversation system, and implementing a draft of the game’s tutorial with it. This was really good because it gave us a general design we would aim towards and showed some edge cases on the conversation UI. Mentioning this on twitter, I got this reply:
I’m not smart enough to have thought this through, so it was not intentional, but “TDD game design” is not only a great assessment of what I did, but also highlights what could be an interesting workflow for future projects! Thanks, @kchplr!
We also used Notion which seemed to work well for getting everything written down in a single place. Definitely something I’ll explore more in the future!
Let’s talk this through
The conversation system was pretty simple: dialogues were defined in ConversationData ScriptableObjects, which had an opening sentence and a list of options, containing the NPC’s reply or a link to another conversation. I added a flag to only show certain prompts after all others were explored to control timing (i.e.: force you to read my crappy jokes).
That’s easy for static conversations, but what about the patients? The game is also partially inspired by the fact that whenever I go to the doctor, I tend to list all the information I can. If you’ve ever had to talk to me in person, you know that I might cram a lot of information in the same long-winded sentence, including useless information; i.e.: doctors probably hate me. Sorry docs!
But I imagine it might be a bit like listening to a bug report: you want as much information as you can get, and you kind of automatically filter things out that you know are unrelated. This was easily represented by the mechanic of clicking certain words to highlight symptoms: unless you “actively listen” to the patient, you won’t uncover what they’re feeling. Fortunately, TextMeshPro has a handy hyperlink tag that I could use to trigger those clicks.
To generate a “natural” sounding conversation, I used simple grammars. If you’re not familiar with the concept, it’s essentially defining the overall “format” of a piece of text via tags, then replacing the tags with words or chunks of text. Kate Compton and Emily Short have lots of great material on this approach.
Ironically, in all this time doing procgen this was actually the first time I wrote a grammar system – not just because I was never concerned with text, but also because it’s a considerable challenge to write the text in a way where everything fits and still feels varied. I probably should have gone for GalaxyKate’s Tracery instead of rolling out my own, but it was one of those “too busy chopping wood” moments.
The system is just recursively going through all words between brackets and replacing them with content. Here’s the gist of it (this was all written in game jam mode, so proceed with caution if you want to use it).
For the patients, a ConversationData file with default parameters was instanced, then the contents of the relevant answers was replaced with something out of the grammar. The only tag left after the grammar processing was ‘[symptom]‘, which was then replaced by the patient’s actual symptoms based on convoluted rules that I wrote with a melted brain and I’m glad I don’t have to touch anymore.
The last detail was a hardcoded processing of “command” tags: if a tag like “[!EXIT_SHOP]” was present in the response text, I’d fire a Signal and the relevant code would take it from there.
If you read the gist above, you probably noticed the System.Random being passed around. That’s because each NPC has its own random number generator, as does the world. A good side effect of that is having deterministic results that allow easier debugging, but the main intent was sharing the world among players. This means that you could, in theory, help somebody cure poor Coilbrit Millard, the apprentice gunsmith, or share the symptoms of THE PLAGUE you found out via the comments in the itch.io page. Sure, this depends on having an active playerbase, but shhhh… only dreams now.
Another (existential dread inducing) side effect for NPCs is that the mere act of choosing which shelves they were walking towards next would impact their chance of survival later on – which given the world I wanted to build with Bestiarium, is hell of on point.
The World RNG is used to create ids, which are then used as seeds on the individual RNGs of NPCs, diseases and herbs. Some of those elements require storing entire data instances (e.g.: disease state at day 3 for some NPC), but others can simply be restored from their id (e.g.: the effects and side effects of a given herb id). And before you ask, yes, the World RNG seed is, in fact, 2020.
All diseases are generated by selecting r unique entries from a pool of n possible symptoms, with r=3 for default diseases. The game has 12 symptoms, so the good old n! / r! (n – r)! tells us that there should be then 220 unique diseases in the game, plus THE PLAGUE which has r=4. Symptoms are defined by an int-based enum and an associated configuration ScriptableObject.
After selecting a set of symptoms, their enum values are combined into a seed, which is then used to start up the RNG that generates disease name and any additional data. It’s a bit convoluted (World RNG selects N numbers -> mix those numbers into a hash -> use this hash as the seed to a new RNG), but it guarantees the determinism and that equal sets of symptoms don’t generate different diseases. I had just imported a xxHash for something else, so it was quicker to just use that to generate the hash from the set of enums rather than… you know, thinking of adding numbers multiplied by powers of 10.
To rid the disease
In real life, there’s 2 main ways medications tend to work: treating the symptoms or treating the causes. For design purposes, in the game, treating the symptoms always cures diseases. Treating a patient down to level 0 in 60% or more of their symptoms causes them to heal completely.
The diseases have a maximum of 3 levels and if any symptom reaches level 4, the NPC buttons up the wooden coat, as we say in Brazil (or at least my grandma did. Say, not button up the wooden coat. Although that too. 2020 was a hell of a year. Ah, humor as a coping mechanism! 🌈)
With every passing day, symptoms that received medication go down a level, and symptoms that haven’t go up a level. To treat diseases, the player must concoct potions using different medicinal herbs.
Each flower treats 2 symptoms and causes 1 side effect which can happen with a given percentage of chance. This means that it’s possible that you can cure regular diseases with a single plant, but you can never cure THE PLAGUE (as it requires at least 3 symptoms being treated).
To increase the amount of symptoms that are treated, you need to mix 2 different plants. The mixture’s effect will be semi-random and given by the percentage of each plant used, as well as the delta in the pH: if the difference in acidity is above 5, you’ll get no side effects, with a chance of having an extra positive effect – which is a mechanic I don’t expect anyone to infer but didn’t bother teaching as people just seemed to get the hang of playing around with different plants and percentages, and accept that sometimes they get a “critical hit”. I did get people trying to counter side effects from one plant with the positive effects from another, which was pretty cool.
The mix itself is also kept deterministic using the same methodology as the diseases: creating a hash from the 2 ingredient ids, then using that as a seed for the mixture’s RNG.
I also wanted to bring back the organs from the Dissection prototype but I wouldn’t have time to add the proper mechanics: what I wanted to do is slot in a little imp coffin that you’d buy from the travelling doctors, along a shopping list and that would give you special ingredients. I did keep the tower model in because a) I had spent precious jam time doing a crappy tower model and b) I could use it as a worldbuilding nudge to Bestiarium as a whole and give a little smile to anyone who got the reference (and by that I mean me).
Workplace Platform Hazards
As awesome as Unity WebGL is, it’s still bound to browser sandboxing. The main issue I faced was that when releasing the mouse via CursorLockMode, the canvas must be clicked again. Since the game would keep alternating between locked cursor (for the FPS controls) and cursor based input (for the UI), having to click the canvas again would make it pretty unplayable. I ended up implementing a software mouse.
I wish I could give you the ultimate solution but I ended up literally duplicating the default input modules, commented stuff out and manually sanitized a pair of HashSets to generate IPointerEnter and IPointerExit events.
Random, but expected
With this game, I yet again attempted to focus a bit more into polish and making sure people understood how to play. There are some solutions that were planned to be elegant (within their limitations of me being neither a designer nor an artist, of course), like delivering the tutorial via the dialogue of a friendly character right at the beginning, but there was also some degree of serendipity: I didn’t calculate the coefficients (number of symptoms and rate of symptoms treated for cure vs number of effects treated), I just typed in whatever seemed to make sense at the time, and lo and behold, things just clicked in the first attempt!
Because all the herbs and diseases are entirely random, and the dialogues usually only show you 2 symptoms, I didn’t think that you’d actually be able to cure THE PLAGUE without doing a bunch of experimentation, buying info and swapping herbs until you got a perfect pH match. Turns out that in the initial herb inventory, there is a combination that can cure it, and I found it entirely by accident!
But the biggest help I got to make the game flow smoothly was from Thiago: by having to organize my thoughts to communicate them to him and having someone who is an expert in UI and UX looking at the mechanics on a “sketch” state, we could iron out a bunch of design questions at the concept level before starting to implement them.
At the end of the day, I make these games for myself, really, so the lack of an audience doesn’t bother me that much. However, I do post it around so I can get some feedback for the concepts in general. The funniest thing happened when posting to r/proceduralgeneration: folks there are very partial to content that is very directly related to procgen (e.g.: terrain generators, generative art etc) and don’t care much about generative game mechanics. My post got around 14 upvotes, and a comment from one user suggesting to post this on r/medievaldoctor, which is a thing that exists apparently. The result? ~1.4k upvotes, even with the automod bots removing my post with the game’s link. In hindsight, it’s a quite obvious result, but would probably be something obvious from the get go to someone who is specialized in digital/game marketing.
So I guess that even with none of the social simulation mechanics implemented, the game did end up being an analogy for 2020 as I perceived it: certain things are only surprising if you’re not an expert… or not a cynical pessimist.
Even if in different levels, it was a tough year for everyone (who doesn’t own massive online sale conglomerates and/or has some level of consideration for complete strangers). Death, misery, lack of social contact, coming to the realization that you might have to *gasp* wash your bumhole with water and soap instead of wiping for a couple of weeks.
That, plus being forced to accept that the “post truth era” is here to stay and anyone can be a victim of it caused my one and only gutural twitter rant thread in recent history, in which I failed to keep my rule of not splurting partially uneducated, visceral opinions on social media. Gut-induced rants come from a (possibly fleeting) certainty you feel deep inside, and if you’re absolutely and unquestionably sure that you’re part of a perfect solution, you’re usually part of the problem. The saddest thing is that even the idea of “always question oneself” was weaponized by simply omitting “oneself” from the equation.
The scientific method has this wonderful built-in failsafe for getting things wrong, or just partially correct: reproducibility. You might even have imperfect understanding of a phenomenon, but if you manage to extract reproducible results out of it, it can be used to build upon. And as soon as someone proves you wrong or does a quantum leap on top of something you did, if you’re a good scientist, you’ll probably be ecstatic (possible side effects: impostor syndrome, 75%).
One of my favorite moments at work this year was talking to a research scientist and congratulating them on some early results, and having received the reply of “no congratulations yet, we have to further analyze the data and verify if there’s indeed a causal link to our hypothesis“. So as grim as it all may sound out there, Science is alive and well. Scientists worldwide focused on a single common enemy – with results that might be tainted by noise, bad faith and politics, sure, but that still brought us a step ahead at a never before seen pace. We’re literally mass producing 3d printed exploits to combat biological enemies.
It’s not the first time we’re going through major crises as a species, but it might be the point where the stakes are the highest to date, because humanity’s ability to access information has far surpassed its ability to assess information. And while properly communicating those things is extremely important, sometimes, scientists are busy doing science. So it’s up to us, laypeople with partial knowledge, to teach others how to trust specialists again. And it’s most likely not something that can be entirely done with a factual-based approach – facts are too prone to psychological pitfalls. Empathy might just be what takes us there.
Ironically, the thing every individual can only learn about from other people’s past experiences is undeniably… death. Memento mori, as they say, but maybe not just because we all die. Maybe remember death because it teaches us to learn from others.
We’re standing on the shoulders of giants who were also making it up as they went along.