This is the story of the making of my simple projectile based FPS in Unreal Engine 4. Someone reading this might know that a few
weeks months ago I posted this tweet a while back.
This is not that post because a lot has happened. As I am writing this it is the
18th of December 19th of February, my website is down as I am moving it and my domain to AWS (Cloud stuff!). By the time you are reading this it is obviously up again. Anyway that isn’t the important part.
Let’s talk about SimpleFPS my FPS game featuring bouncing explosive high-speed projectiles. This is probably my biggest finished project. This post will be in almost chronological order. It all began
2 months and 3 weeks a long long time ago in a galaxy far far away…
For programming class we were supposed to work on a personal project. I had 3 ideas:
I had my project idea so I started writing up a simple structure for classes and components in google docs. Looking at it now… It doesn’t account for half the classes I have. The original plan was just to have shooting and death mechanics then be done but things kinda expanded.
Real work begun in October. This was the first day of writing code. I created all classes I had earlier written up those being:
The Health component held current & max health. The Gun component held all the weapon data (Which weapon you had equipped and it’s stats + your current ammunition) and logic. I want to point out here that there is only one weapon in the game but the underlying structure would technically allow there to be more I just didn’t add it in because I had no idea what other weapons to make that fit with the theme. Maybe a gun that shot multiple smaller bullets that did less damage like a shotgun? I don’t know. It didn’t happen though.
The SimpleFPSCharacter was the player character which held all the above components and their data.
The Projectile was the projectile you shot (makes sense, huh?) and had a bit of data on damage, explosion delay and radius. The way projectiles work is that the first time they hit something it “lights the fuse” and after $ExplosionDelay seconds it does a overlap (in async) for all actors in $ExplosionRadius after which it applies the damage to all of them. This allows you to bounce the projectiles off walls and time the explosion to be right by the enemy, I thought it would give the game a bit more of a skill aspect.
Around here I got respawning working by adding an event to the HealthComponent for health changing, this made it so I could listen in on it from other classes and once the character reached 0 health we could unposses the character and play a death animation. I remember one of the problems here was updating the UI to handle the player dying because all the health data is on the pawn so we need to either A) Move health data to the player controller or state or B) Bodge it together by updating our listening when we get a new pawn. I did the second option. Then I broke it and now I am pretty sure we just try to get it from the pawn and if it fails it just sets it to 0 in the UI.
In hindsight I probably could have just moved the health to the player state because then I wouldn’t have had to create a new health component every time the player dies. Might have given me a miniscule amount of performance when spawning new characters but really not enough to be necessary.
At this point things kinda worked in the editor. You could shoot each other and such just ONE bug… There is a sort of hidden cooldown on guns that determines how often you can shoot (it exists to stop spamming out 10s of projectiles in one tick) it is set both by the client and the server when they try to shoot. The problem I was having was that the listen server could shoot fine but clients couldn’t shoot at the same frequency after a bit of tinkering I realized the problem: The TimeOfLastShot variable was replicated so any delay between the client and server shooting would be added to the client’s cooldown, the reason this isn’t a problem for the listen server is because it is the server and there is no delay there. This was a really simple fix all I had to do was make it not replicate but for a while the server had a huge advantage (beyond being the authority on every position and such).
I also made a basic main menu here. There were a few problems with this one that I addressed later though, mainly clicking the play button would just directly search for a session on the LAN network and then if it didn’t find one create one. That meant that you couldn’t join your friends if they were on a different network. Note the Graphics Quality selector at the bottom-right, it just uses the engine scalability settings and was really quick to implement.
At this point I began turning towards polishing and adding the last few things. After implementing the Kill & Death counter I felt kinda done. One thing I did this time too though was add an ammo-regen mechanic so you would actually get your ammo back over time, I could have made a more conventional thing were ammo-boxes spawned on the map but I just wanted to get this done at this point.
Now it was time to play test a bit against my teacher… After I did a kinda monumental change and finally made Guns DataAssets instead of just structs. This is something I probably should have done earlier as before I had to make changes inside the GunComponent and that was terrible but now the gun was finally a separate asset independent of the character.
DataAssets are really great, I use many different DataAsset types for another one of my projects and their usefulness can’t be overstated. They will only be loaded once into memory and you can keep your data separated from the logic and with a DataAsset you don’t need to replicated all the variables of a struct because clients already have them on their machine. They are great, use them wherever you have data that doesn’t change at runtime. I don’t use them to their full extent in SimpleFPS a perfect place for a DataAsset would have been for the projectile, projectiles only need a few things in SimpleFPS:
After a few minutes of struggling with the school wi-fi not allowing connections between local computers and switching over to my phone’s mobile hotspot I finally got to play against my Programming teacher and I was absolutely obliterated. He knows how to play FPS games and I realized I needed to tune a few knobs related to the projectile speed and jump height because at the moment you could almost jump over them and they were a bit too slow to be a real threat (if you knew what you were doing that is).
Another bug was found in this test: If you shoot a corpse you will get a kill. That is definitely not right, the reason for why this happened was that when we dealt damage we counted it as a kill if they were dead afterwards we didn’t check if they were already dead. My solution to this was to only apply damage if the character was still alive and otherwise just return false.
Over all this test was a huge success, I was pretty much done. I didn’t really need to work more on it and I didn’t for about 1.5 months before things started getting real…
After being away from it for a month and a half I returned to SimpleFPS to remake the main-menu to allow you to host or join by IP. Nothing more really happened.
My teacher wanted to play against me again, I didn’t have my mouse with me so I declined and then he came with a suggestion: What if the whole class played my game next lesson? I said “Yes” and then it was off to the races.
During the two hours lesson I began working on teams prior to this it was just a FFA. I added a datatable for team colors and names, started moving around player data to playerstate and then once I went home I began doing even more work including making a new bigger map. Everything that was related to the PlayerController slowly moved away to playerstate…
This was the start of my weekend long programming marathon.
I had half a week to move the game from “playable” to Playable. I did majority of the work in the following 2 days. This included:
Needless to say this was a lot of work.
(The following is even less chronological than other parts and doesn’t include all of the problems and refactors I had to work through)
First step in making this work was moving things from blueprint to C++. I would need all the performance I could get. The ammo-regen mechanic I mentioned before was earlier completely in blueprint, I moved it to the GunComponent directly instead.
Second step, counting kills for a team. The gamemode didn’t replicate to players to I had to put the team kills somewhere else that they would get: GameState. So each time someone gets a kill that we count (one that isn’t on a member of their own team) we notify the GameState and then every player gets that info and we can display team kills and deaths in the UI. BUT wait GameState doesn’t persist between maps so we need to save it somewhere else when we move back to the lobby. We need to save things on the GameInstance when we go back to the lobby. Then when we reach the lobby we just copy that data back into the gamestate and BOOM! We can see things.
Third step, we need an actual lobby and a way to handle it. We need a UI and controls to start the gamemode and we need to be able to kick players of course and we need to display the playerlist and the teams. Trying to tell all that happened in this step would be very messy so here is summarized summary:
Display the playerlist? To fill the playerlist we need to react to whenever a new playerstate is added. That is simple enough right? NO!- WAIT YES! We need to add an event to the GameState for when a playerstate is added (through replication or otherwise) luckily that is a virtual method so we just hook up to that and grab everything from there and then hook up to that event again from inside the PlayerInfo widget for removing it.
How do we kick a player? Simple enough there is a function for that– Not exposed to blueprints. So we go in and make a function for that in our GameMode which just call GameSession::KickPlayer(). BINGO! Things actually work. Then we just hide that button for non host players.
Assign teams? Super simple barely an inconvenience just set it in AGameModeBase::PostLogin() (also a virtual function). Then we just look at all already logged in characters and count the team members and put this new player in the one with the least members. Unoptimized? Definitely but it only happens when a player joins so it shouldn’t matter for gameplay.
Starting match? Grab all the options set in the UI and add them in as options for server travel to the real map. Then we just parse all those options in the InitGame() function in the GameMode. (Then we also have to pass some of those to GameState so clients get to know about them)
Team names and colors? I store all team-data in a datatable. It is simply an FText and a Color. Pretty much all names are references to Warcraft factions but specifically ones that I didn’t expect many people to know.
Fourth step, performance and polish. I tweaked a few values and began trying to squeeze out performance anywhere I could. Here are a few of my tips:
By default the UE4 MovementComponent will substep physics by 8 times (Max Simulation Iterations). That is a bit too much precision for me. What you can do is find the thinnest wall in your game and then calculate how much you need to substep at your minimum target tickspeed. For me that was 30 ticks per second with the thinnest walls being 25 units. Then you take the movement speed of your character and divide it by the ticks per second then divide that by your thinnest wall which will give you how many substeps you’d need to do at your target tick speed (round up). Beware that setting this low is probably not a great idea for games with thin walls or uncertain tick speeds.
Don’t waste any time determining to who something needs to replicate. I marked both characters and projectiles as Always Relevant under Replication because the map is so small that they would always be relevant anyway and spending any time testing if they would be is a waste. If you have bigger maps and higher player counts this has diminishing returns in the form of bandwidth but for such a small game it doesn’t matter.
Async traces & overlaps. You can do LineTraces & overlaps on a background thread in Unreal, you won’t get the results until the next frame but it is useful if you have to do a lot of them. For SimpleFPS all the projectiles use these when they explode. I recommend this guide by Bryan Corell for how to use them. I also use them in another project of mine and they provide way way better performance than normal traces.
So was any of this really necessary? Not really. The game runs very well on my school laptop even with 10 players on it and by really well I mean I didn’t drop below 60 fps at all during our testing and barely hit 40% cpu usage. It doesn’t mean making use of these in other projects will not make a difference though (step 3 especially is very useful if you do a lot of traces and I might have a post about how I make use of it coming out in the future).
End of the weekend This weekend was a lot of work for me at the time. There was so much work crammed into a two days. I spent pretty much all of it working on SimpleFPS and it payed off, there were a few problems left at the end but nothing that I would really notice until the big playtest.
Oh and lest I forget here is a view of the final map I built.
So, the game was pretty much done but I felt like I needed just one last thing: An explanation for how to play the game. I quickly cobbled together in the main menu and the result was this:
I am quite proud of it.
So, my programming class was in the afternoon which of course means I have a few hours to spend working extra on polish. I made a few small changes to the How To guide and main menu at this time but I also added the team kill-goal to the UI so you could finally see how many kills a team needed to get to win. This was also the time when I marked all actors as always relevant.
Playtest time. Only 10 people could connect to my mobile hotspot at a time so we didn’t get a lot of people in at once but it was still very fun.
I am usually very quiet in class, focusing on my own projects but I will admit I might’ve gotten a bit cocky when everyone was playing my game that I had spent the last months working on. I can’t remember which team won now but I believe it was very even the first two rounds when we played with 2 teams. Each round was very long because of the lack of a time-limit and only the kill-goal ending it. For the last round I switched to three teams hoping that it would be a bit more interesting and it was, by this time I had also gotten at least 3 times as much playtime as everyone else and my skills were at the top and I was on the same time as my teacher who as you might remember beat me easily when I did the first playtest versus him. Anyway, I am pretty sure we won that round.
Of course a few more issues appeared during these huge 10 player battles for one you could accidentally launch yourself out of the map… that was because I positioned the invisible roof a bit too high up. Luckily only one person as far as I know flew out of the map and was forced to restart.
By now it has been almost half a year since I started working on SimpleFPS and 3 months since I began writing this post. SimpleFPS is my biggest finished project so far and it has been a great help when it comes to learning more about Unreal Engine. Much of what I have learnt here I have applied to my other projects and a lot of what I have learnt about is Unreal Engine’s structure and where to put data. GameState, GameMode & GameInstance are really worth looking into if you are doing anything in Unreal Engine because they might do just what you need and if you look at my commits you might notice that I spend a lot of time moving around data between them when trying to figure out how to structure things.
A lot of decisions made when developing SimpleFPS weren’t perfect at the start but I managed to make a lot of improvements during the crunch weekend. It still isn’t perfect though and it will never be because since the last playtest I have barely opened the project except for to do a few bug fixes and I have turned my attention to other things.
During the Christmas break I worked on a Networked Dialogue System for another UE4-project of mine and I have also now been spending a lot of my time working on DodgerV2 my 2D arcade like space shooter in Unity.
I love working on multiplayer things because I love multiplayer, I love going around worlds with other people. It is why I started using Unreal Engine in the first place because it makes multiplayer stuff so easy compared to Unity (though that may change, their tech is catching up). SimpleFPS is the project I am proudest of because I was able to make an actual working FPS game that others can play. This won’t be my last project.
Thank you for reading – Grim
Written 19 Feb 2020 by Grim