Download the game here!






Main Contributions

Suara was my junior year project that was developed in our own custom C++ engine.  I worked on a team with 9 other Programmers, 2 Designers, and 1 Sound Designer at DigiPen.  I was the Producer and Audio Lead where I lead weekly team meetings to keep things organized and on track, as well as meet weekly with our Sound Designers to keep a constant flow of communication between all aspects of the project.  As a developer my main contributions are the Wwise Project / Engine Implementation, Core Engine with Custom Reflection / Serialization / Components, Gameplay, and the Editor.  I was also responsible for making sure our audio content is on track and in theme for the game, while also composing a bit myself.  Detailed information for the systems I worked on are provided below.


Wwise / Gameplay

As Audio Lead for both content and Implementation on a game where audio is a pillar to the gameplay, making sure Wwise and our Engine have as much communication as possible is a must.  I've built in the profiler functionality so our Sound Designers can test audio live, as well as have created Wwise - Engine parallel event calls. This allows Wwise to use callbacks to tell the Engine about audio syncs, and allows the Engine to tell Wwise about gamestates.

As for gameplay, I have created the entire boss level as shown in the prototype, as well as worked on audio reactive elements and enemy behavior.


Core engine

The core that I've designed and built consists of Reflection, Serialization, Custom Component system, Archetypes, and Objects. The base structure of the Reflection System was influenced by both Randy Gaul and Sean Middleditch who had great amounts of information on creating a custom Reflection system.  Once I had enough setup and learned enough to know what I was doing, I catered all of the systems to our engine specifically, knowing what we would want to have for functionality.


Imgui Editor

I used ImGui as our editor API again for this game project.  As it stands, I have an Archetype editor up and running which allows you to do as expected: Create Archetypes, Add Components to Archetypes, and edit the Component's properties. It's much faster than creating archetypes manually in code, and allows for my team to edit content without having to recompile the engine.  The archetype editor also has a model viewer, to allow you to see some of the changes you are making at runtime.


Implementing Wwise in a custom engine turned out to require a lot less than I had originally thought.  To get most of the functionality, all that is required is that you have the correct libraries and initialize all of the Wwise modules correctly.  Most of the work happens in the Authoring tool, and I had to make sure there was enough engine-side functionality for my dev team.  Below is a snippet of the main public interface of the Audio Engine.


I tried to wrap Wwise in such a way that made my team's life easier when accessing the audio interface. Functions such as SetFocus, OnPause, and OnUnPause were designed to be simply called when the engine handles those types of events, while PlayMusic for instance was designed to be told what to play and take care of the rest.  PlayMusic takes a custom AssetString, which allows music to easily be loaded in from serialized levels.  It also automatically subscribes the event callback to Synced Beat and Bar events, so the user never has to think about it.

Below is an example of the Wwise music callbacks passing information to the event manager.  Since the callback is called on the audio thread, I had to make sure I did as little processing to to not hold up the thread.  It just take the information it needs, add it to a thread-safe queue, and wait to flush the events later in the normal game-loop on the main thread.

With the help of Wwise, I was able to make the SoundEmitter component very small and simple.  All it needed to do was handle updating Wwise with information about the object automatically without user prompt.  It was nice to also build in the functionality to post events and set switches from this component as well, since it allowed the user to only need to pass the event, and the ID was taken care of.  While it be a small thing to manage, it make posting Wwise events from objects much shorter and simpler.

Core Engine

Core Engine consists of a couple of large systems.  I put together custom Serialization, Reflection, and Components that the entire engine and game was built upon.  The main reason for custom Serialization  / Reflection was for the customization and catering of features for specifically this game.  It allowed me to make the serialized files very human readable, and easy to edit for my team to work with.

This is an example of the Player Archetype serialized file.  There were very specific choices I made for the formatting, and all of which were meant to making editing a file feel like editing normal variables in C++.  I made sure strings were formatted with "quotes", member variables between  { curly brackets } of the class, and so forth.

You'll notice the Name_ variable under Model seems a bit odd. This was how I chose to format our custom AssetStrings, which are functionally strings, but store an asset type for our engine loading.  Custom serialization allowed for this kind of customization, and allowed my team to iterate without having to recompile, and while they were waiting for me to proceed to make the editor.

Below is an example of part of a Level file.  SpawnObjects were a way for me to only store the necessary information needed to create an object in a level.  Instead of serializing an entire Object's data, I just needed the type of Archetype it was, it's Name, and which Components you wanted to change from it's default state.  This worked well for things like the WaveManager, to customize each wave per level right in the file as shown below.
Loading a level consisted of creating the base archetype, and then only changing the non-default data.  If the ComponentsToChange vector was empty, then it would spawn the object with the defaulted properties.


