このガイドでは、Lua UIスクリプトの基本を学び、簡単なサンプルを作成します。
バージョン7.1では、実行時にモジュールを作成・変形できるようになりました。Lua UIスクリプトは2Dと3Dのどちらの制作にも役立ちます。また、Lua UIスクリプトによって多くのAI生成コンテンツも生まれました(必ずしもよいこととは限りません)。基本的な仕組みを理解すると、エージェントにステージ制作を指示するときにも役立ちます。
Lua UIには制限もあります。例えば、画像をアップロードできないため、独自の図柄を作るには多数の図形モジュールを組み合わせる必要があります。何千個ものモジュールを描画すると性能上の問題が起こることがあります。最適化で改善できますが、完全に解消できるとは限りません。
このガイドでは公式APIドキュメント(英語)を参照します。日本語の関連記事として、コミュニティのカーソル座標の取得方法も参照できます。一部の画像は英語または中国語の画面です。
英語版はこちらからご覧いただけます:英語版
使用するエディター:
- 人が使う場合:VS Code
- エージェントが使う場合:Codex、Claude Codeなど
0. ガイドの構成
この入門ガイドでは、次の内容を扱います。
- Hello World:最初のLua UIスクリプトとその動作の仕組み
- よく使うモジュールと基本操作
- キーボードとマウスのイベント
- Tweenアニメーション
- 信号によるサーバーとの通信
1. Hello World
1.1 最初のスクリプト
まず、必要なツールを準備します。
- VS Codeをインストールします。
- 次の拡張機能をインストールします。
- Lua言語サポート
- MiliLua API Definitions:ステージを検出し、プロジェクトとして開くためのコミュニティ製拡張機能
- Genshin UGC Lua API:型チェックやコード補完を提供するコミュニティ製拡張機能

エディターで新しいステージを作成します。UIコントロールグループ管理 → UIコントロールグループライブラリ → クライアントモジュールテンプレートを開き、テキストボックスを追加してテンプレートとして保存し、インデックスを控えます。

VS CodeでMiliLua API Definitionsを開きます。しばらく待つか更新してステージを表示し、それを開きます。プロジェクトのパスがスクリプトのパスになります。

helloworld.luaを作成します。
function OnStart()
local textbox = game.InstantiateClientUIControl(1073741846, script.object); -- 先ほど作成したテンプレートのインデックスに置き換える
textbox.text = "Hello World";
end
星々の箱庭 → クライアントスクリプトリソースマネージャーでスクリプトマッピングを作成し、作成したスクリプトを選択します。画面レイアウトに戻ってクライアントモジュールコンテナを追加し、編集画面のスクリプト設定からhelloworldを追加します。


ステージを開いてスクリプトを実行します。

1.2 クライアントスクリプトの仕組み
クライアントモジュールコンテナ
Lua UIスクリプトはクライアント側で動作し、通常のサーバーUIはサーバー側で制御されます。クライアントモジュールコンテナはクライアントUIを配置する場所であり、スクリプトの入口になります。上の例でも、このコンテナが必要です。
ライフサイクルイベント
ノードグラフでは、エンティティの作成や破棄などのイベントを処理の入口として使います。Lua UIスクリプトもイベントを使用しますが、スクリプトにはより細かいライフサイクルがあります。

更新イベントを使うと、フレームごとに処理を実行できます。スクリプトの開始時にモジュールを作成するには、APIドキュメントのOnStart()を確認します。
テンプレートとインスタンス化
Luaスクリプトでは、既存のクライアントモジュールテンプレートからテキストや画像を作成します。テンプレートには初期設定やスタイルが保存されていますが、それ自体は表示されません。インスタンス化によってテンプレートから実際のモジュールを作成します。作成されたインスタンスは独自のプロパティを持ちます。
APIドキュメントのgameからUI関連の関数を探します。

最初にあるUI関数がモジュールを作成する関数です。

