26
Introduction to quill, a minimal unity ui framework with lua modding support | devlog_0
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'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;
...
* 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
}
}
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
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)
...
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 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
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.
26