--- Title: How we made Befriendus Ludicrously Accessible Date: 2021-04-13 Category: dev Tags: homestuck, gamedev, python promo_image: options_menu.png ad: Befriendus does what archive of our own't --- Befriendus; everybody's favorite visual novel about making alien friends. It's got trolls, yes, but it also has a slew of accessibility options. You can adjust everything: color, font, motion, even spelling. It's clean, it's easy, and it works. Here's how we did it. ![Befriendus in-game menu, with accessibility options](./options_menu.png){style="image-rendering: -webkit-optimize-contrast;"} When I was designing the basic accessibility framework I had these principles in mind: - Accessible scripts must be easy to write; work should never be duplicated + Demanding people write multiple versions of work is bad design and encourages accessibility to eventually be dropped in favour of efficient production - Humans should never do postprocessing tasks + We're writing software; a computer should do any and all mechanical work, not writers - Accessibility options should have as granular control as possible + Whenever possible, players should be able to select *exactly* what they need, not be forced to use something that doesn't match their needs. + Options should be compatible with each other whenever possible + Just pushing out transcripts is not accessible design. The best way to explain these is probably to explain what we ended up doing, and how each design choice was made carefully in accordance with those principles. ## Colour We knew from the moment we started putting together logic for character colours that we wanted an optional high-contrast mode. One of the first characters written was Mituna, whose light yellow text doesn't show up super well against our grey textbox: ![Mituna dialogue; yellow text with normal spelling](./mituna_colored_crop.png) This is good enough for most cases, but we definitely need a way to turn colors off. Black-on-grey has *much* more contrast and pops nicely for the high-contrast folks: ![Mituna dialogue; black text with normal spelling](./mituna_plain_crop.png) This is handled by the Hemospectrum subsystem, a part of FSE. Characters' colours are defined with their characters are (`mituna, name=Mituna, hemocolor=gold`) and processed by the dialogue system when characters speak. Colours are defined semantically and handled correctly at the appropriate stage in the pipeline. (As opposed to some horrible hack, like binding a listener to the option and manually editing every character's colour in storage every time it's changed.) Anytime a color (`gold`, `orange`, `#f00`, `#a20000`, whatever) needs to be resolved (in the dialogue box, mainly), it's passed to the `hemospectrum` function, which resolves colour names and colour codes to universal colour codes. The logic there is for name resolution, but we also use this entry point to handle contrast logic. Whenever this lookup is made we check if the high contrast user setting is enabled. If it is, we return either black or white, depending on the tone of the colour requested. Since all the colour logic was in one place already, it was easy to add extra logic here. This also gives us the tools to make contextual decisions further down the line -- a very light color might pop better as light grey or white, for instance. ## Typing Quirks Characters in the source material, Homestuck, have what are called "typing quirks". Typing styles, basically. Characters type in different styles, ranging from semi-mundane style choices (lowercase, all caps) to incredibly elaborate (elaborate puns, ending every line with an emote, alphanumeric replacement). I could write a lot about how interesting these are and how they give characters a feeling of tone that the written word rarely conveys, but I'll save that for another discussion. Brass tacks, we want our characters to do this, and that requires some work. **Now, to my eye, this looks like a build artifact.** The "source code" is the script, and once the script is written there's postprocessing replacement step that has to get done before we publish. The alternative is for writers to write with quirks to begin with (`WH1CH 1N 50M3 C4535 15 UNT3N4BL3`) or to run a search-and-replace over the script every time they changed the script. From what I can tell, this is how Homestuck and pretty much every fanfic operate. Fortunately for us, though, we're running on a software engine! We can do better. First, some groundwork. The main gameplay in Ren'py consists of dialogue in script files. Script text looks like this: ```{.renpy .wrap} show !mituna at speaking !mituna confused talk "she wouldnt leave without an ip adaptor that was the ONE thing i told her to bring" !mituna idle frown talk "unless she was an idiot and just didnt. which i guess is unfortunately possible" (show_hashtags="#name of my autobiography: #\"unfortunately possible\"") show !mituna idle ``` We don't care about the stage directions here, let's look at that dialogue: On the first line of dialogue `mituna` is the character speaking, `confused talk` is his "pose" (the sprite displayed, if omitted it just uses the previous one), and the rest is the text that shows up in the dialogue box. When the story gets to this line, the `character` `mituna` says his line with a pose. (Remember that the character does the saying, this will come up later.) The second line is pretty much the same thing, except you can see there's some extra data called "show_hashtags" at the end. In Befriendus, the character dialogue boxes have an optional second line of text called hashtags, and this just tells the engine about that. Here's what that should look like: ![Mituna dialogue; yellow text with normal spelling](./mituna_colored_crop.png) But if you get Befriendus and play with all the default settings, here's what you actually get: ![Mituna dialogue; yellow text with quirky spelling](./mituna_full_crop.png) There's some trickery going on here! Mituna's text has its alphanumeric substitutions. Where did this happen? Here's the trick: all befriendus `character`s automatically modify their dialogue based on the currently set options. When the `mituna` character got the line, it changed the line to this: ```renpy "{quirk=mituna}unless she was an idiot and just didnt. which i guess is unfortunately possible{/quirk}" ``` That bit in brackets is a tag. Ren'py uses [text tags](https://www.renpy.org/doc/html/text.html) as internal control commands for things like text speed or text style like boldness and italics, but it also lets you define your own tags to operate on the text. That's what we're doing here. This notes that the quirk with the name `mituna` should be applied to the text if quirks are enabled, but it hasn't actually done it yet. At the beginning of the route, when we first defined the `mituna` character in the first place, we added this line once: ```renpy init python: QuirkStore["mituna"] = [(c, "31073107"[i]) for i, c in enumerate("ELOTelot")] ``` The specifics of the right-hand side are the instructions for how to translate Mituna's text and are black magic. What matters is we've told something called the `QuirkStore` how to apply the `mituna` quirk. Now every time text is rendered, as part of the engine's default tag parsing (alongside bold, italics, links, etc...), the `{quirk}` tag checks whether quirks are enabled in options and applies them on-the-fly. This lets us do some great stuff. We're able to quirks anywhere we want (title screen tags, route titles, hashtags) without having to worry about extra cases: there's no M\*N problem since all the logic is handled in one place. Keeping the semantic, pre-process copy of the text around has obvious benefits when it comes to translation or text-to-speech. Since the dialogue is stored correctly it's easy for text-to-speech to read from the original text, rather than try to stumble through alphanumeric replacement or some other slurry. ### Puns I mentioned earlier that some quirks involve puns. While you *could* do this with regular expressions, (or write two copies of every script, yikes) we took a better approach and just piggybacked off the tags system again. Here's what Meenah's dialogue looks like: ```renpy !meenah pissed "but HELLO we ended up getting busted {pun=halfwave}halfway{/pun} through and its only cuz of MY quick {pun=sinking}thinking{/pun} that we got here at all" (show_hashtags="#had to {pun=finprovise}improvise{/pun} like MAD") !meenah snarky "{pun=betides}besides{/pun} i gave you my trolltag didnt i" ``` The `{pun}` tag is another Befriendus original. The text inside the tag is the original word, and the text after the `=` is the pun version. As with quirks, this lets the decision of what text to use be made at runtime, and globally. Tags hook into the global text system, so the pun and quirk systems are availible anc consistent anytime we display text. ### Mix-and-match I really can't emphasize this enough: all the text features work together. You can toggle quirks independent of color, you can adjust puns independent of quirks, everything. Because everything is structured correctly, this is exceedingly easy to write for. ## Motion Unfortunately, "don't duplicate work" has its limits, and animation is one of them. All our motion-intensive animations and flashing lights have to be written out twice; one full, one reduced. There's a simple flag in the options menu that determines which one plays in the game. While ATL has some support for conditionals, I haven't found that to work consistently enough for our purposes, so we *do* have some icky logic mixed in with the script. Here's what Mituna's flashing effect looks like: ::: spoiler "ATL Block" ```renpy # FLASH VARIANTS image !psionicborder_flash: block: parallel: "{{assets}}/psionics_01.png" pause(0.08) "{{assets}}/psionics_02.png" pause(0.08) "{{assets}}/psionics_03.png" pause(0.08) repeat parallel: zoom 1.0 ease 2 zoom 0.8 ease 2 zoom 1.0 repeat transform __p__psionicoverlay_flash_template(fadedur=1.0, pausedur=2.4): contains: "#F20000" alpha 0.0 linear fadedur alpha 0.22 pause pausedur linear fadedur alpha 0.0 pause pausedur repeat contains: "#003BFF" alpha 0.22 linear fadedur alpha 0.0 pause pausedur linear fadedur alpha 0.22 pause pausedur repeat image !psionicoverlay_flash: __p__psionicoverlay_flash_template(fadedur=1.0, pausedur=2.4) image !psionicoverlay_noflash: __p__psionicoverlay_flash_template(fadedur=2.4, pausedur=2.4) # NO-FLASH VARIANTS image !psionicborder_noflash: "{{assets}}/psionics_01.png" zoom 1.0 alpha 1.0 linear 1 alpha 0.8 pause 1.0 linear 1 alpha 1.0 repeat image !psionicoverlay_noflash: "#F2000022" with Dissolve(4.4) pause 4.6 "#003BFF22" with Dissolve(4.4) pause 4.6 repeat # This should allow players to change the setting at any time # This shouldn't ever leave the old version on the screen image !psionicborder = ConditionSwitch( "persistent.flash", "__p__psionicborder_flash", "not persistent.flash", "__p__psionicborder_noflash" ) ``` You can see we tried to eliminate duplication as much as possible using subanimations here, and we even have a `ConditionSwitch` there at the end. But still, when we trigger the effect in-game, it looks like ```renpy if persistent.flash: show !psionicoverlay_flash #<- flash variant else: show !psionicoverlay_noflash with dissolve hide !mituna with easeouttop ``` Fortunately, there are only a few cases of this in the game. ## Juicy Builtins Ren'py has plenty of built-in accessibility features that we make sure to expose to the player: ![Renpy accessibility menu](./accessibility_menu.png) As shown you can replace the fonts, adjust text size and spacing, and even turn on automatic text-to-speech. This didn't require any real work on our part, except for communicating to the player that this menu exists, which we do at the bottom of the options menu: ![Befriendus in-game menu, with accessibility options](./options_menu.png){style="image-rendering: -webkit-optimize-contrast;"} As mentioned previously, it was also important for voicing that we kept the original english script around instead of preprocessing the whole thing into quirks. ### Translation [Ren'py also makes it easy for fans to make and distribute dialogue translations.](https://www.renpy.org/doc/html/translation.html) All our hashtags, menu text, and dialogue use the translation system to make this as easy as possible. Things like pun and quirk tags also make it easier to translate non-words by exposing the intent behind them. ## Transcripts I can't take any credit for this one either; [Alien](https://twitter.com/AlienoidNovace) and [Robin](https://twitter.com/RyeRhythmic) maintain incredibly detailed transcripts for all the routes and features on the game page using a web of google docs. I wrote a utility to pull transcripts from Friendsim, but we don't use that because we have access to all the original scripts. This makes making transcripts for Befriendus is much easier than fan transcripts for, say, Pesterquest. Alien and Robin actually maintain two distinct sets of transcripts. Alien makes traditional text transcripts of the routes (with colours and quirks) that are as close to gameplay as possible, while Robin makes transcripts that are as accessible as possible, with quirks and colours removed and with added text descriptions of effects, poses, and animations. Unlike other accessibility features, these aren't mix-and-match; any changes need to be manually propagated through all the transcripts. Transcripts, though, aren't part of the prototyping cycle; they're put together at the very end, sometimes even after the route releases. ## Warnings We have a warnings page with individually spoiled content warnings for each route. There's a large button for this on the main menu, so players who are interested won't miss it. We also try to include appropriate content warnings without spoiling the route, which can be a tricky balance. This isn't a traditional accessibility feature, but it does make the game more accessible. --- Well, that's what we've done so far. There's more game yet to go, though; if we add some other major accessibility feature I'll try to remember to come back to add it here. I don't have any groundbreaking takeaways here that I didn't give away in the first paragraphs. Design your engine in a way to prevent humans doing duplicate work; let the data do the lifting for you. If humans have to do everything twice, no they won't. Give players control; let them tweak their experience precisely to their liking. If done well, it doesn't have to be all-or-nothing. Accessibility is necessary, and it doesn't have to be hard if it's designed properly. # Related Reading - ["Structured programming" on Wikipedia](https://en.wikipedia.org/wiki/Structured_programming){: .related-reading}