What does the same piece of Unity C# UI code look like in 16 game engines? That’s nine different languages!
Unity C# Baseline
Let’s start with the archetypical “Try Again” Button in Unity:
using UnityEngine;
using UnityEngine.UIElements;
using UnityEngine.SceneManagement;
public class TryAgainButton : MonoBehaviour
{
    private Button button;
    private void OnEnable()
    {
        var uiDocument = GetComponent<UIDocument>();
        var root = uiDocument.rootVisualElement;
        button = root.Q<Button>("TryAgainButton");
        button.clicked += OnTryAgainClicked;
    }
    private void OnDisable()
    {
        button.clicked -= OnTryAgainClicked;
    }
    private void OnTryAgainClicked()
    {
        Scene currentScene = SceneManager.GetActiveScene();
        SceneManager.LoadScene(currentScene.name);
    }
}
What this code does is to get a UI button reference, register a click handler, unregister the handler to avoid leaks. When the button is clicked, the current scene gets reloaded.
Every modern game engine should handle this task. Let’s compare!
Table of Contents
C# Engines
Starting on equal footing we’ll compare the C# engines’ code first.
In fact, most of them are strikingly similar.
Flax Engine
using FlaxEngine;
using FlaxEngine.GUI;
public class TryAgainButton : Script
{
    private Button button;
    public override void OnEnable()
    {
	var canvas = Actor.As<UICanvas>();
        var root = canvas.RootControl;
        button = root.GetChild<Button>();
        button.Clicked += OnTryAgainClicked;
    }
    public override void OnDisable()
    {
        button.Clicked -= OnTryAgainClicked;
    }
    private void OnTryAgainClicked()
    {
        Level.ReloadScene();
    }
}
Stride Game Engine
using Stride.Engine;
using Stride.UI;
using Stride.UI.Controls;
using Stride.UI.Events;
public class TryAgainButton : SyncScript
{
    private Button button;
    public override void Start()
    {
        var uiComponent = Entity.Get<UIComponent>();
        var root = uiComponent.Page.RootElement;
        button = root.FindVisualChildOfType<Button>("TryAgainButton");
        button.Click += OnTryAgainClicked;
    }
    public override void Cancel()
    {
        button.Click -= OnTryAgainClicked;
    }
    private void OnTryAgainClicked(object sender, RoutedEventArgs e)
    {
        var inst = SceneSystem.SceneInstance;
        var scene = inst.RootScene.URL;
        inst.RootScene = Content.Load<Scene>(scene);
    }
}
It’s slightly more verbose than other C# engines.
Unigine
I’m always about to write “Unitygine”.
One might think they tried to rip off on Unity, when in fact Unigine was first released one month earlier than Unity’s first release! That was way back in 2005.
using System;
using Unigine;
class TryAgainButton : Component
{
    private WidgetButton button;
    private void Init()
    {
        var gui = Gui.GetCurrent();
        button = new WidgetButton(gui, "TryAgain");
        gui.AddChild(button, Gui.ALIGN_CENTER);
        button.AddCallback(Gui.CALLBACK_INDEX.CLICKED, OnTryAgainClicked);
    }
    private void Shutdown()
    {
        tryAgainButton.RemoveCallback(Gui.CALLBACK_INDEX.CLICKED, OnTryAgainClicked);
    }
    private void OnTryAgainClicked()
    {
        World.Load(World.GetPath());
    }
}
It’s even more verbose than other C# engines. Also SCREAMING!
Evergine
This little known engine dates back to 2015. However, it’s main use case actually isn’t to make games but to power industrial applications.
using Evergine.Framework;
using Evergine.Framework.Services;
using Evergine.UI;
public class TryAgainButton : Component
{
    [BindComponent]
    private Button button;
    protected override void OnActivated()
    {
        base.OnActivated();
        button.Click += OnTryAgainClicked;
    }
    protected override void OnDeactivated()
    {
        base.OnDeactivated();
        button.Click -= OnTryAgainClicked;
    }    
    private void OnTryAgainClicked(object sender, System.EventArgs e)
    {
        var container = Application.Current.Container;
        var screenCtx = container.Resolve<ScreenContextManager>();
        screenCtx.ReloadCurrentContext();
    }
}
CryEngine C#
CryEngine indeed can be programmed in C#. You’ll find its Lua counterpart further down.
using CryEngine;
using CryEngine.UI;
public class TryAgainButton : UIElement
{
    private Button button;
    protected override void OnAwake()
    {
        button = GetComponent<Button>();
        button.OnPressed += OnTryAgainPressed;
    }
    protected override void OnDestroy()
    {
        button.OnPressed -= OnTryAgainPressed;
    }
    private void OnTryAgainPressed()
    {
        Level.Reload();
    }
}
Wow! That’s surprisingly smooth. Good cry, engine!
Godot C#
The Godot C# version stands out as .. puristic. Simplistic. Lovely!
Yes, you needn’t even unsubscribe the event handler, that’s correct!
using Godot;
public partial class TryAgainButton : Control
{
    public override void _Ready()
    {
        var button = GetNode<Button>("TryAgainButton");
        button.Pressed += OnTryAgainPressed;
    }
    private void OnTryAgainPressed()
    {
        GetTree().ReloadCurrentScene();
    }
}
On the downside, you need to accept, or overlook, or at least not pull your hair out when you notice how it gladly violates naming conventions. Not just the established C# guidelines but also its own GDScript conventions.
A _ready method would be plain wrong, sure. But when you’re already mangling names, why not just go with Ready?
Lua Engines
There really should be more engines supporting Lua out of the box!
CryEngine Lua
Lua ought to be more straightforward, simplistic. Right?
TryAgainButton = {
    Properties = {},
}
function TryAgainButton:OnInit()
    self.button = UIElement.Get("TryAgainButton")