関数の形式は次のとおりです。
game.InstantiateClientUIControl(controlPrefabIndex, parent)
controlPrefabIndexは整数のテンプレートインデックス、parentはClientUIBaseControlです。ドキュメントの画像では、関数の表記が崩れています。
各モジュールには親が必要です。この例では、スクリプトが取り付けられたコンテナを親にします。

script.objectでスクリプトが取り付けられたモジュールを取得できます。

作成する処理は次のようになります。
textbox = game.InstantiateClientUIControl(1073741846, script.object);
オブジェクトのプロパティ
テキストボックスを作成したら、そのAPIを確認します。

textプロパティを変更すると表示内容を変更できます。
textbox.text = "Hello World";
これで最初のスクリプトは完成です。処理はイベントを入口として実行され、利用できる操作はAPIドキュメントで確認できます。Lua APIは多数ありますが、このガイドではサンプルに必要なUI機能を扱います。
2. モジュール
この章では、基本的なモジュール操作を紹介します。APIドキュメントには継承関係が示されています。

各クライアントモジュールはClientUIBaseControlを継承します。継承により共通のプロパティやメソッドを使えます。また、それぞれの子クラスには固有の機能があります。共通操作と各モジュール固有の機能の両方を確認しましょう。
2.1 モジュールのプロパティ

APIには多くのプロパティがあります。その一部は読み取り専用で、モジュールの状態確認などに使います。Hello Worldのスクリプトに値を出力する処理を追加します。
function OnStart()
local textbox = game.InstantiateClientUIControl(1073741846, script.object); -- 先ほど作成したテンプレートのインデックスに置き換える
textbox.text = "Hello World";
print(textbox.alive) -- モジュールが有効なインスタンスか
print(textbox.prefabIndex) -- テンプレートID
print(textbox.id) -- モジュールID
print(textbox.active) -- アクティブ状態
end
ステージを実行してログを確認します。これらのプロパティは後で使用します。
2.2 モジュールの変形
モジュールには位置、サイズ、拡大率などのプロパティがあります。現在の値を出力します。
function OnStart()
local textbox = game.InstantiateClientUIControl(1073741846, script.object); -- 先ほど作成したテンプレートのインデックスに置き換える
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
出力例:
x:0.0 y:0.0
size_x:100.0 size_y:40.0
Scale_x:1.0 Scale_y:1.0
この例では、(0, 0)は親の中心にあたります。

テキストボックスの初期幅が足りないため、「Hello」しか表示されていません。幅を広げ、縦方向の拡大率を2倍にし、180度回転させます。
function OnStart()
local textbox = game.InstantiateClientUIControl(1073741846, script.object); -- 先ほど作成したテンプレートのインデックスに置き換える
textbox.text = "Hello World";
textbox.sizeDeltaX = 200;
textbox.localScaleY = 2;
textbox.localRotationZ = 180;
end
ステージを実行して結果を確認します。

APIには複数の変形値をまとめて設定するメソッドもあります。ClientUIBaseControlのレイアウトと変形に関する項目を確認します。

これらのメソッドには各軸の値を指定します。変更しない軸は現在の値を渡し、上のプロパティ設定と同じ結果にします。
function OnStart()
local textbox = game.InstantiateClientUIControl(1073741846, script.object); -- 先ほど作成したテンプレートのインデックスに置き換える
textbox.text = "Hello World";
textbox:SetSizeDelta(200, textbox.sizeDeltaY);
textbox:SetLocalScale(textbox.localScaleX, 2, textbox.localScaleZ);
textbox:SetLocalRotation(textbox.localRotationX, textbox.localRotationY, 180);
end
結果は同じです。

