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)
// a Node 'is a' component
Scene
βββ RigidBody3D "Player"
βββ CollisionShape3D
βββ MeshInstance3D
βββ Node: PlayerController.gd (script)
βββ Node3D "Weapon" (object-like child)
βββ MeshInstance3D
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 Components:
// Works identically across all engines
When.Object.Spawns(
Child("Weapon").Transform.Position = Vector3.Up * 2
);
// Godot: Finds child node "Weapon"
// Unity: Finds child GameObject "Weapon"
// Unreal: Finds child component or actor "Weapon"
The same works with types.
// Works identically across all engines
When.CollisionWith("ball")
.Begins(Log("touching ball"));
// Godot: Finds Rigidbody3D node for "ball" on current or child nodes
// Unity: Finds Rigidbody component on GameObject or children
// Unreal: Finds Rigidbody component on Actor or children
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 |
LunyScript generates its own object lifecycle events for best consistency. This sidelines any behavioural differences across engines.
OnCreate // First setup (maps: Awake/_ready/PostInitializeComponents)
β
OnEnable // Object becomes active (maps: OnEnable/_enter_tree/BeginPlay)
β
OnFixedStep // 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.Created(/* first setup */);
When.Enabled(/* when activated */);
Every.FixedStep(/* physics timestep */);
Every.Frame(/* per frame */);
Every.FrameEnds(/* after updates */);
When.Disabled(/* when deactivated */);
When.Destroyed(/* 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.
Special Note: Destruction
Godot lacks explicit destruction callbacks. LunyScript raises its own OnDestroy event when handled internally, or during scene unload.
If native scripting destroys the native engine object, the LunyScript object will stop working but wonβt throw exceptions.
Special Note: Godot LateUpdate
Godot does not have a native late update phase. LunyScript adds 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.
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.