end
function TryAgainButton:OnActivate()
    self.button:RegisterEventHandler("OnPressed", self, self.OnTryAgainClicked)
end
function TryAgainButton:OnDeactivate()
    self.button:UnregisterEventHandler("OnPressed", self, self.OnTryAgainClicked)
end
function TryAgainButton:OnTryAgainClicked()
    System.LoadLevel(System.GetLevelName())
end
I can see what they are doing here. Push this as self to the Lua global environment _G and run scripts either sequentially or each in a separate Lua state. 
I’m afraid that limits Lua’s usability quite a bit.
Open3D Engine Lua
O3DE … terrible abbreviation by the way. Excuse me, here’s a quick shoutout: Hey Amazon! I thought you employ marketing geeks?
Anyway, O-whatever is based on CryEngine! Don’t know how they made that split/fork/re-release as open source work from a legal perspective. They probably had to rewrite a lot of proprietary stuff, or pay Crytek loads of money. In any case, that should be simpler!
local TryAgainButton = {
    Properties = {},
}
function TryAgainButton:OnActivate()
    self.buttonHandler = UiButtonNotificationBus.Connect(self, self.entityId)
end
function TryAgainButton:OnDeactivate()
    self.buttonHandler:Disconnect()
end
function TryAgainButton:OnButtonClick()
    local levelName = GameManagerRequestBus.Broadcast.GetCurrentLevelName()
    GameManagerRequestBus.Broadcast.LoadLevel(levelName)
end
return TryAgainButton
No. It’s exactly the same as in CryEngine, except O3DE supports the local keyword.
Roblox Luaua
For kicks, I though I’ll try Roblox. After all it’s supposed to be an easy to learn programming environment. So much so that it’s chuck-full of kid creators.
No, just kidding. Just 3% of Roblox’ playerbase are actually using Roblox Studio, and far fewer ever publish anything.
local TryAgainButton = {}
TryAgainButton.__index = TryAgainButton
function TryAgainButton.new(button: TextButton)
    local self = setmetatable({}, TryAgainButton)
    self.button = button
    return self
end
function TryAgainButton:OnEnable()
    self.connection = self.button.MouseButton1Click:Connect(function()
        self:OnTryAgainClicked()
    end)
end
function TryAgainButton:OnDisable()
    if self.connection then
        self.connection:Disconnect()
    end
end
function TryAgainButton:OnTryAgainClicked()
    local TeleportService = game:GetService("TeleportService")
    local placeId = game.PlaceId
    local player = game.Players.LocalPlayer
    TeleportService:Teleport(placeId, player)
