Chlorofell is a fast-paced isometric shooter where you play as a humanoid plant guardian protecting your home against a mechanical scourge that threatens it. Armed with an arm cannon built from their scraps, utilize specialized ammo types, quick thinking, and the wrath of nature itself to storm their compound and eliminate anything that moves.
Team:
A quick example of our custom engine, showing how to make a menu button.
Our game's trailer. Check out the action!
Gameplay/ Audio Programmer, Associate Producer
Developed systems, such as menu functionality, gameplay mechanics, and signals and slots messaging, for our Custom C++ engine.
Integrated the Wwise audio middleware into our engine, and all audio into gameplay, including SFX, and transitioning BGMs.
Created engine editor functionality for systems like menu buttons to make designer adjustments easier.
Collaborated with the head producer to resolve conflict, and create strike teams, to increase communication between disciplines.
8 Months 12
Isometric Action Shooter
PC Flintlock
To create our isometric action shooter game Chlorofell, we built Flintlock, a custom 2D C++ engine, using modular practices, as well as OpenGL for graphics, and Dear ImGui to make its editor.
While our engine is mainly application driven, using the update loops of different modules and components to control the game, it became clear that we could benefit from being able to use events as well, particularly in the way that unity does it, with delegates, and the subscriber model. So, with help from this, and Sergey Ryazanov's impossibly fast C++ delegetes as inspiration, I made our signals and slots-like event system.
This action class was what became our version of delegates, letting you subscribe any number of callback functions, and then invoke them.
template<typename... Args>
class __declspec(dllexport) Action {
public:
// Initialization --------------------------------------------------------------------------------------------
using ActionFunc = std::function<void(Args...)>;
using ScriptCheckFunc = std::function<bool(void)>;
using InvokeFunc = std::function<void()>;
Action(){};
~Action() = default;
// Action use -------------------------------------------------------------------------------------------------
// Calls each subscribed function (given paramaters will be passed to each)
void Invoke(Args... args);
// Adds a function to be called when the event is invoked
// Use for non-script functions
Action& Subscribe(const ActionFunc& func, std::string_view name);
// Adds a function to be called when the event is invoked
// Use for functions inside scripts
Action& Subscribe(const ScriptContainer<Args...>& func, std::string_view name);
// Adds a function to be called when the event is invoked
// Don't call directly, use the SubscribeMember macro. Use for member functions of classes/ structs
template<class T, void (T::*TMethod)(Args...)>
Action& Subscribe(T* reciever, std::string_view name);
// Removes a function from the action
// Use for non-script functions (both member and non-member)
Action& Unsubscribe(std::string_view name);
// Removes a function from the action
// Use for functions inside scripts
Action& Unsubscribe(const ScriptContainer<Args...>& funcPkg);
bool HasActions();
bool HasCallback(const std::string_view name);
// Operators /////////////////////////////////////////////////////////////////////////
bool operator==(const Action& rhs);
private:
// ... Callback class implementation ...
// ... Callback helper functions ...
// Variables ///////////////////////////////////////////////////////
std::vector<Callback>mCallbackList; // Holds all subscribed functions
};
Each callback on an action needs to have the same function signature, but in practice, this restriction was fine, as if multiple functions were subscribed, they were either quite similar, or able to ignore arguments entirely.
When using the actions in code, subscribing class member functions was a bit long to write, but I was able to simplify the syntax of the function using this below macro.
// Used for simplifying subscribing member functions to actions
// type - Name of the class the function is from
// funcName - Name of the member function
// name - Name to be stored as a string
#define SubscribeMember(type, funcName, name) Subscribe<type, &type::funcName>(this, name);
The callback class stores data for each function, such as its packaged function pointer and name. The name was added to make it easy to unsubscribe a callback from an action.
class __declspec(dllexport) Callback
{
public:
Callback(const ActionFunc& func, std::string_view name, HINSTANCE* hProc = nullptr);
Callback();
template <class T, void (T::* TMethod)(Args...)>
static Callback from_method(T* object_ptr);
//Variables /////////////////////////////////////////////////////////////////////////////
// For member functions
typedef void (*memberActionFuncStub)(void* mRecieverObj, Args...);
std::string mName; // Name used for unsubscribing and comparison
ActionFunc mFunction; // mFunction to call on event, for non member , non script callbacks
HINSTANCE* mHProc; // Address the script function comes from (if any)
void* mRecieverObj; // Member object, of any
memberActionFuncStub mMFuncPtr; // Packaged member function, if any
// Operators /////////////////////////////////////////////////////////////////////////
operator bool();
void operator()(Args... args) const;
// Helper functions //////////////////////////////////////////////////////////////
template <class T, void (T::* TMethod)(Args...)> // Class, and member function pointer
static void memberFuncStub(void* object_ptr, Args... args); // Class pointer MFP is in, args for call
// ... Other helper functions ...
};
A container of important game events that can be subscribed to. I subscribed to these events by getting the event reference directly, which is a practice that should be improved in the future, but the overall functionality of the EventList let us easily and modularly add responses to game events as new systems were created.
An example of these events are those for pausing and resuming the game
static Action<int>pauseGame;
static Action<int>resumeGame;
From the sound manager
Action<int>& pauseEvent = EventList::PauseGameEvent();
if (pauseEvent.HasCallback("DuckAudio") == false) // Ensures no duplicates
{
pauseEvent.SubscribeMember(FLSound, DuckAudio, "DuckAudio");
}
Action<int>& resumeEvent = EventList::ResumeGameEvent();
if (resumeEvent.HasCallback("UnduckAudio") == false) // Ensures no duplicates
{
resumeEvent.SubscribeMember(FLSound, UnduckAudio, "UnduckAudio");
}
From the scene manager
// Pause event
EventList::PauseGameEvent().SubscribeMember(FLSceneManager, PauseLayers, "PauseSceneLayers");
// Resume event
EventList::ResumeGameEvent().SubscribeMember(FLSceneManager, ResumePausedLayers, "ResumeSceneLayers");
Then, in the menu manager, we could invoke the pause game event, let the event system deal with whatever is subscribed, and then proceed as normal.
EventList::PauseGameEvent().Invoke(1);
OpenMenu("Pause Menu");
Using events, buttons can be customized with any number of functions to happen when hovering, holding, or releasing the mouse on them, adding to the modularity of the system.
// Button function events
Action<ColliderPtr, Component&> mOnHover;
Action<ColliderPtr, Component&> mOnHold;
Action<ColliderPtr, Component&> mOnRelease;
From the button JSON deserialization functionality:
if (component.HasMember("OnHover"))
{
std::string funcNames = GetMemberValue(component, "OnHover").GetString();
// Clears names vector, so no duplicates carry over
mOnHoverFuncNames.clear();
// Saves as separate function names
JsonHelper::StringToStringList(mOnHoverFuncNames, funcNames);
// Subscribes each function
for (std::string funcName : mOnHoverFuncNames)
{
ButtonFunction::ButtonActionFunc hoverFunc = ButtonFunction::GetFunc(funcName);
// If invalid, function name read was invalid. Also, no duplicates
if (hoverFunc && !mOnHover.HasCallback(funcName))
mOnHover.Subscribe(hoverFunc, funcName);
}
}
The button editor view I created, where you can select the functions you want a button to do for each interaction type.
To maximize the eerie feeling of our corridor level where the player makes it quite far without seeing any enemies, the team's sound designer and I thought to add transitions that intensify the music as the player walks.
We collaborated to find the best spots to place the triggers, and then I used the transitions he made in Wwise to make the triggers for them in-game.
The blue rectangles are the music transition triggers. Once triggered, the transition waits for the right moment in the music to cut in.
For this project, as it was my first time creating software from the ground up as large as a game engine, and my first interdisciplinary team game project (CS, Design, Audio, and Artists), there were a variety of best practices, and lessons I took away from this experience. In the beginning, after being a little lost in finding my role, I learned the importance of being able to find something to start on, regardless of the category. And, as work ramped up, I felt the usefulness of modular practices when designing code for gameplay, and systems in general. I also saw and did my best to help through struggles like managing scope, and observed the kinds of situations where we shot down ideas too much, and other instances where focusing on polish would have been better than new mechanics. All of which helped give me a better perspective on how to balance scope and team ambitions. Lastly, for the main lessons I found, while collaborating with the team's sound designer, I tried different methods to help make the audio vision successful and found consistent check-ins and clarifying discussions to be crucial in matching my vision with the sound designer's, identifying our next steps, and keeping up with growing systems.
In the beginning of the project, there were various specific types of programming roles to choose from, and while some teammates immediately took to things like graphics, AI, tools, and the like, but I wasn't quite sure which role I wanted to take. Since I knew this would be a great opportunity for learning, and potentially doing portfolio-level work, I wanted to find a specific industry role that I could get practice in, while also making sure I could contribute meaningfully. In the beginning, I ended up taking the role of the audio programmer, and it was both a distinct role, and helpful in moving the project forward. But, as I finished the needed audio work at certain times, I eventually just started picking out areas of the project that needed improvement, doing research on what could benefit the project (for example, research on events, which lead to my signals and slots event system), as well as talking with other disciplines, and my fellow programmers, about what may help us. While I can certainly group my contributions in categories like audio, production, and gameplay systems, in hindsight, I found that the most important part of my role during the project was that I was finding ways to move our project forward, regardless of how they I fit a specific role. For example, I may have taken the role of the audio programmer in the beginning, but we still needed a menu system, someone to implement the reload combo gameplay mechanic, and the like. Of course, some people on the team wanted certain tasks, and we needed to make sure tasks were distributed well, but with that in consideration, I found that there was still quite a lot to do. Being able to jump right in to move the project forward was something that helped me make meaningful contributions, even when I wasn't sure quite what my main role was. So, while I knew helping wherever was needed was common in my smaller team projects, it was useful to learn here that the same kind of wearing multiple hats can still be quite useful, even in larger teams.
I had heard much about the benefits of implementing systems in a modular way before starting this project, and as I worked on and eventually used the systems the programming team and I created, I began to see for myself why modularity was so important. Making our component system easy to add new components to was excellent whenever I needed to add a new system for our entities to use, and that ease also encouraged decoupling logic into separate components, likely saving us additional bugs. Furthermore, every time we could find related functionality, and simplify it by creating a base class to derive from, it made that system much simpler to add and tweak, being able to update multiple systems through their shared base class. Modular implementations did sometimes take more time to plan and execute, and I can see knowing what to make modular, and what may not be useful to make modular, being important for implementing systems in a timely manner. But, when it did work, modularity was both a nice feeling for the person who developed it, and a life saver for the people that used/ added to it. For example, when I made my button function system modular, so people could add the functionality they wanted to any button in only 1 extra code step, and using 1 tool in the editor. I was the only one using that system for a while, and I wasn't sure if the modularity would be worth the investment, but when we got close to submission, my teammates were able to quickly add settings for their systems to use in our settings menu, saving valuable near-submission time. My system also allowed for one of our designers to manipulate menu buttons using only our engine editor, and while I may have gone overkill on some parts of the modularity, I plan to keep the benefits, and the knowledge of where modularity was/ wasn't useful close in mind as I design implementations for future functionality.
One issue that we ran into in various instances was balancing scope with our team's creative ambitions. While our initial goal was to shoot for a more polished experience, there were still a lot of mechanics we wanted to test, and we (whether in smaller groups, or as a producer pair of myself and the head producer), found it difficult to decide when to cut off new ideas. We felt we had a fairly solid scope at the beginning, with being an isometric shooter with a few different special attacks, but as ideas came up like adding different bullet patterns, new special attacks, different enemies, and the like, we asked ourselves: "should we let those mechanics be developed, with potential increases to the fun of the game and the amount of testing required, or be more strict, to ensure we can make our polished product and avoid overwork?" While we did our best to predict how certain mechanics would impact the game, and greenlight new features based on that, I believe we shot down too many ideas in the beginning to middle stages of the project. This led to some animosity, as well as some stifling of creativity, and while some decisions do need to be made, I realized that after getting advice later, and seeing what has worked for us, there was a viable middle ground. In adopting a more prototype-based approach, and understanding the benefits of polish vs new features, we could enable creativity, while proving the feasibility of mechanics before they became too costly, and also prioritizing what creates the best experience.
For prototyping, I do want to mention that this project was a bit of a strange situation, as us programmers were building the engine while building the game. So, any prototypes we wanted in-engine came at a bit of a higher cost than developing in an engine like unity or unreal. Likewise, project requirements were a little unclear in whether we were allowed to use unity to supplement prototyping. Though even with this situation, I was still shown the value of prototyping, in that it shows the team in a live example what a person is thinking, and that it also helps prove the feasibility of the mechanic. And, if we had prioritized prototyping over discussions, we would likely have been able to feasibly get more mechanics into the game. Because, instead of this, we often opted for theoretical discussions with the team, where we weighed what could happen, and decided on mechanic there. Other benefits include the ability to adjust or change mechanics quickly once bringing the prototype to the team, and I think with this improved understanding, and an increased focus on prototyping first would be the better way to support creativity, while considering feasibility/ game fit, when a teammate brings up an idea for a mechanic.
Additionally, I gained an improved understanding of how polish benefits an experience, vs new features, and I think this will be crucial for making decisions on how to improve projects in the future. This applies more to the middle to end stages of a project, because sufficient systems need to exist for this to matter, but once a core gameplay loop exists, I realized over this project that it's the polish on core mechanics that makes the experience great. Take for example, towards the end of this project, where we were deciding whether we wanted to add our planned final boss. The main idea was to have this massive fight to make the experience memorable, but while we did settle on a compromise final encounter, if we had done any more work on the boss, we would have missed out on all the smaller polish features that testers reacted positively to the most. To name just a few, we devised bullet particles to make each shot feel powerful, and multiple layers of shield break feedback, to make getting hit a pivotal event (and it was, you had 1 heath and 1 shield!). Others included a flashing red low-heath vignette to make danger feel dangerous, eerie yellow lights, and dynamic music to turn our empty corridor sequence from strange to suspenseful. It was these features that often took a fraction of the time of mechanics and yielded major rewards when seeing how players reacted to our game. So, when deciding whether a last-minute mechanic idea is worth it, instead of saying no, I feel like the better answer is: is there some exciting polish you could be doing instead? Because this way, whether it's out of passion, or absence of things to do, that person can add things to the project that are quite impactful to the experience, while also requiring less pieces to move, and hopefully, less stress on the implementer.
While audio affected many aspects of the game, for implementation, it was largely the pair of myself, and the sound designer, with the responsibility of identifying the needs/ vision of audio and integrating them into the game. With this, I tried a few different strategies to find a workflow that worked for us and found that some of the best collaborations occurred when we had consistent check-in meetings, and discussions to clarify our vision for the audio. One unique part that I noticed about audio is that it can often come after gameplay mechanics are established, because if mechanics are scrapped and changed, most audio is scrapped with it. In our case, music was focused on while mechanics were being prototyped, and this meant that the needs of audio could change quite quickly once mechanics were solid enough to be given SFX. We needed to be on point in identifying when it was time to jump in with sound effects, and weekly check-ins helped us to be on top of new developments, and to form a plan for the near future. These check-ins were also a good time to discuss the audio vision, because as the game changes, the audio vision often evolves with it. Sometimes the vision doesn't, or can't, but either way, we needed to be on the same page about what the goals were, and we needed to make sure that other relevant disciplines were in the loop on that vision. We did struggle to find that vision, and progress often dragged when we had periods of sparse communication. As life happens, some of that will happen but when we were able to come together to discuss the audio vision, share ideas, and come to a clear, common understanding, that's when some of our best work happened. We were able to work together and create features like the dynamic music in the corridor level, shown above, and some of the more cohesive and impactful SFX, like those of the player's explosion abilities. Once we had a common goal, and were in the habit of consistent check ins, the audio progress flowed much more smoothly, and I intend to keep these collaborative tools in mind for future teams and strike teams of any kind.