2.3 表示・非表示
モジュールを見えなくする方法はいくつかあり、それぞれ処理負荷が異なります。
- 画面外に移動する:実行時の負荷は画面内にある場合とほぼ同じですが、再表示の負荷は低くなります。
- 非アクティブにする:モジュールは階層内に残りますが、無効になります。実行時の負荷は低く、再びアクティブにするときの負荷は中程度から高めです。
- 非表示にする:よく使われる方法です。実行時の負荷は中程度で、再表示の負荷も低くなります。
後の2つに対応するAPIメソッドは次のとおりです。

API画像では括弧の表示が崩れている場合があります。SetActive(active)とSetVisible(visible)として読み、続くメソッドも意味に応じて区切ってください。
2.4 階層
階層には親子関係と描画順の2つの側面があります。
親子関係
クライアントモジュールには親が必要です。コンテナの階層を整理すると、複雑なプロジェクトを管理しやすくなり、性能の改善にも役立ちます。作成時に親を指定しますが、作成後も読み書き可能なparentプロパティで変更できます。
コンテナのクライアントモジュールテンプレートを作成します。2つのコンテナとテキストボックスを作成し、それぞれの子の数を出力した後、テキストボックスを2つ目のコンテナに移動します。'
function OnStart()
local container1 = game.InstantiateClientUIControl(1073741851, script.object);
local container2 = game.InstantiateClientUIControl(1073741851, script.object);
local textbox = game.InstantiateClientUIControl(1073741846, container1); -- 先ほど作成したテンプレートのインデックスに置き換える
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
ログ出力:
container1 len:1
container2 len:0
container1 len:0
container2 len:1
親を変更すると、子の数も変わります。
描画順
サーバーUIには階層順と時系列順の描画方式があります。クライアントモジュールの描画順を確認するため、画像モジュールのテンプレートを作成し、インデックスを控えます。
imageColorプロパティを使い、2つの画像を異なる色にします。

型はColorValueです。RGBAを指定するコンストラクタで作成できます。

SetImage(imageSource, imageId)で画像を指定します。画像IDはUIコントロールグループライブラリで確認できます。APIでimageSourceを調べます。

静的画像にはEnum.ImageSource.StaticReferenceを指定します。例:SetImage(Enum.ImageSource.StaticReference, 10001)。2つ目の画像を(50, 50)だけ移動し、次のスクリプトを実行します。
function OnStart()
local pic1 = game.InstantiateClientUIControl(1073741853, script.object); -- 先ほど作成したテンプレートのインデックスに置き換える
local pic2 = game.InstantiateClientUIControl(1073741853, script.object); -- 先ほど作成したテンプレートのインデックスに置き換える
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

作成順を入れ替えて確認します。

ほかに変更がなければ、描画順は作成順に従います。APIの同階層インデックスでも順番を指定でき、値が大きいものほど上に描画されます。

緑を先、赤を後に作成したまま、順番を明示的に変更します。
function OnStart()
local pic2 = game.InstantiateClientUIControl(1073741853, script.object); -- 先ほど作成したテンプレートのインデックスに置き換える
local pic1 = game.InstantiateClientUIControl(1073741853, script.object); -- 先ほど作成したテンプレートのインデックスに置き換える
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

緑の画像が上に表示されます。
2.5 簡単な動き
前の例を使い、赤い画像を左右に、緑の画像を上下に動かします。OnUpdateイベントでは、フレームごとに範囲を確認して位置を更新できます。APIで更新処理の使い方を確認します。

開始時に更新を明示的に有効にします。
script:EnableUpdate(true);
モジュールの作成はOnStart()に残し、移動処理はOnUpdate()に記述します。両方の関数から画像にアクセスできるよう、local宣言を関数の外に移動します。
同階層インデックスは0から始まり、最大値は親の子モジュール数から1を引いた値です。SetSiblingIndexは真偽値を返します。この例では、インデックス0と1を使って赤い画像を緑の画像の下に配置します。
local pic1 = nil
local pic2 = nil
local timer = 0 -- 経過時間
function OnStart()
script:EnableUpdate(true);
pic2 = game.InstantiateClientUIControl(1073741853, script.object); -- 先ほど作成したテンプレートのインデックスに置き換える
pic1 = game.InstantiateClientUIControl(1073741853, script.object); -- 先ほど作成したテンプレートのインデックスに置き換える
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
動作を確認します。