end
return TryAgainButton
I’m surpised. That’s quite a bit of text you have to write!
Type-/Javascript Engines
I meant Typescript but didn’t want to linebreak the headline.
Cocos Engine Typescript
Wait, isn’t it Cocos Creator? No, that’s the editor.
But isn’t it Cocos Runtime? No, that’s the commercial, somehow “optimized” engine replacement/extension for Cocos Engine.
Huh, isn’t it Cocos2d-x? No no, that’s legacy. And also 3D btw.
In any case, this is Typescript. SPARTAAA! @punchkick(Well)
import { _decorator, Component, Button, director } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('TryAgainButton')
export class TryAgainButton extends Component {
    @property(Button)
    private button: Button = null;
    onEnable() {
    this.button.node.on(Button.EventType.CLICK, this.onTryAgainClicked, this);
    }
    onDisable() {
    this.button.node.off(Button.EventType.CLICK, this.onTryAgainClicked, this);
    }
    onTryAgainClicked() {
        director.loadScene(director.getScene().name);
    }
}
Yeah. I mean. It’s okay. If you have to do web, you deal with way worse sh$§%&t than that!
Though I’m afraid that engine isn’t web-only.
GDevelop Javascript
For comparison, here’s Javascript. It’s also more of a visual game creation tool so it better be simpler.
gdjs.TryAgainButtonCode = {};
gdjs.TryAgainButtonCode.onSceneLoaded = function(runtimeScene) {
    const button = runtimeScene.getObjects("TryAgainBtn")[0];
    const game = runtimeScene.getGame();
    const inputManager = game.getInputManager();
    const sceneName = runtimeScene.getName();
    
    button.onCursorOnObject = function() {
        const pressed = inputManager.isMouseButtonPressed(0);
        if (pressed) {
            gdjs.evtTools.runtimeScene.replaceScene(
                runtimeScene,
                sceneName,
                false
            );
        }
    };
};
I had to use more lines to fit it in this narrow box, still very nice!
Haxe Engines
I’m german. To me, this is a “Haxe”:

A Schweinshaxe to be precise. It’s a roasted pork knuckle.
Armory 3D Engine Haxe
That’s how it looks as code in Armory, the Blender game engine:
package arm;
import iron.Scene;
import iron.object.Object;
import armory.trait.internal.UniformsManager;
class TryAgainButton extends iron.Trait {
    var button:Object;
    public function new() {
        super();
		
        notifyOnInit(function() {
            var scene = Scene.active;
            button = scene.getChild("TryAgainButton");
        });
		
        notifyOnUpdate(function() {
            var mouse = iron.system.Input.getMouse();
            var mouseStarted = mouse.started();
            var buttonExists = button != null;
			
            if (buttonExists && mouseStarted) {
                var scene = Scene.active;
                    scene.reload();
            }
        });
    }
}
So what is Haxe, really? It’s a heavily underutilized scripting language that trans-compiles to many other languages. It kind of looks like a syntax makeover to C languages and that’s about what it has going for it.
For what it is, it’s really okay. If you love plain C style syntax, you’ll enjoy Schweinshaxe. But .. practically impossible to find Haxe in use anywhere. Armory itself is a pretty fringe engine and that’s among the most popular public use-cases for Haxe.
Rust ‘n Python Engines
There’s just two of these.
Bevy Rust
Bevy is an Entity Component System engine through and through. They’re also working on an editor for it. So it might become quite the popular choice.
If it weren’t for …
use bevy::prelude::*;
#[derive(Component)]
struct TryAgainButton;
fn setup_button(mut commands: Commands) {
    commands.spawn((
        ButtonBundle {
            style: Style {
                width: Val::Px(150.0),
                height: Val::Px(65.0),
                justify_content: JustifyContent::Center,
                align_items: AlignItems::Center,
                ..default()
            },
            ..default()
        },
        TryAgainButton,
    ));
}
fn button_system(
    interaction_query: Query<&Interaction, (Changed<Interaction>, With<TryAgainButton>)>,
    mut app_exit_events: EventWriter<AppExit>,
) {
    for interaction in &interaction_query {
        if *interaction == Interaction::Pressed {
            app_exit_events.send(AppExit);
        }
    }
}
fn plugin(app: &mut App) {
    app.add_systems(Startup, setup_button)
        .add_systems(Update, button_system);
}
Ugh. Rust in peace. Rust is so distinctively ugly, I’m astounded that Bevy has the second-most GitHub stars of all open source engines!
In fact, it amasses 40% of Godot’s GitHub stars! 43,000 stars, can you believe that? Godot has 106,000 stars. Weird.
Frightening, too. I worry Rust developers are all worshipping Syntax-Satan.
Panda3D Python
One of the first, complex game engines to be released to the public in 2002. Panda is now mainly used for education because – you guessed it – Python.
from direct.showbase.DirectObject import DirectObject
from direct.gui.DirectButton import DirectButton
from panda3d.core import *
class TryAgainButton(DirectObject):
    def __init__(self):
        super().__init__()
        self.button = DirectButton(
            text="Try Again",
            command=self.on_try_again_clicked
        )
    def destroy(self):
        self.button.destroy()
        self.ignoreAll()
    
    def on_try_again_clicked(self):
        base.restart()
