-
Notifications
You must be signed in to change notification settings - Fork 1
Features
To have a structured codebase from the start to make the rest of the development cycle of 5 months as easy as possible, I designed a component-based hierarchy. One of the requirements I set for myself was that I could reuse every component (if relevant. For example, a camera controller is not relevant for AI agents since they do not have a camera) for other tank entities. I should be able to attach a “shoot component” on both the player and an enemy instance, so it is important to make the component as generic as possible.
While developing the components, I found that it started to become harder to work with. Because there were different “states” the tank could be in, it was very hard to properly organize what input should be accepted (for example, to prevent the tank from being able to drive while it already exploded into a million pieces). I quickly realized there should be a system in place to handle these transitions, so I started working on a Finite State Machine. Again, it should be as reusable as possible. I wrote the FSM into a template-format, so I could create a “tank-FSM”, a “camera-FSM”, and a “HUD-FSM”. This made all logic incredibly easy to organize, and access (but only when I need to) and prevents writing the same logic multiple times.



However, during a large part of the development, I struggled with making the WheelColliders behave the way I expected them to. The tank wasn’t even able to rotate properly. The way tanks handle rotation is to move the track opposite the tank is trying to move towards. For example: if the tank should rotate clockwise (so to the right), then the track on the left should be activated. Optionally, the track on the right can be reversely activated to create a stronger rotation force. This is how I tried it with the WheelCollider approach as well, but it did not work as expected. What happened was that it would either barely move at all, or the tank would need to travel a very large distance forward before having rotated the desired angle on the Y-axis.
Another reason why I struggled with implementing this WheelCollider approach was that, no matter how much research I did, I couldn’t wrap my head around the WheelCollider parameters that could be customized. Think of variables such as “Extrenum Slip” and “Extrenum Value”, and how they influenced each other. I spent too much time trying to find a balance between these values.
Since one of my learning goals was to improve my editor scripting skills, I worked on a custom editor window (so a separate window, not buttons and other GUI elements attached to a component). In “Figure 4 Custom tank editor”, you can see what the window looks like. As you can see, the most part is related to the Wheel Colliders.
However, I decided to greatly simplify the whole movement component, and just resort to just adding forces to the Rigidbody of the tank. When I look at how much time I spent on the movement part of the tank, I spent way too much time developing it. I suppose that is a lesson for future projects. Scrapping the Wheel Collider-related functionality also makes most of the editor window obsolete. I still learned a lot from designing/writing this editor window though. When driving the tank, the FOV is widened to provide more visual feedback and add “juice” to the experience.

- 3rd person
The camera follows the tank at a set distance and can rotate a full 360° around the tank, as well as a limited range of motion on the x-axis. This will likely be the camera view that players use the most, to navigate around the scene and shoot relatively nearby targets. For this camera view, I did my best to visualize how the camera worked, by showing the boundaries for movement, and how it relates to the barrel
- 1st person
A camera placed on any of the 4 positions marked in Figure 5, is unable to move by itself. It was an attempt to mimic reality, but it wasn’t a gameplay element I had any use for. That is why I decided to remove it, improving gameplay quality by decreasing complexity, at the cost of decreasing realism.
- ADS
A camera is placed right above the barrel, which follows the y-rotation of the turret, and the x-rotation of the barrel. The player can zoom in using the right mouse button up to 5 times. This is done by decreasing the FOV, rather than physically moving the camera to a position in the distance. This would have introduced issues, such as moving into walls, being able to see behind walls and other edge cases that could not be solved which is why I changed it to FOV adjustments. To make zooming feel better, I let the camera lerp between its current FOV and the new FOV. This makes the transition feel less instant and much smoother. This camera view should be used to target enemies at larger distances since the zooming ability helps with hitting those targets with high accuracy.


