I was playing around with this idea and came up with this demo:
Here’s a scene structure:

Here’s the script on the Planet node:
class_name Planet
extends Node3D
class PlanetState:
var velocity: Vector3
var position: Vector3
var sun_position: Vector3 = Vector3.ZERO
var gravitational_force: float = 5.0
var future_states: Array[PlanetState]
var simulation_steps_per_frame: int = 200
var max_states: int = 2000
var lines: Array[MeshInstance3D]
var margin: float = 0.3
var min_states_for_margin: int = 50
var max_speed: float
func _physics_process(delta: float) -> void:
simulate_future_positions(delta)
move()
func calculate_forces(from_position: Vector3 = global_position) -> Vector3:
var direction: Vector3 = sun_position - from_position
var direction_normalized: Vector3 = direction.normalized()
var distance: float = direction.length()
return direction_normalized * (gravitational_force / pow(distance, 2))
func simulate_future_positions(delta: float) -> void:
if future_states.is_empty():
var initial_velocity = 5.0 * Vector3.ONE * transform.basis.z
var new_state: PlanetState = PlanetState.new()
new_state.position = global_position
new_state.velocity = initial_velocity
future_states.append(new_state)
if future_states.size() > max_states:
return
for step in simulation_steps_per_frame:
var first_state: PlanetState = future_states[0]
var last_state: PlanetState = future_states[-1]
var new_state: PlanetState = PlanetState.new()
new_state.position = last_state.position + last_state.velocity * delta
new_state.velocity = last_state.velocity + calculate_forces(new_state.position)
if future_states.size() > min_states_for_margin \
and first_state.position.distance_to(last_state.position) <= margin:
return
future_states.append(new_state)
if new_state.velocity.length_squared() > max_speed:
max_speed = new_state.velocity.length_squared()
var new_line: MeshInstance3D = draw_line(last_state.position, new_state.position, Color.WHITE * (last_state.velocity.length_squared() / max_speed), 0.0)
lines.append(new_line)
func move() -> void:
global_position = (future_states.pop_front() as PlanetState).position
(lines.pop_front() as MeshInstance3D).queue_free()
func draw_line(pos1: Vector3, pos2: Vector3, color = Color.WHITE_SMOKE, persist_ms = 0) -> MeshInstance3D:
var mesh_instance := MeshInstance3D.new()
var immediate_mesh := ImmediateMesh.new()
var material := ORMMaterial3D.new()
mesh_instance.mesh = immediate_mesh
mesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
immediate_mesh.surface_begin(Mesh.PRIMITIVE_LINES, material)
immediate_mesh.surface_add_vertex(pos1)
immediate_mesh.surface_add_vertex(pos2)
immediate_mesh.surface_end()
material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
material.albedo_color = color
get_tree().get_root().add_child(mesh_instance)
return mesh_instance
(for the draw_line() method credit goes to Line-and-Sphere-Drawing/Draw3D.gd at main · Ryan-Mirch/Line-and-Sphere-Drawing · GitHub, I just slightly modified it)
It’s definitely not ideal, but maybe will get you started in your own direction.
You could use the same principle to simulate multi-body physics (currently the planets interact only with the Sun’s gravity).
Let me know if you have any issues implementing this in your project!