Or, Under the Neons, with unforeseeable problems.
I should write more about Neon Town development progress, and I was (still am) planning to write about technical details to cover for my absence.
At this point, I thought it would be nice to share what I’d been through these last few weeks for a start. This post will be a series of unfortunate events, explaining how a series of wrong decisions eventually came back to bite me. It will mostly be technical details, but also I’ll just self-criticize.
For all who aren’t familiar with Neon Town, I should start by explaining what it is.
Around middle of 2015, I’ve applied to a freelance game development job. Then the game was cancelled and even though I was told that the decision was irrelevant to me, I still think that it was me messing up. Talk about self-confidence. Then the game was renewed and became what it is today.
During this period, development had a few breaks because of ‘reasons’; but generally I progressed from a mere programmer to a lead programmer, then into a team member who now has input for key points. Of course I don’t seek anything more than Lead Programmer in the credits; but I can tell what happens if you are in game development to make money or gain fame, just from experience.
Back to topic. Everything started after I added dynamic 2D shadows to the game (I’ll write another technical post about it). For anyone wondering, dynamic 2D shadows look like this.
— Benjamin Basic (@BenBasic) February 24, 2018
A boss named Priestess can be seen to cast shadows dynamically whenever she waves her staff. Alongside, colliders are being updated with the animation, too. Normally, I didn’t really want to implement this system because it was hard, because it was too much work, because it required a lot of planning and designing, et cetera, et cetera, and had the probability of producing a lot of bugs (and it did). When Benjamin asked me if I could do it, I said it was too much work and let it go. Then on a Friday night when I felt myself quite productive, I sat down and started to work on it.
At first, there wasn’t really a cache mechanic, thus scene loading time would take 1 or 2 seconds more. But as per popular saying, don’t fix it, if it ain’t broken, and I left it like that, because it was working. Then I added a code piece in case there was a sprite I forgot, and it would fill the dictionary dynamically at runtime, then using it from this dictionary. I also added a debug message in case something goes wrong. (sprite is null. This was the message. It’s an important detail.)
Technically, during runtime I’d read the sprite and create a PolygonCollider2D component, add this component to a GameObject, then deliver this collider’s point data to the object which is responsible of shadows. Normally, I was doing this on Awake function (which runs first before gameplay begins), so most of the things were already cached for that runtime instance; however, again in the case of I missed something, I’d create the relevant data and put missing data to (C#) Dictionary. I made the first and most important mistake here, I was putting all the relevant sprites from files one by one, and creating the relevant data from this list. When I think about it now, hard labor aside, I wonder how I made such a bad design decision. I mean, maybe I was high, but I am not using alcohol for a very long time either. I don’t know.
And the expected crash came a few weeks after. When Benjamin said “this doesn’t work”, all I could muster was a simple “wat” as a reaction. I asked for a footage and player log, “Ask and you shall receive” was what I received, and it was unbelievable that log file was millions of lines…
When I search for the issue, I found a few answers that incredibly misled me. When sprites are imported into Unity without Read/Write enabled option, this might be the case, as the answer suggested, engine may not be able to read the data from the sprite. I haven’t enabled Read/Write option in my import settings, because everything was being initialized in ‘initialization time‘. Furthermore, Read/Write enabled sprites were using twice as much memory, and meddle with batching, which is bad. Was it necessary? No, I said at the time, but when I faced this issue that No became Hmmm quite fast, if this is the issue, I’d update import settings for all sprites that are being used in dynamic collision and shadow to Read/Write enabled, and be done with it I thought to myself, in a quite clever way. r/iamverysmart material here. But I mean, game is using under 1GB RAM, that’s how a good optimization is made. But sometimes I’m so clever (!), I just act dumb. Anyway, I went ahead and updated all sprite import settings to Read/Write enabled, moreover I enabled MipMaps like it was necessary (and why I did think of doing that, I have no idea!), and tried to take a build.
Second and bigger crash came here.
I’ve recently tweeted this.
— Tansel Altınel (@Tanshaydar) February 26, 2018
Build size was under 800 MB. But when I tried to take a build after these last changes, Unity displayed an error and refused to take the build. Maximum file size should not exceed 4GB, it said, and my file size was 17 GB…
17 GB? Hmmm 🤔
There is something wrong, really, really wrong.
First and foremost, I’ve never seen an error like that. And mind you, I had quite a few errors displayed to me by Unity. Second, even my Library folder wasn’t that big, what’s going on?!?!
When I searched for the issue, again, I saw that Unity had this 4GB file size limit for a long time, and why they didn’t change it. Then I realized what I’ve been doing wrong. Firstly, enabling mipmaps was increasing file size nearly 3 times; secondly, almost all of my spritesheet files were NPOT (non power of two). POT means Power of Two. Graphic cards are using matrix-like systems and textures and sprite files should be POT in resolution, like 512×512, 1024×1024 etc. You’d probably seen them. Secondly, Unity is able to use some compression algorithms with POT files. If a sprite or texture file is NPOT, like 517*347 resolution, compression algorithm does not work and graphic card has to work 2 or 2.5 times more to process these kind of textures.
As a solution I was offered Asset Bundle. Asset Bundle is a nice and strong solution, but it is too much of a static solution in order to use it in such a dynamic game. Furthermore, at this point in development, switching to Asset Bundle is not an option. No, I’m not saying that because I don’t want to deal with it. Asset Bundle has no good tutorial around. Every single hecking tutorial just shows how to put one single hecking sprite on the screen and using Asset Bundle to switch between SD and HD versions. I have tens of animations and hundreds of sprites, each independent of the scene. I can do that much without watching a stupid tutorial, just by reading Unity documentation. An advanced, complex use case is something you cannot find, which is exactly what I need. Maybe this is the weakest part of Unity, there are quite a few nice examples, tutorials, and documentation for entry level cases; however when you get advanced and complex use cases, you are on your own.
And with a project in its last phases, missing such an important detail made me feel inferior and bad. Welcome intruder syndrome again. But this still didn’t answer why I had never had this issue before.
Now… It’s time to go back…
While making a 2D game, first rule is to use atlas. I mean, each animation of the characters, movement, reaction etc. you’d have tens of sprites or spritesheets, and loading each of them from hard disk one by one means that your game will run as fast as your hard disk. This generally means 5 FPS. You can have the Ifastest SSD all you like, still the slowest process in your whole system is file i/o. Therefore you take all your sprites, images, and textures and put them in a big atlas (basically an image with quite a big resolution), and this file will only be read once from hard disk, then you’d get all your sprites and textures from this file in the memory. However, this brings another issue, RAM and VRAM usage increases considerably and you either use streaming or another solution. Sadly, little portion of people knows how hard it is to develop HD 2D games. You have two option: either your game will be slow/laggy, or you’ll use great amount of memory from system. Either way, someone will look at it and say “it’s bad programming…“. Good luck with that.
Anyway, I had solved this issue. Unity had a tool called Sprite Packer since version 5.4 or so. You’d enter a packer tag to your sprite or spritesheet file for any character, enemy or whatever you are having, and Unity would create an Atlas, fitting all these sprites into it. Then your game only uses this atlas file instead of all those sprites and spritesheets. These atlas files are both POT (power of two), and compressed with Unity’s compression. They have a low file size, good process by graphic cards, and you’ll probably solve the problems I’ve listed earlier.
Of course, everything related to Sprite Packer was entry level. An advanced usage in a big project was as much as an issue in Unity like everything advanced and complex. Once in every engine update, Sprite Packer would stop working and I’d have to deal with it.
Eventually Unity solved all these issues and I was finally able to use Sprite Packer without an issue. I even went ahead and write my own Sprite Packer Policy (TanselsAwesomeSpritePackerPolicy). All was good. Then Unity announced that they would use something called Sprite Atlas in their 2017 versions, and said that future of the sprite atlasing would be this new tool. Get off I thought to myself, and never looked at it. I already had something working. I had.
While I was trying to take a build, I realized why I was having 17 GB file size causing Unity to throw an error and quit, and it was too late. Basically:
- Because I was using Sprite Packer, all my sprites were in atlases that were POT (power of two), and it didn’t really matter if my imported sprite and spritesheet files were POT or NPOT.
- If you enable Read/Write in import settings, Sprite Packer would refuse to atlas these files.
- Any file that are not atlased would be taken into build as is.
- Resulting the build size increasing from 900 MB to 20+ GB.
- If I were to disable Read/Write option, I could no longer use dynamic collision and shadows. (That turned out to be a huge misunderstanding)
- Sprite Packer would in no way use Read/Write enabled files.
- All these (due to being NPOT) uncompressed and non-atlased sprites, with the inclusion of Mipmaps enabled, was reaching ridiculous sizes.
- Unity’s maximum 4GB file size was being exceeded nearly 4 times, taking a build is impossible.
And I was like.
Then I sat and thought. Well, I said, if technology is advancing, and if my only solution is to use it, so be it, and I’d leave Sprite Packer behind and use Sprite Atlas instead. I did so, and realized that, Sprite Packer was already in Legacy state. It will no longer receive updates or bugfixes, and will be removed from editor eventually. Very nice -_-
Again, as per Unity usage case, Sprite Atlas also had little to no useful documentation, videos, explanations, tutorials which made me wonder if I was going to be able to use it. No manual or tutorial mention if I put a sprite in an atlas? Would the game automatically use that sprite from atlas instead of sprite file (updating reference), or would I need to use some coding to update relevant sprite fields using atlas instead of file reference. This was going to be frustrating. Another issue was what was going to happen if I were to exceed 8192×8192 in size, would the sprite atlas just go into second page like Sprite Packer did? Nothing refers to these information I need, and formerly Sprite Packer would generate 7-8 pages for the same tag due to excessive amount of sprites used in animations.
I found all the answers I was searching for in a Korean game developer’s YouTube video, which was watched only a few hundred times. Sprite Atlas, in the case of exceeding maximum size of 8192×8192, will go to a second, third, and consecutive pages, and when the atlas is built, Unity will stop using sprite and spritesheet files and move all references to atlas. You can still use and play with the editor like nothing changed, but whenever you hit the play button or take the build (depending on your editor atlas settings) you’ll just use Sprite Atlas references. Hooray.
After I updated whole system to use Sprite Atlas, I tried to take a build and it was a success. Moreover, Sprite Atlas with a new API enables me to use Read/Write enabled atlases, even enable Mipmap creation. Due to my stubbornness against new technologies, I wasted a lot of time. Now everything is gonna be beautiful.
With a smooth transition into Sprite Atlas I realized taking a build is much faster. When I used Sprite Packer, whenever I changed a sprite or texture file, a new packing process was being triggered, and even testing would get back to me as a waste of time. Sprite Atlas just looks out for the changes in its reference sprite and spritesheets, thus more convenient. I sent the new build to Benjamin and thought we were over the hardest part…
“Game isn’t loading“, was the immediate response. All the scene independent objects and pools were being created in the initialization time, I did so on purpose, I wrote a system that would load/instantiate/pool anything relevant upon game start. On his computer, this system was taking more than 5 minutes. Any loading screen over a minute is unacceptable today. Therefore I told Benjamin to wait a little bit more to see if the game actually loads, and decided to re-approach this so-called cache mechanism I used in dynamic shadow and collision.
Well, I thought to myself, now that I have an atlas file, why would I select all sprites one by one to put on a list to access them, while I already have that list in the atlas? I’d read all the sprites in the atlas, create their relevant data, and cache them under a child object, and use from there. I wrote an editor extension for that purpose (in another post, I’ll talk about this), I worked on all characters and enemies and took another build. Only then I realized that some of the characters or enemies would take a few minutes to create all relevant data on runtime (especially creating a PolygonCollider2D component and using its point data), but now each character/enemy would take at most 0.002 seconds to process. When it is paralleled, opening, initialization, and loading screens would have a latency of 0.1 seconds for this system. At runtime, it is just enabling/disabling game objects, which takes around 0.0025 millisecond. This time I did better. Dynamic collision and shadow system was added, working, and I kept the resource usage at minimum.
Until that infamous bug stroke back.
Last stage attack from one of the weapons would freeze the game. I immediately went back to debugging and opened the logs. Problem was still relevant to my dynamic shadow script. If I disable the script, game would run as expected, but if I open it, game would freeze at that last stage attack.
But… but I solved it!
This time I started an extensive debugging. I tested all characters, weapons, enemies one by one. Only issue was at this specific weapon’s last stage attack. I wrote a special case for that, tried to dynamically create things at runtime, and no solution at all.
And then I started to see which sprite specifically was causing the issue and tried to debug that sprite. When relevant animation started, I got an error: sprite is null. Rings a bell?
Same problem as the first one.
And the problem is: I’ve updated the last attack animation sprites with a new version, but somehow sprite references in the animation file did not update, and when I deleted the old spritesheet file, all references were gone. Therefore, when animation starts to play, it tries to reach a sprite which is non-existent, and I try to reach that sprites data, thus creating all these errors leading the game to freeze. I updated the animation file in less then 20 seconds and… Gone are the errors.
I mean, this roller coaster was only because of missing sprite reference in an animation file. I felt so stupid, I got angry with myself, so much so that I couldn’t look at the bright side. Bright side:
- I got rid of an old technology like Sprite Packer and switched to a new technology like Sprite Atlas, and it was beneficial in more than one ways.
- I finally got a reason and courage to get rid of my old badly designed / implemented dynamic collision and shadow system. I created an editor extension, and turned whole system into a more performant and stable state.
- I opened another bug report for Unity. Sprite Atlas, too, has similar problems to Sprite Packer, if I ever try to atlas all sprite atlases at once, I get a neat and clean out of memory error. They’ve found the root issue and will be releasing the solution into next release. In the meantime, I’ve processed all atlas files one by one, no problem.
- Even though I’m not going to use it, I’ve learned about Asset Bundle.
- Since my old account that’d been banned, I’ve become a moderator on QA site again.
And most importantly, it takes a great deal of good planning to start a work. When you encounter hard to overcome problems, your journey to find solutions may receive answers like this:
Gökhan Doğramacı is a close friend of mine and a Unity professional. When he said what he said, I was fuming, but I guess next time we gotta think about that. Some planning requires good decisions at the start, before you find yourself at some point that is impossible to go back from, and finding workarounds often proves very tiresome and discouraging. You’d guess why some games are being postponed, why it takes long time to finish them.
When I started as a programmer for Neon Town I thought I knew a lot, but in these 3 years I’ve learned a lot, and I saw how inexperienced I was. Now I’m adamant at using Unity editor and engine features, and finding workarounds for the walls it puts around you; though still learning. This post may be taken as a summary of my journey, as this was neither the first nor the last problem. For example, upgrading to 2017.4 LTS was another decision which proved problematic, and I’ve had to dealt with in the passing weeks.
What’s beautiful here is that we are back at square one. I open a bug report, and they solve it.
I think I can also say something about the main idea of this post.
Firstly, debugging is hard and it can often mislead you from the root cause. For this problem, it’s what happened. A problem so simple, so easy to solve in its essence, made me lose a few weeks. Even after I solved all irrelevant errors, it still took me some time to see the main issue.
Secondly, fair is foul, foul is fair. I’ve built a much better system. All the things I should’ve fixed, updated, rewritten are now fixed, updated, and rewritten. Dynamic shadows and collisions work much better now. Wait until you see Cassius.
It’s time I get back to solve issues in AI.