Spite: The Curse of Tzalozel
🎮 Link to game: https://there-goes-the-goose.itch.io/spite-the-curse-of-tzalozel
My Contributions:
- UI
- Scene Loading & Transitions
- Engine Maintenance
This was my 5th game at The Game Assembly. It is a Diablo III inspired hack and slash where you traverse the jungle and defeat an evil goddess.
Between previous projects we were assigned to new teams, but starting with this project we would have the same team throughout the second year. We would also make our games in our own custom engine. This was challenging since we had to create tools for the engine and develop the game at the same time. My contributions were mainly on the engine side, such as creating our UI editor and particle system editor. On the game side I worked on the UI functionality, scene loading, and various minor elements such as the arena combat encounters.
Scene loading
In our scene editor, every scene object is saved in a separate json file. This allows multiple designers to work on different parts of the levels without getting merge conflicts.
It has a drawback though and that is loading times. Loading thousands of different files takes a lot of time, especially if the filepaths are not loaded in windows cache. To solve this problem, without removing the benefits of multiple object files, I added a build process for our release version of the game. This process would go through every scene, and combine all scene objects into 1 larger json file.

This alone sped up loading times on average from 5 seconds to 1 second. I would iterate on this even more by saving the build file in a binary format instead of json. To achieve this I made a binary buffer class that was easy to read from and write to.
1namespace Goose
2{
3 class BinaryBuffer
4 {
5 public:
6 void InputFromStream(std::ifstream& aIfStream);
7 void OutputToStream(std::ofstream& aOfStream) const;
8
9 char* data();
10 const char* data() const;
11
12 char* GetCurrentByteAddress();
13 const char* GetCurrentByteAddress() const;
14
15 bool HasEnoughBytes(size_t aSize) const;
16 void EnsureBytes(size_t aSize);
17 void IncrementPosition(size_t aCount);
18
19 void ResetPosition();
20 size_t GetCurrentPosition() const;
21 private:
22 std::vector<char> myData;
23 size_t myCurrentPosition = 0;
24 };
25}
With this buffer, it was easy to serialize the scene data by using templated read & write functions.
1template<typename T>
2void WriteData(BinaryBuffer& aBinaryBuffer, const T& aData);
3template<typename T>
4void WriteArrayData(BinaryBuffer& aBinaryBuffer, const T* aData, size_t aCount);
5
6template<typename T>
7void WriteData(BinaryBuffer& aBinaryBuffer, const T& aData)
8{
9 aBinaryBuffer.EnsureBytes(sizeof(T));
10 std::copy_n(reinterpret_cast<const char*>(&aData), sizeof(T), aBinaryBuffer.GetCurrentByteAddress());
11 aBinaryBuffer.IncrementPosition(sizeof(T));
12}
13
14template<typename T>
15void WriteArrayData(BinaryBuffer& aBinaryBuffer, const T* aData, const size_t aCount)
16{
17 const size_t byteSize = aCount * sizeof(T);
18 aBinaryBuffer.EnsureBytes(byteSize);
19 std::copy_n(reinterpret_cast<const char*>(aData), byteSize, aBinaryBuffer.GetCurrentByteAddress());
20 aBinaryBuffer.IncrementPosition(byteSize);
21}
22
23void Goose::Serialization::WriteData(BinaryBuffer& aBinaryBuffer, const SimpleMap<SceneInstanceID, GameSceneObjectInstance>& aData)
24{
25 const SmallSizeType mapSize = static_cast<uint16_t>(aData.size());
26
27 Base::WriteData(aBinaryBuffer, mapSize);
28 Base::WriteArrayData(aBinaryBuffer, aData.dataKeys(), aData.size());
29
30 for (int i = 0; i < static_cast<int>(aData.size()); ++i)
31 {
32 const GameSceneObjectInstance& instance = aData.data()[i];
33
34 Base::WriteData(aBinaryBuffer, instance.transform);
35 Base::WriteData(aBinaryBuffer, instance.prefabId);
36
37 const SmallSizeType nameLength = static_cast<uint16_t>(instance.name.size());
38 Base::WriteData(aBinaryBuffer, nameLength);
39 Base::WriteArrayData(aBinaryBuffer, instance.name.c_str(), instance.name.size());
40 }
41}
Storing the scene data in binary reduced load times even further, from 1 second to around 0.05 (almost instant). This way of serializing was also easy to write which later allowed me to store models and animations in binary as well, which was also significantly faster than loading from fbx.