時計アプリで詰まりやすいところ(実行中に変えるラベルの扱い、アプリ内のダイアログ、feed の読み方、K.Set のフィールド名)を先に説明します。
パッケージの形
Packages/vrc.ulexite.app.<id>/
package.json VPM の情報(vpmDependencies に vrc.ulexite.tv)
ulexite-app.json マニフェスト(kind: "native"、profiles: ["tv"])
Runtime/
Ulexite.App.<Id>.asmdef 参照:Ulexite(本体)
UlexiteApp<Id>.cs UlexiteApp を継承(アプリの本体)
Ulexite.App.<Id>.asset U# のプログラムアセット(エディタが作ります)
Editor/
Ulexite.App.<Id>.Editor.asmdef
Ulexite<Id>Builder.cs UlexiteAppBuilder を継承(画面のビルダー)
Localization/<アプリ>-lang.json アプリの翻訳表(任意)
- パッケージは
Packages/に置きます。アプリ ID は英小文字(例clock) - 画面はエディタで作り、実行時には組み立てません。ビルダーは「Tools → Ulexite → テレビを作り直す(再設置と再紐づけ)」から呼ばれます
画面は Builder で作る
UlexiteAppBuilderを継承し、Build(app, screen, launcher)で画面を作ります- 部品は
UlexiteBuildKit(using K = Ulexite.EditorTools.UlexiteBuildKit;)から:K.Rect・K.Fill・K.Img・K.Tmp・K.Button・K.Icon・K.Roundedなど。寸法はデザインキャンバス(1600×900)の数値で書き、K.Scale(1.2 倍)で 1920×1080 にします K.Set(app, "フィールド名", 値)・K.SetArray(app, "フィールド名", 配列)のフィールド名は文字列です。C# の型チェックでは分かりません。- 名前を間違えると
SerializedObject.FindPropertyが null になり、「テレビを作り直す(再設置と再紐づけ)」の途中で例外になって止まります(Console に NullReferenceException) K.Setを書き足したら、一度「テレビを作り直す(再設置と再紐づけ)」を実行して Console を見てください(アプリを開く前でかまいません)。エラーが出なければそのまま進めます- フィールドを足したときは、U# をコンパイルしてプログラムアセットを作り直してから、テレビを作り直します
- 名前を間違えると
文字と翻訳(実行中に変えるラベルは空文字で作る)
K.Tmp(rt, "オン", …)のように日本語の固定文字を渡すと、ビルダーが翻訳表からキーを引いて LanguageReceiver を付けます(言語が変わると表の文言に入れ替わります)。表にない文言は Console に警告が出ます- 状態で変わるボタン(「オン」「オフ」「削除」など)は
""で作り、コードで入れます。固定文字で作ると、言語を切り替えたときに表の文言で上書きされます(時計アプリでは、この形にしています)
// ビルダー:空文字で作る(あとでコードが入れる)
K.Set(app, "toggleLabel", K.Tmp(K.Rect(rt, "Label", 20, 0, 200, 40), "", 18, K.Text1, false, TextAlignmentOptions.MidlineLeft));
// アプリ:状態に合わせて入れ直す(言語が変わったら _OnLanguageChanged でもう一度)
toggleLabel.text = running ? L("Ulexite.Clock.On") : L("Ulexite.Clock.Off");
- 文言は
L("Ulexite.<アプリ>.<キー>")で引きます。翻訳表はLocalization/<アプリ>-lang.jsonに置くと、テレビを作り直すときにまとめられます _OnLanguageChangedで、コードで入れた文言を入れ直してください- 外から来た文字(動画のタイトルなど)はリッチテキストを切るか
<をエスケープします
フォーカスと操作
| 返す・受ける | 何を |
|---|---|
GetRowCount() | 行数(行 0 はナビゲーションバー。アプリの行は 1 から) |
GetColCount(row) | その行の列数 |
GetFocusTarget(row, col) | フォーカス枠を重ねる相手(ないときは null) |
_OnSubmit(row, col) | 決定 |
_OnDirection(row, col, dir) | 方向。自分で処理したら true。false ならランチャーが行・列を動かす |
_OnBack() | 戻る。アプリ内で戻れたら true(ダイアログを閉じるなど)、false ならホームへ |
- フォーカスを移すのは
launcher._SetFocus(row, col)。アプリの小さな状態はlauncher._SetAppState(byte)/launcher.AppStateで全員に同期できます - 方向の定数は
UlexiteLauncher.Up/Down/Left/Right - 空の行・列には入らないようにします。
_OnDirectionで true を返して握るか、GetFocusTargetが null の行へ動かないようにします
アプリ内のダイアログ(時計の例)
- 行数は固定にします。ダイアログを閉じていても、ダイアログの行まで
GetRowCountに含めておきます(時計はRowCityPageまで返しています) - 入る・出るは
_OnDirectionで決めます。「メニューの行で下 → 一覧を開いて一覧の先頭へ」「一覧の先頭で上 → 閉じてメニューへ」のように、trueを返して自分で動かします。閉じているときに下を押したら開く(true)ので、ランチャーの既定の移動でダイアログの行に迷い込むことはありません _OnBack()で閉じます(ダイアログの行にいるときは閉じて true)- ダイアログは
GameObject.SetActiveで出し入れします(実行時に作りません)
骨組みの例:
const int RowMain = 1, RowMenu = 10, RowList = 11, RowListRows = 8, RowPage = 19;
public override int GetRowCount() => RowPage; // 閉じていても固定
public override int GetColCount(int row) => row == RowMain ? cards.Length : 1;
public override RectTransform GetFocusTarget(int row, int col)
{
if (row == RowMain) return Pick(cards, col);
if (row == RowMenu) return Pick(menuButtons, col);
if (row >= RowList && row < RowList + RowListRows) return Pick(listButtons, row - RowList);
if (row == RowPage) return Pick(pageButtons, col);
return null;
}
public override bool _OnDirection(int row, int col, int dir)
{
if (row == RowMain && dir == UlexiteLauncher.Down) { OpenMenu(col); return true; } // 下で開く
if (row == RowMenu && dir == UlexiteLauncher.Up)
{
launcher._SetFocus(RowMain, Mathf.Clamp(edit, 0, cards.Length - 1)); return true; // 上で戻る
}
if (row == RowMenu && dir == UlexiteLauncher.Down)
{
if (listOpen) launcher._SetFocus(RowList, 0); return true; // 開いていれば一覧へ
}
if (row == RowList && dir == UlexiteLauncher.Up)
{
CloseList(); launcher._SetFocus(RowMenu, 0); return true; // 先頭で上は閉じる
}
return false;
}
public override bool _OnBack()
{
if (launcher.Row >= RowMenu) { CloseAll(); return true; } // ダイアログを閉じる
return false;
}
- 実例:
Packages/vrc.ulexite.app.clock/Runtime/UlexiteClockApp.csのGetRowCount・GetColCount・GetFocusTarget・_OnDirection・_OnBack(都市の一覧と、アラームの編集が同じ形です)
App Services
アプリは services(UlexiteApp.services)だけを使います。Ulexite TV の内部は直接触りません。
データ(feed)
_RegisterFeed(appId, mode, interval)・_FeedJson(appId)・_FeedText(appId)・_FeedStatus(appId)・_FeedLoadedAt(appId)・_ReloadFeed(appId)modeは 0 常に/1 裏でも/2 開いているときだけ。everyは目安で、実際の登録はアプリが_RegisterFeedで行います_FeedJson・_FeedTextはアプリ ID の feed(v1/feed/<アプリID>.json)だけです。アプリ ID 以外の feed は読めません(ここで詰まりやすいところです)- ほかの URL(例:時差の表
v1/timezones.json)は、マニフェストのdataにid(データの名前)とurlを書いて、_RegisterDataFeed(appId, id, mode, interval)・_DataFeedJson(appId, id)・_DataFeedText・_DataFeedStatus・_DataFeedLoadedAt・_ReloadDataFeedで読みます(App Services 3 以降。Ulexite TV 0.4.0 から使えます。それより前の版では、アプリ ID の feed だけです)。焼き込む名前は<アプリID>-<id>です
- ほかの URL(例:時差の表
- 読み込みの表示は
_LoadingBegin(this, hasData)/_LoadingEnd(this)。15秒で「読み込めませんでした」になり、決定か「再試行」で_OnServicesLoadRetryが届きます
// ulexite-app.json(時計の例)
"data": [
{ "id": "timezones", "url": "https://thesilver0913.github.io/ulexite-catalog/v1/timezones.json", "every": "24h" }
]
保存・設定・音・通知
- 保存:
_SaveInt/_LoadInt/_SaveString/_LoadString。_OnServicesStorageReadyを待ってから読み書きします。予算は7章 - 設定:マニフェストに
settingsを書くと、OS の共通パネルで編集できます(_OpenSettings(this))。読みは_SettingInt/_SettingString/_SettingBool、書きは_SetSettingString。orderの値はカンマ区切り(先頭が左側)で、選択ダイアログでは入っていない項目は末尾に足され、✓ の項目は決定で外れます(最後の1つは消せない。並べ替えはアプリ側で_SetSettingString)。変えたら_OnServicesSettingsChangedが届きます。設定の種類はchoice・toggle・order・time(時刻)です。マニフェストのlabel・noteには翻訳キーをそのまま書きます(t:を付けません)。データで書くアプリの定義ではt:を付けます(9章のsettingsの表の下) - 音:
_PlayCursor/_PlayDecide/_PlayNotify/_PlayError、_PlaySound(clip)。鳴り続ける音は、アラームのようにその人にだけ鳴らすなら_PlayLoopLocal(clip, seconds)/_StopLoopLocal()(テレビの位置・その人の操作音の音量。secondsは自動停止で 0 は止めるまで)、全員に鳴らすなら_PlayLoop(clip)/_StopLoop()(操作する人だけ)。アプリ自身の AudioSource で鳴らすと、操作音の音量・音量補正に従いません - 通知・トースト:
_Notify(...)/_Toast(...)。アイコンはUlexiteAlerts.Icon*(IconAlarm・IconCityなど) - 確認ダイアログ・文字入力:
_Confirm(...)/_RequestText(...)。答えは渡したイベント名で届き、DialogAccepted/TextResultを読みます - 共有データ:
_SharedSetIntなど(インスタンスの間だけ。書けるのは操作する人)
同期
- 画面の状態(開いているアプリ・フォーカス・アプリの状態)はシェルが同期します。アプリが自分でフォーカスを同期しないでください
- アプリの小さな状態は
AppState(1バイト)か共有データ。[UdonSynced]の配列は初期化が必要です - 遅れて来た人にはネットワークイベントが再送されません。状態は同期変数で持ちます
はまりどころ
- 例外が出た UdonBehaviour はそのセッション止まります(例外は呼び出し元にも伝わります)。アプリは自分のフィールドに書き写し、呼ぶ側(ランチャー)は戻り値を信用しません
Listは使えません(配列)。DataDictionaryはStartで作ります(フィールドの初期化子はコンパイル時に評価されるため)- 構造体の変更メソッドは効きません(
Vector3などは代入し直す) - 再帰は
[RecursiveMethod]を付けます - U# をコンパイル → プログラムアセットを作り直す → テレビを作り直す、の順です。作り直したら Console を必ず見てください(
K.Setのフィールド名の間違いはここで初めて分かります)