Post Details Page

Lua UI Scripting: A Beginner's Guide

Translate

This guide introduces the basics of Lua UI scripts and builds several small demos.


Version 7.1 added support for creating and transforming controls at runtime. Lua UI scripts are useful for both 2D and 3D creations. They have also led to a lot of AI-generated content, which may not be a good thing. Understanding how they work can help when directing agents to create stages.


Lua UI has limitations. For example, there is no image upload function, so custom artwork may require many primitive controls. Rendering thousands of controls can cause performance problems; optimization can help but may not eliminate them.


We will refer to the official API documentation throughout this guide. Some screenshots show the Chinese interface.


You can also check out the Japanese version.


Editors:


  • For people: VS Code.
  • For agents: Codex, Claude Code, and similar tools.


0. Guide Structure


This introductory guide covers:


  • Hello World: the first Lua UI script and how scripts run.
  • Common controls and their operations.
  • Keyboard and mouse events.
  • Tween animations.
  • Communication with the server through signals.


1. Hello World


1.1 The First Script


First, prepare the tools:


  1. Install VS Code.
  2. Install the extensions:
  • Lua language support.
  • MiliLua API Definitions: a community extension that scans for stages and opens them as projects.
  • Genshin UGC Lua API: a community extension with additional type checking and code completion.

Open the editor and create a new stage. Open Manage UI Control Groups → UI Control Group Library → Client Control Templates. Add a client Text Box control, save it as a template, and note its index.

In VS Code, open MiliLua API Definitions. Wait or refresh until the stage appears, then open it. The project path is the script path.

Create helloworld.lua:


function OnStart()

local textbox = game.InstantiateClientUIControl(1073741846, script.object); -- Replace with the template index created earlier.

textbox.text = "Hello World";

end


In Miliastra Sandbox → Client Script Resource Explorer, create a script mapping and select the script. Return to Interface Layout, add a Client Control Container, and under Edit → Scripts, add helloworld.


Open the stage to run the script.


1.2 How Client Scripts Work

Client Control Container

Lua UI scripts run on the client, while ordinary server UI controls are controlled by the server. A Client Control Container provides a place for client UI and is the entry point for the client script. The example above needs such a container to display the client UI.


Lifecycle Events

Node graphs use events, such as entity creation and destruction, as entry points for logic. Lua UI scripts also use events, with a more complete script lifecycle:


Update events can be used to run logic every frame. To create a control when the script starts, look up OnStart() in the API documentation.

Templates and Instantiation

A Lua script creates text and images from existing Client Control Templates. A template stores default settings and styles but is not displayed directly. Instantiation creates a control from a template; the instance has its own properties.

Find UI-related functions under game in the API documentation:


The first UI function creates a control:

The function signature is:

game.InstantiateClientUIControl(controlPrefabIndex, parent)

controlPrefabIndex is an integer template index; parent is a ClientUIBaseControl. The documentation screenshot has a formatting error in the signature.

Every control needs a parent. The known parent in this example is the container to which the script is attached:

The script.object property provides that control:

The resulting statement is:


textbox = game.InstantiateClientUIControl(1073741846, script.object);

Object Properties

After creating the Text Box, look up its API:

Set the text property to change its contents:


textbox.text = "Hello World";


The first script is complete. Its logic starts from an event, and the API documentation supplies the available operations. The Lua API is large; this guide covers only the UI features used in its examples.


2. Controls

This chapter introduces basic control operations. The API documentation shows an inheritance hierarchy:

All concrete client controls inherit from ClientUIBaseControl. Inheritance gives them shared properties and methods, while each subclass can also have its own features. Learn both the shared operations and the features specific to each control.

2.1 Control Properties

The API provides many properties. Some are read-only and are commonly used to inspect a control's state. Extend the Hello World script to print a few values:


function OnStart()

local textbox = game.InstantiateClientUIControl(1073741846, script.object); -- Replace with the template index created earlier.

textbox.text = "Hello World";

print(textbox.alive) -- whether the control is alive

print(textbox.prefabIndex) -- template ID

print(textbox.id) -- control ID

print(textbox.active) -- active state

end


Run the stage and check the log. These properties will be useful later.


2.2 Control Transforms

Controls have position, size, scale, and other transform properties. Print the current values:


function OnStart()

local textbox = game.InstantiateClientUIControl(1073741846, script.object); -- Replace with the template index created earlier.

textbox.text = "Hello World";

print("x:"..textbox.anchoredPositionX.." y:"..textbox.anchoredPositionY)

print("size_x:"..textbox.sizeDeltaX.." size_y:"..textbox.sizeDeltaY)

