Pangram verdict · v3.3
We believe that this document is fully human-written
AI likelihood · overall
HumanArticle text · 1,920 words · 6 segments analyzed
IntroOver half a decade, 500 commits, and the better part of a thousand hours ago, I spent a boring afternoon wondering what GTFO would look like in VR. Then I made the mistake of trying to find out.I’ve kept an eye on GTFO ever since its gameplay trailer at the Game Awards in 2017. It ticked all the right boxes for me: Survival horror, co-op, shooter, extreme difficulty (allegedly.)I managed to join every playtest and could not get enough of the game each time. GTFO’s setting; the ‘Complex’ is a marvel to look at and the monsters inhabiting it were unique and interesting. The game was (and still is) extremely difficult and extremely fun, just as promised.First Foray Into ModdingI started tinkering with GTFO even before the first alpha test was finished. I quickly found out that decompiling and modifying Unity games is quite trivial. Using DnSpy/DotPeek, you can easily peer into basically-source-code of the game, because Mono (Unity’s default scripting backend) compiles to .NET bytecode, which retains almost everything needed to reconstruct the original source.For example, this is what we can retrieve from a Unity project I quickly cobbled together:Decompiled code1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class DeleteIfNear : MonoBehaviour { [SerializeField] private GameObject Offender; [SerializeField] private float Distance = 50f;
private void Update() { if (!((Object) this.Offender != (Object) null) || (double) (this.transform.position - this.Offender.transform.position).sqrMagnitude >= (double) this.Distance * (double) this.Distance) return; Debug.Log((object) ("Deleting offender " + this.Offender.name)); Object.Destroy((Object) this.Offender); this.
Offender = (GameObject) null; } } From the original source:Source code1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 /// <summary> /// Deletes an object if it gets close enough /// </summary> public class DeleteIfNear : MonoBehaviour { [SerializeField] private GameObject Offender; [SerializeField] private float Distance = 50.0f; void Update() { // If distance to target is less than Distance, we delete the offender if (Offender != null) { Vector3 toOffender = transform.position - Offender.transform.position; if (toOffender.sqrMagnitude < Distance * Distance) { Debug.Log($"Deleting offender {Offender.name}"); Destroy(Offender); Offender = null; } } } } Comments are stripped out, casts are explicit and the code is ‘optimized’ in general. All in all, it’s still perfectly readable. It’s not easy to navigate a large codebase like GTFO’s in the first place, but this definitely beats staring at raw disassembly.At first, I only used DnSpy to modify assemblies in-place. Quite quickly, I was able to create my first (unreleased) mod for an alpha version of the game — a more ‘efficient’ solution to dealing with the game’s (at the time) most dangerous enemy, the scout, featuring custom code and ‘borrowed’ assets.We blow up a scout with the BFG9000The game’s full release soon followed and yet again, after getting through all the content, I still couldn’t get enough of it. With the next update nowhere in sight, that fateful, boring afternoon arrived.
Armed with knowledge of Unity and VR dev from my day job, beginner-level modding knowledge and some experience with SteamVR, I started cobbling together something that would allow you to admire GTFO’s bestiary up close and personal.And up close and personal it was. Within VR, enemies that looked somewhat big on-screen were suddenly towering over you. Gruesome details became far more apparent and getting disoriented in the Complex was never this easy. It was everything I had hoped for.Over the next few weeks I added fixes that allowed me to play even full matches in VR.The community expressed interest in a public release. Meanwhile, DnSpy was giving me issues. Turns out modifying assemblies in-place repeatedly is not that stable. Keeping track of my modifications also started to become cumbersome. After one too many random crashes and the inability to legally distribute the mod as altered game assemblies, I needed a solution.Harmony and BepInEx to the rescueThe solution was runtime patching. Harmony is a library that allows one to prepend, append (or even replace) code in existing methods at runtime. Much like the name suggests, because of prepending/appending code being the main paradigm, most mods can coexist by default. BepInEx is a mod manager for Unity games built around loading Harmony-based mods.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 /// <summary> /// Add event calls for the player shooting weapons /// </summary> [HarmonyPatch(typeof(Weapon), nameof(Weapon.ApplyRecoil))] internal class InjectPlayerWeaponFireEvents { private static void Postfix(Weapon __instance) { if(__instance.Owner.IsLocallyOwned) { PlayerFireWeaponEvents.WeaponFired(__instance); } } } When the game starts, this code will be added to the end of the Weapon.ApplyRecoil method in GTFO’s code.The main idea is that distributing modified game code is not legal because most of what you’d be distributing is copyrighted. However, distributing code that modifies the game code on the user’s PC is fine.
Switching to the Harmony/BepInEx stack allowed me to finally upgrade the mod into a proper, organized project with source control and throw it on GitHub. That marks the first commit, on the 1st of February, 2020.With the project in a maintainable state and released to the public, I continued adding features. The first goal was to get the game playable entirely within VR. Most notably, this meant getting motion controls, the game menu, in-game UI, and the in-game terminal interaction working. Over the next couple of months, this all ended up making it into the mod. Over the next couple of years, the mod was kept up to date with game updates. Various improvements have been made to many systems, including large contributions by other developers(!)The Gameplay in VRSwitching to VR came with its own set of challenges. As GTFO is all about difficulty and cooperative gameplay, my main driving idea was that players should not be too disadvantaged or advantaged relative to non-VR players.Shooting accurately was difficult in the VR version. For a long time, ironsights/optics did not render properly in VR and/or did not closely match the aiming direction. For this reason, I decided to include a built-in laser pointer for all weapons. Eventually, the devs themselves switched to a shader which made sights line up (and look) quite nicely, so I made the laser pointer toggleable.Some effort also had to be spent on combating cheating. Peeking through doors by walking through them in roomscale or shooting through them by sticking your gun behind them were easy to do in VR. Thankfully this was easily mitigated by blacking out the screen if your head was inside collision. Had I not done this, players would find a way to optimize their own fun away.UI was challenging in general. I wanted to avoid copying the 2D UI where possible to promote immersion. I ended up doing some custom watch-style UI like most other VR shooters. While being diegetic is very cool, I think it could have definitely used an artist’s/designer’s touch. MrKerag did end up creating a better model for the watch, so at the least you don’t have to brave the Complex with a Fitbit anymore.
The Fitbit in question And the improved versionFor the menu UI I went with a simple throwaway hack where I show an interactable, curved SteamVR overlay with the 2D game menu pasted onto it. This ended up working rather well, so I kept it. The steam overlay API was easy to work with and raycasting against it to position the in-game cursor was trivial. Most of all, I was able to completely circumvent dealing with any custom menu UI rendering and related problems. The VR game menuGTFO also features interactive computer terminals, which are quite an important part of the game. At first I used a virtual keyboard, which lived inside the VR runtime. I had some crappy shortcuts built-in as upper-case keys. It worked well enough, but it was far from user friendly. Thankfully, due to the valiant efforts of Nordskogg, a new version of the terminal interaction appeared with a full, user-friendly, in-game keyboard with some amazing features like on-screen text selection. Nordskogg’s improved terminalAdditionally, I tried to keep QoL and/or ‘cool’ features in mind:Support for left-handedness.Crouching in the game if you crouch for-real.Various 3D/2D UI toggles.VR controller mechanics (mostly two-handed aiming related settings.)VR specific graphics options.Laser pointer colors (because why not.)Another notable setting is a height offset; some levels contain neck-height poisonous fog, and players of shorter stature found their heads didn’t quite reach above it in VR.In a similar vein, I added an optional ammo display hologram. Looking down at your watch to check your ammo count in the middle of fights wasn’t always the most appealing idea. The more UX friendly ammo display.All of these settings live in a configuration framework that lets players tailor the mod to their liking. Because the framework was created up-front, adding new options like the accessibility features above was trivial. It also made debugging easier by way of in-game runtime UI debug flags. Well worth the time investment.The ArchitectureBecause of the game’s early access nature, things basically broke on every game update. Due to this, (and my inexperience at the time) architecture and maintainability took somewhat of a backseat in the early stages of the project.
The design of each overarching part of the codebase was iterated on extensively, but at first emerged from what made the most sense at the time of implementation. Because of this, I had to do a big refactor along the way to split self-contained player-related functionality into components and did another rewrite when the game switched to IL2CPP to clean things up again.Over time, I realized the biggest problem with mod code in general is that changes to the game are very implicitly coupled to the game code, so you often have to consider both codebases. This sounds rather obvious, but it only gets worse as a mod interacts with more game systems. If the game systems change because of updates, things get very messy, very quickly.However, making features work from self-contained objects and explicitly separating code injections into different parts of the codebase insulates us from this problem. This did not go as far as - but did have similar benefits to - the general modding APIs we see created and used by modders in games such as Risk of Rain 2.In simple terms and a simplified example, a lot of cognitive overhead is required to interact with game code. For example, determining where to inject various checks and data retrieval for several different haptics-enhanced events like firing different weapons, taking damage, receiving ammo, etc. just sounds like a pain to do every time. It’s easier to write an in-between layer (or two) once, that will handle this for us. Is the player aiming with two hands? Is he crouching? How much ‘kick’ does the current gun have? Our internal ‘API’ will tell us!Even if a particular ‘API’ only gets used once, it still makes things very explicit in the codebase, which, in my opinion, is the name of the game when keeping things bug-resistant and easy to understand. Additionally, if things break due to game updates, we often only have to fix our ‘API.’In the end, the codebase ended up organized in roughly the following splits:Injection code, per category (rendering, gameplay, UI, Input, events) that interacted with or directly modified game code.Game events that would get called by injected code, broadcasting the act of taking damage, shooting, etc.Self-contained behaviour that would get coupled to corresponding Gameobjects such as the player or weapons, or react to game events.