2.6 よく使うモジュール
2.6.1 画像
画像の基本的な使い方は前の例で説明しました。画像にはX方向とY方向のエッジのぼかしや塗りつぶしの設定もあり、プロパティやメソッドで操作できます。
エッジのぼかしを有効にするには、enableMaskとenableSoftEdgeをどちらもtrueにします。ぼかしの幅はSetSoftEdgeWidth(widthX, widthY)で設定します。前の例の動きを止め、緑の画像にぼかしを追加します。
local pic1 = nil
local pic2 = nil
function OnStart()
script:EnableUpdate(true);
pic2 = game.InstantiateClientUIControl(1073741853, script.object); -- 先ほど作成したテンプレートのインデックスに置き換える
pic1 = game.InstantiateClientUIControl(1073741853, script.object); -- 先ほど作成したテンプレートのインデックスに置き換える
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
ステージを実行します。

緑の画像のエッジがぼかされました。pic2.reverseMaskArea = trueにすると、マスク範囲を反転できます。

2.6.2 テキストボックス
テキストボックスの使い方は、画面上で設定する場合とほぼ同じです。スクリプトからプロパティを設定して外観を変更します。
function OnStart()
local textbox = game.InstantiateClientUIControl(1073741846, script.object); -- 先ほど作成したテンプレートのインデックスに置き換える
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. キーボードとマウスのイベント
ここまではモジュールの表示を扱いました。この章ではクリックなどの操作を追加します。サンプルではコールバック関数を使います。Luaやプログラミングに慣れていない場合は、先にコールバックを調べておくと理解しやすくなります。
3.1 プリセットボタン
プリセットボタンは、ボタン操作とカーソルイベントのリスナーを提供します。各状態の外観をあらかじめ設定し、状態用のモジュールを子として配置します。ポインター検出エリアより自由度は低いものの、スクリプトの操作は簡単になります。

先にボタンを設定してください。設定手順はこのガイドでは扱いません。
このサンプルでは、中央のパネル上にボタンとテキストボックスを作成します。ボタンをクリックするたびにテキストボックスの数字が1増えます。APIでリスナーを確認します。

リスナーにはイベントの種類とコールバック関数を渡します。APIでイベントの種類を調べると、マウス、キーボード、コントローラーなどのイベントが見つかります。今回はカーソルのクリックを使います。

共有する変数を定義します。
local pic = 1073741853
local button = 1073741866
local text = 1073741846
local count = 0
local textbox = 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)
local button = game.InstantiateClientUIControl(button, panel)
button:SetSizeDelta(180, 70)
button:SetAnchoredPosition(0, -60)
button:AddCursorEventListener(Enum.CursorEventType.CursorClick, OnButtonClick)
end
動作を確認します。

