top of page

Basic Level Builder Retrospective

  • Writer: peacefulillumination
    peacefulillumination
  • Feb 9
  • 7 min read

Overview

The Basic Level Builder (referred to as BLB from here on out) is a 2d level editor tool used to teach level design at DigiPen. While using it for my classes, I noticed many improvements that would enhance my experience. During my senior year, I asked if I could work on some improvements for the application as my technical design capstone which they highly encouraged.

The first thing I did was list out all of the features and bug fixes I would like to see and added them as issues to the GitHub hosting the original source code, which can still be seen on their issues list. After that, the professor and I triaged them, putting them into A, B, or C bucket priorities. With that done, I hand-picked the first thing that I wanted to start working on, and little did I know that would take me over a year to finish.

Problem Identification

The task that I had chosen was issue #50: Level Version History. During my time using the BLB, I had issues wanting to test out a change but also wanting to backtrack if I didn’t like it. The only way to do that was by creating duplicate levels, and those files quickly filled my computer.

In addition to that issue, there was a constant frustration with level saving and loading. As the application was single‑threaded, performing large operations caused the app to seize up and freeze for a couple of seconds before the user could do anything again.

With those two problems identified, I knew what the first step would be.


File Compression

Compression! My first thought wasn’t multi-threading at the time. Instead, I thought that reading and writing large text files could cause problems, especially as level sizes grew too large. The save files were being written with each placed tile on its own line, along with any related data next to it.

File compression in Unity (.NET) is quite standard and took no time at all to implement. Using the built‑in GZip compression algorithm, I was able to compress and decompress the whole file. I even created a small helper class to encapsulate the compression logic.

 


Multi-threading the Save Process

After file compression, I started work on multi-threading. The main solution was to run file saving on its own thread so that the main thread wouldn’t be stuck showing noticeable lag. Simply creating a tile grid buffer and sending it to the saving thread solved most of the problem.

One thing I was unaware of at the time was the requirement that certain functions could only be run on the main thread. I was able to get around this by creating a main thread dispatcher that queued actions to perform on the main thread. Thankfully, none of the actions were required to continue saving and were mainly print statements that didn’t need to sync directly with the main thread.



File Version History

Once the performance issue was addressed, I started work on the final piece: file history. This portion took the longest to plan, develop, and test, as there were far more edge cases than I initially thought.

File Structure

I planned the file structure to look something like this:

File (JSON): {

FileHeader\n

FileData

}



The file would contain one line of uncompressed data that informs the application whether the file is compressed after the header, and which application version the file was created with. This allows us to determine whether the file is compatible with the current version.

If we are missing a header, we know we are reading an older version and can either abort reading or implement a method for reading older files. After confirming that we can read the file, we decompress the remainder and load the JSON data.


The file data contained saves from both automatically performed actions and intentional manual saves. Referencing a Git‑style versioning approach, I treated each sequential manual save as the trunk and each auto save as leaves branching off an individual save. This allowed us to avoid writing the same data repeatedly and instead store only the deltas between saves.




Edge Case Analysis

As I continued to implement the functionality, I encountered many edge cases related to the different states the application could be in. To manage this, I created a matrix of each state and defined what should happen in each case, then condensed similar results.

Edge Cases Matrix


Once all the cases were identified, I was able to link each case to its result and get it working, until I found more edge cases. (ah the life of a programmer)

Code Organization

As the FileSystem file grew close to 2,000 lines, I split related functionality into separate files:

  • FileSystem – Public wrapper for file system operations

  • FileDirUtilities – Directory management and file discovery

  • FileSystemInternal – Core loading and saving logic

  • FileVersioning – Version data operations. (Should really be renamed to “LevelVersioning” in the future, as this file doesn’t ever touch a save file)


UI Iteration

With the backend complete, I moved on to the UI. The UI went through significant iteration and paper playtesting. Only selected samples are shown here; the rest can be found on Figma:

History Window

My first rendition of the history window displayed both manual and auto saves in their own scroll boxes, but after a bit of deliberation with the professor, I realized that auto saves should not be as prominent as the manual saves since they would only be needed on rare occasions where the user forgot to save.



To fix this I decided to lay them all out together and allow the user to filter the list if they only want to view one or the other. However this still had the issue of hiding the underlying structure of the data and how the auto saves relate to the manual saves.



Remembering that the versions are laid out like a tree, I redesigned the UI to reflect that structure. Auto saves branch off manual saves and can be collapsed, making it easier to browse manual versions. With this distinction, prefixes became unnecessary, and manual versions could be renamed similar to Git commits.


History window mock-ups and final

Mock-up 4


Mock-up 10


Final


Files List

In addition to managing the files version, we should probably be able to manage the whole file itself, so I went ahead an created another window for displaying the files properties and allowing for some edits to the files name and description. Although I was just tacking it onto the history window which didn’t look all that great. So, I decided to throw into a new window tab that could be selected on the side and swapped to. With the new tabs bar we could add more windows to the files property window, yet I only used two tabs, so that is just set to be modular for future use.


The file info window was pretty straightforward as you can see little change from the mock-up to the final. The only complaint I have, in retrospect, is the enormous space for the description. I didn’t have anything else to fill the space, so I let the description box fill it. I could have at least increased the text size to make it feel less big, but I’ll leave that for the next update.


Mock-up


Final


Files List

Lastly is the files list. The original idea was to display a button for each sub window (history and file info). Although that takes up quite a bit of space, so I ended up only showing one button when the mouse is hovered over the file. In addition to highlighting the currently mounted file and a few small touch ups, I reached the final version.


Mock-up 1


Mock-up 4


Final


Version Actions

After finishing the UI, I implemented actions that could be performed on versions: deleting, exporting, and promoting.

Exporting

Exporting was quite simple, all you need to do is flatten the deltas up the tree till you reach the selected version, then save the data to a new file. Each versions level data contains the deltas for removed and added tiles, so to flatten the changes we essentially paste the deltas to a blank level grid and repeat till the needed version. Then we can add each tile in the grid to the new files added tile list.



Deleting

Deleting a version is slightly challenging as we need to make sure the needed deltas get moved over to the next version on the tree. Although deleting an auto save, which is a leaf, is as simple as deleting that data.



Promoting

Promoting an auto save requires adding possibly redundant or unneeded tiles to the tree, so we have to check which tiles are not present in the next version and add them to that version’s remove list. We also will possibly have tiles that are added twice, once in the newly promoted auto save and once in the next manual version, but we can ignore them as it won’t cause any technical problems, just take up a bit of storage. If we were to clean up the tree, we could check if any tiles were added in both versions and remove them from the latter. In addition to that, we also need to push the new manual save into the tree stack, increasing the versions of any saves higher than it.

If you were wondering, the “Ex” version of functions are private extended implementations where the non-Ex versions are public wrappers.



Conclusion

This covers the majority of the updates I made to BLB without diving into every implementation detail. If you would like to check out the full source code, it is all up on my GitHub. This project has been a blast to work on even with all the hardships that surfaced. Farley recently, I released the projects first beta and is in its final testing before it will be merged upstream. I am excited to finally see my work in the hands of students!

Project links

Comments


bottom of page