You want to make an airplane controller in 3D, but don’t need a fully accurate flight-simulator.
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.
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
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 happens in
_physics_process(), first lerping the speed towards the target speed and then using
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
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.
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
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
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")
Here’s the full script:
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.
Download the project file here: airplane_test.zip