INTRODUCTION
In this project, my aim was to explore the systems of a map designer and simulator for first-person-shooter games enthusiasts. If this was a commercial project, the player would be able to create FPS maps and see how balanced the two sides of a match are, testing the principles of shooter map design. These include:
Soldier AI for the enemies and teammates controlled by the CPU to simulate a match
A map creation system that allows the player to place, move, and delete map elements until they are happy with the result
A saving and loading system that allows players to go back and forth between simulating and tweaking the maps they make, as well as revising old maps, and even duplicating or sharing them if they knew the file location
Please watch the video below to get a clearer idea of the project and its capabilities​
AI FOR THE "BOTS"
The AI of the agents in this project, to which I will refer as "soldiers", responds to the standard design of enemy AI in modern shooter games, excluding some details such as tossing grenades, which could nevertheless be easily implemented with the current system.
This behavior is intended to mimic that of a human player.
The AI soldiers are programmed to:
When no enemies are in sight, go to points of interest in the map, which are by default the spawn points throughout it, but can also be placed manually, like "capture points" usually are in FPS games
After reaching their destination, move around, crouch, and stand up to avoid enemy fire
Shoot enemies on sight, as well as approach them up to a set distance while firing to maximize accuracy
Additionally, I explored​ Unity's animation (including inverse kinematics) and particle systems, so there will be some references to those in the code.
The code for the AI is fairly simple and little is noteworthy. It is formed by a number of nested if-statements and sometimes for-loops, triggered by conditions related to seeing the enemy and distance to it or their goals. The movement is done with Unity's pathfinding system; and the sighting of enemies, with raycasting.
I'll share a few examples here, but you can download the whole Soldier.cs file below.
Here, for instance, is a loop that tries to find visible enemies while the soldier walks between points of interest before making contact:
//See enemies
foreach (var item in FindObjectsOfType<Soldier>())
{
if (item.team != this.team)
{
RaycastHit hit;
shotPoint.LookAt(item.targetTransform.position);
if (Physics.Raycast(shotPoint.position, shotPoint.forward, out hit))
{
if (hit.transform.root.GetComponent<Soldier>() && hit.transform.root.GetComponent<Soldier>().team != team && hit.transform.root.GetComponent<Soldier>() == item)
{
currenTarget = item.transform.root.transform;
targetPos = currenTarget.position;
lastPos = targetPos;
if (!GetComponent<AudioSource>().isPlaying)
{
GetComponent<AudioSource>().clip = sightClips[Random.Range(0, sightClips.Length)];
GetComponent<AudioSource>().Play();
}
}
}
}
}
It also plays a voice recording to indicate contact, following the style of recent FPS titles. While this loop being performed every frame might seem unoptimized, it did not reduce performance in my lowest-tier laptop, so I decided not to give it an every-second timer or anything of the sort.
Here is part of the function that enables shooting, which is done by physics as opposed to raycasting. Some variation of it is fairly common in most FPS titles, so I thought it might be interesting to show how I have done it:
if (ammoLeft > 0)
{
//Shoot if ammo available
var _muzzleFlash = Instantiate(muzzleVFX, shotPoint.position, shotPoint.rotation);
_muzzleFlash.GetComponent<AudioSource>().clip = weapons[selWeaponIndex].clip;
_muzzleFlash.GetComponent<AudioSource>().Play();
shotPoint.Rotate(Random.insideUnitSphere * spread);
GameObject bullet = Instantiate(bulletPrefab, shotPoint.position, shotPoint.rotation);
bullet.GetComponent<Rigidbody>().AddForce(shotPoint.forward * bulletSpeed);
animator.SetTrigger("shoot");
ammoLeft--;
shootDelayLeft = maxShootDelay + Random.Range(-.01f, .01f);
}
The laser projectile (here called "bullet" because I named it before designing the asset), on enemy impact, calls a function of the hit soldier that reduces its health. When that reaches zero, the soldier dies and respawns after a short while.
MAP CREATION SYSTEM
Map creation is offered to the players in the shape of a very simple object selection interface, a point-and-click-to-spawn procedure, and handles for moving, rotating, and scaling them very similar to the editor interface of Unity or Unreal Engine. The manipulation system is built upon a third-party open-source plugin because I wanted to make it fluid and intuitive but did not understand the mathematics involved in such as process at the time, so I will not discuss it here as it is mostly not my work.
Here is the part of the code that shows a transparent preview of the asset until the user clicks to spawn it:
[...]
else
{
if (transformHandle.gameObject.activeSelf)
{
transformHandle.gameObject.SetActive(false);
}
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit))
{
previewObject.transform.position = hit.point;
previewObject.transform.position = new Vector3(Mathf.Round(previewObject.transform.position.x / .5f) * .5f,
Mathf.Round(previewObject.transform.position.y / .5f) * .5f,
Mathf.Round(previewObject.transform.position.z / .5f) * .5f);
if (Input.GetKeyDown(KeyCode.Mouse0))
{
transformHandle.target = Instantiate(propPrefabs[selPropIndex], previewObject.transform.position, previewObject.transform.rotation).transform;
spawning = false;
}
}
}
More can be found in SelectionManager.cs in the Drive folder.
SAVING AND LOADING MAPS
The save-and-load system is enabled by Unity's tools to parse to and from JSON files. Maps are converted to and from serializable (saveable) classes so that a file can be saved and read whenever I need it. Specifically, the saving function is called by other scripts during the map creation process, while loading is called on the menu for map selection and in the map editor and simulator when existing maps are opened.
Here are the essential classes of the system, which are easy to understand:
[System.Serializable]
public class SavedProp
{
public string propName;
public Vector3 propPos;
public Quaternion propRot;
public Vector3 propScale;
}
[System.Serializable]
public class SavedMap
{
public string mapName;
public int mapBaseIndex;
public List<SavedProp> savedProps;
public string lastTimeSaved, dateOfCreation;
}
As you can probably see, each map object's information is stored in a saveable class, and all of them get stored in a saveable map class that also contains some information about said map to make it clear for the user which one they are choosing. This information gets loaded in the opposite direction to render the loaded map in MapLoader.cs.
I will not showcase more here because it is simple and very specific to Unity's way of handling things, but if you are interested the rest of the code is completely available as SaveManager.cs in the Drive folder.
ROOM FOR IMPROVEMENT
It is important to do a self-evaluation after each project. In this one, it is especially significant, because I made it in 2021, before learning many consequential things about programming at school.
The things that could be improved in this project are:
Clearer code: the code is very convoluted sometimes, even if we ignore the confusion arising from seeing it in isolation. Annotations could be added, some variables and files have outdated or confusing names, and the way of approaching problems shows a lack of planning and modularity.
Optimization: because of the small scale of the project and the fact I was missing my last year of programming in high school, optimization is mostly ignored, but would be the first thing I would focus on if I had to edit the project to make it commercially viable
Polishing: on the same note, the UI and design do not look very attractive or intuitive, since this project is focused on coding practice and showcase, but I would also fix it if time was not an issue.
I also like to make a list of features to add and how I would approach doing them:​
Integrated map sharing: while it is theoretically already available to users, it would probably be interesting to add a simple couple of buttons in the main menu to allow exporting and importing maps. After all, this project is supposed to be for hobbyist FPS-game designers, so they would probably appreciate a feature to share and edit each other's maps. This would be done simply by duplicating the save file in question in or out of the save folder and in or out of a more accessible location selected by the user.
First-person direct player intervention in the match: even though it would unbalance the simulation, surely some players would appreciate the ability to walk around the map and shoot some enemies to see how the map feels. This is too complex to describe here but I have made several FPS controllers: all that is needed is a controller for the body that rotates with the camera, a script for the weapon, and code that allows the player to get hit and respawn.
Different scoring systems for the simulation: right now it is based on eliminations, but it would be easy to add the option of having a zone of control around each flag (aka point of interest) and score based on that.
Controlling soldier stats: FPS designers would probably be interested in tweaking the respawn times, health, etc. of their soldiers to match the style of shooter game they are interested in.
​
(Note: the term "FPS" is used in documents related to this project and within the project itself to refer to the genre the users would be designing for. Nothing in this project is supposed to be seen from a first-person camera, in case you thought it was a mistake or a bug)