print("Scale_x:"..textbox.localScaleX.." Scale_y:"..textbox.localScaleY)

end


Example output:


x:0.0 y:0.0

size_x:100.0 size_y:40.0

Scale_x:1.0 Scale_y:1.0


In this example, (0, 0) places the control at the center of its parent.

The Text Box only shows “Hello” because its default width is too small. Increase its width, double its height through scaling, and rotate it 180 degrees:


function OnStart()

local textbox = game.InstantiateClientUIControl(1073741846, script.object); -- Replace with the template index created earlier.

textbox.text = "Hello World";

textbox.sizeDeltaX = 200;

textbox.localScaleY = 2;

textbox.localRotationZ = 180;

end


Run the stage to see the result:


The API also provides methods for setting multiple transform components. Find Layout and Transformation under ClientUIBaseControl:

The methods take all components. Read the unchanged components from the control so the result matches the property assignments above:


function OnStart()

local textbox = game.InstantiateClientUIControl(1073741846, script.object); -- Replace with the template index created earlier.

textbox.text = "Hello World";

textbox:SetSizeDelta(200, textbox.sizeDeltaY);

textbox:SetLocalScale(textbox.localScaleX, 2, textbox.localScaleZ);

textbox:SetLocalRotation(textbox.localRotationX, textbox.localRotationY, 180);

end

The result is the same:

2.3 Visibility


There are several ways to hide a control, with different costs:

  • Move it outside the screen. Runtime cost is almost the same as when it is on screen, but showing it again is inexpensive.
  • Deactivate it. The control remains in the hierarchy but is inactive. Runtime cost is low, while reactivation has a medium to high cost.
  • Set it invisible. This is commonly used; runtime cost is moderate and making it visible again is inexpensive.

The last two correspond to these API methods:

The API screenshot may display the brackets incorrectly. Read these methods as SetActive(active) and SetVisible(visible); separate subsequent methods according to their meaning.


2.4 Hierarchy

Hierarchy has two aspects: parent-child relationships and draw order.


Parent and Child

Client controls must have a parent. Planning a container hierarchy helps organize complex projects and can help performance. A parent is specified when creating a control; afterward, the readable and writable parent property can change it.

Create a Client Control Template for a container. Then create two containers and a Text Box, print their child counts, and move the Text Box to the second container:


function OnStart()

local container1 = game.InstantiateClientUIControl(1073741851, script.object);

local container2 = game.InstantiateClientUIControl(1073741851, script.object);

local textbox = game.InstantiateClientUIControl(1073741846, container1); -- Replace with the template index created earlier.

textbox.text = "Hello World";

textbox.sizeDeltaX = 200;

