Introduction to quill, a minimal unity ui framework with lua modding support | devlog_0

Motivation

As a developer we craft interface elements and since so many visual element involved in the process, our engine would have a graphical view or tool for help you to handle shapes, color and layout. After that, when you add logical behavior to these elements you have to deal a reference hell.

In unity, visual ui objects can be created in canvas and can be modified via their component. Later you would save those as a prefab and reference them to another component. But at some point, things start to get complicated. Some prefabs may needed on the fly, so Resources would be handful.

But how other engines approach to graphical aspect of creation user interface?

  • Win Forms have a design editor built in to visual studio BUT it has a XAML option.
  • Similar to that, android studio has a design tool BUT it has a XML output.
  • XCode has a design tool BUT now they got SwiftUI.
  • And there is Flutter and React Native as declerative flow.

So these engines/frameworks has option to drive user interface programming from code.

Returning to unity, when you have a prefab, unity creates a YAML file, but you wouldn't want to mess with it.

Nevertheless, In theory, you would create any gameObject by creating and adding necessary components in runtime. But it would take some effort. At this point, I asked myself, why not create a wrapper api for that?

Recently, I played around with LÖVE2D framework and I love it! You wouldn't believe it how it's easy to setup and draw something to the screen.

Here is a snippet from LÖVE2D. Similar to OnGUI in unity, love.draw will execute block and draw graphic related stuff. It's that easy.
(Also I'm aware of the old IMGUI api, but unity left and moved on with UnityUI).

function love.draw()
    love.graphics.rectangle("fill", 20, 50, 60, 120 )

    love.graphics.setColor(0, 1, 0, 1)
    love.graphics.print("This is a pretty lame example.", 10, 200)
end

There are plenty of example after I researched:

LÖVE2D
LIKO-12
Pixel-Vision(MonoGame based, also uses MoonSharp)
TIC-80
PICO-8
Raylib
Defold
Some MonoGame libraries

So this was the motivation of my how I started to make Quill, a UnityUI wrapper api to create user interface in runtime with lua scripting option. Although its not a pro feature api, its more than a pleasure to spend time with it.

Let's take a look how its going so far.

Quill C# API

Quill's building blocks are just standart UnityUI components.

In Unity, When you create a ui object in a scene, if there is none, it'll create a Canvas and InputSystem object first, then your object goes as a child to Canvas.

So, when Quill initilaze, it'll create a Canvas and InputSystem.

private void Start()
{
    Quill.Init();
}

It would be possible to use any Canvas that exist in the scene or hand crafted prefabs could be nested in Quill's canvas, since it holds a reference for canvas

...
Quill.mainCanvas.sortingOrder = 100;
Quill.mainCanvas.renderMode   = RenderMode.WorldSpace;
...

Elements

* QuillElement (Empty)      //  Wraps RectTransform and provides base functionality
* QuillLabel                //  Derives from Text
* QuillBox                  //  Derives from Image
* QuillButton               //  Derives from Button
private void Start()
{
    Quill.Init();

    var box     = Quill.CreateBox(Color.red);
    Quill.mainRoot.Add(box);
    box.SetSize(200, 200);
    box.SetPosition(50, -50);

    var label   = Quill.CreateLabel("hello world");
    box.root.Add(label);
    label.SetPosition(50, -50);
}

All elements implement IQuillElements so base functionality of RectTransorm is available for each elements.

QuillButton button;

private void Start()
{
    Quill.Init();

    var box = Quill.CreateBox(Color.red);
    Quill.mainRoot.Add(box);
    box.SetSize(300, 300);

    button = Quill.CreateButton("hello button");
    button.box.color = Color.blue;
    box.root.Add(button);

    button.box.SetSize(200, 40);
    button.box.SetPosition(50, -50);
    button.onClick.AddListener(OnButtonClick);

    box.SetPosition(50, -50);
}

private void OnButtonClick()
{
    button.label.text = "this button clicked";
}

And finally, a broadcast option to handle events

//  ScoreManager
int score;
private void Update()
{
    score += 5;
    if (Input.GetKeyDown(KeyCode.Space))
    {
        var data = new MessageData("score");
        data.container.Add("playerName", "John");
        data.container.Add("playerScore", score);
        Quill.message.Post(data);
    }
}
//  ScoreView
QuillLabel scoreLabel;
int currentScore;

private void Start()
{
    scoreLabel = Quill.CreateLabel("score: " + currentScore);
    Quill.message.Register(MessageListener);
}

private void MessageListener(MessageData data)
{
    if (data.id == "score")
    {
        //  handle event
    }
}

Quill Lua API

Lua lang is widely used for as scripting and modding tool in game development. I wanted to explore lua ecosystem and idea of modding unity projects. Thanks to MoonSharp, running lua code is fairly easy.

Lua API is completely optional. After Quill initialization, QuillLua takes step in.

private void Start()
{
    Quill.Init();
    QuillLua.Run();
}

private void Update()
{
    QuillLua.OnUpdate();
}

private void OnDestroy()
{
    QuillLua.Exit();
}

And like Resources folder in Unity there is another special folder StreamingAssets. This folder will be available after built. QuillLua will read all lua files placed in StreamingAssets/LUA directory and these special functions will be called, accordingly.

function OnInit()

end

function OnUpdate(dt)

end

function OnMessage(data)

end

function OnExit()

end

OnMessage callback listens for events dispatched from c# api. Any data added to message data container will be coverted to a lua table

...
var data = new MessageData("on_player_score");
data.container.Add("score", 3);
data.container.Add("player", "cemuka");
Quill.message.Post(data);       //  this is opttional, if scope is lua
QuillLua.MessagePost(data);
...
function OnMessage(data)
    if data.id == "on_player_score" then
        quill.log(data.container.player)
        quill.log(data.container.score)
    end
end

Lua api is a wrapper too, it bridges around Quill and MoonSharp with little required tweaks. This way all components available for lua api.

Here is a complicated example, a button with a label. After click, it will execute clickHandler function. Its label will hide after 3 seconds.

local button        = nil
local timer         = 0
local timerStarted  = false

function cilckHandler()
    button.label.setText("button clicked")
    local buttonColor = button.box.getColor()
    buttonColor.a = 0.3

    button.box.setColor(buttonColor)

    timer = 3
    timerStarted = true
end

function OnInit()

    root    = quill.mainRoot()

    color = {}
    color.r = 0.4
    color.g = 0.8
    color.b = 0.3
    color.a = 1

    button = quill.button("this is a button")
    button.box.setColor(color)
    button.onClick.add(cilckHandler)

    root.addChild(button)

    button.setPosition(20,-200)

end

function OnUpdate(dt)

    if timerStarted then
        timer = timer - dt

        if timer < 0 then
            button.label.hide()

            timerStarted = false;
        end
    end

end

Fonts

After a quick search, I found that you can't load a font directly into unity. Happily you can load installed fonts in OS. That said, if there is no font found, it'll fallback to default font.

Please, let me know if you have further info on this topic.

...
root    = quill.mainRoot()
local firaCode = "Fira Code"
quill.loadFont(firaCode, 24);          

local label1 = quill.label("label1")    -- default font (Arial)
local label2 = quill.label("label2")    -- default font (Arial)
label2.setFont(firaCode)                -- Fira Code

quill.setDefaultFont(firaCode)          -- now default font is Fira Code


local label3 = quill.label("label3")    -- default font (Fira Code)

root.addChild(label1)
root.addChild(label2)
root.addChild(label3)

label1.setPosition(100, -100)
label2.setPosition(100, -150)
label3.setPosition(100, -200)
...

Images

Also another folder QuillLua will look up is StreamingAssets/IMAGE.

...
local box = quill.box()
box.setColor(color)
box.setSize(300, 100)
box.sprite("icons/body.png");
...

You can have full control on sprite creation. 9-Sliced supported.

...
options = {}
options.filterMode = "Point"
options.pivotX = 0.5
options.pivotY = 0.5
options.extrude = 0
options.pixelsPerUnit = 100
options.borderX = 3
options.borderY = 3
options.borderZ = 3
options.borderW = 3


local box = quill.box()
box.setColor(color)
box.setSize(300, 100)
box.sprite("body.png", options);
box.setImageType("Sliced");

root.addChild(box);
box.setPosition(100,-100)
...

Modules

Modules are supported.

-- lib/test.lua
test = {}

function test.greeter()
    quill.log("hello modules")
end

return test
-- main.lua
local module = require("lib/test")

function OnInit()
    module.greeter()
end

Closing words

I love creating and engaging user interfaces, mostly games and tools.

Quill is such an experimental and heavily work in progress project for me. While I'm learning and researching for other modding mature apis (i.e. wow), I'll try to complete other ui components. Besides, I'll start to documenting the api and will post lots of examples.

Thanks for reading, feel free to contribute or ask a question.

27