Simplified Airplane Controller
Problem
You want to make an airplane controller in 3D, but don’t need a fully accurate flight-simulator.
Solution
In this recipe, we’re going to make a simplified airplane controller. By “simplified” we mean stripping things down to the basics. We’re looking for the “feel” of flying a plane - one that you can just jump in and start flying effortlessly, with a minimal control scheme.
This recipe is not an accurate flight simulator. We are not simulating aerodynamics, so this doesn’t fly like a real airplane. We’re going for simplicity and “fun” here, not accuracy.
Node setup
We’re going to use a KinematicBody
for this. Since we won’t be simulating actual flight physics (lift, drag, etc.), we don’t need RigidBody
in this case.
Here’s our model setup:
We’re using a cylinder for the collision shape, sized to match the plane’s fuselage. This will allow for detecting the ground, which is all we’re concerned with for this demo.
To start the script, let’s look at our plane’s properties:
extends KinematicBody
# Can't fly below this speed
var min_flight_speed = 10
# Maximum airspeed
var max_flight_speed = 30
# Turn rate
var turn_speed = 0.75
# Climb/dive rate
var pitch_speed = 0.5
# Wings "autolevel" speed
var level_speed = 3.0
# Throttle change speed
var throttle_delta = 30
# Acceleration/deceleration
var acceleration = 6.0
# Current speed
var forward_speed = 0
# Throttle input speed
var target_speed = 0
# Lets us change behavior when grounded
var grounded = false
var velocity = Vector3.ZERO
var turn_input = 0
var pitch_input = 0
Controls
We’ll need the following input actions for our controls. We’re using a game controller in this demo, but you can add keyboard inputs as well if you like.
This function captures the inputs and sets the input values. Note that increasing/decreasing throttle changes the target_speed
, not the actual speed.
func get_input(delta):
# Throttle input
if Input.is_action_pressed("throttle_up"):
target_speed = min(forward_speed + throttle_delta * delta, max_flight_speed)
if Input.is_action_pressed("throttle_down"):
var limit = 0 if grounded else min_flight_speed
target_speed = max(forward_speed - throttle_delta * delta, limit)
# Turn (roll/yaw) input
turn_input = 0
turn_input -= Input.get_action_strength("roll_right")
turn_input += Input.get_action_strength("roll_left")
# Pitch (climb/dive) input
pitch_input = 0
pitch_input -= Input.get_action_strength("pitch_down")
pitch_input += Input.get_action_strength("pitch_up")
Movement
Movement happens in _physics_process()
, first lerping the speed towards the target speed and then using move_and_slide()
:
func _physics_process(delta):
# Accelerate/decelerate
forward_speed = lerp(forward_speed, target_speed, acceleration * delta)
# Movement is always forward
velocity = -transform.basis.z * forward_speed
velocity = move_and_slide(velocity, Vector3.UP)
To test, add the plane to a test scene (don’t forget a Camera
). Press the "throttle_up"
input and you should see the plane accelerate forward.
We’re using the Interpolated Camera recipe in this demo.
Next, let’s handle changing the pitch of the plane. Add this right after calling get_input()
:
transform.basis = transform.basis.rotated(transform.basis.x, pitch_input * pitch_speed * delta)
Run the scene again and try pitching up and down:
After that, add the following for the turn input:
transform.basis = transform.basis.rotated(Vector3.UP, turn_input * turn_speed * delta)
Notice that while the plane turns, it doesn’t really look natural. Airplanes bank when they turn, so let’s animate that by changing the rotation of the mesh:
$Mesh/Body.rotation.y = lerp($Mesh/Body.rotation.y, turn_input, level_speed * delta)
The amount of roll is related to the turn_input
so a shallow turn banks less. Going straight will “auto” level the plane.
That’s it! You now have the basic flying controls working correctly, and it should feel comfortable and natural to fly around. Try adjusting the various properties to see how they affect the movement.
Landing/taking off
While the above is fine for flying, it doesn’t handle the ground very well. Here, we’ll handle the ground using a simplistic approach (by “simplistic”, we mean doing so in a very basic way - you’ll probably want to expand on it depending on what your game may need).
First, we’ll want to distinguish between being on the ground and being in the air. On the ground, we can slow down to 0; in the air, we must maintain the minimum airspeed. Also, when on the ground, we won’t bank when turning (we don’t want our wings digging into the ground!).
func _physics_process(delta):
get_input(delta)
transform.basis = transform.basis.rotated(transform.basis.x, pitch_input * pitch_speed * delta)
transform.basis = transform.basis.rotated(Vector3.UP, turn_input * turn_speed * delta)
# If on the ground, don't roll the body
if grounded:
$Mesh/Body.rotation.y = 0
else:
$Mesh/Body.rotation.y = lerp($Mesh/Body.rotation.y, turn_input, level_speed * delta)
forward_speed = lerp(forward_speed, target_speed, acceleration * delta)
velocity = -transform.basis.z * forward_speed
# Handle landing/taking off
if is_on_floor():
if not grounded:
rotation.x = 0
velocity.y -= 1
grounded = true
else:
grounded = false
velocity = move_and_slide(velocity, Vector3.UP)
Note that while grounded, we’re setting velocity.y
to keep us “stuck” to the ground. Alternatively, this could be done via move_and_slide_with_snap()
.
Meanwhile, in the get_input()
function, we’ll also take into account grounded
when throttling down and when pitching down, and by only allowing takeoff if above min_flight_speed
:
func get_input(delta):
if Input.is_action_pressed("throttle_up"):
target_speed = min(forward_speed + throttle_delta * delta, max_flight_speed)
if Input.is_action_pressed("throttle_down"):
var limit = 0 if grounded else min_flight_speed
target_speed = max(forward_speed - throttle_delta * delta, limit)
turn_input = 0
if forward_speed > 0.5:
turn_input -= Input.get_action_strength("roll_right")
turn_input += Input.get_action_strength("roll_left")
pitch_input = 0
if not grounded:
pitch_input -= Input.get_action_strength("pitch_down")
if forward_speed >= min_flight_speed:
pitch_input += Input.get_action_strength("pitch_up")
Full script
Here’s the full script:
Wrapping up
You can adapt this technique to a variety of arcade-style flying games. For example, for mouse control, you could use the relative
property of InputEventMouseMotion
to set the pitch and turn input.