Home

Awesome

Simplified Flight Simulation Library (addon) for Godot Engine

This addon in a simple flight simulation system, in pure GDScript, and generic enough to cover several types of aircrafts and spacecrafts.

The main branch is for Godot 4. If you are using Godot 3.x, check branch 3.x.

There are very realistic flight simulation libraries elsewhere (usually in C), but if you only want your players to vooosh and pew pew around, a scientifically precise library is overkill. So this one is optimized for games, not pilot training.

Simplified

This is a simplified library because we get rid of many headaches by doing simplifications. Instead of many parameters (wing shape, area, material this, volume that, blabla yadda yadda), you just drop some values which you think feel right for your gameplay.

They are explained in more detail further down this file.

Modular

This library is very flexible and covers a wide range of vehicle types, by being modular. Your plane is one Aircraft node and several module nodes inside.

The Aircraft node per se does very little: only takes care of things happening to the fuselage itself (lift, drag, temperature, etc), and routes energy/fuel between modules. Everything else is done by modules. You need an engine? Put an engine module. You need the player to control that engine (turn on, off, speed)? Put an engine control module. You need to steer the plane? Put a steering module and control it with a steering control module. Fuel? Fuel tank module.

Scene tree showing modular system

This way, different combinations of modules result in different aircrafts. E.g. an airplane would have an air-based engine (e.g. propeller), which is affected by air density, while a spacecraft would have a rocket based engine, which would work regardless of air density, but is less efficient. Or an aileron steering module would be passive, but only works in air, while a thruster based module works in zero speed, but uses fuel.

Examples