3.2 キーの割り当て
キーリスナーはタッチ入力には直接反応しないため、対象デバイスに応じた対応が必要です。
プリセットボタンは基底モジュールからAddKeyEventListenerを継承するため、カーソルイベントだけでなくキーイベントも受け取れます。画像などのモジュールもキーリスナーを継承します。画像モジュールにはカーソルイベントを直接監視するメソッドがないため、後述するポインター検出エリアを組み合わせます。
前のスクリプトからプリセットボタンの作成処理を削除し、代わりに画像でボタンを作ります。
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 = "キー [1]"
btnLabel.horizontalAlignment = Enum.TextHorizontalAlignment.Middle
btnLabel.verticalAlignment = Enum.TextVerticalAlignment.Middle
キーイベントのコールバックは真偽値を返す必要があります。OnStart()の外に、既存のクリック処理を再利用するコールバックを定義します。
local function OnKeyPress()
OnButtonClick()
return true
end
trueはこのサンプルの戻り値です。trueとfalseがイベントの処理や伝播に与える影響は、現在のドキュメントと実機で確認してください。
OnStart()内でcustomButtonにキーリスナーを登録します。
customButton:AddKeyEventListener(Enum.KeyEventType.KeyboardCraftspersonKey1Down, OnKeyPress)
完全なコード:
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 = "キー [1]"
btnLabel.horizontalAlignment = Enum.TextHorizontalAlignment.Middle
btnLabel.verticalAlignment = Enum.TextVerticalAlignment.Middle
customButton:AddKeyEventListener(Enum.KeyEventType.KeyboardCraftspersonKey1Down, OnKeyPress)
end
実行してキーの動作を確認します。
3.3 ルートモジュールでキーを監視する
この例では、スクリプトが取り付けられたモジュールであるscript.objectにリスナーを登録します。ここではクライアントモジュールコンテナのルートノードです。
これもモジュール上のリスナーであり、すべてのキーイベントを受け取れる保証はありません。描画順、他のコールバックによるイベント処理、モジュールの状態などが応答に影響するため、実際のプロジェクトで確認してください。
スクリプトのコンテナにキー2のリスナーを追加します。
script.object:AddKeyEventListener(Enum.KeyEventType.KeyboardCraftspersonKey2Down, OnKeyPress)
実行して動作を確認します。
3.4 ポインター検出エリア
通常のモジュールすべてがカーソルイベントを直接監視できるわけではありません。ポインター検出エリアを使うと、画像やテキスト、画面上の領域でクリック、ホバー、ドラッグを検出できます。透明な当たり判定領域として機能します。カスタムボタンの子として作成し、カーソルイベントを受け取ります。
始める前に、UIコントロールグループライブラリ → クライアントモジュールテンプレートを開き、ポインター検出エリアを追加してテンプレートとして保存し、インデックスを控えます。
検出エリアを作成し、クリックのリスナーを登録します。
local clickArea = game.InstantiateClientUIControl(cursorArea, customButton)
clickArea:SetSizeDelta(180, 70)
clickArea:SetAnchoredPosition(0, 0)
clickArea:AddCursorEventListener(Enum.CursorEventType.CursorClick, OnButtonClick)
完全なコード:
local pic = 1073741853 -- 画像テンプレート
local text = 1073741846 -- テキストボックスのテンプレート
local cursorArea = 1073741884 -- ポインター検出エリアのテンプレート
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 = "クリック / [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
ボタンをクリックすると数字が増えるか確認します。
3.5 サンプル:画像のドラッグ
ポインター検出エリアはドラッグにも使えます。例えば、インベントリのカード、マップ上のマーカー、自由に配置できる小窓などを移動できます。
ドラッグイベント
Enum.CursorEventType.CursorDragは、カーソルを動かし続けている間に発生します。コールバックにはCursorEventData型のeventDataが渡されます。GetUIPosDelta()は今回の移動によるUI画面上の変位を返します。
local dx, dy = eventData:GetUIPosDelta()
この変位を画像の現在位置に加えると、画像がマウスに追従します。
local function OnDrag(eventData)
local dx, dy = eventData:GetUIPosDelta()
local curX, curY = dragBox:GetAnchoredPosition()
dragBox:SetAnchoredPosition(curX + dx, curY + dy)
end
完全なコード
ドラッグする画像、案内テキスト、ポインター検出エリアを組み合わせます。
local pic = 1073741853 -- 画像テンプレート
local text = 1073741846 -- テキストボックスのテンプレート
local cursorArea = 1073741884 -- ポインター検出エリアのテンプレート
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()
-- ドラッグする画像を作成
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)
-- ポインター検出エリアを取り付け
local dragArea = game.InstantiateClientUIControl(cursorArea, dragBox)
dragArea:SetSizeDelta(120, 120)
dragArea:SetAnchoredPosition(0, 0)
-- ドラッグリスナーを登録
dragArea:AddCursorEventListener(Enum.CursorEventType.CursorDrag, OnDrag)
end
四角形の上でマウスの左ボタンを押したままドラッグし、色が変わってマウスに追従するか確認します。
このサンプルは簡略化されています。実際のプロジェクトでは、ドラッグ開始時に対象をフォーカスしてカーソルに合わせたり、ドロップ時に位置を調整したりする場合があります。
4. Tweenアニメーション
UIの表現を豊かにするにはアニメーションが役立ちますが、状態の変化を自分で実装するのは手間がかかります。星々の幻境にはTweenシステムがあり、位置、サイズ、拡大率、透明度などを指定した時間で滑らかに変化させられます。
4.1 基本のTween:game.Tween
APIでTweenの作成方法を確認します。

