The first real post in the series! For now, just posting my free form thoughts but open to feedback on the style, format, etc… Feel free to reach out with any feedback, questions, comments, etc…
This has been a great week. Overall, finally feeling like I have my motivation back. The past few months I have REALLY been struggling to work on Junk in the Trunk. I keep waffling in my head. I told myself at the outset of this project that the core goal was to use it as a learning experience. I am not trying to make some crazy breakthrough hit. I am trying to set myself up to be a better game dev tomorrow. That being said, with my personality I just can’t put shitty work out there. I need to force my brain to distinguish the difference between a simple game and a bad game. Junk in the Trunk is pretty simple, but it will be GREAT because I have high standards and I will learn a ton.
Anywho…. here are my notes for the week! High level summary is that I built my game early in the week and playtested a lot. Realized that the performance was unacceptable and went on a deep dive to optimize. Some great learnings and links below for those of you who might be new but not that new to Unity / C#.
- Built my game after getting the above done. On playtesting, I found that it essentially ran like shit. Most notably the input lag was unbearable, especially when precision platforming is so important. Framerate was super choppy as well. So I went on a big performance journey the past couple of days.
- Performance, Debugging: I did a lot here and am pretty happy with how I approached the problem. I made an effort to attack this from the perspective of trying to learn for retention. As such, spent a lot of time exploring such that I have a better understanding of what I could do better next time from the get go. On top of that, I actually did a great job cleaning up the game. On a frame by frame basis (not in combat, just the base case of the character moving through a level) I am now at 0 bytes of GC which is great and the framerate is stable at 30 FPS (which is I think what I have decided to target). More details / learnings:
- In my exploration, I spent some time digging into test frameworks / best practices there. While I haven’t implemented anything yet, I am planning to refactor and implement some automated tests over the next few days. Should have done this from the beginning. Shout out to Jason for making this great video BTW: https://www.youtube.com/watch?v=qCghhGLUa-Y&t=1562s
- I also found this AMAZING forum page that lays out all of the options currently around testing in unity (from their own Unity Test Runner to various plugins). The thread is up to date as of December 2020 so definitely worth checking out: https://forum.unity.com/threads/what-options-do-i-have-for-automation-and-unit-testing-in-unity.682720/
- There are a LOT of tools out there to make profiling / generally debugging easier, especially on mobile. Some that I have been using this week that made my life way easier:
- Device Simulator Package (in the Unity Package Manager): Allows you to switch the game view into a simulator of various mobile devices (Apple, android, tablets, phones) as well as some other cool settings like simulating a low memory situation on the simulated device. Note the package is still in preview so you will need to tick the “Show Preview Packages” advanced option in the package manager to get it and there are some bugs still. Notably: The simulator really slowed things down in the editor and in the game during Play Mode when I had the Unity Timeline window open.
- Android Logcat Plugin: Again, available on the asset store. This lets you easily pull in log info to the editor from the android device you build to. The tool lets you do some basic filtering which works great but my favorite feature is that it allows you to easily save the logs for analysis in another more analytical tool. Just to play around, I was saving the logs, pulling them into excel and doing some analysis of my Debug.Logs which worked pretty well. Helped me do some analysis like measuring how much input lag correlated to FPS drops and how that changed as I made performance improvements.
- I am honestly pretty surprised how many issues URP has. To be honest, I am a little bit worried about Unity’s long term ability to compete in the marketplace. I can’t help but speculate that the people who made money off of Unity’s IPO are in for a bit of a rude awakening. It seems to me (I could be wrong, this is obviously off the cuff as is the purpose of these posts) that Unity is cashing in on the bubbly of smaller / indie game devs right now. This is a whole other conversation but since I dove into this industry, the cynical part of me is convinced that the indie game dev market right now is inflated by an abundance of noobies (essentially, people like me) who are spending money / supporting the indie dev scene incestuously. It is great and I am so happy the community is so nice and supportive, but building a platform for that community is a very different thing than building a first class game engine with AAA support. Anyways, rant over. Here is at least one concrete example:
- Noobie Unity / C# Garbage Collection Mistakes I found and crushed this week
- Raycast vs RaycastNonAlloc: I really wish I just used RaycastNonAlloc from the beginning of my Unity programming career. Essentially, you pass it a buffer as an out parameter and it returns it with any hits as opposed to creating a new array of RaycastHits (and thus generating garbage) every time it runs. To me this seems like it should be standard practice especially because it seems like a common use case of raycasting is to do it frequently enough (like in Update / generally a part of the game loop) to where that small amount of garbage actually matters.
- Note I was worried initially about using RaycastNonAlloc because I’d have to figure out the size of the buffer needed and stuff but it turns out to be pretty easy / the method itself handles corner cases well. First, you can pass it a layer mask which helps reduce the number of unexpected raycasthits (which would eat up your buffer), also it stops running when it reaches your buffer’s limit (so no need to handle any weird exceptions).
- Coroutines: Don’t add a “new Waitfor____” every time you yield. Holy shit I wish some of the tutorial videos that first showed coroutines in the first place just told me to cache a new Waitforseconds(x) and reuse it instead of making a new one every frame.
- Strings: Jesus I had no idea how annoying strings and UI is. I learned this week (the hard way) about the dangers of strings and garbage collection. Turns out strings are essentially and immutable array of chars which means concatenation involves a whole lot of making arrays, copying stuff, etc etc (aka, garbage creation). That, coupled with my inclination to use strings and update them frequently in UI components (updating a timer, score board, etc…) and you get a big drain on performance. I went down a rabbit hole of techniques to convert a float to a string without generating any garbage but mostly this is where I landed:
- Just don’t update the goddamn UI so frequently. It is hardly ever really needed. If you are updating a UI element’s text value in Update(), you are probably doing something wrong. Even so, I thought I was being smart by making my UI update only when told to by events / the processes feeding them but it turns out those processes were running way too frequently anyways…
- When needed, concatenate strings using StringBuilder. Garbage is still allocated but it is pretty efficient overall
- URP shenanigans like the one from the link above. Make sure to be aware of that bug because it can cause a TON of GC alloc (creates a new shader / material on the fly because for some reason it can’t find the URP Fallback Shader)
- If you use Sprite Shape Renderers, make sure you don’t leave them on “Update Collider” mode during your build. They will re bake every frame or something and cause a big mess
- Raycast vs RaycastNonAlloc: I really wish I just used RaycastNonAlloc from the beginning of my Unity programming career. Essentially, you pass it a buffer as an out parameter and it returns it with any hits as opposed to creating a new array of RaycastHits (and thus generating garbage) every time it runs. To me this seems like it should be standard practice especially because it seems like a common use case of raycasting is to do it frequently enough (like in Update / generally a part of the game loop) to where that small amount of garbage actually matters.
- Memory Management: Audio Import Settings and you
- I saved a ton of memory by properly configuring my various audio import settings. Big deal thing that I didn’t know: The compression algo matters a lot. So many articles and tutorials I read before focus so much on the “Load Type” setting but properly configuring that without understanding which compression algo you are using can cause its own issues. Here is my spark notes of the article linked below:
- PCM: Basically no compression. Good for small, frequently used audio clips (like a footstep sfx)
- Vorbis: Big daddy compression. Very powerful. Use this to compress big audio files used infrequently like music.
- ADPCM: The middle ground between the above
- This article basically says it all: https://blog.theknightsofunity.com/wrong-import-settings-killing-unity-game-part-2/
- I saved a ton of memory by properly configuring my various audio import settings. Big deal thing that I didn’t know: The compression algo matters a lot. So many articles and tutorials I read before focus so much on the “Load Type” setting but properly configuring that without understanding which compression algo you are using can cause its own issues. Here is my spark notes of the article linked below:
- Memory Management: Graphics Rendering Pipelines and You
- Unity’s memory profiler is great. Use it, especially on your actual build (instead of the editor).
- Make sure your textures are imported with as small a “Max Size” as makes sense for your use cases. This seems obvious but I found I got lazy and a huge chunk of my memory was getting eaten up by textures I forgot to tweak on import
- Post processing, Camera Stacking is no joke: I am using the URP and camera stacking. I simply did not have a good enough understanding of how the rendering pipeline works at the start of this week. I have essentially 3 cameras that are stacked in most levels (BG, Core Gameplay, UI). Each of those cameras had the post processing tick box checked so each of them were carrying extra render texture copies for post processing purposes. I learned that you only need to tick that on the cameras that you actually want to post process…. Furthermore, I learned that, for a mobile game, it seems like post processing is a pretty hard sell. Even just using some basic bloom or blur made my render texture memory go from about 8 mb to 90.
- In my exploration, I spent some time digging into test frameworks / best practices there. While I haven’t implemented anything yet, I am planning to refactor and implement some automated tests over the next few days. Should have done this from the beginning. Shout out to Jason for making this great video BTW: https://www.youtube.com/watch?v=qCghhGLUa-Y&t=1562s