Okay, minus the __init__() crap. But hey, it’s Python, so. Must not complain. Don’t anger the data wiznerds!
Proprietary Lang. Engines
These engines use proprietary, use-only-here scripting languages.
GameMaker GML
GML stands for GameMaker Language and is a special brew from one of the earliest, and definitely most successful game creation tools.
// Create Event
button_x = room_width / 2;
button_y = room_height / 2;
button_width = 200;
button_height = 50;
// Step Event
if (mouse_check_button_pressed(mb_left)) {
    if (point_in_rectangle(mouse_x, mouse_y,
        button_x - button_width/2, button_y - button_height/2,
        button_x + button_width/2, button_y + button_height/2)) 
    {
        room_restart();
    }
}
Given that it has no UI system .. nice job!
Godot GDScript
GDScript stands for Godot Script. It is NOT Python. It just LOOKS like Python on first glance, but totally isn’t.
Which also makes me wonder .. Python is so popular in education, but Godot is also popular in education. Probably because GDScript LOOKS like Python. I can’t imagine the student’s frustration when everything they’ve learned so far now is awkwardly different.
extends Control
@onready var button = $TryAgainButton
func _ready():
    button.pressed.connect(_on_try_again_pressed)
func _on_try_again_pressed():
    get_tree().reload_current_scene()