This repository contains a project with the addon and 4 examples. They demonstrate how the modules can be used in the aircraft for different scenarios, how to update UI from the simulation as well as visual meshes, and also demonstrate different ways to achieve the same things. The "look-and-feel" of the user interface, instruments and etc does not belong to the addon, the art and layout is entirely game-dependent (you don't even need any UI at all to run the examples - they are just aesthetic).

The examples are:

Example 1 - Simple Plane

Example 2 - Complex Plane

Example 3 - Helicopter

Example 4 - Spaceship 1 Example 4 - Spaceship 2


Aircraft node

The Aircraft node takes care of the overall behaviour of the aircraft fuselage, including air interaction with the body (drag), wings (lift), friction (heating), etc. This simulation uses Godot axes convention (unlike aerospace engineering), so -Z is forward, X is right, Y is up.

The node must have a CollisionShape child node and it should be a primitive node (not trimesh). This is because in Godot collisions between Primitive-Primitive and Primitive-Trimesh are OK, but Trimesh-Trimesh is not. So the aircraft is a Primitive so the scenario can be a Trimesh.

If using a landing gear module, the Aircraft node must have a second CollisionShape node for the landing gear, explained in the LandingGear module.

The following parameters can be set:

Any other configuration is done via modules.

Signals:


Modules

Each module can receive input, process physics, and output information, all optionally.

Those are usually decoupled: there are modules altering the aircraft behaviour (e.g. Steering), modules receiving input to control them (e.g. ControlSteering) and modules to display information which is not tied to another module (e.g. InstrumentAttitude for altitude, compass, etc). When information is tied to a module, that module can output the information directly (e.g. the engine's current power in an Engine module is emitted by the Engine module itself), but an Instrument could be used to read grouped or indirect information from other modules (e.g. a InstrumentTotalEnergy which could read the level of all energy container modules and show the total).

Modules which alter the aircraft have direct names (e.g. Engine, Steering, etc), modules which control other modules have the Control prefix (e.g. ControlEngine, ControlSteering) and modules which read information from the aircraft or other modules have the Instrument prefix (e.g. InstrumentAttitude).

Modules can find each other via module type and tags: both are String identifiers, the difference being the module type is a single string fixed for all instances of that module (e.g. all engine modules have module type "engine"), while tags are a list of user defned strings which can be in any amount and different for each instance. Modules searching for other modules by tag will find any modules containing that tag, regardless of other tags (e.g. a module searching for "engine" with the tag "left" will find all modules with type "engine" having the tag "left" in their list of tags).

The tasks are decoupled (between action and control modules) to allow switching modules without unnecessary work on shared parts (e.g. replacing the type of landing gear doesn't need rework on input processing, UI, etc), but also to allow cases when modules and their controls are not 1:1. The Example 4 - Spaceship demonstrates this: there is a group of engines "forward" with the "left" and "right" engines, and a "bottom" engine, so there are three Engine modules, but there are four ControlEngine modules: one for each individual engine (using the tags "left", "right", "bottom") and one to simultaneously control both the forward engines (using the "forward" tag).

You must use the modules' own system to implement tasks (instead of Godot's internal _input(), _physics_process(delta), etc) because the aircraft has to be changed/interacted at the correct times for it to work properly - so the aircraft calls the module methods, not the game engine. E.g: on each physics frame, the aircraft calculates internal state variables, then calls the modules' process_physic_frame(delta) method, and then applies lift and drag using the values (optionally) modified by the modules (or externally). So any physics in the modules must be in their process_physic_frame method, not in _physics_process. Same for everything else: the logic presented here must be respected.

All modules inherit from either AircraftModule (descending from Node) or AircraftModuleSpatial (descending from Spatial). Use the second only if you need 3D coordinates in the module (e.g. Engine which uses its position and rotation to apply forces), otherwise use the first to save CPU.

All of this sounds very intimidating, but often end up being simple. As example, here is a minimalist flaps module (which doesn't do any error checking or animations, but still works):

extends AircraftModule

signal update_interface(values)

export(float) var LiftFlapFactor = 1.2
export(float) var DragFlapFactor = 1.25

var flap_position = 0.0

func _ready():
	ProcessPhysics = true
	ModuleType = "flaps"

func process_physic_frame(_delta):
    aircraft.lift_intensity *= lerp(1.0, LiftFlapFactor, flap_position)          # apply LiftFlapFactor proportionally
    aircraft.drag_intensity_vector.z *= lerp(1.0, DragFlapFactor, flap_position) # apply DragFlapFactor proportionally

func flap_set_position(value: float):
    flap_position = clamp(value, 0.0, 1.0)
    emit_signal("update_interface", {"flap": flap_position })

func flap_increase_position(step: float):
    flap_set_position(flap_position + step)

And here is a minimalist control flap module to go along with that one:

extends AircraftModule

export(bool) var ControlActive = true

var flaps_module = null

func _ready():
	ReceiveInput = true
    
func setup(aircraft_node):
	aircraft = aircraft_node
	flaps_module = aircraft.find_modules_by_type("flaps").pop_front()

func receive_input(event):
	if ControlActive:
		if Input.is_action_just_pressed("decrease_flaps):
			flaps_module.flap_increase_position(-0.2)
		if Input.is_action_just_pressed("increase_flaps):
			flaps_module.flap_increase_position(0.2)

No special node configuration needed. Just drop these two nodes as children of your aircraft and your flaps are already working and good to go. (You should put some error checking and polishing in there, but you get the idea.)

Properties

All modules have the following properties:

Methods

The following methods are predefined (implement only the ones you'll use):

func setup(aircraft_node):
	aircraft = aircraft_node

So if you implement it yourself you must include the aircraft = aircraft_node line. If your module emits state information (e.g. for UI) it is important to emit a first time here to make sure UI shows with initialized values.

Signals

The following signals are predefined (implement only if used):


Module Sets

Modules can have specific properties, methods and signals when they are expected to be paired to other modules made as part of a same module set - e.g. all Engine modules have the engine_set_power method which is called by all ControlEngine modules, as Engine and ControlEngine types consist of a module set. In that case, when creating a new module to be used in an existing set, the existing formats must be respected.

Currently implemented sets:

The descriptions below show the minimum required implementation to respect the system. Modules in this repository will have more optional features implemented (such as animations and sound loops).

EnergyContainer Set

Energy containers (think fuel tanks) store energy and are used by the aircraft to calculate energy budgets (how much energy is available to modules). They are a special type of module because energy containers are the only modules the aircraft itself searches for and interacts directly (instead of leaving the modules to just talk to each other). Energy in the aircraft has types, identified by strings (e.g. "fuel", "battery", etc). There is no limit to the amount of types you can have, and they have no predefined meaning.

Energy containers can be connected or disconnected from the system. When they are disconnected, they are treated as if they were not there, but still hold their charge values (as if they were removed and stored away, as they were).

All connected containers of the same type have their values added together to form the "energy budget" of that energy type. When modules request energy, the value is taken from the energy budget and all connected containers of that type are depleted, proportional to their contribution. Similarly, when the aircraft is loaded with energy ("refuel"/"recharge"), only connected containers are charged, and loading energy is split equally between them.

All EnergyContainer modules have Module Type as "energy_container" and have the following properties:

The following methods are implemented:

This module implements the update_interface(values) signal.

All ControlEnergyContainer modules affect only modules with Module Type as "energy_container", and have the following properties:

Actual input implementation in the ControlEnergyContainer module is game-specific and the code provided in this repository (body of receive_input method) is only for example purposes.

Engine Set

There can be any number of Engine modules in an aircraft. All Engine modules have Module Type set as "engine", and have the following properties:

Engines are Spatial-derived nodes and therefore have a 3D position and rotation, which is respected when applying forces. Force is applied at the engine position and toward's the module's -z.

Engines have the following methods:

This module implements the update_interface(values) signal.

All ControlEngine modules affect only modules with Module Type as "engine", and have the following properties:

Actual input implementation in the ControlEngine module is game-specific and the code provided in this repository (body of receive_input method) is only for example purposes.

Steering Set

There can be only one Steering module at a time in an aircraft. All Steering modules have Module Type set as "steering", and have the following properties:

Steering modules have the following methods:

This module implements the update_interface(values) signal.

There can be only one ControlSteering module at a time in an aircraft. All ControlSteering modules affect only modules with Module Type as "steering", and have the following properties:

Actual input implementation in the ControlSteering module is game-specific and the code provided in this repository (body of receive_input method) is only for example purposes.

Flaps Set

There can be only one Flaps module at a time in an aircraft. All Flaps modules have Module Type set as "flaps", and have the following properties:

Flaps modules have the following methods:

This module implements the update_interface(values) signal.

There can be only one ControlFlaps module at a time in an aircraft. All ControlFlaps modules affect only modules with Module Type as "flaps", and have the following properties:

Actual input implementation in the ControlFlaps module is game-specific and the code provided in this repository (body of receive_input method) is only for example purposes.

Landing Gear Set

There can be more than one LandingGear in an aircraft (e.g. wheels for runways and stand pads for mud). Currently there is no implementation to restrict movement direction when landed (that is, wheels will slide sideways as well). All LandingGear modules have Module Type set as "landing_gear", and have the following properties:

Landing gear modules have the following methods implemented:

This module implements the update_interface(values) signal.

All ControlLandingGear modules affect only modules with Module Type as "landing_gear", and have the following properties:

Actual input implementation in the ControlLandingGear module is game-specific and the code provided in this repository (body of receive_input method) is only for example purposes.