game.TweenはTweenオブジェクトを作成して返します。引数は3つです。
- object:対象のモジュール
- tweenDataTable:プロパティ名と目標値を対応させたテーブル
- duration:アニメーションの長さ
画像を作成し、Tweenを再生します。
local pic = 1073741853 -- 画像テンプレート
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
この例ではlocalScaleXが3秒かけて2になります。実行して画像が大きくなる様子を確認します。

すべてのプロパティを補間できるわけではありません。Tweenで使用できるのはTweenableと記されたフィールドです。英語のAPIドキュメントでは、この表示が一部不足している場合があります。主な例は次のとおりです。
補間できるフィールド:localScaleX, localScaleY
対象:すべてのモジュール
効果:拡大・縮小。ボタンの反応や呼吸するような動きなど
補間できるフィールド:anchoredPositionX, anchoredPositionY
対象:すべてのモジュール
効果:移動。パネルのスライドや浮遊など
補間できるフィールド:sizeDeltaX, sizeDeltaY
対象:すべてのモジュール
効果:サイズの滑らかな変化
補間できるフィールド:imageColor
対象:画像
効果:色の変化やアルファ値を含むフェード
補間できるフィールド:fillAmount
対象:画像
効果:HPバーやクールダウンなどの進捗表示
補間できるフィールド:fontSize, fontColor
対象:テキスト
効果:文字サイズや文字色の変化
4.2 イージング:EaseType
一定速度の動きが適さない場合もあります。TweenではEnum.EaseTypeを使って、次のようなイージングを選べます。
- Enum.EaseType.Linear:一定速度
- Enum.EaseType.OutBack:目標を少し超えてから戻る
- Enum.EaseType.OutBounce:最後に弾む
- Enum.EaseType.OutQuad / Enum.EaseType.OutCubic:滑らかに減速する
SetEaseでイージングを指定します。次の例ではキーを押すたびにモジュールの状態を戻し、アニメーションを比較します。
local pic = 1073741853 -- 画像テンプレート
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
1、2、3、4キーを押して違いを確認します。ほかのイージングも同じように使えます。
4.3 アニメーションのループと制御
Tweenは1回だけ再生することも、繰り返すこともできます。制御用のメソッドをAPIで確認します。

- Play():再生を開始する
- Pause():一時停止する
- Restart():先頭に戻して再生する
- SetLoops(times):ループ回数を設定する。-1は無限ループ
- SetOnComplete(callback):アニメーションの完了時にコールバックを実行する
- Kill(false):アニメーションを破棄してリソースを解放する。画面を閉じるときやOnDestroy()での呼び出しを推奨
SetLoops(-1)でアニメーションを繰り返します。基本のTweenサンプルに、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シーケンスと待機
Tweenシーケンスを使うと、複数のアニメーションを順番に再生できます。APIを確認します。