Below is an example of the default serialize function, meant for Components, Classes, and Structs that have member data.  A Component for example upon serialization would call this function, which sets up the scope for the class as shown in the above files, and then iterates through all of its member variables and calls their respective serialize functions.  To save space, I made sure to only serialize the data that was changed from its Component's default value.

Here is an example of a specialized Serialize function for std::string.  Each serializable type had it's own specialized  function that it stored in its MetaData.

This is the matching Deserialize function for std::string.  I had to account for the "quote" formatting when reading the data, so using a scanset made that simple.

One of the things I made sure to get implemented as a feature was automatically registered and serialized enums.  Enums are a great way to keep systems organized with readable states, and if I didn't implement serializable enums, I essentially would have made it impossible for my team to use them in Components.

The approach I took with enums were similar to the custom components, and that was wrapping all of the functionality into macros that my team would use.  You can see here a simple creation of the enum used for the window mode.  The reason implementing enums was a project of its own, is because each enum is a different type.

I really didn't want to manually create specialized serialization functions for each enum someone created, so I made a way for them to be automatically created.

The macros would expand into an enum class with comma separated values.  It would add INVALID_ENUM for error checking in reflection and serialization.

In order for to serialize the enums, I needed a way to go to and from the string / enum representation. ToEnum takes a string as the value name and and returns the corresponding enum. To avoid requiring the enum type in the ADD_VAL macro, I just declared the enum at the start of the function which allowed me to get the type via decltype in the following macros. I similarly created a ToString function, which was also needed for serialization.

I decided to represent enums by just displaying the value as a word without quotes to differentiate from strings.  This is why I needed the ToString function.  I am given the value to be serialized as an enum, but I can't print that.  I could have printed the numerical index value of the enum, but that isn't as human readable and would have been more difficult for my team to edit the files.

This meant for deserialization I needed to read the string and somehow set the variable to the correct enum value; which is why I needed ToEnum.  This made it very easy to see when a serialized variable was an enum vs. an integer, and also which enum state it was being set to.

Many other functions were automatically created for the use of enums, such as being able to automatically display them as comboboxes in editor, but this is the core of the system.



When it came to making the editor, I had to decide what API would be the most worth it.  I had originally considered either MFC or Qt, but I knew we wouldn't need an extensive editor for the game we were making, so I decided to go with IMGUI for it's simplicity.

The one main goal for the editor was to be able to create and edit archetypes.  Levels weren't as important since most of the gameplay was dynamic.

I focused on the Archetype Editor and made sure to implement features such as: New / Delete Archetypes, Add / Remove Components on Archetypes, as well as be able to edit the specified properties on each Component. Once I got the main functionality in, I decided it would be worth adding a model viewer, to allow users to see some of the changes they are making at runtime.

I originally had planned to add many more than 3 modules, but I realized all of the extra features wouldn't be worth it for how much they would be used, so I decided to cut them.

In designing the Editor System, I set it up to have the Core Editor that had instances of modules it would update.

It worked like a normal game loop, and each frame would update the individual modules. The Core was a good place to also store data that every module needed, while still containing the data within the Editor System.

Every module had Update and CheckKeys functions, as well as an Active state. This allowed the core to treat them all the same.

The design process for each module was to functionalize every action. Since IMGUI is immediate mode, I have to tell it to draw things every frame.  Instead of leaving it all together, it was much cleaner to call the functions for actions.

This is the interface for the Archetype Editor module. Just by looking at some of the functions listed, you can see the thought process I had when creating the actions. These were more helpers for the main actions of the module.

These are the main actions of the Archetype Editor, and they are probably as you would expect. In order to edit archetypes, you essentially have to have these main functions. I started the module with these functions, and added the rest of the interface as helpers when I needed them.  It was a good way to think about what each of the primary actions would involve as sub-actions.

The main part of making an editor however, is not all of the features; but the user-error handling.  Below is an example of the function responsible for Adding Components. It displays a list of components the user can add, and all the user has to do is select a component and click "Add".

The main thing I needed to check for in this case, was if the Archetype and Component were valid, and to also make sure a virtual interface wasn't going to be added from a base Component.

I also had to make sure that the Component wasn't already on the Archetype.  All of this information was saved in the MetaData, so it was simple in editor to check for these things. The MetaData of every component stored information about what it inherited from, and what it depended on.

In addition, it is very easy to go ahead an add a component without first adding its dependencies.  Since this was so common to forget, I decided to make everyone's life easier and automatically add all components it depended on.  If some of the components it depended on were added, in this case it wouldn't warn the user and just skip that component, since this was more of a behind-the-scenes feature. It would however, log that it was automatically adding the components so the user was aware of what was added.