print("container1 len:"..#container1:GetChildren());

print("container2 len:"..#container2:GetChildren());

textbox.parent = container2;

print("container1 len:"..#container1:GetChildren());

print("container2 len:"..#container2:GetChildren());

end


Log output:


container1 len:1

container2 len:0

container1 len:0

container2 len:1


The child counts change after reparenting.

Draw Order

Server UI has hierarchy-based and time-based rendering modes. To see how client controls are ordered, create an Image control template and note its index.

Give two images different colors using the imageColor field:

Its type is ColorValue, which can be created with the RGBA constructor:

Use SetImage(imageSource, imageId) to select an image. The image ID can be found in the UI Control Group Library. Look up imageSource in the API:

A static image uses Enum.ImageSource.StaticReference, for example SetImage(Enum.ImageSource.StaticReference, 10001). Offset the second image by (50, 50) and run this script:


function OnStart()

local pic1 = game.InstantiateClientUIControl(1073741853, script.object); -- Replace with the template index created earlier.

local pic2 = game.InstantiateClientUIControl(1073741853, script.object); -- Replace with the template index created earlier.

pic1:SetSizeDelta(200, 200);

pic2:SetSizeDelta(200, 200);

pic1:SetImage(Enum.ImageSource.StaticReference, 100001)

pic2:SetImage(Enum.ImageSource.StaticReference, 100001)

pic1.imageColor = Color(244, 0, 0)

pic2.imageColor = Color(0, 244, 0)

pic2:SetAnchoredPosition(50, 50)

end

Swap the creation order and test again:



Without other changes, the draw order follows creation order. The API also provides an index to control sibling order; a higher index draws above a lower one:


Keep the green image created before the red image, but change their order explicitly:


function OnStart()

local pic2 = game.InstantiateClientUIControl(1073741853, script.object); -- Replace with the template index created earlier.

local pic1 = game.InstantiateClientUIControl(1073741853, script.object); -- Replace with the template index created earlier.

pic1:SetSizeDelta(200, 200);

pic2:SetSizeDelta(200, 200);

pic1:SetImage(Enum.ImageSource.StaticReference, 100001)

pic2:SetImage(Enum.ImageSource.StaticReference, 100001)

pic1.imageColor = Color(244, 0, 0)

pic2.imageColor = Color(0, 244, 0)

pic2:SetAnchoredPosition(50, 50)

pic1:SetSiblingIndex(0)

pic2:SetSiblingIndex(1)

end

The green image is now on top.


2.5 Simple Motion

Using the previous example, move the red image horizontally and the green image vertically. The OnUpdate event can check boundaries and update positions each frame. Look up update usage in the API:


Enable updates explicitly at startup:


script:EnableUpdate(true);


Keep the control creation in OnStart() and put the motion in OnUpdate(). Move the local declarations for the images outside the functions so both functions can access them.


Sibling indices start at 0 and end at the parent's child count minus 1. SetSiblingIndex returns a boolean. Here, indices 0 and 1 place the red image below the green image:


local pic1 = nil

local pic2 = nil

local timer = 0 -- time accumulator


function OnStart()

script:EnableUpdate(true);

pic2 = game.InstantiateClientUIControl(1073741853, script.object); -- Replace with the template index created earlier.

pic1 = game.InstantiateClientUIControl(1073741853, script.object); -- Replace with the template index created earlier.

pic1:SetSizeDelta(200, 200);

pic2:SetSizeDelta(200, 200);

pic1:SetImage(Enum.ImageSource.StaticReference, 100001)

pic2:SetImage(Enum.ImageSource.StaticReference, 100001)

pic1.imageColor = Color(244, 0, 0)

pic2.imageColor = Color(0, 244, 0)

pic2:SetAnchoredPosition(50, 50)

pic1:SetSiblingIndex(0)

pic2:SetSiblingIndex(1)

end


function OnUpdate(dt)

timer = timer + dt * 2

local offset = math.sin(timer) * 200

pic1:SetAnchoredPosition(offset, 0)

pic2:SetAnchoredPosition(0, offset)

end


Observe the result:

2.6 Common Controls


2.6.1 Image

The earlier examples covered basic Image usage. Images also support soft edges in the X and Y directions and fill settings, controlled through properties and methods.


To enable soft edges, set both enableMask and enableSoftEdge to true. Use SetSoftEdgeWidth(widthX, widthY) to set the widths. Stop the movement from the previous example and add soft edges to the green image:


local pic1 = nil

local pic2 = nil


function OnStart()

script:EnableUpdate(true);

pic2 = game.InstantiateClientUIControl(1073741853, script.object); -- Replace with the template index created earlier.

pic1 = game.InstantiateClientUIControl(1073741853, script.object); -- Replace with the template index created earlier.

pic1:SetSizeDelta(200, 200);

pic2:SetSizeDelta(200, 200);

pic1:SetImage(Enum.ImageSource.StaticReference, 100001)

pic2:SetImage(Enum.ImageSource.StaticReference, 100001)

pic1.imageColor = Color(244, 0, 0)

pic2.imageColor = Color(0, 244, 0)

pic2:SetAnchoredPosition(50, 50)

pic2.enableMask = true

pic2.enableSoftEdge = true

pic2:SetSoftEdgeWidth(30, 30)

pic1:SetSiblingIndex(0)

pic2:SetSiblingIndex(1)

end


Run the stage:

The green image now has soft edges. To try an inverted mask, set pic2.reverseMaskArea = true:

2.6.2 Text Box


Using a Text Box is similar to configuring one in the interface, except that its properties are set by a script. This example changes its appearance:


function OnStart()

local textbox = game.InstantiateClientUIControl(1073741846, script.object); -- Replace with the template index created earlier.

textbox.text = "Hello World";

textbox:SetSizeDelta(200, 100)

textbox.fontSize = 24

textbox.fontColor = Color(255, 0, 255)

textbox.enableOutline = true

textbox.horizontalAlignment = Enum.TextHorizontalAlignment.Middle

end


3. Keyboard and Mouse Events


The previous examples display controls. This chapter adds clicks and other interactions. The examples use callback functions; if you are new to Lua or programming, learn about callbacks first.


3.1 Preset Button


A Preset Button provides button interaction and cursor event listeners. Configure its appearance for each state and attach the state controls as children. It is less flexible than a Cursor Event Area because the styles must be configured in advance, but it requires less scripting.

Configure the button before continuing. This guide does not cover the configuration steps.


For the demo, create a button and a Text Box on a centered panel. Each click increments the number shown in the Text Box. Look up the listener in the API:

The listener takes an event type and a callback function. Search the API for event types to find mouse, keyboard, controller, and other events. For this example, use a cursor click:

Define shared variables:


local pic = 1073741853

local button = 1073741866

local text = 1073741846

local count = 0

local textbox = nil


Define the callback. It runs when the event occurs:


local function OnButtonClick()

count = count + 1

textbox.text = tostring(count)

end


Create the controls and register the callback:


function OnStart()

local panel = game.InstantiateClientUIControl(pic, script.object)

panel:SetSizeDelta(400, 300)

panel:SetAnchoredPosition(0, 0)

panel.imageColor = Color(30, 30, 30, 220)

panel:SetImage(Enum.ImageSource.StaticReference, 100001)

textbox = game.InstantiateClientUIControl(text, panel)

textbox:SetSizeDelta(300, 80)

textbox:SetAnchoredPosition(0, 50)

textbox.fontSize = 48

textbox.fontColor = Color(255, 255, 255)

textbox.horizontalAlignment = Enum.TextHorizontalAlignment.Middle

textbox.verticalAlignment = Enum.TextVerticalAlignment.Middle

textbox.text = tostring(count)

local button = game.InstantiateClientUIControl(button, panel)

button:SetSizeDelta(180, 70)

button:SetAnchoredPosition(0, -60)

button:AddCursorEventListener(Enum.CursorEventType.CursorClick, OnButtonClick)

end


Test it:

3.2 Key Bindings


Key listeners do not directly respond to touch input, so adapt the interface for the target devices.


Preset Buttons inherit AddKeyEventListener from the base control and can listen for keys as well as cursor events. Images and other controls also inherit key listeners. An Image does not provide its own cursor event listener; use a Cursor Event Area for cursor input, as shown below.


Return to the previous script and remove the Preset Button creation code. Create an image-based button instead:


local customButton = game.InstantiateClientUIControl(pic, panel)

customButton:SetSizeDelta(180, 70)

customButton:SetAnchoredPosition(0, -60)

customButton:SetImage(Enum.ImageSource.StaticReference, 100001)

customButton.imageColor = Color(60, 120, 210)


Add a label:


local btnLabel = game.InstantiateClientUIControl(text, customButton)

btnLabel:SetSizeDelta(180, 70)

btnLabel:SetAnchoredPosition(0, 0)

btnLabel.text = "key [1]"

btnLabel.horizontalAlignment = Enum.TextHorizontalAlignment.Middle

btnLabel.verticalAlignment = Enum.TextVerticalAlignment.Middle


A key event callback must return a boolean. Define a callback outside OnStart() that reuses the existing click logic:


local function OnKeyPress()

OnButtonClick()

return true

end


true is an example return value. Check the current documentation and test how true and false affect event handling and propagation.


In OnStart(), register a key listener on customButton:


customButton:AddKeyEventListener(Enum.KeyEventType.KeyboardCraftspersonKey1Down, OnKeyPress)


Complete code:


local pic = 1073741853

local text = 1073741846

local count = 0

local textbox = nil


local function OnButtonClick()

count = count + 1

textbox.text = tostring(count)

end


local function OnKeyPress()

OnButtonClick()

return true

end


function OnStart()

local panel = game.InstantiateClientUIControl(pic, script.object)

panel:SetSizeDelta(400, 300)

panel:SetAnchoredPosition(0, 0)

panel.imageColor = Color(30, 30, 30, 220)

panel:SetImage(Enum.ImageSource.StaticReference, 100001)

textbox = game.InstantiateClientUIControl(text, panel)

textbox:SetSizeDelta(300, 80)

textbox:SetAnchoredPosition(0, 50)

textbox.fontSize = 48

textbox.fontColor = Color(255, 255, 255)

textbox.horizontalAlignment = Enum.TextHorizontalAlignment.Middle

textbox.verticalAlignment = Enum.TextVerticalAlignment.Middle

textbox.text = tostring(count)

local customButton = game.InstantiateClientUIControl(pic, panel)

customButton:SetSizeDelta(180, 70)

customButton:SetAnchoredPosition(0, -60)

customButton:SetImage(Enum.ImageSource.StaticReference, 100001)

customButton.imageColor = Color(60, 120, 210)

local btnLabel = game.InstantiateClientUIControl(text, customButton)

btnLabel:SetSizeDelta(180, 70)

btnLabel:SetAnchoredPosition(0, 0)

btnLabel.text = "key [1]"

btnLabel.horizontalAlignment = Enum.TextHorizontalAlignment.Middle

btnLabel.verticalAlignment = Enum.TextVerticalAlignment.Middle

customButton:AddKeyEventListener(Enum.KeyEventType.KeyboardCraftspersonKey1Down, OnKeyPress)

end


Run the example to test the key binding.


3.3 Listening for Keys on the Root Control


This example registers a listener on script.object, the control to which the script is attached. Here it is the root node of the Client Control Container.


This remains a control listener; it is not guaranteed to receive every key event. Rendering order, how other callbacks handle the event, and control state can affect the response. Test it in your project.


Add a listener for key 2 to the script container:


script.object:AddKeyEventListener(Enum.KeyEventType.KeyboardCraftspersonKey2Down, OnKeyPress)

Run the example to test it.

3.4 Cursor Event Area

Ordinary controls cannot all listen for cursor events directly. A Cursor Event Area lets a custom image, text, or screen region respond to clicks, hover, and dragging. It acts as a transparent hit-test area. Instantiate one as a child of the custom button to capture cursor events.


Before continuing, open UI Control Group Library → Client Control Templates, add a Cursor Event Area control, save it as a template, and note its index.


Create the area and register a click listener:


local clickArea = game.InstantiateClientUIControl(cursorArea, customButton)

clickArea:SetSizeDelta(180, 70)

clickArea:SetAnchoredPosition(0, 0)

clickArea:AddCursorEventListener(Enum.CursorEventType.CursorClick, OnButtonClick)


Complete code:


local pic = 1073741853 -- Image template

local text = 1073741846 -- Text Box template

local cursorArea = 1073741884 -- Cursor Event Area template

local count = 0

local textbox = nil

local customButton = nil


local function OnButtonClick()

count = count + 1

textbox.text = tostring(count)

end


function OnStart()

local panel = game.InstantiateClientUIControl(pic, script.object)

panel:SetSizeDelta(400, 300)

panel:SetAnchoredPosition(0, 0)

panel.imageColor = Color(30, 30, 30, 220)

panel:SetImage(Enum.ImageSource.StaticReference, 100001)

textbox = game.InstantiateClientUIControl(text, panel)

textbox:SetSizeDelta(300, 80)

textbox:SetAnchoredPosition(0, 50)

textbox.fontSize = 48

textbox.fontColor = Color(255, 255, 255)

textbox.horizontalAlignment = Enum.TextHorizontalAlignment.Middle

textbox.verticalAlignment = Enum.TextVerticalAlignment.Middle

textbox.text = tostring(count)

customButton = game.InstantiateClientUIControl(pic, panel)

customButton:SetSizeDelta(180, 70)

customButton:SetAnchoredPosition(0, -60)

customButton:SetImage(Enum.ImageSource.StaticReference, 100001)

customButton.imageColor = Color(60, 120, 210)

local btnLabel = game.InstantiateClientUIControl(text, customButton)

btnLabel:SetSizeDelta(180, 70)

btnLabel:SetAnchoredPosition(0, 0)

btnLabel.text = "Click / [1]"

btnLabel.fontSize = 26

btnLabel.fontColor = Color(255, 255, 255)

btnLabel.horizontalAlignment = Enum.TextHorizontalAlignment.Middle

btnLabel.verticalAlignment = Enum.TextVerticalAlignment.Middle

local clickArea = game.InstantiateClientUIControl(cursorArea, customButton)

clickArea:SetSizeDelta(180, 70)

clickArea:SetAnchoredPosition(0, 0)

clickArea:AddCursorEventListener(Enum.CursorEventType.CursorClick, OnButtonClick)

end


Test whether clicking the button increments the number.


3.5 Example: Dragging an Image


A Cursor Event Area can also detect dragging, for example when moving inventory cards, map markers, or floating windows.

Drag Event


Enum.CursorEventType.CursorDrag is triggered as the cursor continues to move. The callback receives eventData, a CursorEventData object. Its GetUIPosDelta() method returns the UI screen displacement for the current movement:


local dx, dy = eventData:GetUIPosDelta()


Add the displacement to the image position so it follows the mouse:


local function OnDrag(eventData)

local dx, dy = eventData:GetUIPosDelta()

local curX, curY = dragBox:GetAnchoredPosition()

dragBox:SetAnchoredPosition(curX + dx, curY + dy)

end

Complete Code

Combine the draggable image, hint text, and Cursor Event Area:


local pic = 1073741853 -- Image template

local text = 1073741846 -- Text Box template

local cursorArea = 1073741884 -- Cursor Event Area template

local dragBox = nil


local function OnDrag(eventData)

local dx, dy = eventData:GetUIPosDelta()

local curX, curY = dragBox:GetAnchoredPosition()

dragBox:SetAnchoredPosition(curX + dx, curY + dy)

end


function OnStart()

-- Create the draggable image

dragBox = game.InstantiateClientUIControl(pic, script.object)

dragBox:SetSizeDelta(120, 120)

dragBox:SetAnchoredPosition(0, 0)

dragBox:SetImage(Enum.ImageSource.StaticReference, 100001)

dragBox.imageColor = Color(60, 120, 210)

-- Attach a Cursor Event Area

local dragArea = game.InstantiateClientUIControl(cursorArea, dragBox)

dragArea:SetSizeDelta(120, 120)

dragArea:SetAnchoredPosition(0, 0)

-- Register the drag listener

dragArea:AddCursorEventListener(Enum.CursorEventType.CursorDrag, OnDrag)

end


Hold the left mouse button over the square and drag it. Observe whether it changes color and follows the mouse.


This example is simplified. A real project may focus the item and align it with the cursor when dragging starts, then adjust its position when it is dropped.

4. Tween Animations

Animations make a UI more expressive, but implementing their state changes manually can be cumbersome. Miliastra Wonderland includes a Tween system for smoothly changing control properties such as position, size, scale, and opacity over time.

4.1 Basic Tween: game.Tween

Look up Tween creation in the API:

game.Tween creates and returns a Tween object. It takes three arguments:

  • object: the target control.
  • tweenDataTable: a table mapping property names to target values.
  • duration: the animation duration.


Create an image and play a Tween:


local pic = 1073741853 -- Image template


function OnStart()

local pic = game.InstantiateClientUIControl(pic, script.object)

pic:SetSizeDelta(120, 120)

pic:SetImage(Enum.ImageSource.StaticReference, 100001)

pic.imageColor = Color(60, 120, 210)

local test_tween = game.Tween(pic, {localScaleX = 2}, 3)

test_tween:Play()

end


This changes localScaleX to 2 over three seconds. Run it to see the image grow:

Not every property supports interpolation. Only fields marked Tweenable can be used with Tween; the English API documentation may not mark every such field. Common examples include:


Tweenable fields:localScaleX, localScaleY

Control:All controls

Effect:Scale, such as a button response or breathing effect


Tweenable fields:anchoredPositionX, anchoredPositionY

Control:All controls

Effect:Movement, such as a sliding or floating panel


Tweenable fields:sizeDeltaX, sizeDeltaY

Control:All controls

Effect:Smooth size changes


Tweenable fields:imageColor

Control:Image

Effect:Color transitions and fades, including alpha


Tweenable fields:fillAmount

Control:Image

Effect:Progress fills, such as health bars or cooldowns


Tweenable fields:fontSize, fontColor

Control:Text

Effect:Font size and color changes


4.2 Easing: EaseType

A constant speed is not always suitable. Tween supports different easing types through Enum.EaseType, including:


  • Enum.EaseType.Linear: constant speed.
  • Enum.EaseType.OutBack: overshoots and returns.
  • Enum.EaseType.OutBounce: bounces at the end.
  • Enum.EaseType.OutQuad and Enum.EaseType.OutCubic: decelerate smoothly.


Use SetEase to select an easing type. This example binds keys to reset the control and compare effects:


local pic = 1073741853 -- Image template

local box = nil

local currentTween = nil


local function resetui()

if currentTween then

currentTween:Kill(false)

currentTween = nil

end

box.localScaleX = 1

end


function OnStart()

box = game.InstantiateClientUIControl(pic, script.object)

box:SetSizeDelta(120, 120)

box:SetImage(Enum.ImageSource.StaticReference, 100001)

box.imageColor = Color(60, 120, 210)

resetui()

script.object:AddKeyEventListener(Enum.KeyEventType.KeyboardCraftspersonKey1Down, function()

resetui()

currentTween = game.Tween(box, { localScaleX = 2 }, 1):SetEase(Enum.EaseType.Linear)

currentTween:Play()

return true

end)

script.object:AddKeyEventListener(Enum.KeyEventType.KeyboardCraftspersonKey2Down, function()

resetui()

currentTween = game.Tween(box, { localScaleX = 2 }, 1):SetEase(Enum.EaseType.OutBack)

currentTween:Play()

return true

end)

script.object:AddKeyEventListener(Enum.KeyEventType.KeyboardCraftspersonKey3Down, function()

resetui()

currentTween = game.Tween(box, { localScaleX = 2 }, 1):SetEase(Enum.EaseType.OutBounce)

currentTween:Play()

return true

end)

script.object:AddKeyEventListener(Enum.KeyEventType.KeyboardCraftspersonKey4Down, function()

resetui()

currentTween = game.Tween(box, { localScaleX = 2 }, 1.2):SetEase(Enum.EaseType.OutElastic)

currentTween:Play()

return true

end)

end

Press 1, 2, 3, and 4 to compare the animations. Other easing types work in the same way.


4.3 Animation Loops and Control

A Tween can play once or loop. The API lists methods for controlling it:


  • Play(): start playback.
  • Pause(): pause playback.
  • Restart(): return to the beginning and play again.
  • SetLoops(times): set the loop count; -1 loops indefinitely.
  • SetOnComplete(callback): run a callback when the animation completes.
  • Kill(false): destroy the animation and release its resources; this is recommended when closing the interface or in OnDestroy().


Use SetLoops(-1) to repeat an animation. In the basic example, add a callback that prints complete:


local pic = 1073741853

local test_tween = nil


function OnStart()

local pic = game.InstantiateClientUIControl(pic, script.object)

pic:SetSizeDelta(120, 120)

pic:SetImage(Enum.ImageSource.StaticReference, 100001)

pic.imageColor = Color(60, 120, 210)

test_tween = game.Tween(pic, {localScaleX = 2}, 3)

test_tween:SetLoops(-1)

test_tween:SetOnStepComplete(function()

print("complete")

end)

test_tween:Play()

end


4.4 Tween Sequences and Delays


A Tween sequence plays multiple effects in order. Look up the sequence API:


Create an empty TweenSequence:


local seq = game.TweenSequence()


A sequence acts like a timeline: append Tweens, intervals, and callbacks to create a combined animation.

Common Sequence Methods

Method:seq:Append(tween)

Description:Add an animation after the previous step.


Method:seq:AppendInterval(seconds)

Description:Add a delay in seconds.


Method:seq:AppendCallback(function)

Description:Add a callback at the current end of the timeline.


Method:seq:Join(tween)

Description:Play an animation alongside the previous step.


Method:seq:SetLoops(times)

Description:Set the sequence loop count; -1 loops indefinitely.


Method:seq:Play()

Description:Start playback.


Method:seq:Pause() / seq:Resume()

Description:Pause or resume playback.


Method:seq:Kill(false)

Description:Destroy the sequence and release its resources.


Smooth Breathing Effect


In the previous example, a single Tween that grows to twice its size jumps back to its original size at the start of each loop. A sequence can alternate between growing and shrinking:


local pic = 1073741853 -- Image template

local box = nil

local seq = nil


function OnStart()

box = game.InstantiateClientUIControl(pic, script.object)

box:SetSizeDelta(120, 120)

box:SetImage(Enum.ImageSource.StaticReference, 100001)

box.imageColor = Color(60, 120, 210)


-- 1. Create a Tween sequence

seq = game.TweenSequence()


-- 2. Scale to 1.8 over 1.5 seconds

seq:Append(game.Tween(box, { localScaleX = 1.8, localScaleY = 1.8 }, 1.5):SetEase(Enum.EaseType.InOutSine))


-- 3. Wait for 0.2 seconds

seq:AppendInterval(0.2)


-- 4. Scale back to 1.0 over 1.5 seconds

seq:Append(game.Tween(box, { localScaleX = 1, localScaleY = 1 }, 1.5):SetEase(Enum.EaseType.InOutSine))


-- 5. Loop the sequence indefinitely and play it

seq:SetLoops(-1)

seq:Play()

end


5. Signals


Lua UI scripts run locally and are separate from server logic. Signals provide communication between them. This chapter covers receiving and sending signals.


5.1 Receiving Signals


Miliastra Wonderland uses an authoritative server architecture. Player data such as currency, level, health, and the stage timer is managed by the server; client UI does not own that authoritative data.

The server can send signals to client scripts. A Lua script registers a listener to receive and handle them.

Main API

The client script instance script provides two methods:


1.Register a listener with script:RegisterServerSignalHandler(signalName, callback).

  • signalName is the name configured in Signal Manager.
  • callback receives signalName and signalParams, the array of parameters sent by the server.


2. Remove a listener with script:UnregisterServerSignalHandler(signalName) when it is no longer needed or the control is destroyed, to avoid duplicate handling.


For this example, make a server-side number increase every two seconds and send it to the client script. The script displays the received number.


Create a server node graph and attach it to the stage. Add a signal sync with a num integer parameter:


Add a repeating timer and send the count to the client. The screenshot uses When Entity Is Created, Start Timer, Assembly List, When Timer Is Triggered, and Send Client Scripted Signal. The player list and Get Corresponding Value From List supply the target:


In the script, create a centered Text Box and use RegisterServerSignalHandler to listen for "sync". When the server broadcast arrives, display signalParams[1]:


local pic = 1073741853 -- Image template

local text = 1073741846 -- Text Box template


local countLabel = nil


-- Callback for the server sync signal

local function OnReceiveSync(signalName, signalParams)

-- Read the first parameter sent by the server

local num = signalParams[1] or 0

countLabel.text = tostring(num)

end


function OnStart()

-- 1. Create a centered background panel

local panel = game.InstantiateClientUIControl(pic, script.object)

panel:SetSizeDelta(200, 100)

panel:SetAnchoredPosition(0, 0)

panel:SetImage(Enum.ImageSource.StaticReference, 100001)

panel.imageColor = Color(30, 30, 30, 220)


-- 2. Create a centered Text Box for the number

countLabel = game.InstantiateClientUIControl(text, panel)

countLabel:SetSizeDelta(300, 100)

countLabel.fontSize = 32

countLabel.horizontalAlignment = Enum.TextHorizontalAlignment.Middle

countLabel.verticalAlignment = Enum.TextVerticalAlignment.Middle

countLabel.text = "Waiting for signal..."


-- 3. Listen for the server signal named "sync"

script:RegisterServerSignalHandler("sync", OnReceiveSync)

end


Run the stage. The number should increase as the server timer sends an update every two seconds:


5.2 Sending Signals


Client-side data can be lost on disconnect or forced exit. Important data and save data can be sent to the server through signals. UI interactions may also need to be sent to the server in projects combining 2D and 3D gameplay.


A server node graph can receive client signals with the Monitor Signal event node. On the client, create a signal object and send it through the Lua API:

  1. Create a signal with local signal = game.ServerSignal(signalName).
  2. Add parameters in the order agreed with the server:

Method:signal:AddInt(intValue)

Description:Add an integer, such as an item ID or quantity.


Method:signal:AddFloat(floatValue)

Description:Add a floating-point number.


Method:signal:AddString(stringValue)

Description:Add a string, such as an action or note.


Method:signal:AddBool(boolValue)

Description:Add a boolean.


Method:signal:AddVector3(vector3Table)

Description:Add a 3D vector as a table such as {x=0, y=0, z=0}.


3.Send it with signal:SendSignal().


Extend the previous example so clicks add to the score using the same signal. The client does not send a meaningful value; the server adds one to the score when it receives the signal.


Modify the server node graph. Create a node graph variable to record the accumulated client score. The screenshot uses Monitor Signal, Get Node Graph Variable, Addition, and Set Node Graph Variable:

On the client, attach a Cursor Event Area to the scoreboard panel and listen for CursorDown. Send a signal when the panel is pressed:


local pic = 1073741853 -- Image template

local text = 1073741846 -- Text Box template

local cursorArea = 1073741884 -- Cursor Event Area template


local countLabel = nil


-- 1. Update the display when the server sync signal arrives

local function OnReceiveSync(signalName, signalParams)

local num = signalParams[1] or 0

countLabel.text = tostring(num)

end


-- 2. Send a scoring signal when the scoreboard is pressed

local function OnPanelDown()

local signal = game.ServerSignal("sync")

signal:AddInt(1)

signal:SendSignal()

end


function OnStart()

-- 1. Create a centered scoreboard panel

local panel = game.InstantiateClientUIControl(pic, script.object)

panel:SetSizeDelta(300, 100)

panel:SetAnchoredPosition(0, 0)

panel:SetImage(Enum.ImageSource.StaticReference, 100001)

panel.imageColor = Color(30, 30, 30, 220)


-- 2. Create a centered Text Box for the number

countLabel = game.InstantiateClientUIControl(text, panel)

countLabel:SetSizeDelta(300, 100)

countLabel:SetAnchoredPosition(0, 0)

countLabel.fontSize = 48

countLabel.fontColor = Color(255, 255, 255)

countLabel.horizontalAlignment = Enum.TextHorizontalAlignment.Middle

countLabel.verticalAlignment = Enum.TextVerticalAlignment.Middle

countLabel.text = "Waiting for signal..."


-- 3. Attach a Cursor Event Area and listen for presses

local clickArea = game.InstantiateClientUIControl(cursorArea, panel)

clickArea:SetSizeDelta(300, 100)

clickArea:SetAnchoredPosition(0, 0)

clickArea:AddCursorEventListener(Enum.CursorEventType.CursorDown, OnPanelDown)


-- 4. Register the server sync signal listener

script:RegisterServerSignalHandler("sync", OnReceiveSync)

end


Test it:

After a click, the next server update includes the added click count. This completes the server-to-client and client-to-server communication example.



6. Summary


This guide covered the basics through several small examples and showed how to find Lua API operations in the official documentation. Once you know the overall structure, use the documentation to look up details as needed.

Genshin Impact • Discussions & Sharing
Repost allowed
Miliastra Wonderland
4
0
0
4
0
All comments

Loading...

All Tools

Recommended Users