空のTweenSequenceを作成します。
local seq = game.TweenSequence()
シーケンスはタイムラインのように使えます。Tween、待機時間、コールバックを追加し、複数の処理を組み合わせます。
主なシーケンスメソッド
メソッド:seq:Append(tween)
説明:前の処理の後にアニメーションを追加する
メソッド:seq:AppendInterval(seconds)
説明:指定秒数の待機を追加する
メソッド:seq:AppendCallback(function)
説明:タイムラインの現在の末尾にコールバックを追加する
メソッド:seq:Join(tween)
説明:前の処理と同時に再生するアニメーションを追加する
メソッド:seq:SetLoops(times)
説明:シーケンス全体のループ回数を設定する。-1は無限ループ
メソッド:seq:Play()
説明:再生を開始する
メソッド:seq:Pause() / seq:Resume()
説明:再生を一時停止/再開する
メソッド:seq:Kill(false)
説明:シーケンスを破棄してリソースを解放する
滑らかな呼吸アニメーション
前の例では、単一のTweenで2倍まで拡大した後、次のループの開始時に元の大きさへ一瞬で戻ります。シーケンスを使い、拡大と縮小を交互に再生します。
local pic = 1073741853 -- 画像テンプレート
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. Tweenシーケンスを作成
seq = game.TweenSequence()
-- 2. 1.5秒かけて1.8倍に拡大
seq:Append(game.Tween(box, { localScaleX = 1.8, localScaleY = 1.8 }, 1.5):SetEase(Enum.EaseType.InOutSine))
-- 3. 0.2秒待機
seq:AppendInterval(0.2)
-- 4. 1.5秒かけて1.0倍に戻す
seq:Append(game.Tween(box, { localScaleX = 1, localScaleY = 1 }, 1.5):SetEase(Enum.EaseType.InOutSine))
-- 5. シーケンスを無限ループで再生
seq:SetLoops(-1)
seq:Play()
end
5. 信号
Lua UIスクリプトはクライアント側で動作し、サーバー側の処理とは分かれています。両者の通信には信号を使います。この章では受信と送信を説明します。
5.1 信号の受信
星々の幻境はサーバー権威型の構成を採用しています。通貨、レベル、HP、ステージの残り時間などのプレイヤーデータはサーバーで管理され、クライアントUIはその正本データを保持しません。
サーバーはクライアントスクリプトに信号を送れます。Luaスクリプトはリスナーを登録して信号を受信し、処理します。
主なAPI

クライアントスクリプトのscriptには、次の2つのメソッドがあります。
- script:RegisterServerSignalHandler(signalName, callback)でリスナーを登録します。
- signalName:信号管理画面で設定した信号名
- callback:信号名signalNameと、サーバーから渡された引数の配列signalParamsを受け取る関数
- script:UnregisterServerSignalHandler(signalName)でリスナーを解除します。重複処理を避けるため、不要になったときやモジュールが破棄されるときに解除します。
この例では、サーバー側の数値を2秒ごとに増やしてクライアントスクリプトに送り、受け取った数値を表示します。
サーバーノードグラフを作成してステージに取り付けます。信号syncを追加し、整数の引数numを設定します。

繰り返しタイマーを追加し、回数をクライアントに送ります。画像ではWhen Entity Is Created、Start Timer、Assembly List、When Timer Is Triggered、Send Client Scripted ServerSignalの各ノードを使っています。プレイヤーリストとGet Corresponding Value From Listで送信先を指定します。

スクリプトで中央にテキストボックスを作成し、RegisterServerSignalHandlerで"sync"を監視します。サーバーからのブロードキャストを受け取ったら、signalParams[1]を表示します。
local pic = 1073741853 -- 画像テンプレート
local text = 1073741846 -- テキストボックスのテンプレート
local countLabel = nil
-- サーバーからのsync信号を受け取るコールバック
local function OnReceiveSync(signalName, signalParams)
-- サーバーから渡された最初の引数を取得
local num = signalParams[1] or 0
countLabel.text = tostring(num)
end
function OnStart()
-- 1. 中央に背景パネルを作成
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. 数字を表示するテキストボックスを中央に作成
countLabel = game.InstantiateClientUIControl(text, panel)
countLabel:SetSizeDelta(300, 100)
countLabel.fontSize = 32
countLabel.horizontalAlignment = Enum.TextHorizontalAlignment.Middle
countLabel.verticalAlignment = Enum.TextVerticalAlignment.Middle
countLabel.text = "信号を待っています..."
-- 3. サーバー信号「sync」を監視
script:RegisterServerSignalHandler("sync", OnReceiveSync)
end
ステージを実行します。サーバーのタイマーから2秒ごとに更新が送られ、数字が増えていきます。

