LunyScript provides a uniform API across game engines by abstracting their architectural differences. This document details how Godot, Unity, and Unreal differ structurally, and how LunyScript unifies them.
Godot: Node Hierarchy (Contextual Components)
Scene
βββ RigidBody3D "Player"
βββ CollisionShape3D (component-like child)
βββ MeshInstance3D (component-like child)
βββ Node: PlayerController.gd (script)
βββ Node3D "Weapon" (object-like child)
βββ MeshInstance3D (component-like child)
Unity: GameObject + Components
Scene
βββ GameObject "Player"
βββ Component: Transform
βββ Component: Rigidbody
βββ Component: PlayerController (script)
βββ GameObject "Weapon" (child)
βββ Component: Transform
βββ Component: MeshRenderer
Unreal: Actor + Components
Level
βββ Actor "Player"
βββ Component: RootComponent (SceneComponent)
β βββ Component: StaticMeshComponent
β βββ Component: CapsuleComponent
βββ Component: CharacterMovementComponent
βββ Component: PlayerController (script/blueprint)
βββ Component: ChildActorComponent "Weapon"
βββ Actor reference with nested components
| Aspect | Godot | Unity | Unreal |
|---|---|---|---|
| Primary Structure | Node hierarchy | GameObject + Components | Actor + Components |
| Child Semantics | Context-dependent (objects OR components) | Always sub-objects | Components or nested actors |
| Script Attachment | Node property | Component | Component or blueprint |
| Transform | Inherited from node type | Component | Component (SceneComponent) |
Think of the grouping elements as folders and files in an Explorer/Finder tree view:
The π§©symbol denotes the types we write logic for. The above analogy also maps perfectly to the perceived complexity of each engine.
Unreal Engine is significantly more challenging for new users due to the explosion of subclassing and arrangement choices. The PoC also relied on the UnrealSharp plugin for C# support - which is a non-standard setup. Unreal is therefore not a preferred target for LunyScript, but the PoC proofs that it would even work for the βworst caseβ scenario.
LunyScript treats all entities uniformly as Objects with Behaviors:
// Works identically across all engines
When.Self.Spawns(
Get.Child("Weapon").Transform.Position = Vector3.Up * 2
);
// Godot: Finds child node "Weapon"
// Unity: Finds child GameObject "Weapon"
// Unreal: Finds child component or actor "Weapon"
Unified Hierarchy Model:
Object "Player"
βββ Behavior: LunyScript behaviors
βββ Children: Nested objects
βββ Properties: Transform, Physics, etc.
Adapters translate:
Godot Lifecycle
Constructor
β
_init() // Node initialization
β
_ready() // Scene tree ready (like Start)
β
_enter_tree() // Added to scene tree (can repeat)
β
_process() // Per-frame update
_physics_process() // Physics timestep update
β
_exit_tree() // Removed from scene tree
β
(Node destroyed when no references - no reliable destruction callback)
Unity Lifecycle
Awake // Object initialization, called once
β
OnEnable // Called when enabled (can repeat)
β
Start // First frame initialization
β
FixedUpdate // Physics timestep update
Update // Per-frame update
LateUpdate // After all Updates
β
OnDisable // Called when disabled
β
OnDestroy // Cleanup before destruction
β
(Object destroyed)
Unreal Lifecycle
Constructor
β
PostInitializeComponents // After all components initialized (used for Awake/_init purposes)
β
BeginPlay // When gameplay starts
β
Tick // Per-frame update (enacts all update types)
β
EndPlay // When gameplay ends or destroyed
β
BeginDestroy // Cleanup phase
β
(Object destroyed)
| Phase | Godot | Unity | Unreal |
|---|---|---|---|
| Construction | _init() | N/A (discouraged) | Constructor |
| Pre-Start Init | N/A | Awake | PostInitializeComponents |
| Enable/Enter | _enter_tree() | OnEnable | BeginPlay |
| First Frame | _ready() | Start | BeginPlay |
| Physics Step | _physics_process() | FixedUpdate | Tick |
| Per-Frame | _process() | Update | Tick |
| Late Update | N/A | LateUpdate | Tick |
| Disable/Exit | _exit_tree() | OnDisable | EndPlay |
| Destruction | N/A (unreliable) | OnDestroy | EndPlay + BeginDestroy |
OnAwake // First setup (maps: Awake/_ready/PostInitializeComponents)
β
OnEnable // Object becomes active (maps: OnEnable/_enter_tree/BeginPlay)
β
OnStep // Physics timestep (maps: FixedUpdate/_physics_process/Tick)
OnUpdate // Per-frame (maps: Update/_process/Tick)
OnLateUpdate // After updates (maps: LateUpdate/custom/Tick)
β
OnDisable // Object becomes inactive (maps: OnDisable/_exit_tree/EndPlay)
β
OnDestroy // Cleanup (maps: OnDestroy/custom/BeginDestroy)
β
(Cleaned up)
Event Mapping:
// Unified lifecycle events (wrapped in When, not directly exposed)
When.Self.Awakes(/* first setup */);
When.Self.Enables(/* when activated */);
When.Self.Steps(/* physics timestep */);
When.Self.Updates(/* per frame */);
When.Self.LateUpdates(/* after updates */);
When.Self.Disables(/* when deactivated */);
When.Self.Destroys(/* final cleanup */);
Note: Lifecycle events are wrapped in
Whenand not directly exposed to LunyScript users. Developers using Luny abstractions may utilize them directly for advanced scenarios.
Adapter Behavior:
| LunyScript Event | Godot | Unity | Unreal |
|---|---|---|---|
| Awakes | _ready() | Awake | PostInitializeComponents |
| Enables | _enter_tree() | OnEnable | BeginPlay |
| Steps | _physics_process() | FixedUpdate | Tick |
| Updates | _process() | Update | Tick |
| LateUpdates | (added) | LateUpdate | Tick |
| Disables | _exit_tree() | OnDisable | EndPlay |
| Destroys | (added) | OnDestroy | BeginDestroy |
Special Note: Godot Destruction
Godot lacks explicit destruction callbacks. LunyScript raises its own OnDestroy event when:
queue_free() or free()// In Godot adapter - LunyScript raises destruction event
public override void _ExitTree() {
if (_isBeingDestroyed) {
SendDestroyEvent(this);
}
}
Special Note: Godot LateUpdate
Godot does not have a native late update phase. LunyScript may add its own OnLateUpdate event for Godot to maintain cross-engine consistency.
LunyScript unifies three seemingly very diverse and unique conceptual models. But as the proof of concept has shown, the differences are superficial on a technical level. They can be unified in a straightforward manner by mere mapping and re-ordering.
This enables truly portable gameplay code without sacrificing engine-specific capabilities. The overhead is minimal but varies between engines depending on how well they match the uniform behaviour.
Under consideration: Configurability of Lifecycle event ordering. In many cases the precise order of events simply does not matter, not even across engines.
TO BE REVIEWED: Technical accuracy verification needed, especially for Godot destruction handling and Unreal component hierarchy mapping.