It is visualized how the camera position relates to the “look at” position of the turret. If the camera is at its lowest position, the turret aims as high up as it can. When the opposite is true and the camera is at the max height, the turret aims straight forward. These boundaries are of course easily editable by a designer, so they have complete freedom of how far the camera and turret can move up and down. The speed of the camera & turret rotation (different variables!) can be customized as well, so the rotation can be instant, or more realistic and the turret will slowly follow the camera rotation.
Developing this component was another way to try and make the game a bit closer to reality. When using the proper input, a shell is spawned at and shot from the tip of the barrel.During the early/mid-development of this feature, I spent a lot of time on a “ranging” system. During the research phase of my minor, I found that a lot of games had some kind of range-finding way. For example, you would manually measure the distance to a point using UI called “mildots” (or the distance was measured automatically), and you would have to adjust the rotation of the barrel on the x-axis to account for shell drop, making sure that the shell would hit the target in the distance accurately. Refer to “Mildots” in the appendices for an explanation.
Unfortunately, this did not work out. I decided to entirely scrap the range-finding feature and to let the player rely on their intuition. While playing, I found that it is mostly a matter of finding the right camera view. If the selected camera view is ADS, then the player only has to use the correct zoom level.
One thing that has worked out, is the shell drop-off. Some distance after firing a tank shell, the shell will start to lose velocity & therefore, height. This is implemented with the Drag Coefficient [1], although I have not gone too in-depth on this. I spent a lot of time on the mildots system and there were still other time-intensive features I had to work on.
I developed this feature after having a conversation with a friend that spent much of his time playing tank simulators. With this shell drop-off, the game is a bit closer to a simulator (assuming I used the correct values in the formula). The formula for this mechanic [1] is the following:
Drag force=DragCoefficient*0.5f*AirDensity*currentVelocity^2*frontalArea
My implementation of the formula can be found in the appendices (“Drag coefficient implementation”).
While designing the behaviour tree (see “AI behaviour tree”), I found that the agents do not handle the slopes/hills in the level well at all. I used them to bring some more variety to the desert environment. However, the agents were not able to reach certain patrol/cover points due to these slopes. Even though tweaking the NavMesh (& agent) properties, I couldn’t manage to have them smoothly go over these hills. Therefore, I had to compromise and remove any height differences. Still, the agents occasionally have some trouble moving through the level (even though the NavMesh is clear and there aren’t any obstacles).
With feedback from artist fans and my QA, I added some much-needed postprocessing. This makes the scene more interesting to look at, rather than a dull, flat-coloured boring level. On his suggestion, I also tried to add volumetric lighting using a 3rd party asset, but as of writing this paragraph, I couldn’t get it to work, unfortunately.  To blow more life into a game, in my opinion, it needs a presence other than the player itself. That is why I decided to add AI agents. They can move around the world and make their decisions by the use of a behaviour tree. To design this behaviour tree and convert my thoughts into usable AI behaviour, I started with 4 “core nodes”, from which all other behaviour would derive. The 4 nodes are described next. You can see a visualization of every node (kind of like pseudocode, including what type of node it is. For a detailed explanation of what a behaviour tree is and how it works, refer to the “Behaviour tree” appendix.
| Behaviour tree branch | Description | Visualization |
|---|---|---|
| Cover | If the armour stats of any tank part reaches a certain threshold, the AI will generate a set of points on the NavMesh. These points are all candidates for positions of cover. After generating, the AI will filter out the best options, and then choose the closest option. The AI will move to the chosen cover point. | ![]() |
| Patrol | The AI generates a set of points on the NavMesh. This time, these positions are possible patrol points. Again, the AI will filter out the best patrol points and then moves to the chosen patrol point. At first, I was planning to manually place patrol points (empty transforms) around the level from which the AI would filter a proper point, but that wouldn’t scale well at all if I decide I want to adjust the level design or create new levels. So I went for “random” point generation on the NavMesh. | ![]() |
| Shoot | The AI will check if the player is in range, inside the Field of View and if there is a clear Line of Sight. If all conditions are met, the AI will check if it meets the requirements to fire a shell towards the player, and proceeds to do so if possible. | ![]() |
| Investigate | The player will check if the player meets all the requirements from the “shoot” node but with higher values (more range, wider FOV). When these nodes return success, the AI will start to move towards the player. Eventually, the player will get into shooting range and therefore the tree will switch to the “shoot” branch. | ![]() |
Sometimes it would be rather difficult to debug what the cause of a certain problem was because I do not have a visual node editor (something to develop in the future, so it is possible to release this behaviour tree code as a plugin). Designing the tree with the website https://miro.com/ made it more doable to get an understanding of how I would imagine the final behaviour. I used this method to structure the tree in Unity as well, as can be seen in “Figure 15 Behaviour tree hierarchy”. In “Figure 16 Behaviour tree inspector”, the base setup for every node can be seen. Any logic node has this setup, as well as possible additional variables for that node. Another way to understand why the behaviour did not work as expected and desired, was by logging the “nodeState” of all (or just the selected) nodes, as can be seen in “Figure 14 Behaviour tree console logging“. To take one of the logs as a demonstration: “SEQUENCE: Branch [2] Action – Set Player As Turret Focus – [ log count ]”. SEQUENCE = Clarification on the type of parent node: selector, sequence (these are called “composite nodes”), or an inverter (which is a “decorator” type node). Branch = is either red, orange, or green. Red means FAILURE, orange means RUNNING, and green means SUCCESS. Action = the type of child node. Can be either an Action or a Condition. Although this type is just for debugging & organizing the tree. I have not made concrete “action” and “condition” nodes for them, unlike the composite nodes.
The advantage of a behaviour tree compared to a finite state is that I do not have to manage a ton of transition conditions. Instead, I only have to check conditions that are relevant to that specific branch. Example: to transition into the cover branch, I only need to check if the agent needs to repair (armour < certain percentage). If that condition fails, I know the entire branch doesn’t need to be executed.

In a tank combat game, the tanks need to be able to be destroyed. That is why I spent a few weeks developing a damage registration/health & armour system, as well as a way to repair that damage.
With this component, the shells that hit the tank get registered and damage is applied to the part it hit. Once a part (can be the left track, right track, hull or the turret) has been destroyed (no armour and no health left), the tank explodes & therefore has been defeated. If a part has not been destroyed but its armour has been depleted, the armour can be regenerated. The same cannot be said for the health bar, as that is considered permanent damage.
The player can inspect their tank by pressing the corresponding input. The camera view will then switch to an “inspection view” (Figure 17 Tank inspection view), where the properties for each tank can be seen, and repaired if necessary. The camera can rotate around the tank in a full 360° circle, as well as zoom in to a certain depth.
This same component is being reused for the enemy AI, as I have made it as generic as possible. This way, I did not have to write the same logic twice in a slightly different manner. For hostile tank inspection, I did make a “hostile tank inspection view”.