5.2 信号の送信
クライアント側のデータは、切断や強制終了によって失われることがあります。重要なデータやセーブデータは信号でサーバーに送れます。2Dと3Dを組み合わせた作品では、UIの操作をサーバーに伝える場合にも使います。
サーバーノードグラフでは、信号を監視するイベントノード(画像ではMonitor Signal)でクライアントからの信号を受け取れます。クライアント側ではLua APIで信号オブジェクトを作成して送信します。

- local signal = game.ServerSignal(signalName)で信号を作成します。
- サーバー側と取り決めた順序で引数を追加します。
メソッド:signal:AddInt(intValue)
説明:アイテムIDや数量などの整数を追加する
メソッド:signal:AddFloat(floatValue)
説明:浮動小数点数を追加する
メソッド:signal:AddString(stringValue)
説明:操作内容やメモなどの文字列を追加する
メソッド:signal:AddBool(boolValue)
説明:真偽値を追加する
メソッド:signal:AddVector3(vector3Table)
説明:{x=0, y=0, z=0}のようなテーブルで3Dベクトルを追加する
- signal:SendSignal()で送信します。
前の例を拡張し、同じ信号を使ってクリックでもスコアを増やします。クライアントは意味のある値を送らず、サーバーは信号を受け取るとスコアに1を加えます。
サーバーノードグラフを変更し、クライアントの累計スコアを記録するノードグラフ変数を作成します。画像ではMonitor Signal、Get Node Graph Variable、Addition、Set Node Graph Variableを使っています。

クライアント側では、スコアボードのパネルにポインター検出エリアを取り付け、CursorDownを監視します。パネルが押されたら信号を送ります。
local pic = 1073741853 -- 画像テンプレート
local text = 1073741846 -- テキストボックスのテンプレート
local cursorArea = 1073741884 -- ポインター検出エリアのテンプレート
local countLabel = nil
-- 1. サーバーのsync信号を受信して表示を更新
local function OnReceiveSync(signalName, signalParams)
local num = signalParams[1] or 0
countLabel.text = tostring(num)
end
-- 2. スコアボードが押されたら加点信号を送信
local function OnPanelDown()
local signal = game.ServerSignal("sync")
signal:AddInt(1)
signal:SendSignal()
end
function OnStart()
-- 1. 中央にスコアボードのパネルを作成
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. 数字を表示するテキストボックスを中央に作成
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 = "信号を待っています..."
-- 3. ポインター検出エリアを取り付けて押下を監視
local clickArea = game.InstantiateClientUIControl(cursorArea, panel)
clickArea:SetSizeDelta(300, 100)
clickArea:SetAnchoredPosition(0, 0)
clickArea:AddCursorEventListener(Enum.CursorEventType.CursorDown, OnPanelDown)
-- 4. サーバーのsync信号リスナーを登録
script:RegisterServerSignalHandler("sync", OnReceiveSync)
end
動作を確認します。

クリック後、次のサーバー更新にクリック回数が加算されます。これでサーバーからクライアント、クライアントからサーバーへの通信ができました。
6. まとめ
このガイドでは、いくつかのサンプルを通して基本を学び、公式ドキュメントでLua APIの使い方を調べました。全体の仕組みを理解した後は、必要に応じてドキュメントで詳細を確認してください。

