Terminal Ballistics - Part 2

Building upon my ongoing 3D project by adding new vehicle types, quality of life improvements, and updated graphics and HUD styles.


This post expands on the work from several previous posts:

Adding a new vehicle

Revisiting the vehicle type from my first 3D game, an eight-wheeled vehicle is added here. It has a style that is similar to the tank vehicle, and is also using test textures for now.

Before getting to the point where this could be added, it was required that a decent amount of code be refactored.

A new "Unit" type was required which was abstracted out from the original vehicle, and provided functionality that was agnostic to the specific type of vehicle. This included things like storing information about what team the unit is on, or the functionality to select it. Then the existing tank unit needed to be moved over to an inherited version of that new Unit type, followed by creating a new Unit variant for this new wheeled vehicle.

All of the places that interfaced with the tank in the earlier code (like sending navigation commands, being hit by a projectile, being controlled by the player, etc) had to updated to function for any abstract Unit now.

Besides just having wheels, to give this vehicle a different play style than the tank it has a different weapon style too.

This new vehicle has a faster firing rate than the tank, with an "auto-cannon" style turret on top. It uses a 15 round magazine before reloading, with 0.25 seconds between rounds. That is compared to the tank's 1 round before reloading, with 6.5 seconds between rounds.

The projectiles have less velocity than the tank shells and thus a lower maximum distance. The ballistic solutions function can account for this because the velocity is a parameter. The updated weapon did require revisiting the HUD distance tick marks.

Because this vehicle has wheels instead of tracks, the number of ray-cast elements for the player's vehicle was reduced from 16 to 8 (one for each wheel). In the future I may also experiment with replacing the RayCast3D with a ShapeCast3D which can project a cylindrical wheel-sized shape to detect the ground, but for now the ray version works well enough.

Vehicle Selection Screen

The new vehicle required a new vehicle selection screen to let the player pick it.

When spawning, the player is now presented with a screen that allows them to select either of the two vehicles. It also has some basic information about the map (name, description, objectives) and some in-game hints.

I also took this opportunity to also give the vehicles nicknames and basic made-up information.


This was posted to the Godot subreddit More than just tanks! I refactored my code and was finally able to add a new type of vehicle (and turret, and projectile.) Opening the door to even more vehicles and part swapping.

Flight Controls with a Quadcopter Drone

With the new abstracted Unit type, and the ability to add different types of vehicles, a quadcopter-style drone was added next.

The drone is spawned from either vehicle by pressing a hotkey, and then control focus is transferred to the newly spawned drone. Control between the drone and the original vehicle can be toggled anytime with another hotkey.

The drone has its own UI which shows the compass, pitch, yaw, and roll inputs, as well as the collective input. It also shows the altitude.

In the center of the drone's HUD is a movement indicator. It consists of a square and a line, where the direction and magnitude of the line is related to the speed and direction of the drone. This makes it easier to pilot the drone, especially at altitude where there are fewer visual landmarks.

The drone's code simulates an "air density" value which decreases as the drone goes up in altitude. The air density is a factor in the upward force, so by decreasing it as the drone goes up it imposes a soft ceiling on the maximum altitude of the vehicle. It also approximates real-life conditions.

Drone Propulsion Code

In the code below, the values for forward, strafe, yaw, and collective_input are set on the vehicle by a player controller script.

func _physics_process(delta):
    if !powered: return

func _smooth_inputs(delta):
    if !is_equal_approx(forward, 0.0):
        applied_forward = lerp(applied_forward, forward, delta * 2)
        applied_forward = lerp(applied_forward, max(min(global_transform.basis.y.signed_angle_to(Vector3.UP, global_transform.basis.x) * 0.2, 1.0), -1.0), delta * 10)

    if !is_equal_approx(strafe, 0.0):
        applied_strafe = lerp(applied_strafe, strafe, delta * 2)
        applied_strafe = lerp(applied_strafe, max(min(global_transform.basis.y.signed_angle_to(Vector3.UP, global_transform.basis.z) * 0.2, 1.0), -1.0), delta * 10)

    if !is_equal_approx(collective_input, 0.0):
        collective_rate_of_change = min(max(lerp(collective_rate_of_change, collective_input, delta/100), -1.0), 1.0)
        collective_rate_of_change = lerp(collective_rate_of_change, 0.0, delta * 10)
    collective = min(max(collective + collective_rate_of_change, 0.0), 1.0)

func _apply_thrust():
    var air_density = min(max(remap(global_position.y, 20.0, 1000.0, 1.0, 0.01), 0.01), 1.0)
    apply_force(global_transform.basis.y * mass * gravity_strength * air_density * remap(collective, 0, hover_collective, 0, 1))

func _apply_torques():
    apply_torque(global_transform.basis.x * applied_forward * 2.5)
    apply_torque(global_transform.basis.z * applied_strafe * 2.5)
    apply_torque(global_transform.basis.y * -yaw * 2.5)

Enemy Focus

This wasn't something originally planned, but was a welcome effect. Because the drone inherited from the Unit parent class and thus had a team value, it was picked up by the units on the opposing team and were targeted by their AIs.

With how well and straight-forward the drone work was, I may attempt a larger helicopter or fixed-wing plane vehicle in the near future too.


This was posted to the Godot subreddit Taking a small departure from ground vehicles to work on a quadcopter drone. Didn't expect the enemy AI to take potshots at it, but it was pretty neat... until I crashed while stylin on em.

Quality of Life

As small quality of life improvements, a settings menu and control-information menu were added.

At the moment, the control page simply lists the key bindings, but does not yet support rebinding them on the fly. But that is something I'll add in the near future.

Currently the settings page allows for adjusting the volume of the different audio buses. More settings will be added over time.

Both of these menus were made available on the title screen and in-game from the pause menu.

Minor Graphics Upgrade

Upgrading my graphics card for the first time in ~8 years from a 680 to a 3050 allowed me to use the Forward+ rendering mode in Godot. This is because the newer card supports the Vulkan drivers.

Old vs New

On the left is a scene captured using the Compatibility mode on my 680, and on the right is the same shot captured with the Forward+ mode on the new 3050.


Here's an assorted set of images taken around this point with the new graphics.

Published: July 28, 2023

Categories: gamedev