Dec 2025: Charge! Is Available on Steam!
Click here to check out the steam page if you're interested!
Click here to check out the steam page if you're interested!
Creating a first-person shooter with fresh, casually fun gameplay through prioritizing the multiplayer experience, combining both older fun ideas from classic FPS games and specific experimental mechanics, and utilizing laser-tag like themes and sports inspirations.
Team:
Gameplay Programmer, Systems Designer, Tech Lead
Enabled stable online 3v3 matches by creating networked player and gamemode features using both Unreal Blueprints and C++.
Organized discussions to define our target audience and MVP mechanics, and rapidly prototyped to achieve the vision iteratively.
Collaborated with 4 animators, 3 UI artists, and other artists, to refine the character pipelines, and implement all game menus.
Led a team of 4 programmers, getting new members up to speed, and prioritizing and collaboratively delagating tasks to release the game on Steam.
Wrote documentation like our Unreal/ Perforce setup instructions, which helped 7+ new teammates set up the project quickly.
Profiled for performance, integrating vertex animation textures and optimizing lighting with our Art Lead to improve our framerate by 20%.
~1.5 yrs ~12
3v3 Multiplayer First-Person Shooter
PC Unreal 5
Networked gameplay programming, Unreal Engine 5, team leadership, rapid prototyping, interdisciplinary collaboration, delegation, systems design, game AI programming, UI programming
This project, with being a startup-style remote team, has been an opportunity to support development in a variety of areas throughout our project phases; such as programming, design, and production.
I am writing about aspects from the dev cycle like learning networked programming, which documentation I wrote was useful, and lessons from them on this blog page (Click Here).
And, here are a few features I'd like to share that I've made in the development of Charge!:
Light-based weapon modifiers like the prism passthrough used here, designed to support both simple fun and tactical options.
Player bots to enable more consistent playtesting, even with only one or a couple of players.
Main menu implemented in collaboration with our 2D artists, featuring mouse-reactive spotlights and themed button animations.
Networked weapon system, supporting hitscan shots, traveling projectiles, and melee attacks.
Pregame lobby, where you can wait for players, and test out game/ player mechanics before the match begins.
In approaching making the networked setup, gameplay, and other systems for this project, I aimed to use practices learned from working on Remanence, to inform our use of C++ and blueprints, as well as to test how long the benefits lasted of using blueprints over C++. So, to fully explore this, I used a hybrid of C++ and blueprints to try and leverage the quick prototyping and iteration blueprints are tailored for, even in some cases where I would have preferred primarily using C++.
Taking a practice we used on Remanence, in setting up a new feature, I would usually create a C++ class of whatever Unreal type suited the new mechanic (AActor, AActorComponent, etc.), and then derive it into blueprints, letting me create a variety of its aspects using the blueprint class and see how far that got me, but still leverage the flexibility and features of C++ when it seemed more useful.
And, for examples of this, right below is a case where a hybrid of C++ was useful, for creating a base C++ class for extensible abilities that could interface with project pieces created in blueprints.
To make modifiers to mix up gameplay by changing the behaviour of weapon shots, I tested out Unreal's Gameplay Ability (GAS) to see if it would be a good fit for our project. After testing, it's scalable base ability class and built in networked ability activations seemed quite useful for making different abilities, but it was structured to only call ability activations on the player that owned it, and the server, relying on their own replicated attribute system to hold stats, and gameplay cue system to change them, to send changes to the other clients in the game. Our system was set up to use our own stat structures on the clients and server, and use RPCs for sending immediate changes to ability related stats on clients, so being later in the project, and weighing the benefits of refactoring to use GAS, I decided to stick with our current setup based on the scope of what we needed the abilities for. However, I drew from GAS's generic functionality to make our own extendable ability class used in making our weapon modifiers.
After prototyping the modifiers in blueprints to determine what we'd need for them, I made a base class in C++ for modifier abilities to derive from.
I then made child blueprint classes for each ability, overriding the necessary functions to define each modifier's different behaviour, and allowing for future modifiers/ abilities to be slotted in, and have the system automatically activate them for the server and clients.
EX: Overwritten function for the "Rebound" ability to restore the player's firing behaviour to normal, after being changed by using the ability
These were the resulting abilities I created that we used in the arena:
"Rebound" reflects your shots off walls
"Prism Passthrough" bends and splits shots through the map
"Hazard Trail" weaponizes lingering tracers
Following this, one example of a gameplay feature where blueprints were plenty for what I needed was making an extendable team game mode class out of Unreal's base game mode, game state, and player state:
In finding what our main game mode would be, we started out with deathmatch, but wanted to try multiple options. And, after prototyping deathmatch, learning what I could utilize from Unreal's default AGameMode class (Unreal 5 documentation link), and adding features such as sorting players into teams and having a win condition/ win messages, I realized that a lot of this functionality could be reused for most game modes. So, I generalized the team support I had created, and as much other functionality as I could into my own team-supporting extensions of the 3 main Unreal game mode classes: a game mode blueprint called GM_TeamModeBase, a game state blueprint called GS_TeamModeBase, and a player state blueprint called PS_TeamModeBase, which our prototypes and our current game mode derive from.
By approaching it modularly, where I made as much code generically applicable as possible, and made clearly overridable functions to help derived game modes handle key differences, I minimized the code needed to make new game modes, while still having flexibility to make the prototypes unique.
Among the gamemode prototypes were...
Deathmatch
Capture The Flag
"Key Hunter" Idea
By default, the base game mode class provides server authoritative (meaning clients don't run this) functionality for things like player spawning, starting, and restarting the game.
Building on this in-engine support saved a lot of time, and then adding my own base functionality and using inheritance through blueprints, I made each new game mode be able to:
Automaticaly sort players into teams, and use defined team colors.
Have an easily changed win condition, which a lot of times is the main thing needed to make a new game mode work at first, and made prototyping game modes much quicker.
Give the player any gamemode specific weapons/ user interface, set up win/ loose messages, fill a match with bots, and more.
Game State Variables
Player State Variables
The other two base classes, the team game mode and game state bases, are more focused on updating information that needs to be available for all players, as these classes are not server authorititive, and are replicated to all player's games. For instance, the game state keeps the team colors and names, and has getter functions for easy access. Additionaly, the player state allows for accessing what team a player is on, and helps update features when a player's color is changed through a delagate that UI and other features subscribe to.
To prototype our territory control mode named "Path Command", once the objective objects to capture were created, which were necessary regardless, there were only a few main game mode changes needed to get the prototype working. Mainly, I needed to:
Add a match timer
Override the "Check Team Won" function from my base team game mode, to have the attackers win when all objectives were complete,
and have the defenders win when time ran out.
Once either win condition was met, the GM_TeamModeBase would automatically trigger the match end, putting up the win or lose messages, and restarting the game after a post match sequence.
Finishing up the prototype:
I also needed to send some of the information like the match time and current scores to the game state, to make the current game status visible in each player's UI (this is a polished version), and add showing which team players were on. For this, most of the info was already available and replicated to the clients in the base team player state, like what team a player was on.
Finally, my base team mode automatically put players on teams, and the team colors, weapons we wanted players to have, win and loose messages, and more, were able to be customized right away using the variables provided from the base team game mode.
And, after getting the initial prototype and testing it in matches, we liked how it was playing, so it was on to iteration from there!
Path Command Initial Prototype (All teams on attack)
Path Command Semi-Final Version (Attack vs Defense)
The above case mostly benefitted from using blueprints, deriving from the base game mode C++ classes and enabling the new game mode code to be quickly adjusted at runtime. And, in cases where either it became clear I needed to use features only available in C++ by default, like for serializing/ deserializing the stats for making our networked weapon stat editor, or that C++ would be more effective, I would use the C++ base class more, or refactor into C++.
When nailing down the core gunplay was the main focus in December of 2024, I knew frequently editing weapon variables would be key. So, to help everyone on the team iterate on the feel of weapons, and potentially even testers do so as well, I created a networked in-game stat editor for our game's weapons.
In addition to letting stats be edited at runtime during online games, making it quicker to test changes with remote teammates, I used a composite data table so that we could have multiple versions of our weapon stats, enabling people to test and save stat edits at the same time even with perforce/ Unreal exclusive file locking.
It works for each weapon, which is helped by the fact that weapons are children of a base weapon, and pull from the same base stat information structure.
And, as a bonus, using Unreal's UStruct and UProperty reflection system, the editor menu items are generated directly from the members of the weapon stat struct, so adding in a new stat variable to the weapon data struct automatically adds it to the editing menu!
I first use the UStruct and Uproperty reflection data from the weapon struct to get the names of all the stats members (currently I only make editors for float and int type variables). I have since experimented to make a generic version of this function that works for all structs, using a custom thunk, but I kept it simple for the first implementation.
I then loop through the names and make a stat editor sub-widget for each one, which are added to a vertical box that serves as a list of all the editors in the main weapon stat editor widget.
This also uses a GetWeaponStatF function I made that gets the value of the struct's property by name, so that the editor gets initialized with the correct value for the current weapon in each stat's box.
The main stat editor widget implements an interface for setting values, which is a general function that each stat editor calls on it's parent whenever its value is changed. Since each stat box just needs to pass the name and value of the stat it represents to the parent widget, this decouples the editor UI logic from the game logic of going and setting the weapon stat, and means we can fairly easily add new types of boxes for different types of stats, if needed.
Here is the "On Commited" event for each stat's input box, which sends the new value to the parent widget through the "Set Stat Float" interface function
Now being more familiar with the benefits and limitations of blueprints, and how the set of unreal features is quite similar in blueprints and C++, I would move out of blueprints quicker for certain mechanics, and start the development of more features in C++ so they are easier to access in C++ for other systems down the road. Also, especially with the tedium of organizing nodes, and some use cases that would be trivial in C++ but can be a pain without plugins in blueprints, like sorting an array, C++ can be more useful in certain situations, even at a prototyping stage. But, with the reliable quick compilation at editor runtime, blueprints were still quite helpful, especially for most networked mechanics where the syntax of making RPC events can be faster to write and adjust in blueprints. And, because in profiling, the performance impacts of blueprints haven't been too significant. With this, using the hybrid approach of creating C++ classes and deriving into a blueprint class for most actors and components made certain mechanics quite quick to add and modify, with minimal downsides. So, while blueprints aren't as crucial to use as I originally expected, as a tool to decrease the complexity/ friction of editing mechanics, I think blueprints have meaningful benefits when used for prototyping, and for general gameplay components that are edited frequently.
Making the prototype of Charge!, we made a lot of the functionality necessary or helpful for a first person shooter, such as:
modular weapons for easily adding new ones
making the gamemodes, shooting, UI, etc. in a networked way to support multiplayer
various character animation for each weapon type (1st person for you, 3rd for opponents)
making both hitscan lasers and projectiles that can reflect or refract when hitting certain materials in-game, which we thought would be the interesting twist our game brought to combat.
However, through testing the game, reflections as a main mechanic hasn't worked as well as we hoped, and after trying a few different solutions, we determined with the time it had taken, we needed to pivot to more of the basics. I have details in the reflection below on the reasons I think our development has led to this state, and ways I think would have helped us to better achieve our gameplay goals sooner, but currently, we are shifting to nailing down the gunplay through revisiting the core feel target, and prioritizing testing, before any other higher scope details are considered.
I am currently away from the project focusing on my senior year of DigiPen, and the Remanence project also on this website, but I did my best to set the team up for progressing on the core gameplay while I'm gone by implementing the above stat editor so that weapons can be easily refined by anyone, a capture the flag spinoff gamemode as potentially our key gameplay twist, fleshing out missing implementation documentation, and advising a bit when I can.
In having a bit of a skewed focus towards implementation over doing more of the design and testing early, focusing on a few to many aspects of the game at once, and issues with finding a vision we all could picture well, we have had troubles getting towards a core prototype we all feel is fun, and represents a complete loop of gameplay that satisfies our project goals, not just basic FPS gameplay. We have been able to make a full basic loop of networked deathmatch & capture the flag, with pipelines we've used for implementing melee and ranged weapon models, 1st and 3rd person animations, easy to create weapons, and even an additional place-able mirror mechanic which we tried out when default mirrors weren't working. However, coming to a full experience where we can clearly find the specific fun has been our current main issue. Realistically, we should have uncovered these problems much sooner, by starting testing much sooner, but through a combination of things, most important of which I believe have been the need for a clearer MVP, so that we could have had a more reasonable target for the game's state that was required before testing began, mitigating the player count required for playtesting, as with our target of 5v5, testing was much more difficult, and having a clearer the overall vision for what we wanted to achieve, would have all helped us to reach this sooner, and these have led me to realize things I can do in the future to reach the core fun of a game sooner:
Clearer MVP: When waiting to test Charge! I kept trying to get us to gameplay that felt representative, first making the basic laser weapons shoot reflective shots in blueprints, and making a function library for reflections and refractions, then learning multiplayer shooting and gamemode implementation through unreal's Replication, Blueprint RPCs, and sessions, and then how to combine that with unreal's gamemode system. However, even after doing this, and reaching a functional deathmatch, we still had considerations for other game mechanics, like melee, throwables, and other things, and I continued to prioritize making things new or better, vs testing what we already had, even if it wasn't 100 of what our vision was. I realize now that without the core experience, none of that other stuff can be helpful to us, and if I were to do it over, I'd make sure we were 100% clear on what base Charge! needed, and once we had that, go all in on testing, to bring out those issues with reflections as a main mechanic, and have more time to find solutions. And, during this time, I would also establish an understanding that we should avoid talking about implementing new things until the core prototype is solid. In balancing a more tech leadership focused role for the first time, I tried to be available to establish the art pipeline, and implement the new assets that were being created, even if they weren't core feature related, so that art could still progress while core mechanics were being fleshed out, but I realize now that this still took too much attention away that I couldn't properly finish the MVP in a timely manner. It was just me as a programmer for a while, so this may be more feasible when a team starts out with more programmers, but I know that if we could have made the concessions necessary to be able to focus on finishing our MVP for unique gameplay, then take stock at what we could achive from there, we could get a lot farther in making a uniquely fun game with the scrappy power of our team.
Realism of making 5v5 combat: Given our team size, in hindsight, 5v5 combat is quite the task, and looking at it now, I likely should have had us make a more concrete plan for how we'd be able to test that, given our team size, and situation, being an online team in the summertime. 5v5 isn't impossible, but it requires a lot more resources for testing, having an ideal player count of 10 players, and especially when the player count matters more in a collaboration-focused game. So, it would have been worth it to consider more closely whether this player count was necessary to our goals, or if we could cut down on players, and achieved the same effect. To help with this recently, I made a playtesting discord to organize times to test with a larger group, and this was helpful, but doing this sooner would have helped us gain a lot more momentum, seeing the game in action sooner, and identifying ways to make it better from a realistic set of players. Also, if I was to do this again, and we still wanted 5v5, I would make implementing AI player bots a much higher priority. We did start this, but by the time we did, I was already more busy with school, and we had needed to be playtesting much sooner. Plus, having even simple AI that can shoot and complete an objective would have been a great help for those working on level design, to more easily see how combat plays out in their space.
Scope and clearer vision: If real estate is all about location, location, location, this project has further cemented that game development is vision, vison, vison. So, even if this is a lot like point 1, I'll say it this way too. Whenever we have had a clear vision of what we wanted, we've been so much more effective in utilizing the capabilities of our team, and collaborating, such as when we implemented the first arena level and got reflections working on it, implemented basic character animation, or other things that we knew we wanted, where we could make clear tasks to collaborate on, and ultimately, feel they were complete. However, with mixed ideas on how a basic match of Charge! should play out (slow and tactical vs medium speed and high pace, all about teamwork or can an individual carry a match?, etc.), things dragged out more, and this affected other aspects of the development cycle, such as our ability to say we were ready for playtests, or confidently implement more features. In hindsight, scoping down on the features we were willing to consider (throwables, melee weapons, potential gamemodes, player count, etc.), and the fidelity of art features we wanted to make a pipeline for (full 3rd person blendspaces for opponents, 1st person ones for you, different character models, etc.), would have helped immenesly in letting us get past that milestone of being confident in the viability of our MVP. It's not that implementing the blendspaces, 1st and 3rd person animations, melee weapons, and all that, were an immense challenge, but needing to consider all that while trying to hammer down a core fun experience meant I and eventually the 1 other programmer, only had so much bandwidth, and couldn't focus as well on just refining the reflection-based shooting, identifying the issues with it, and finding a solution/ deciding to pivot much earlier. In being the lead programmer, I felt bad when I could clearly see the artists were doing work fast, and needing pipeline help to get their work in game, and so I split my attention to try and do it all, but as I've stepped away and been able to focus on other school projects, I've been reminded of just how important focus is for making the best thing I can, and also being able to work fast. So, if I was to do this in the future, I would make sure that we can our clear vision of what the core gameplay, but then be stalwart in giving my team's gameplay programmers, provided it had a reasonable deadline, a headspace free of other polish considerations, so that we can focus on making the core gameplay work, and then when the team and testers are happy with that, then open conversations for how we can polish it, and add better art features, from there. And, even if we would have to make some concessions, at least we'd have a solid game foundation to build on, so that the assets we would have the scope to implement would matter more.
Older screenshots featuring certain prototype mechanics, and an older WIP level