But hey … this is ALMOST NOTHING!
Yes, no, I’m totally not surprised why they use it for game education. It’s borderline elegant. If it _were_not_for_snake_case.
C++ Engines
Now we’re going hardcore!
Unreal Engine C++
We need to split this in two. You know, header and implementation. Header first.
// TryAgainWidget.h
#pragma once
#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "Components/Button.h"
#include "TryAgainWidget.generated.h"
UCLASS()
class YOURGAME_API UTryAgainWidget : public UUserWidget
{
    GENERATED_BODY()
protected:
    virtual void NativeConstruct() override;
    UPROPERTY(meta = (BindWidget))
    UButton* button;
    UFUNCTION()
    void OnTryAgainClicked();
};
And here’s the IMPL:
// TryAgainWidget.cpp
#include "TryAgainWidget.h"
#include "Kismet/GameplayStatics.h"
void UTryAgainWidget::NativeConstruct()
{
    Super::NativeConstruct();
    button->OnClicked.AddDynamic(this,
        &UTryAgainWidget::OnTryAgainClicked);
}
void UTryAgainWidget::OnTryAgainClicked()
{
    UGameplayStatics::OpenLevel(this, 
        FName(*UGameplayStatics::GetCurrentLevelName(this)));
}
Unreal is actually quite nice given that it’s C++. Of course, it’s still more boilerplate code overall in C++.
Open3D Engine C++
O3DE is also C++ in nature. Here’s the Header:
// TryAgainButton.h
#pragma once
#include <AzCore/Component/Component.h>
#include <AzCore/Component/TickBus.h>
#include <LyShine/Bus/UiButtonBus.h>
#include <LyShine/Bus/UiCanvasBus.h>
namespace MyGame
{
    class TryAgainButton
        : public AZ::Component
        , public UiButtonNotificationBus::Handler
    {
    public:
        AZ_COMPONENT(TryAgainButton, "{YOUR-GUID-HERE}");
        static void Reflect(AZ::ReflectContext* context);
    
    protected:
        void Activate() override;
        void Deactivate() override;
        void OnButtonClick() override;
    
    private:
        AZ::EntityId m_buttonEntityId;
    };
}
And then comes the implementation:
// TryAgainButton.cpp
#include "TryAgainButton.h"
#include <AzCore/Serialization/SerializeContext.h>
#include <AzFramework/Entity/GameEntityContextBus.h>
namespace MyGame
{
    void TryAgainButton::Reflect(AZ::ReflectContext* context)
    {
        if (auto serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
        {
            serializeContext->Class<TryAgainButton, AZ::Component>()->Version(1);
        }
    }
    void TryAgainButton::Activate()
    {
        m_buttonEntityId = GetEntityId();
        UiButtonNotificationBus::Handler::BusConnect(m_buttonEntityId);
    }
    void TryAgainButton::Deactivate()
    {
        UiButtonNotificationBus::Handler::BusDisconnect();
    }
    void TryAgainButton::OnButtonClick()
    {
        AzFramework::GameEntityContextRequestBus::Broadcast(
           &AzFramework::GameEntityContextRequestBus::Events::ReloadCurrentLevel);
    }
}
Certainly worse than Unreal’s C++ although less heavy on macros.
Summary
That’s all I tried. I’m quite shocked HOW MUCH more simple GDScript turns out to be. It’s not something you notice much, and perhaps it’s easy to dismiss because it’s such a fringe outlier domain specific language.
But I do understand know why it’s so popular, especially among starters. Though this also worries me a bit since Godot’s stringent OOP nature is likely going to teach newcomers that “deep OOP hierarchies are okay”. Which they aren’t.
This might actually turn out to become Godot’s biggest boon for serious adoption. Why?
I digress … but this is important!
Every other pro-grade game engine out there uses a component system. It has proven itself for three decades now – Unreal Engine got started in 1995!
The component-based approach has pushed every OOP engine out of existence – most of them custom in-house solutions, and they were the first to get abandoned due to how costly they were to update and maintain.
My expectation is that Godot will sooner or later adopt a component-based system, probably as an option at first but ultimately migrating towards it. If they do it in time, they might just make the pivot (or fork) in time.
But if they wait for 10 years, they’re going to be locked into the small-to-medium complexity games area forever, and primarily be a stepping stone towards “real” game engines for students.
We need more Scr1pt¡ng!
In the language department, Godot (currently) rules the pack! Other engines should reconsider for a second.
Unreal cut UnrealScript well over 10 years ago around 2012. Why? It became too powerful. That was a burden on maintenance. The solution, since it moved ever closer to C++, was to just kill it.
Unity first removed the ill-conceived Boo, also a Python look-alike. That was in 2014. Three years later, in 2017 they ended support for UnityScript. Guess what? It looked like Javascript so much so that everyone called it Javascript.
All of them had (and CryEngine/O3DE’s Lua still have) one thing in common: they exposed the engine’s API nearly 1:1 – just in a syntactically different text file. That’s not working in favor of scripting!
Lesson learned for Godot? I guess not. They keep adding more and more features to the language. It grows ever closer towards C# in complexity. It is much better integrated though, but it’s STILL going to be a maintenance burden for them moving forward!
Right now, though, it works wonders for Godot! No thanks to Unreal and Unity REMOVING their entry-level scripting languages all the while ADDING tons more complexity in the decade since. So what once were approachable tools have just about doubled in complexity.
Oh no. Don’t! Visual Scripting is dead! Blueprints only work because they have a ton of high-level functionality built-in. Developers however cringe at the refactoring, readability, and source control issues. Especially given that every asset in Unreal is a binary file. No wonder AAA studios are crunching this much!
Though I’m afraid Unreal is applying good lessons when it comes to introducing newcomers to their Ecosystem. With their Fortnite for Unreal Creators. Or something to that end. On the other hand, they’re doing it again: Verse. Another DSL. Ugh.
Panda3D has proven for decades that Python works well as a learning tool, but remains a fringe usecase in game engines.
Roblox has proven that Lua works well with young talents. Games for three decades now have proven it, again and again.
So, please, let’s revisit Lua as a standard in game engines!
But do it right this time. Not a 1:1 API, but a clean, simple, entry-level language that acts as the glue much like Blueprints do!


Leave a Reply