Remote Control
jp.lilium.livestudio.remotecontrol の変更履歴です。
[0.26.0] - 2026-09-27
Added
- 復元が終わったことを知らせます(
LiveSceneSerializer.onLiveSceneRestored)。 復元が動かし始めることの中には、その中では終えられないものがあります:アセットがまだ登録されていない@prefabエントリは、捨てられずにPendingPrefabStoreにキューされ、解決できるようになったら誰かがそれを取りに戻ってくる必要があります。その誰かとはアセットの持ち主であり、アセットの解決は非同期です——復元の中から始めると呼び出しより長生きし、次に来るもの(テストスイートなら次のテスト)の最中に走ってしまいます。そこで、復元は終わったことを告げ、持ち主がいつ動くかを決めます。各パスの後に、実際に読まれたファイルについてのみ発行されるため、存在しなかったシーンのために購読者が起こされることはありません。ここにあるほかの静的イベントと同じくドメインリロードでクリアされるので、Domain Reload がオフのときに前のプレイセッションの購読者が残り続けることはありません。 - ライブシーンのファイルを、読む前に今の形へ引き上げられます(
LiveSceneMigrations)。 復元はobjects[]の各エントリを、それが名指すオブジェクトへ渡すため、その後移動したりなくなったりしたメンバーを名指すエントリは持ち主が見つからずに捨てられます——古いビルドが書いたシーンは、何も告げられないまま、ただ空っぽに見える状態で開きます。LiveSceneFromJsonは、ファイル全体がまだ見えている唯一の場所で、ホストアプリケーションが登録した書き換えを、パース済みのファイルに対して先に実行するようになりました。フックはここにあり、書き換えはここにはありません:古いファイルが何を意味していたかはホスト自身のクラスについての知識なので、このパッケージが知っているのは、いつ尋ねるかだけです。登録はキー付きなので、2 回発火するインストーラー(エディタでは[RuntimeInitializeOnLoadMethod]と[InitializeOnLoadMethod]の両方が発火します)は 2 つ目のコピーを積み重ねずに自身のエントリを置き換えます。また、例外を投げた書き換えは捕捉されません——握りつぶせば、この仕組みが防ぐために存在する、黙ったままの中途半端な復元に戻ってしまうからです。読み取りのみ:古い形を再び書くものは何もなく、オペレーターが保存するまでディスク上のファイルには触れません。 LiveObjectSnapshotは、1 つの永続化スコープについて、差分とその背後にある既定値を取れるようになりました。CaptureDelta(handle, scope)/CaptureScopedDefaults(handle, scope)/EnsureScopedDefaults(handle, scope)により、PersistScope.Customメンバーの持ち主は、ライブシーンが自身のファイルを書くのと同じ方法で自身のファイルを書き、ほかの書き手が持つものに触れずに、まさにそのメンバーだけを戻せます。LiveObjectDefaultRegistryはそのスコープのための 2 つ目の基準を持ちます:既存の基準は REST の差分読み取りが変更として報告するものでもあるため、それを広げるとその答えが変わってしまうからです。AssetStateSnapshotには、対応するCaptureDelta(instance, scope)/RestoreScopedDefaults/EnsureScopedDefaultsと、両方のスコープを求める名前付きプリセットのためのCaptureFullDeltaが加わりました。- ライブデータ:アプリケーションの状態をフレームごとに取り込み、テイクとして収録・再生できます。
FrameGateは PlayerLoop にフックし、毎フレームの先頭で 3 つのレーンから 1 フレームを埋めます。state レーンは、運ばれるすべてのメンバーの値をバイト列としてコピーします——[LiveClass]型ごとにソースジェネレーターが生成するStateBlockと、ライブクラスアセットで宣言された型のためのDeclaredStateBridgeです。structure レーンは、どのオブジェクトが存在するかを記録します——id、型、親、それを組み立てたレシピ、そして[LiveKey]コレクションの要素とその順序です。event レーンは、ゲートを通った書き込みと呼び出し(REST、デッキのオペレーション)の疎なログです。FrameRecorderはフレームをテイク(*.live.bin)に書き込み、FrameReplayerはライブのプロデューサーの代わりにそれを供給し直します。state はメンバーごとに、バイトが変化した箇所だけ書き戻されます。structure は実行中のシーンと突き合わせて調整され、テイクにあって今回の実行に無いものはILiveRecipe/LiveRecipesで立ち上げられます(PrefabRecipeは保存された@prefabと同じ方法でプレハブのインスタンスを再構築し、リプレイが作ったものだけが再び取り除かれます)。event は再適用されます。テイクはシーク可能なまま保たれます。キーフレームが一定間隔(keyframeInterval)でワールド全体を再表明し、末尾のインデックスで読み手がジャンプでき、チャンクは圧縮されます(FrameChunkCodec、既定で有効)。圧縮前に state のエントリをフレーム方向に転置することで、実測で生サイズの約 19% になりました。各 state ブロックはメンバーごとのスキーマを持つため、メンバーを追加・削除したビルドでも古いテイクを再生できます——一致するメンバーはコピーされ、欠けているメンバーはそのまま残され、型全体が拒否されることはありません。文字列は既定で、フレームの記号表(symbol table)への id として state レーンを運ばれます。textCapacityを指定すると、代わりに固定幅のスロットに入ります。外部からフレームに供給するものは[assembly: FrameSource("name")]で宣言します。型名だけでアドレスされるコンポーネント(/live/object/Screen)にはLiveObjectRosterを通じて到達します。FrameGate.HoldSupplyは、必要なものがまだ読み込み中の間リプレイを止めます。RealtimeFrameClockは時刻でフレームに番号を振るため、キャプチャー側のクロックが既にそうしているように、フレーム番号が実時間と揃います。フレームの型、ブロック、記録フォーマットは新しいLilium.RemoteControl.Simulationアセンブリにあります(Changed を参照)。 - メンバーをどのレーンが運ぶかは、それがどこに保存されるかから決まります(
FrameLaneRules)。 ライブシーンが保存するメンバーはフレームに運ばれます——型がバイト列として移動できるなら state レーンで、そうでなければ event として。プロジェクト設定に保存されるメンバー(PersistScope.Project)、所有者が自分で保存するメンバー(PersistScope.Custom)、まったく保存されないメンバーはフレームに乗りません。つまり、シーンが保持するものはテイクが運び、マシンに属するもの(ポート、解像度、ポーリング間隔)はリプレイで押し戻されることがありません。唯一の例外は明示的に宣言します:シーンは保存しないがテイクは運ばなければならない値(ポーズ、表情ウェイト、実測したオフセット)には[LiveField(persistable = false, lane = FrameLane.State)]を使います。フィールドやプロパティにlane = FrameLane.Noneを宣言するとエラー(LRC013)になります。保存されるのに運ばれないメンバーがあると、シーンファイルがテイクと食い違うワールドを記述することになるためです。型全体をフレームから外すにはクラスに宣言します:[LiveClass(lane = FrameLane.None)]はすべてのメンバーと入れ子のオブジェクトを、自身の id で登録された最初のオブジェクトまで覆います。[LiveField(onApplied = ...)]は、リプレイが state レーンのフィールドを変更したときに、プロパティのセッターと同じように効果を実行します。carriedByはプロパティを別メンバーのストレージのビューとしてマークし、そのメンバーのレーンを報告させます。GET /live/typesは各メンバーの解決済みlaneを報告し、ジェネレーターとランタイムは同じテーブルからそれを解決します。 - LiveData Viewer ウィンドウ(
Window/Lilium Remote Control/LiveData Viewer)で、エディタ内でライブデータを調べられます。 state ブロックはメンバーごとにデコードされます——固定バッファは新しい[LiveArray]属性でラベル付けされ、文字列は固定幅でも記号表の id でもテキストとして表示されます——あわせて structure の一覧(各エントリのレシピキー付き)とそのフレームの event も表示されます。空の state 型は折りたためます。実行中のビルドのスキーマが記録のものと異なる場合、Viewer と Sequencer は、どちらか一方にしか無いメンバーを示すバナー(TypeName +onlyInBuild, -onlyInRecording)を表示します。 - リプレイをループできます。
FrameReplayer.loopは、テイクの終わりに達したら終了せずに最初から再開します。FrameRecorderController.loopはそのスイッチで、リプレイの開始前だけでなく実行中にも設定できます——オペレーターが手を伸ばすのは最後の数フレームのところだからです。巻き戻しは最初のフレームへのシークなので、ほかのジャンプと同様に、そこのキーフレームからワールドの形を復元します。途中で何かを生成するテイクでも、次の周回で 2 つ目のコピーが積み重なることはありません。末尾インデックスの無い記録はシークできないため、スイッチにかかわらず 1 回再生して終わります——スクラブと同じ制限で、理由も同じです。既定では何もループせず、終了したリプレイは従来どおりソースを切り離してonSourceEndedを発行します。 - 2 つのアナライザーがシミュレーションアセンブリを健全に保ちます:
LRC010(ビューオブジェクトに触れない)とLRC011(独自のクロックを読まない)。 これらは既存のジェネレーター DLL に同梱され、Lilium.RemoteControl.Simulationをコンパイルするときだけ実行されるため、プロジェクトのほかの部分には影響しません。LRC010はUnityEngine.Objectから派生するものすべてで発動します——評価済みのシーンオブジェクトから読んだ値は記録から再現できないためです——Vector3、NativeArray、ジョブ構造体などの値型は対象外です。LRC011はUnityEngine.Time、DateTime.Now、Stopwatch、TimeUtilityを対象とします。シミュレーションを駆動する時間はフレームに乗って届かなければならないためです。どちらも警告なので、唯一の正当な例外であるRealtimeFrameClock——実時間が入ってくる扉です——は理由を書いたコメント付きでLRC011を抑制しています。これは意図的に、エンジンを丸ごと禁止するより狭くしています。以前、アセンブリにnoEngineReferencesを宣言しようとしたところ、このレイヤーの目的そのものであるジョブシステムとネイティブコンテナまで失われ、しかもDateTimeはカバーできませんでした。 - LiveData Sequencer ウィンドウ(
Window/Lilium Remote Control/LiveData Sequencer)でテイクを収録・再生できます。 LiveData Viewer の対になるウィンドウで、見た目も揃えています:位置を表示するステータスバー(クリックでフレーム数とタイムコードを切り替え。どちらも実行開始からではなくテイクの開始から数えます)、トランスポート、ディスク上のテイク一覧です。各テイクは、末尾インデックスから読み出した実際のフレーム数と再生時間とともに一覧されます。途中で切れたテイクや、記録フォーマットが進んだビルドのテイクは、一覧から外されるのではなく、その行自体にそう表示されます。そこにあるが読めないテイクと、書かれなかったテイクは、消えてしまえば見分けがつかないためです。1 つを選んで再生を押すと、実行中のアプリケーションにリプレイされ、スライダーでスクラブできます。テイクは、ファイル名を示す確認のうえで行から削除できます——テイクは Unity プロジェクトの外にあるため、エディタのゴミ箱は届かず、削除は取り消せません。書き込み中のテイクは削除できず、再生中のテイクは先に停止します。ファイルへのハンドルがあると、削除は延期されるのではなく拒否されるためです。ヘッダーからは書き込み先のフォルダを開けます(何かを収録するまで存在しないため、先に作成します)。ウィンドウは登録されたFrameRecorderControllerを駆動し、独自のレコーダーは持たないため、リモートのページとこのウィンドウを同時に開けます——停止中のエディタや、レコーダーが登録されていない実行では、コントロールが無効になり、そのどちらなのかが表示されます。 FrameRecorderControllerがライブデータを収録・再生します——jp.lilium.livestudioからこちらへ移動し、コンポーネントではなくなりました。 これが行うことはすべてこのパッケージのものです:ゲート、レコーダー、リプレイヤー、テイクの間の state / structure システムの保持、供給されたフレームへの一覧の復元。この手順を正しく行うことを、各アプリケーションが改めて見つけ出す必要はありません。唯一知らないのはアプリケーションが成果物をどこに置くかで、これはrecordingFolderProviderに問い合わせます(既定はプレイヤーのデータの隣のフォルダ)。ExcludeControlObject(id)は、これを駆動する側が自分のボタンをテイクから除外する手段です——これが無いと、リプレイがそれらを再び押し、2 つ目の収録を始めたり、実行中のリプレイを壊したりします。Lilium.LiveStudio.FrameRecorderControllerという名前を使うソースには破壊的変更ですが、まだ誰にも出荷されておらず、公開されるクラス名は引き続きFrameRecorderです。MonoBehaviour ではなく、固定 id を持つ素の登録オブジェクトです:コンポーネントだったときは 2 つのアドレス——自身の id と、それを持つ GameObject のコンポーネント——を持ち、自身の設定をテイクから除外する仕組みは前者しか覆っていなかったため、リプレイが収録時のテイク番号と圧縮設定をオペレーターの設定の上に押し戻していました。Record()とReplay()はエディタが再生中でない間は即座に拒否します。そこではゲートが回らず、テイクはファイルを開いたままフレームを 1 つも受け取らないためです。- コントローラーでのリプレイの一時停止とスクラブ:
replayPaused、replayFrameCount、replayIndex、SeekReplay(index)、recordingPath。replayIndexは収録されたフレーム番号ではなくテイク内の位置です——それらが定数のオフセットだけずれた同じ数ではない理由は、後述のシークの修正を参照してください。 - フレームソースを一時停止できます:
FrameReplayer.isPaused。 一時停止中のリプレイヤーは最後に再生したフレームを供給し続けます——レーンはそのフレームが残したものを指し、何も進めず再適用もしません——そのためゲートは引き続き記録からフレームを取ります。切り離してしまうとワールドがライブのプロデューサーに戻り、見ているフレームが上書きされます。これは一時停止の正反対です。最初のフレームの前に一時停止した場合も 1 フレームは再生します。それまではプレイヤーのレーンが空で、それを供給するとワールドを保持するのではなく空にしてしまうためです。 - ライブクラスアセットを Project Settings で指定できるようになり、宣言が 1 つのシーンではなくプロジェクト全体に適用されます。 属性なし公開の入口はこれまで 1 つだけでした:
RemoteControlContainerが、有効な間、自身が持つアセットを適用していました。これはセットバンドルとともに移動する宣言には正しい——バンドルが去れば宣言も去らなければならない——のですが、アプリ自体を記述する宣言には不適切で、すべてのシーンに配線しなければならず、参照を忘れたシーンでは黙って 保存 されなくなっていました(シリアライザーは登録されていない型をスキップします)。新しいRemoteControlProjectSettingsは、読み込み時に 1 度適用されるアセットのリストを保持します——プレイヤーでは最初のシーンのオブジェクトが起動する前に、そしてエディタの読み込み時にも(リモートコントロールは停止中のエディタにも応答するため)。解決方法は LiveStudio の設定と同じです:config オブジェクトとして登録されたプロジェクトごとのオーバーライド、なければ AssetDatabase で見つかる同じオーバーライド、それもなければこのパッケージの Resources に同梱された既定アセット。Project Settings ページ(Project/Lilium Remote Control)はパッケージの既定値を読み取り専用で表示し、最初の編集時にプロジェクトごとのオーバーライドをAssets/Settings/RemoteControlProjectSettings.assetに昇格させ、config オブジェクトとして登録し、preloaded assets に追加します。これによりプレイヤービルドがそれを——そしてそれを通じて、指定されたライブクラスアセットを——運びます。リストを編集すると、次のドメインリロードを待たずにすぐ再適用されます。コンテナのリストは変わらず、バンドルが運ぶ宣言には引き続きそちらが適切です。 - LiveData Sequencer と LiveData Viewer が翻訳されました(英語、日本語、簡体字中国語)。このパッケージのエディタウィンドウが表示するほかのテキストも同様です。 アプリケーションとリモートアプリが共有するのと同じ
LocalizationSystemを通じてテキストを読むため、ウィンドウは独自の言語を持たず、そこで選ばれた言語に従います——また、テキストの原文は英語になりました(以前はそうではありませんでした)。ファイルはエディタ専用で、Resourcesの外のEditor/Localization/RemoteControlEditorLocales/にあります。エディタにしか存在しないウィンドウを説明する文字列はプレイヤービルドには不要なためです。アクティブな言語に無いキーは、キーそのものにフォールバックする前に英語へフォールバックします:まだ誰も翻訳していないウィンドウでも読めますが、LDS_トークンだらけのウィンドウは読めません。これを支えるため、LocalizationSystemにFormat(key, args)(プレースホルダーは翻訳されたテキストの 中に 置くものです——語順は翻訳で最初に変わるものです)、独自のフォールバックを選ぶ呼び出し側のためのTryTranslate(language, key, out text)、そしてアクティブな言語やその背後のテーブルが変わるたびに動くgenerationが追加されました。ラベルを 1 度だけ(そのラベルが置かれる行を構築するときに)解決するウィンドウは、これを監視して問い合わせ直す必要を知ります——テーブルが空になるプレイモード突入後も含みます。 LiveClassAssetまたはUIDefinitionアセットをダブルクリックすると、そのエディタウィンドウで開きます。Timecode.ToSmpteString()はHH:MM:SS:FF形式で整形し、FrameRate.framesPerSecondNominalはタイムコードが数える整数のレートを返します。- Transform マニピュレーターがこのパッケージの一部になりました——
POST/DELETE /live/manipulator/open、GET /live/manipulator/image、PUT /live/manipulator/camera(jp.lilium.livestudioから移動、ワイヤは不変)。 これはこのパッケージの概念(ライブオブジェクト、TransformValue、プロパティパス)にしか触れていなかったため、セッションを保持するオフスクリーンカメラ、X-Manipulator-Stateポーズヘッダー付きの PNG フレーム、ポーズのルートはすべてここに属します。どのホストアプリケーションも独自のハンドラーを持たずにギズモ編集を得られます。編集される Transform が動的な親(たとえばアバターのソケット)に対する相対で表されるオブジェクトは、移動したIManipulatorTargetを実装して自身の座標系を記述します。これは意図的に画像メンバーではなくルートにしています:セッションはダイアログごとのサーバー状態であり、フレームはそれを描画した正確なポーズとともに運ばれなければならず、ポーズの書き込みはエディタ書き込みスコープを通してはなりません(視点はシーンの状態ではないので、シーンや Undo スタックを dirty にしてはいけません)。 - 画像メンバー:ライブオブジェクトが汎用プロパティの面から直接画像を提供できます。 新しい
LiveImageData型(エンコード済みバイト列 + MIME タイプ)の、ゲッターのみの[LiveProperty, ImagePreview]メンバーを宣言すれば、専用のルートは不要です:直接のGET /live/object/{id}/{path}は正しい Content-Type で生のバイト列を返し、JSON 形のあらゆる読み取り——オブジェクト全体のシリアライズ、単一プロパティの読み取り、/live/batch——はメンバーをまさにそのアドレスに畳み込み、ゲッターを呼び出しません。そのため、オブジェクトを一覧しても画像が描画されることはありません。リモートアプリの既存の ImagePreview コントロールは値が持つアドレスをポーリングするので、宣言が自己記述的になり、クライアントは汎用になります。ゲッターは画像リクエストごとにメインスレッドで 1 回実行され、キャッシュせずに描画・エンコード・返却すべきです——クライアントのポーリングがリフレッシュループです。無効なLiveImageDataを返すと 404 になり、これはアセットサムネイルのルートと同じ「プレースホルダーにフォールバック」の約束です。画像メンバーはchangedを報告せず(画像は基準値の無い派生状態であり、比較するとポーリングごとに 1 フレーム描画することになります)、永続化もされません。文字列メンバーに付けた[ImagePreview]は従来の意味のままです——値はほかで提供されるアドレスで、ゲッターは通常どおり呼ばれます。トップレベルのメンバーでのみサポートされ、入れ子の複合値の中では、畳み込むアドレスが無いため値は null に劣化します。 GET /live/type/{name}とGET /live/enum/{name}——1 つの定義を直接取得。/live/typesと/live/enumsの単数形で、/live/object/{id}が/live/objectsに対するのとまったく同じ関係です:レスポンスは要素 1 つのコレクションではなく配列要素そのもので、該当しない名前は空リストではなく 404 になります。1 つの型が古くなったと知らされた(/live/changesが疑似 id@typesを報告した)クライアントや、見たことのない型に出会ったクライアントは、テーブル全体を取り直す代わりにその 1 つの定義を問い合わせられます。複数形のエンドポイントとその?type=フィルターは変わらないため、移行は不要です。型名は識別子なので、ここでは名前をパスに入れます。アセットキーは別の方法で同じ形に到達します(上のエントリを参照)。キーはスラッシュを含み、単一のパスセグメントにできないためです。GET /live/asset/{key}/@image——/live/asset/{key}が既に返す名前と型に加えて、アセットの画像。 アセットにラベルを付けられるクライアントは、ホストアプリケーションが独自の画像ルートを提供しなくても、それを表示できるようになりました。画像はそれ自体のルートではなくアセットの疑似メンバーで、オブジェクトの@parent疑似プロパティと同じ綴りです。キーが URL の末尾まで続くためです(上のエントリを参照)。バイト列の出所はアセットの所有者に委ねられます:AssetRegistry.SetThumbnailProviderが注入用のフックで(既存の名前フォールバック/カタログ事前ウォーム/グループ展開のフックと同じ形)、キーはそのまま渡されるため、レジストリのキーだけでなくアプリケーション固有の参照でも機能します。プロバイダーが無い場合——または画像の無いアセットの場合——応答は 404 で、クライアントは既に「プレースホルダーにフォールバック」として扱っています。これは LiveStudio の/live/avatar/imageと/live/snapshot/imageを置き換えます。どちらも、アセットであるものの画像をアプリケーションが提供していたものでした。エンドポイントを 1 つここへ移すのはワイヤの破壊的変更で、互換用の仕組みなしで出荷されます。PersistScope.Custom:これを宣言したメンバーは所有者が自身のファイルに書き込むため、ライブシーンもプロジェクト設定もそれを運びません。スコープフィルターは既にメンバーをどちらかのファイルに振り分けていましたが、状態がそれ自体アセットである機能(LiveStudio のデッキファイル)には、各書き手での個別の除外ではなく、3 つ目の答え——「ここではない」——が必要でした。そうした所有者は自身の基準値も持ち、ユーザーが編集したときに書き込むため、ほかの誰もそれが古くなったタイミングを知る必要がありません。逆シリアライズはスコープを無視するため、メンバーがCustomに移る前に書かれたファイルでも復元されます。LiveObjectSnapshot.Capture(handle, scope)は、別アセンブリから 1 つのスコープをシリアライズするための公開手段です。- エディタツールバーのサーバートグル。Live Studio のリモートアプリボタンの左にあります(Material Symbols の "devices" アイコン。停止中はグレー、待ち受け中は緑、ツールチップにポート)。サーバーはスレッドベースなので、プレイモードと同様にエディタを開いただけの状態でもリモートアプリに応答します——このボタンは、Remote Control Server ウィンドウを探さずにそれをオンにするものです。開始すると読み込まれたシーンの
RemoteControlBehaviourホストにバインドされるため、シーンがエディットモードで登録するライブオブジェクトをすぐに閲覧できます。サーバーをウィンドウで構成しているプロジェクトは、そちらから開始されます(クリックの副作用で設定アセットが作られることはありません)。停止はサーバーを保留するのではなく破棄し、ツールバーの停止状態を「一度も開始していない」と同一に保ちます。プレイモード実行中はアプリケーションがサーバーを所有する——アプリ独自のルートはインスタンスとともにあり、ツールバーから再登録できない——ため、ボタンは切り替えではなく状態の表示になり、ツールチップにそう表示します。 LiveClassAssetによる任意のオブジェクトの属性なし公開。[LiveClass]のアセット版です:シーン上の任意のコンポーネント(組み込みでもサードパーティでも。ScriptableObject などのアセットは将来の対象として想定)を、コードを変更せずにリモートアプリへ公開できます。公開設定はシーン間で共有されるLiveClassAssetにあります:型ごとのメンバー定義(ラベル、ヘルプテキスト、永続化メタデータ、多態な[SerializeReference]のコントロール選択——SliderControl、HideControl、LiveBindingControlを派生させて拡張可能——を持ち、型レベルのLiveClass登録の唯一の出所なので、インスタンスごとのメンバーセットの曖昧さはありません)と、安定した GUID キーだけを持つインスタンスバインディングのエントリです。シーン側はRemoteControlContainer自体で、標準のIExposedPropertyTable(PlayableDirector が使うのと同じ仕組み)を実装します:キーをシーンオブジェクトへの直接参照に対応付け——名前変更や階層移動に強い——解決された各エントリをランタイムのLiveBindingILiveObject に変換するため、一覧、編集、ライブシーンの保存/復元はすべてそのまま動きます。属性なし公開は付属コンポーネントではなくコンテナの基本機能です:バインディングが加わるオブジェクトリストと、それらを適用・破棄する有効化/無効化はコンテナ自身のものです。バインディングキーは LiveObject の id を兼ねるため、アセットを共有するシーン間で永続化された値が安定します。セットバンドルは独自のコンテナを運ぶため、アセットとそのシーン参照はバンドルとともに移動し、読み込み時に適用・アンロード時に破棄されます——LiveClassAssetSystem.UnregisterTypesは、型を定義する最後のコンテナが去るときに登録を取り消し、次のバンドルに何も残しません。RemoteControlBehaviourはRemoteControlContainerになり(独自のオブジェクトリストとアセットは基底クラスのもの)、読み込まれたシーンのほかのコンテナを自身の上にマージします。ランタイムのバインディングラッパーは意図的にシリアライズされる_objectsリストの外に置かれ、独自のソースとしてマージされます:_objectsは[SerializeReference]なので、そこに置かれたラッパーは保存のたびにシーンファイルに書き込まれ——[ExecuteAlways]の下では編集しているだけで存在します——空のLiveBindingエントリとして戻り、保存ごとに 1 セットずつ溜まっていきました。クラスは新しい Window ▸ Lilium Remote Control ▸ Live Class Asset パネルで選びます:「Add Class」は検索可能なドロップダウン(名前空間でグループ化)から Component/ScriptableObject クラスを選び、「From Selected」は現在選択中の GameObject 自身のコンポーネントから直接選びます——どちらもクラスリストに空の型定義を追加します。続いて「Add Member」がそのクラスの公開可能なプロパティ/フィールド/メソッドのチェックボックスリストを別ウィンドウで開きます。開いたままにできるので、ドロップダウンで 1 つずつ選ぶのではなく、一度に複数を切り替えられます。ヘッダーにはアセットとコンテナがあり、フッターのインスタンスバインディングのリストは、コンテナが割り当てられるまで完全に非表示です(コンテナが無ければバインド先が無いため)。新しく公開したメンバーのラベルは、既定で整形されたメンバー名になります(useColorTemperature→ "Use Color Temperature")。LiveClass.Register(Type, ...)——メソッドを公開するためのLiveFunctionDefine[]も受け取る、非ジェネリックの属性なし登録。LivePropertyDefineに、メンバーの属性より優先される定義側のメタデータ上書き(control、label、help、section)が追加され、LivePropertyType/LiveFunctionTypeのコンストラクタは対応する上書きを受け取ります——属性なしの呼び出し側(バインディング)が UI メタデータを渡すための登録経路です。GET /live/changesは、あるリビジョン以降に変更されたライブオブジェクトを id のみで報告します。LiveChangeLogはイベントのキューではなくオブジェクト id ごとの最新リビジョンを保持するため、メモリは異なるオブジェクトの数で上限が決まり、どれだけ離れていたクライアントでも見逃した集合をちょうど受け取れます——「カーソルが末尾から落ちた」ケースから回復する必要はありません。GET /api/events?since={lastEventId}は、クライアント宛てにキューされた一回限りの通知——トースト通知と確認プロンプト。状態を読み直しても回復できない唯一のものです——を渡します。キューはクライアントごとに独自の読み取り位置を持つため、ポーリングを逃しても通知は落とされず遅れるだけです。POST /live/batchのサブリクエストとしても解決でき、リモートアプリはこの方法で受け取ります:表示中の値のために既に送っているポーリングに相乗りするので、受信箱のための追加の往復はかかりません。/api/statusはサーバーが再構築されるたびに変わるinstanceIdを報告し、ポーリングすると呼び出したクライアントが在席扱いになります。接続を開いたままにしないと、ポーリングがたまたまそのまま通過した再起動に気づくものが無く、クライアントはもう存在しない状態を記述するカーソルと型テーブルを持ち続けることになります。IFrameScheduleとIFrameClockSync——フレームバリアの 2 つの半分。IFrameScheduleを実装するソースは、次のフレームがどのクロックフレームで期限を迎えるかを示します(PacedFrameSourceが実装)。IFrameClockSyncを実装するクロックは待機できます(RealtimeFrameClockが実装。時間に余裕がある間はスリープし、残りはスピンします)。LTC のような外部同期ソースがペース配分を引き継ぐのはここです:自身のティックで待機を実装し、FrameGate.SetClockでインストールします。どちらも任意です——オフラインの再描画用のFrameCounterClockのように待機できないクロックは、単に待たれません。
Changed
-
⚠
[LiveFunction]は既定でフレームに乗りません:lane = FrameLane.Eventと宣言しない限り、呼び出しは記録されなくなりました。 呼び出しがテイクに届くことに依存しているものには破壊的変更です。メンバーはどこに保存されるかからレーンを導出します——ライブシーンが保持するものをフレームが運ぶ——が、メソッドには導出元となる永続化が無く、シグネチャのどこにも、それがワールドを変えるのかマシンを変えるのかは書かれていません。どちらの既定も一部の呼び出しには誤りなので、選ぶべきは誤ったときのコストが小さい方です。既定で記録すると、フォルダを開き、スナップショットを復元し、シーンファイルを書き込むリプレイという代償がありました。記録しない場合の代償は、テイクから一瞬が欠けることです。前者は、別の場所の記録によってマシンが操作されることです。古い既定が誤りだったことは、それが繰り返し必要とした修正に既に表れていました:
OperationManager、FrameRecorderPage、ExternalAssetManagerは、呼び出しが記録されないよう、それぞれ丸ごとフレームから外されていました([LiveClass(lane = None)])。効果がフレームが既に運ぶメンバーに現れる呼び出しには何も要りません——リプレイが読むのはそのメンバーであり、呼び出しも記録すると同じ変更を 2 回書くことになります。
SetActiveSetはactiveSetに、WarpToはアバターのアンカーのtransformに、LiveCamera.Switchはpriorityに、AvatarChair.Activateはそのrest*フィールドに届き、いずれも既に state レーンにあります。効果がどのレーンにも痕跡を残さず、呼び出しが唯一の記録である場合はlane = FrameLane.Eventと宣言します——AvatarController.ResetPhysics(効果はシミュレーションが意図的に保持しないボディに及びます)、MultiSceneManager.CreateScene/UnloadScene/SetActiveScene(そのscenesは構造体の配列なので、ブロック(unmanaged でない)にも structure レーン(参照型でない)にも届きません)。ObjectFactoryBase.DestroyObjectは、以前は既定に頼っていたところでEventと宣言するようになりました。これは structure レーンが届かないものに届きます——そのレーンは自分が立ち上げたものしか取り除かないため、テイク開始前からシーンにあったものの削除は、この呼び出しだけが運びます。⚠ 保持すべきだった呼び出しを検出するものはありません。 運べないレーンを宣言したメンバーには診断が出ますが、テイクから黙って欠けている呼び出しは、リプレイして初めて見つかります。宣言がガードのすべてです——上の
DestroyObjectは呼び出しを仕分けたときに見落とされ、それを捕まえたのは、削除ボタンがライブデータに残ることを確認するテストでした。 -
⚠
EventKindはSetとCallの 2 値になりました:PropertyWriteはSet、FunctionCallはCallになり、StructureChangeとRegisteredSourceは廃止されました。 これらのメンバー名を使うものには破壊的変更ですが、修正は機械的です。種別を読むのはこれまで 2 箇所だけでした——繰り返しの書き込みを数えるFrameGateと、シークで何を畳み込むかを決めるFrameReplayer——そしてどちらも同じ問いを発していました:この記録はターゲットごとに最後の 1 つへ畳み込めるか、それとも回数と順序に意味があるか。今ではその問いが enum の内容のすべてで、意図的に HTTP の動詞でもイベントが何をするかでもありません:作用する対象をターゲットが名指す冪等な呼び出しはSetに属します。2 回リプレイしても何も変わらず、畳み込むことでシークがそこに着地できるためです。⚠ id でアドレスされるだけでは不十分です——畳み込みはターゲットだけをキーにし、動詞もペイロードも読まないため、id は ターゲットの中に なければなりません。廃止された 2 値は、このレーンが一度も答えなかったこと——1 つはイベントが何をしたか、もう 1 つはそれがどこから来たか——を問うもので、それらは既にverbId、sourceId、EventFlagsが運んでいます。RegisteredSourceを書いたものはありませんでした。StructureChangeを書いていた 6 つのルートのうち 5 つは、呼び出しになりました(シーンのエクスポートとインポート、孤立オブジェクトの削除、マニピュレーターの開閉)。FunctionCallの外にあったためにそれらは畳み込みの対象になっており、ターゲットが定数パスのremoveOrphanはシークをまたいで最後の呼び出ししか適用されず、ターゲットを共有し動詞だけが異なるマニピュレーターの開閉は 1 つに潰れていました。⚠@parentはSetのままです:structure レーンはparentIdを記録しますが適用はしない(ApplyFromは作成・破棄と要素の調整だけを行う)ため、このイベントの畳み込みがシークで階層を復元する唯一の経路です——ターゲットは 1 つの子の@parentなので、畳み込めばちょうど「各子が最終的にどこに落ち着いたか」が得られます。記録フォーマットは変わりません。 種別は同じオフセットにある 1 つの int のままで、取り得た 4 値のうち 2 つが書かれなくなっただけです。読み手は廃止された値を対応付けます(
2はCallに、未知の値はSetになります)。 -
⚠ テイクのフォーマットはバージョン 1 から公開を始めます:
FrameRecord.kVersionは 1 です(以前は 8)。 ライブデータはこのリリースで初めて出荷されるため、開発中にフォーマットが経た世代は公開版に持ち込みません。バイトレイアウトは変わらず、番号がリセットされるだけです。開発ビルドで収録したテイクは、移行されずに開く時点で拒否されます——収録し直してください。Unreal 側も同じレイアウトを書き、足並みを揃えてリセットするため、テイクは引き続きどちらのエンジンでも開けます。 -
⚠ どのインスタンスを公開するかはライブクラスアセットではなくシーンが示します:
LiveClassAsset.bindingsは廃止されました。 コンテナ自身のオブジェクトリストのエントリ——オブジェクトを指し、公開される id を持つLiveComponentまたはLiveAsset——が、オブジェクトに独自の id を与える唯一の方法になりました。インスタンスバインディングを持つアセットと、それを供給していたシーン側には破壊的変更です:RemoteControlContainerはIExposedPropertyTableを実装しなくなり(SetReferenceValue/GetReferenceValue/ClearReferenceValue/ResolveKeyは削除)、bindingObjectsとLiveBindingラッパーもなくなり、LiveObjectContainer.ResolveAllReferences/RebindLiveObjectとResolveReferences(IExposedPropertyTable)のオーバーライドも削除されました——これらには呼び出し元がまったくありませんでした。すべての参照はコンテナがアセットを適用するときに即座に解決され、テーブルを経由することはなかったためです。バインディングを移行するには:コンテナのオブジェクトリストにエントリを追加し、同じオブジェクトを指し、バインディングのキーを id として与えます。id は live.json がアドレスするものなので、それを保てば保存済みの値も保たれます。理由は、バインディングが 1 つの事実を 2 つのファイルに持っていたことです。アセットはキーと想定型を、シーンはキー → オブジェクトを持ち、両者をつなぐのはキーだけだったため、どちらか一方だけを編集すると組が黙って壊れました——しかもアセット側の半分はほぼ冗長で、シーンのテーブルが既にキーを持っていました。アセットはまた、プロジェクトレベルの宣言にもなっていました(Project Settings はすべてのシーンに適用されるアセットを指定し、パッケージも 1 つ同梱します)。その経路にはコンテナがまったく無いため、そこでのバインディングは決して解決できませんでした:シーンに依存しない宣言にぶら下がる、シーン固有のインスタンスです。オブジェクトリストなら、ほかのすべての公開オブジェクトが既に名指されているのと同じ場所で、参照の隣に id を置いて 1 度で示せるため、壊れた組が目に見えます。
ワイヤは何も変わりません。エントリはバインディングと同じく対象をその型で登録するため、
@id、@type、メンバーは従来どおりで、これ以前に保存したライブシーンもこれ以降に読み込めます。Live Class Asset ウィンドウからは Instance Bindings のフッターと「Add Binding」ボタンがなくなります——通常のケースでは、クラスを宣言することが常に仕事のすべてでした。オブジェクトリストにある GameObject のコンポーネントは、宣言だけで公開されるためです。 -
⚠ 複数のレシピリゾルバーを同時に置けます:
LiveRecipes.RegisterResolverはAddResolverになり、取り外すRemoveResolverが加わりました。 旧名の唯一の呼び出し元(このパッケージ内)には破壊的変更です。リゾルバーは、キーで登録されていないメーカーについて「このキーは何でありうるか」に答えるもので、枠はちょうど 1 つでした——その答えの所有者が 1 人であることを前提にしていたわけです。実際はそうではありません:既にメモリにあるプレハブと、先にバンドルから読み出さなければならないプレハブは所有者が異なり、前者が枠を占めている間、後者は存在できませんでした。リゾルバーは追加された順に問い合わせられ、最初の非 null の答えが採用されるため、各リゾルバーは自分が所有しない範囲のキーには null を返さなければなりません——何にでも答えるリゾルバーは、ほかを黙らせるリゾルバーです。PrefabRecipeはこれを守るようになりました:PrefabRegistryがそのプレハブを今見つけられるキーにだけ答え、キャッシュを信用せず毎回の検索で確認し直します。そのため、その後アンロードされたプレハブのためにキャッシュされたメーカーが、いつまでも空の答えを返し続け、それを再取得できるリゾルバーに順番が回らない、ということがなくなりました。 -
⚠ リモートからシーンに追加できるプレハブは、UI 定義ではなくライブクラスアセットで宣言します。
LiveClassAsset.prefabsは提供するプレハブごとに 1 エントリを持ちます——ページが以前持っていたのと同じILiveObjectFactoryと、それを提供するページのカテゴリです——そしてStandardObjectFactory.factoriesはなくなりました。ファクトリーリストを持つ UI 定義には破壊的変更です:そのエントリはライブクラスアセットへ移す必要があり、古いリストは次にアセットが書き込まれるときに破棄されます。理由は、プレハブのリストがページのプロパティだったことは一度も無いことです。それはアプリケーションが何を立ち上げられるかを示すもので、ライブクラスアセットが型について既に示しているのと同じことであり、ページがそれを一覧するかどうかにかかわらず成立していなければなりません——保存された@prefabは、どのページが描画されるよりずっと前に、読み込み時にPrefabRegistryから再構築されます。アセットを適用すると、型を登録するのと同じ操作でそのプレハブもそこへ登録されるようになったため、バンドルとともに届いたアセットは生成できるものを一緒に持ち込み、アンロード時には提供をやめます。ページの「+」はカタログ(LivePrefabCatalog)を読みます:シーンページは宣言されたすべてのプレハブを、カテゴリページはそのカテゴリを宣言したものを提供します。プレハブの GUID はアセット自身のOnValidateで解決されるため、UI Designer の Reset はプレハブキーを更新しなくなり、UIDefinitionPrefabKeyRefresherは削除されました。 -
所有者が自分で保存するメンバーについて、型テーブルが
"persistScope": "custom"と示すようになりました。 プロジェクト設定に書かれるメンバーには既に"project"と示し、ライブシーンは既定として省略していましたが、所有者が保存するメンバー(デッキ)はシーンのものと区別できませんでした。保存先でオブジェクトを振り分けるクライアント——シーンファイルが持つものを表示するライブシーンページ、マシンが持つものを表示するプロジェクトページ——には 3 つ目の答えが必要で、そうでないと所有者保存のメンバーがシーンのものとして通ってしまいます。レスポンスのほかの部分は変わりません:このキーはPersistScope.Customと宣言されたメンバーにだけ現れます。 -
収録したテイクは
*.live.binとして保存されます。 ほかの複合拡張子がすべて中身を表している(.set.lsb、.prop.lsb、.deck.json)プロジェクトの中で、.livedataだけが浮いていました。形式は変わっておらず、変わるのは新しいテイクを書き出すときの名前だけです。.livedataとして収録したテイクは、両方の拡張子を列挙するようになった LiveData Sequencer と収録ページで、引き続き一覧に表示され、再生できます。 -
event レーンの名前は
InputではなくEventです:InputFrameはEventFrame、InputRecordはEventRecord、FrameLane.InputはFrameLane.Eventになり、ほかに 11 個が改名されました。 これらの型名を使うものには破壊的変更ですが、修正は機械的です。「Input」は 3 つの異なるものを意味するようになっていました——FrameSource(フレームに供給するもの)、このレーン、そして LiveStudio 独自のオペレーション用InputSource——そして 1 つ目は、このレーンと同じくらい頻繁に state レーンに供給します(キャプチャーのポーズと時間はどちらも宣言されたソースから届くため)。設計を説明するのに「入力ソースはこれを入力レーンではなく state レーンに置く」という文が必要になったのは、言葉が機能しなくなった兆候です。2 つ目の理由は、このレーンが入力ストリームのように振る舞ったことが一度も無いことです:プロパティの書き込みは自身の値を持つため、リプレイに再計算は要りません。起きたことの疎なログであり、それはイベントそのものです。FrameSourceとFrameGateは名前を保ちます——入力の源は引き続き入力の源です。InputSymbolTableは、両方のレーンが使う(state ブロックも型名をそこに登録する)ため、ついでにFrameSymbolTableになりました。記録には影響しません——エントリとイベントの種別は数値として書かれるため、フォーマットは 1 バイトも動きません。 -
フレームの型が新しいアセンブリ
Lilium.RemoteControl.Simulationに移りました。 フレームの型を名指すアセンブリには破壊的変更です:参照を追加してください。シミュレーションはライブデータが定義される対象の状態を持ち、ビュー(GameObject、Animator、Transform)はそれを提示します。決して存在してはならない唯一の方向は、シミュレーションがビューから読み戻すことです。評価済みのTransformから読んだ値は記録から再現できないためです。ここで引いた境界は依存の方向です——新しいアセンブリはLilium.RemoteControlを参照しないため、ライブオブジェクト、レジストリ、ブリッジに到達できず、内部からオブジェクトグラフに入る手段はありません。エンジン参照は保たれます:ネイティブコンテナとジョブシステムは UnityEngine にあり、このレイヤーはそれらの上で動くためのものです。新しいアセンブリにあるもの:フレーム構造体、state / structure ブロック、記録の フォーマット(FrameRecord、FrameRecordReader、FrameRecordWriter)。残るもの:FrameGate(PlayerLoop にフックする)、ブリッジ、記録の ドライバー(FrameRecorder、FrameReplayer、FrameRecordPlayer)、そして公開の面全体。2 つのルールはコンパイルエラーではなく規約のままです——シミュレーションからビューの出力を読まないこと、そして時間はUnityEngine.Timeや壁時計ではなくライブデータから読むこと。 -
FrameLaneはフレームの型と一緒ではなく、Lilium.RemoteControl.Coreに置かれました。 これは[LiveField(lane: ...)]と[LiveClass]に現れるため、新しいアセンブリに置くと、公開を 宣言する だけのアセンブリすべてにシミュレーションへの参照を強いることになります。既に Core を参照している呼び出し側は変更不要で、型と名前空間(Lilium.RemoteControl)は変わりません。 -
⚠
lane = FrameLane.Stateを宣言するアセンブリはLilium.RemoteControl.Simulationを参照しなければなりません。 生成される state ブリッジがそこにある型を名指すためです。ジェネレーターはこれを確認して、型と修正方法を示すLRC004を報告し、作者が書いていない生成コード内のCS0246として失敗が表面化する代わりに、state ブロックの生成を控えます。 -
⚠ サーバーは独自のアセンブリ
Lilium.RemoteControl.Serverに置かれました。 HTTP サーバー、ルートハンドラー、プロトコル、通知と確認、RemoteControlBehaviourホスト、FrameRecorderControllerがLilium.RemoteControlとLilium.RemoteControl.LiveSceneから移動したため、ドメイン——ライブオブジェクト、レジストリ、シリアライズ——はもう通信を抱えていません。これらの型を名指すアセンブリには破壊的変更です:参照にLilium.RemoteControl.Serverを追加してください。移動したファイルは名前空間を保つため、ほかに変更は要りません。 -
Project Settings ページは
Project/Lilium Remote Controlになりました(以前はProject/Remote Control)。設定ツリーや検索で、Unity 自身のページではなくこのベンダーのページとして読めるようにするためです。 -
Live Class Asset ウィンドウは、パッケージのほかのすべてのウィンドウと同じく UI Toolkit で構築され、ウィンドウは 1 つのスタイルシート(
Editor/Styles/RemoteControlEditor.uss)を共有します。メンバーの Control はドロップダウンで、そのフィールドは下に入れ子になります。Auto のままのメンバーは、それが落ち着いたレーン(シーンがそれを保存するか、型がバイト列として移動できるか)を説明します。 -
UI Designer に RemoteApp の起動/終了ボタンを表示しなくなりました。何もしないボタンでした。
-
Service<T>.subjectsは読み取り専用のビュー(IReadOnlyList<T>)になり、ブロードキャストは新しいService<T>.ForEach(action)を通します。 生のList<T>は public かつ変更可能だったため、どの呼び出し側もRegister/Unregisterを迂回でき、コールバック内でサブジェクトが自身を登録解除した瞬間にsubjects.ForEach(...)がコレクション変更例外で壊れていました。ForEachはプールされたスナップショットを反復するため、サブジェクトはブロードキャスト中に安全に登録・登録解除(自分自身も含む)できます。subjectsに対する読み取り専用の LINQ は変わりません。リストを直接変更していたコードや、それにList.ForEachを呼んでいたコードにのみ破壊的変更です——後者はService<T>.ForEachに置き換えてください。 -
SingletonService<T>.subjectは読み取り専用プロパティになりました。登録は引き続きRegister/Unregisterを通します。デバッグアサートも二重登録で実際に発動するようになりました——以前はobj != nullをアサートしており、実際には決して false にならない一方、メッセージは既に登録済みのサブジェクトについて述べていました。リリースビルドでの登録時の上書き動作は変わりません。 -
SelectableService<T>の id→subjects 辞書は private になりました。Register/Unregister/Select(変更なし)を使ってください。 -
プロパティのリセットは
POST /live/object/{id}/{path}/@resetになりました。 破壊的変更で、互換用の仕組みはありません。素のresetは、たまたまresetという 名前の メンバーに対する配列追加のルートと曖昧でした——POST /live/object/1/resetはどちらの意味にもなり得て、ルートテーブルの順序だけがそれを決めていたため、そうしたメンバーには決して追加できませんでした。@は実際のメンバー名の先頭には来られないため、疑似メンバーの綴りは曖昧さに優先順位を付けるのではなく曖昧さそのものを取り除きます。オブジェクトの@parent、アセットの@imageと揃えています。バッチエンドポイントも同じ綴りを受け付けます。 -
アセットはクエリパラメーターではなくパスで名指すようになりました:
GET /live/asset/{key}がGET /live/asset?guid=を置き換えます。 ワイヤの破壊的変更で、互換用の仕組みなしで出荷されます。オブジェクトは常にGET /live/object/{id}で、アセットのエンドポイントだけが対象を別の方法で名指す最後のものでした。キーは、/live/object/{id}/{path}のプロパティパスとまったく同じように URL の末尾全体を取ります——内部のスラッシュは保たれるため、ファイル参照(file:C:/props/chair.lsb)もエンジンのアセットパス(/Game/Props/Chair.Chair。URL にスラッシュが 2 つ並びます)も往復で壊れません。キーが末尾まで続くため、サブリソースはどのキーも末尾に持たない名前でしか区別できず、それが@image疑似メンバーです。オブジェクトの@parent疑似プロパティと同じ綴りで、image単独ではその名前のフォルダで終わるキーと曖昧になります。クライアントは、キー内の#と?(生の#はフラグメントの区切りでサーバーに届きません)とリテラルの%をパーセントエンコードし、/はそのままにしなければなりません。..を含むキーは、届く前に URL の正規化で書き換えられることに変わりはありません。パッケージ内での帰結が 2 つあります:アセットのルートは、エスケープされた#を既に落とし二重スラッシュを潰しているUrl.AbsolutePathではなく、生の リクエストパスでマッチ・解析します。また、リゾルバーが/live/asset/接頭辞全体を取るため、画像ハンドラーはリゾルバーより先に登録しなければなりません。 -
このパッケージが提供するすべてのルートが
/api/*と/ui/*から/live/*へ移りました。 RemoteControl はライブラリで、そのハンドラーはホストアプリケーション自身のものと 1 つのサーバー・1 つのルートテーブルを共有します——そのため/api/の下で汎用名を 1 つ取るたびに、アプリケーションがその名前を二度と使えなくなり、取っていた名前(status、events、language、commands、asset)は、まさにアプリケーションが最初に使いたくなる名前でした。衝突はツリー内で既に実証されていました:1 つの Unity プロセス内の 2 つのサーバーが、それぞれ独立に/api/commandsを選んでいたのです。/api/は完全にホストに委ねられました。/api/status→/live/status、/api/events→/live/events、/api/confirm→/live/confirm、/api/performance→/live/performance、/api/language→/live/language、/api/commands/quit→/live/commands/quit、/api/commands/reset→/live/commands/reset、/api/asset→/live/asset、そして Web UI の/ui/sidemenu//ui/info→/live/ui/sidemenu//live/ui/info。ライブオブジェクトの面(/live/objects、/live/object/…、/live/types、/live/batchなど)は変わりません。/live/直下はパッケージ用に予約されています。そこにルートを置きたいアプリケーションは、将来のパッケージのルートと衝突しないよう 1 階層入れ子にしてください(/live/app/*)。ワイヤの破壊的変更で、互換用の仕組みなしで出荷されます——クライアントも一緒に移行します。 -
ライブシーンのルートは
/live/scene/*の下にまとめられました。export、import、orphansは/live/直下にあり、それぞれ特定の対象の無い動詞として読めていました——「export」は何をエクスポートするのかも、2 つのファイルエンドポイントと孤立オブジェクトのリストが 1 つのハンドラー(LiveSceneIoHandler)が提供する 1 つの機能であることも示しません。ライブシーンはパッケージが提供するいくつかの対象の 1 つなので、ほかと同じく独自のセグメントを持ちます:/live/export→/live/scene/export、/live/import→/live/scene/import、/live/orphans→/live/scene/orphans、/live/orphans/remove→/live/scene/orphans/remove。リクエストとレスポンスの本文は変わらず、シーンのファイルフォーマットにも影響はありません——パスだけが移ります。破壊的変更で、互換用の仕組みはありません。 -
サーバーの生成を config アセットから移しました:
RemoteControlServerConfig.CreateServerはなくなり、RemoteControlServerManager.GetOrCreateServerがインスタンスを自ら構築・配線・登録します。config は純粋なデータになり、2 つの経路がそれぞれ持っていた「このポートには既に存在する」チェックの重複もなくなりました。 -
サーバーの開始時に、在席リストをクリアして "Remote Control Server started" をブロードキャストしなくなりました。ブロードキャストは誰にも届かず(新しいサーバーにはまだ受信箱が無い)、クリアはクライアントを追跡する 2 箇所のうち 1 箇所にしか触れていませんでした。在席は代わりにステータスポーリングのタイムアウトで自己回復します。
-
使われていない接続周りのコードを削除しました:
HttpServerCore.IsPortAvailable、RemoteControlServerManager.GetServer/GetAllPorts、RemoteControlServerCore.SendToClient、EventQueue.SendToClientsAsync/ClearEvents/GetStats/GetActiveClientIds(およびTargetedMessage/DirectMessage/EventQueueStatsの DTO)、そしてRestApiConnectionManagerの統計の面(ProcessClientMessage、GetStats、クライアントごとのメッセージ/応答時間の追跡、GetClient/GetActiveClients/GetAllClients/GetActiveClientIds)。どれも呼び出し元がありませんでした。クライアントの記録は、今では識別情報と最終アクティビティだけです。 -
「Exposed」の語彙をすべて「Live」に改名しました。 このシステムはとうに単に「リモートに公開される」だけのものではなくなっていました——同じプロパティセットが、LiveScene が永続化し、オペレーションがバインドし、リモートアプリが編集するものです——そのため名前を LiveStudio / LiveScene /
live.jsonに合わせました。属性:[ExposedClass]→[LiveClass]、[ExposedProperty]→[LiveProperty]、[ExposedField]→[LiveField]、[ExposedFunction]→[LiveFunction]、[ExposedEnum]→[LiveEnum]、[ExposedExternalEnum]→[LiveExternalEnum]、[ExposedKey]→[LiveKey]、[ExposedDefault]→[LiveDefault]。[ExposedHelp]は[Help]に簡略化され(装飾用の属性は接頭辞を持ちません)、[FormerlyExposedAs]は[FormerlyNamedAs]になりました。すべてのExposed*型とファイルはそれに応じてLive*になり(ExposedObjectHandle→LiveObjectHandle、ExposedObjectRegistry→LiveObjectRegistry、ExposedPropertySerializer→LivePropertySerializer、「ExposedObjects Viewer」ウィンドウ → 「LiveObjects Viewer」など)、SendMessageフックのOnExposedChanged/OnExposedResetはOnLiveChanged/OnLiveResetになり、ソースジェネレーターは Live の属性名にマッチします。REST のルートは/exposed/*から/live/*へ移ります(objects、object、types、enums、changes、batch、function、export、import、orphans)——意図的なワイヤの破壊で、リモートクライアントは足並みを揃えて更新されます。保存データは引き続き読み込めます:組み込みのプロキシの typeName(GameObject、Asset、Componentなど)は "Exposed" を含んだことがなく、Unity でシリアライズされる改名には[MovedFrom]が付いています。 -
[LiveValue]を削除しました。表示中プロパティの同期が既に表示中のすべてのプロパティをポーリングしているため、プロパティごとの専用ポーリングコントロールにはもう役割がありませんでした。読み取り専用のメンバーは単純な読み取り専用の値として描画され、通常の同期で更新されます。 -
パッケージのアセット作成メニューを 1 つの Live Studio ▸ Remote Control サブメニューにまとめました。
Assets ▸ Createはそれらを 2 つのルート——Remote Control(UI Definition、Server Config)とLilium ▸ Remote Control(Live Binding Preset)——の下に並べており、どちらも一緒に出荷される Live Studio パッケージと並んでいませんでした。メニューパスはコマンドの表示場所にしか影響せず、既存のアセットとそのシリアライズされた型は変わりません。 -
プロパティの変更を SSE でプッシュしなくなりました。
LivePropertyBroadcastは、値をシリアライズして接続中の全クライアントに配る代わりに、変更されたオブジェクトの id をLiveChangeLogに記録します。ブロードキャストは各クライアントが何を見ているかにかかわらず全クライアントに送られ、値は誰も表示していなくても変更ごとに 1 回シリアライズされていました。今はクライアントが変更フィードをポーリングし、自分が持つオブジェクトだけを取り直します。入口とその呼び出し箇所は変わりません。 -
types_updateとui_updateイベントは、同じ変更フィード内の疑似 id@typesと@uiに置き換えられ、キャッシュの無効化もほかのすべてと同じ経路を通るようになりました。 -
リモートアプリへは何もプッシュしなくなりました。SSE ストリーム(
/api/stream)とそのロングポーリングの仕組みはなくなり、通知と確認プロンプトは上記のクライアントごとの受信箱を通ります。EventQueueはキューと読み取り位置を保ち、それらを吸い出していた開きっぱなしの接続だけが置き換えられました。 -
「リモートアプリが接続しているか」(確認プロンプトの表示先があるかを決める)は、「直近 5 秒以内にポーリングしてきたか」になりました——接続確認間隔の 5 倍です。これより短いと、スマートフォンの応答が遅いたびにプロンプトが落ちてしまいます。イベント受信箱は、別の問い(クライアントが戻ってきたときに渡すものが残っているか)に答えるため、独自のずっと長い保持期間を持ちます。
-
GetClientIdは TCP エンドポイントよりも、クライアントが宣言したX-Client-IDヘッダーを優先します。受信箱の配達は別々のリクエストをまたいで続かなければならず、送信元ポートはそうではないためです。 -
/api/heartbeatを削除しました。どのルートにも登録されたことがなく、それが運ぶはずだったクライアントの生存確認は、今ではステータスポーリングから得ています。 -
タイマーでブロードキャストされるものはなくなりました。最後のそうしたプロデューサーである表情ウェイトの更新は、各クライアントがどのページを開いていても、アクティブなすべてのウェイトを接続中の全クライアントへ毎秒 10 回送っていました。リモートアプリは今では通常の表示中プロパティの同期で
AvatarController.expressionsをポーリングするため、表情カードが閉じている間ウェイトはコストがかからず、見ているクライアントにだけ届きます。 -
FrameRecorderControllerはLiveDataRecorderに改名され、公開されるクラス名もFrameRecorderからLiveDataRecorderに変わりました。 ユーザーは、LiveData Viewer や LiveData Sequencer ウィンドウのように、収録をどこでも「ライブデータ」という名前で目にします。一方Frame*は、その下の仕組み(FrameRecorder、FrameReplayer)の名前のままです。旧来の公開名は、これが駆動する下位のレコーダークラスの名前でもありました。ソースには破壊的変更です。既存のデータは引き継がれます:- プロジェクトの
FrameRecorder.settings.jsonは、エントリがレコーダーの固定 id で照合されるため、引き続き適用されます。[FormerlyNamedAs("FrameRecorder")]により、旧い型名が不一致として報告されることはありません。次の保存でLiveDataRecorder.settings.jsonが書かれ、旧いファイルは削除されます。 [MovedFrom]により、旧い C# 型へのシリアライズされた参照も読み込めます。
- プロジェクトの
-
FrameRecorderControllerが公開するのは、2 つのプロジェクト設定keyframeIntervalとcompressだけになりました。 テイク番号、再生ファイル、ループのスイッチ、状態表示(isRecording、recordedFrames、recordedMegabytes、isReplaying、replayPaused、replayFrameCount、replayIndex)は公開されなくなりました。これらは LiveData Sequencer ウィンドウや、コントローラーを駆動する側のために、public な C# メンバーとして残ります。リモートのページはアプリケーション自身のトランスポート(jp.lilium.livestudioのRecordingManager)を駆動するため、公開されたコピーは、プロジェクトの設定ページにある同じコントロールの、使われていない 2 つ目のセットにすぎませんでした。replayFrameは、読むものが無かったため削除されました。
Fixed
-
Live Data Viewer が、開いている間ずっとフレームの先頭ごとにアロケーションしていました。 そのタップは、キャプチャーに渡す id リゾルバーを、ローカル変数を閉じ込めるラムダとして作っていたため、フレームごとにクロージャーとデリゲートが生成されていました——フレームあたり 2 回、毎秒 60 回、ウィンドウが開いている間ずっとです。これはまさにこのウィンドウが探すために存在するヒッチであり、それを見るという行為そのものが生み出していました。テーブルはフィールドになり、デリゲートは何もキャプチャーしないため、セッションにつき 1 つだけ作られます。ゲートは計測対象のフレームの中でオブザーバーに通知するため、ウィンドウがたまたま開いているとゼロアロケーションのテストが失敗していましたが、その問題も解消しました。
-
相対パスで指定したライブシーンファイル——Fusion のようにシーンを一切記録しないアプリの既定ファイルを含む——が、
Application.persistentDataPathではなく開いているプロジェクト内で解決されるようになりました。LiveSceneSaveSystemは起動時の状態とプロジェクト設定をプロジェクトフォルダに置きながら、シーンファイル自体はプレイヤーのデータフォルダを基準に解決していました。シーンを切り替えないアプリはstartup.jsonにシーンを記録しないため、どのプロジェクトを指定されても、マシン共通の 1 つのファイルを読み、自動保存していました:分離のために-projectを与えた実行でも、オペレーター自身の状態を読み込み、終了時には上書きしかねませんでした。相対パスは同じ状態ディレクトリを基準に解決されるようになりました(プロジェクトが設定されていない場合は、これまでどおりpersistentDataPathです)。以前のビルドがそこに保存したシーンファイルは引き継がれません。 -
アプリがポートで応答し始めた直後に行った編集が保持されるようになりました。 ホストは
OnEnableでサーバーを開き、起動時のライブシーンをStartで適用していました。適用の最後には、今ライブにあるすべてを「未保存なし」の基準として採用します(適用したばかりのものはオペレーターではなくディスク由来のため)。その窓——約 1 秒の幅で、リモートアプリやテストが接続して書き込むには十分な長さ——に着地した REST の書き込みは、この基準の取り直しに飲み込まれていました:値は残るものの、アプリはそれを編集として数えなくなるため、終了時に確認なしで失われ、ファイルが同じメンバーを持つ場合は復元が値をそのまま上書きしていました。サーバーは復元の適用後に開くようになりました(_LoadInitialDataThenStartServer)。読み込みがベースシーンを切り替え、逆シリアライズがリロードを待つ遅延ケースも含みます。ポートで応答することが、今行った編集は保持されるというアプリの約束になりました。ヘルスの応答は以前より約 1 秒遅くなります。エディタ自身のサーバーは別の場所で開始されるため影響はありません。 -
加算読み込みされたワールドが立っている状態で保存したライブシーンが、そのワールドをベースシーンとして記録しなくなりました。
ResolveBaseSceneNameはアクティブシーンを優先していました。アプリの上に読み込まれるワールドは常に AssetBundle 由来でビルドインデックスを持たない、という前提です。ビルドの 中に 同梱されたワールドはこの前提を崩します:ホストはライティングのためにそのシーンをアクティブにし、そのシーンはほかと同じくビルドインデックスを持ち、保存はそれをbaseSceneNameとして記録していました。そのファイルを読み込むと、ワールドをアプリ全体として起動し、さらにその上にもう 1 度読み込むため、ワールドが 2 つ、アプリのシーンは無し、ファイルの残りがバインドする先も無し、という状態になっていました。今は最初に読み込まれたビルドシーン——アプリが起動したシーン——を採用します。それ以降に読み込まれたものはすべてその上に読み込まれたもので、定義上ベースではないためです。(これに該当したのはjp.lilium.livestudioの組み込みセットです。既にこの方法で保存されたファイルは誤った名前を持ち続けるため、baseSceneNameを修正するか、状態を保存し直す必要があります。) -
クラスがコンポーネントだった間に書かれたプロジェクト設定が、コンポーネントでなくなった後も届くようになりました。 そうしたエントリは
@parent——コンポーネントが載っていた GameObject の id——でキー付けされているため、GameObject から離れて素の登録オブジェクトになったクラスでは、それらすべてが解決できなくなり、オペレーターが保存した設定が次の起動時に黙って既定値に戻っていました。ProjectSettingsSerializerは名前ではなく形を読むようになりました:@parentを持ち、@typeがComponentでないクラスに解決されるエントリは、代わりにそのクラスの登録済みインスタンスに適用され、@parentと@componentIndexはエントリがもう持たない形に属するものとして捨てられます。これはクラスのインスタンスがちょうど 1 つの場合に限ります。この位置にあるクラスとはそういうものです——設定は 1 台のマシンのもので、GameObject から離れたクラスは、それを唯一保持するために離れたのです。それ以外は推測せず、警告を出してスキップします。形に関するルールとして述べているため、次に同じ移動をするクラスはここで追加の変更を必要としません。(最初にそれを行うのはjp.lilium.livestudioのScreenです。) -
処理が追いつかないマシンで収録したテイクが速く再生されていました:毎秒 20 フレームで収録して 60 フレームで見ると、実際の 3 倍の速さで進んでいました。 リプレイはフレームの先頭ごとに 1 レコードずつ進んでおり、それは回数であって時間の長さではありません。記録には何の問題もありませんでした——フレーム番号がレコード間の距離を正確に示しており、レートを下回った実行は番号を飛ばすため、3 番号ごとにレコードがあるテイクはレコード数から想像される長さの 3 倍です——が、それを読むものがありませんでした。
PacedFrameSourceがリプレイヤーを包み、各先頭でそのレコードのうちいくつが期限を迎えたかを、埋めているフレームで時間を測って決めるようになりました:ゲートはソースにフレームを埋めさせる前に、ライブクロックの番号とレートをフレームに刻むため、ペーサーは独自のクロックを読みません。期限を迎えたものが無ければ表示中のフレームを保持し、複数あれば、その先頭ですべてを順に戻します。FrameRecorderControllerは開始するすべてのリプレイを包みます。従来の歩き方を望む呼び出し側はFrameReplayerを直接使います。それはオフラインの再描画が望むもので、今では 2 つのモードの違いになっています。⚠ これで直らないのはティックです。 ペース付きリプレイでの
FrameGate.deltaTimeはテイクのものではなくこのマシンのステップなので、テイクが運ぶものの外で積分するもの(スプリング、パーティクル、カメラリグ)は実時間で進み、したがって別のレートで描画するマシンでは異なって進みます。テイク自身のステップが配られるのはペースなしの歩き方の方で、driveEngineTimeOnSuppliedFramesは依然として供給されたフレームに収録時のフレーム番号を載せません——そのため、そのモードもこのマシンの間隔でエンジンのクロックを駆動します。どちらも同じ未完了の仕事に属し、一緒に行う価値があります。ペース付きリプレイが正しく扱わなければならないことが 4 つあり、いずれもテイクを再生してではなく問いを立てることで見つかりました:読み込みのために保留された供給は、テイクがあとで駆け抜ける時間ではありません(
FrameGate.heldFrameCountが、待ちとマシンの遅れを見分けるものです)。大きく遅れた先頭は、全力で追いつくのではなく、上限のある数のレコードを戻して残りを捨てます。追いつきはワールドの形を変えたレコードで止まります。一覧はまだ実行されていないフレームの先頭でしか処理されないためです。そしてリプレイを停止するレコード(テイクはその間に行われたリクエストを運びます)は、そこでその先頭を終えます。その後のすべては、閉じたばかりの記録から読まれることになるためです。FrameGateは、ソースがまだ接続中のものである場合にのみ切り離すようになったため、別のテイクを開始して終わるテイクが、新しいテイクの最初のフレームの前にそれを消してしまうことはありません。 -
LiveData Sequencer のタイムコードがフレームではなくレコードを数えていたため、レートを下回って収録したテイクが、かかった時間の 3 分の 1 と表示されていました。
recordedFramesとreplayIndexは記録内の位置で、タイムコードは時計上の距離です。両者が同じ数になるのはマシンがレートを保っている間だけです。ウィンドウは収録中はFrameRecorderController.recordedFrameSpanを、再生中はreplayFrameSpanを読むようになり(どちらもフレーム番号での距離)、再生はこのマシンのレートではなくテイク自身のレート(replayFrameRate)で数えます。前者のためにFrameRecorderはfirstFrameNumberとlastFrameNumberを公開します。 -
アセットで宣言された型が、そのインスタンスを持つシーンがホストなしで破棄された後、セッションの残りの間登録されたままになっていました。 登録は、何かがインスタンスを公開している間は意図的に保たれます——ハンドルがライブクラスを保持しているため——が、オブジェクトが既に破棄されたラッパーも数に入っており、そうしたラッパーを切り離すものは何もありませんでした。そのため、最後に去るコンテナが宣言を一緒に持ち去れず、型は
/live/typesやコンポーネントの一覧に現れ続け、アセットを再適用してもシグネチャが変わっていないため再構築がスキップされていました。数えるときに、破棄されたターゲットを除外するようになりました。 -
収録中に押した「+」が、再生時にオブジェクトを 1 つではなく 2 つ立ち上げていました。
ObjectFactoryBase.CreateObjectはlane = FrameLane.Noneと宣言され、記録を残さなくなりました。DestroyObjectは event レーンに残ります。「+」が生み出すものは既に一覧が運んでいます——生成されたオブジェクトは元のプレハブを名指し、structure レーンが収録時の id でそれを再構築します——そのため押下も記録すると、両方のレーンが 1 つずつ立ち上げていました:順再生では一覧のコピーに加えて、記録されたどの値もアドレスしない新しい id の 2 つ目が生まれ、シークでは関数呼び出しが落とされるため一覧のものだけでした。押し直しを正しくすることもできませんでした。呼び出しが受け取るのは、次の実行でも同じものを意味する id ではなく、読み込まれたものに応じて伸び縮みするリストへの インデックス だからです。削除は逆のケースで、残されます:id でアドレスされ、2 回適用しても無害で(2 回目は何も見つけない)、一覧が届かないものに届きます——structure レーンは自分が立ち上げたものしか取り除かないため、テイク開始前からシーンにあったものの削除はイベントだけが運びます。⚠ その削除も、関数呼び出しが一般にそうであるのと同じ理由で、シークをまたいでは残りません。 -
フレームへのシークが、最初から数えるのではなく番号でフレームを探すようになり、スクラブが求めた位置に着地します。
FrameRecordReader.TrySeekFrameはインデックスを連続とみなしていました——フレームnは位置n - first——が、それが成り立つのは一度も拍を落とさない実行だけです。フレーム番号はクロックから来るため、レートを下回った実行は番号を飛ばします:60 Hz で収録した 2628 フレームのテイクは 2805 番号にわたり 58 箇所の欠番があると実測され、そこへのシークは求めた位置から最大数十フレームずれて着地していました。着地したフレームは完全に正常なフレームなので、黙ってずれていました。検索はインデックス上の二分探索になり、各探索点の境界をファイルから読みます——十数回の小さな読み取りで、インデックスが避けるために存在する全フレームの走査とは対照的です。FrameRecordReaderとFrameRecordPlayerにFrameNumberAt(index)とIndexOfFrame(frameNumber)が加わり、呼び出し側は仮定せずに記録内の位置とフレーム番号を変換できます。firstFrameNumberも公開されますが、あくまで記録の開始番号としてです。フォーマットは何も変わっていないため、既存の記録は従来どおり読め、正しくシークできるようになっただけです。 -
起動後に登録された型が、既に接続していたリモートアプリで「型情報なし」と表示されなくなりました。 型を後から登録すると——遅延で、あるいはシーンやバンドルとともに届くライブクラスアセットを通じて——誰にも知らせずに型テーブルが大きくなるため、クライアントはセッションの残りの間、古い
/live/typesを持ち続けていました。すべての登録と登録解除が変更フィードに@typesを報告するようになりました。また、静的コンストラクタがたまたまワーカースレッドから起動時スキャンを実行した場合、まだ読み込まれていないアセンブリの型を見逃すことがありました。アセンブリ読み込み後の再スキャンは、メインスレッドでTypeCacheと読み込み済みアセンブリを組み合わせるようになりました。 -
LiveClassAssetSystem.UnregisterTypesは、アセットの現在の定義を読み直すのではなく、アセットが実際に登録した型を取り消すようになりました。そのため、アンロード前にアセットから削除された定義も、一緒に登録解除されます。 -
翻訳がエディットモードでも登録されるようになりました(
RuntimeInitializeOnLoadMethodに加えて[InitializeOnLoadMethod])。サーバーは停止中のエディタからもリモートアプリに応答しますが、再生を開始するまで、すべてのラベルとヘルプテキストが生のキーのまま届いていました。 -
ライブオブジェクト自身が保持するメンバーへのエディタからの編集が、Undo でき、保存され、再描画されるようになりました。 Undo、dirty 化、再描画は、ライブオブジェクト自体が
UnityEngine.Objectの場合にしか行われなかったため、プレイモード外では、素の C# ラッパーのメンバー——あるいはラッパーを所有するコンテナがシリアライズするメンバー——への変更が次の保存で失われていました。LiveEditorWriteScopeは実際に値をシリアライズするオブジェクト(ラップされたコンポーネント、または所有するコンテナ)を解決し、[ExecuteAlways]コンポーネントがすぐ再描画されるようプレイヤーループの更新を強制するようになりました。 -
LiveObjects Viewer がエディタを遅くしなくなりました:dirty マークはオプトインになり、プロパティパネルは間引いたレートで更新されます。
SelectPropertyDrawerはドロワー間で派生型の検索結果をキャッシュします。 -
復元するものが無いときにリセットが失敗を報告し、保存されない書き込み可能なメンバーは開始時の値にリセットされるようになりました。
RevertValue/ResetValueは常に成功を報告しており、プロパティの背後にある永続化されないメンバーにはどこにも基準値が無かったため、リセットしても何も起きませんでした。そうしたメンバーは永続化の基準値と一緒に開始時の値が取得されるようになりました(アセットが読み込み後に基準を取り直すときには再取得されません)。@resetはメンバーのレーンも尊重するため、event レーンに乗らないメンバーをリセットしても、リプレイが再び押すイベントは記録されなくなりました。 -
フレーム数から作った
Timecodeが、秒の区切り付近でフレームを失わなくなりました(60 fps でフレーム 60 と 61 がどちらもフレーム 0 になっていました)。double の秒を経由せず、フレーム数全体から導出するようになりました。 -
Stop → Start で実際にサーバーが再起動するようになりました。
HttpServerCore.StopServerは登録済みのすべてのルートを破棄していたため、Remote Control Server ウィンドウから再起動したインスタンスは 404 しか返しませんでした。ハンドラーは停止後も残り、サーバーインスタンス自体が破棄されたときにだけ破棄されるようになり、停止と開始が対称になりました。 -
HttpServerCore.Disposeは、破棄済みインスタンスでは実行を拒否するStopServerを呼ぶ前にインスタンスを破棄済みにしていたため、事前に明示的に停止せずに実行中のサーバーを破棄するとリスナーがリークしていました。先に停止(とリスナーの解放)を行うようになりました。 -
リスナーループ内の予期しない例外が
Task.Runに飲み込まれ、IsRunningが true を報告したままサーバーが黙って死んでいました。ループは例外をログに出して終了するようになったため、実行状態とOnServerStoppedが正しい状態を示します。 -
プレイヤービルドでは、サーバーを破棄するものが何もありませんでした:破棄のフック(
beforeAssemblyReload、プレイモード終了、エディタ終了)はすべてエディタのイベントだったため、各サーバーが生成するバックグラウンドのクリーンアップタスクはプロセスとともにしか終わりませんでした。プレイヤーでもApplication.quittingですべてのサーバーを削除するようになりました。 -
設定アセットが、現在のプロジェクトに存在しないアセットの config を列挙している場合(共有の設定アセットは、プロジェクトに含まれないパッケージの config を参照し得ます)に、Remote Control Server ウィンドウとそのエディタ自動開始が例外を出さなくなりました。
-
[LiveClass]型を遅延登録する型をシリアライズしたときに、GET /live/typesが "Collection was modified" で 500 になることがありました(ToJObject 内の派生型解決がレジストリを変更するため)。ハンドラーはシリアライズ前に型リストのスナップショットを取るようになりました。 -
テイクの再生で、
Time.deltaTimeで動くものすべてが早送りされなくなりました。 スプリング、パーティクル、アニメーターが、描画レートを 60 で割った速さで動いていました——144 Hz で 2.5 倍、vSync を切ったエディタではさらに速く。2 つの原因が重なっていました:Time.captureDeltaTimeはゲートを回したエンジンフレームで設定され、その間のすべてのフレームでも設定されたままだったため、テイクの 1 ステップが描画フレームごとに 1 回ずつ消費されていました。そしてステップ自体がライブのクロックで測られていました。FrameReplayerが、供給するフレームにテイク自身のフレーム番号を刻んでいなかったためです。今は刻むようになりました——供給されたフレームはテイクの番号とレートを持つため、FrameGate.deltaTimeはテイクのステップになり(1 つの先頭で複数のレコードを戻すと 1 つの幅広いステップになり、その場に留まる先頭は何も進みません)、供給されたフレームから読むタイムコードもテイクのものになります。リプレイがエンジンのクロックを駆動している間、ゲートは次のレコードの期限まで先頭で待機するため、エンジンはテイクの 1 ステップにつき 1 フレームを描画します:テイクがレンダークラスターのプライマリで、リプレイがその時間を適用するわけです。テイクが動かない先頭(一時停止や、読み込みの待ち)ではエンジンを実時間に戻し、フレームなしで諦めた待機も同様です。ティックが 4 分の 1 秒より広くなることはありません。ほかの何か(レコーダー)が既にTime.captureDeltaTimeを保持している場合、リプレイはそれに触れず、1 度だけ警告します。
[0.25.3] - 2026-07-22
Added
RemoteConfirmSystemが、確認をすべての画面へ同時に上げます——アプリを動かしているマシンの OS ダイアログと、接続中の各リモートアプリのモーダルです。最初に答えた画面で解決し、残りは閉じられます。スマートフォンから操作しているオペレーターは PC 側にしか出ていないプロンプトに答えられず、PC の前にいる人はリモートアプリにしか出ていないプロンプトに答えられませんでした。プロンプトはローカライズキーとして送られ、アプリの翻訳がフォールバックとして添えられるため、各リモートアプリはそれぞれの言語で描画します。回答はPOST /api/confirm(フレームワーク自体がプロンプトを上げるため、既定のルートです)で返ります。NativeConfirmDialogは OS ダイアログを専用スレッドで表示し、別スレッドから閉じられます。ブロッキングするConfirmDialogではミラーされたプロンプトを支えられません。表示中はアプリがリモートの回答を運ぶ REST 呼び出しを処理できず、それに基づいて動くこともできないためです。ConfirmDialogは、単一画面で完結する終端の経路のために残ります。RemoteControlBehaviour.TrySaveOrPromptは、現在のパスへ保存するか、プラットフォームの「名前を付けて保存」ピッカーを開きます(Editor / プレイヤーのどちらのダイアログを使うかは自身で選びます)。QuitApiHandler.onQuitRequestingが、REST 経由で要求された終了を拒否できます。プレイヤーにはApplication.wantsToQuitがありますが、Editor ではリモートからの終了がプレイモードを強制的に抜けさせ、その結果のExitingPlayModeは中断できません。ここに拒否の仕組みが無いと、非同期の未保存確認に答える前に Editor が破棄され、プロンプトがリモートアプリに届きませんでした。Editor で Stop を押した場合は、従来どおりローカルのミラーされないダイアログを使います(その経路は開いたままにできないためです)。LiveSceneSaveSystem.ResetAllToDefaultは、dirty 状態にかかわらず、含まれるすべてのオブジェクトを取得済みの既定値へ戻します。RevertAllToDefaultは dirty なプロパティを報告するオブジェクトしか触らず、読み込みはすべてを clean として基準を取り直すため、読み込み直後には何も戻しません。現在の状態を破棄しなければならない呼び出し側(別ライブプロジェクトへの切り替え)には、無条件の形が必要です。LoadCurrentData/LoadCurrentDataFromがforceBaseSceneReloadを受け取り、ファイルが既にアクティブなベースシーンを指している場合でもベースシーンを再読み込みします。これが無いと、同じベースシーンへの読み込みは現在の状態の上に逆シリアライズされ、(差分)ファイルが省略した値はすべて以前のまま残ります。
Changed
- 終了時に表示される未保存確認が
RemoteConfirmSystemを経由するようになり、マシン上だけでなくリモートアプリにも表示されます。遅延用のコルーチンも不要になりました。プロンプトがwantsToQuitをブロックせず、単に終了を拒否したうえで、回答が新しい終了を開始するためです。
Fixed
- Unity 6.5(6000.5)でパッケージがコンパイルできるようになりました。同バージョンでは
Object.GetInstanceID/Resources.InstanceIDToObject——および Unity 6.3 以降それらを動かし続けていたEntityId⇔intの暗黙変換——が Obsolete エラーに格上げされました。すべての呼び出し箇所はExposedObjectUtilityを通るようになり、ここがバージョン差分を持つ唯一の場所になります。ID はlongで運ばれるため、EntityIdが 32 ビットを超えても収まります。@instanceIDフィールドとExposedObjectHandlerの数値 id フォールバックは呼び出し側から見て不変です。値はセッション内でのみ有効で、永続化してはいけません。
[0.25.2] - 2026-07-21
Added
ExposedObjectHandle.MarkClean/ExposedObjectDefaultRegistry.MarkCleanは、オブジェクトの現在の状態をユーザー変更の基準として採用します。これにより、次の書き込みまで、そのオブジェクト上のどの状態も未保存の編集として数えられなくなります。動くのはユーザー変更の基準だけで、シリアライズの基準は取得済みの既定値を保持するため、その後の差分保存でも全差分が書き出されます。ディスクから状態が届く箇所で呼び出します。
Fixed
- 未保存判定が、起動のたびにシーンを dirty と報告しなくなりました。この誤判定は、既存ライブプロジェクトを開いたスタジオの終了をブロックしていました。旧来のチェックはシーン全体をシリアライズしてファイルと文字列比較していたため、「ユーザーが何かを編集した」のか「このビルドが、ファイルを書いたビルドとは異なる形でシーンをシリアライズする」のかを区別できませんでした——古いバージョンで書かれた
live.jsonを持つライブプロジェクトは、常にこの判定に失敗し続けていました。現在は、ディスクから状態が届くたびに基準を取り直した、オブジェクト単位の dirty 追跡(シーン読み込み、保存、遅延した保留エントリ、アセット読み込みの後処理)に問い合わせます。 IsDirtyとGetDirtyPropertiesを、永続化の形状(forPersistence: true)で比較するようにしました。CaptureDefaultsとプロパティ単位の基準に揃えます。dirty は「保存したら何か異なるものが書き出される」を意味すべきなので、読み取り専用や永続化しないメンバー——まさに復元後の非同期読み込みが churn させるもの、すなわちクロールで構築されるExternalAssetManager.assetsカタログ、再構築されるStageManager.sets、TransformRef.availableOwnerNames——はもう数えられません。
[0.25.0] - 2026-07-17
Added
- 引数つき・プロパティパス経由での公開関数の呼び出しに対応しました。
ExposedPropertySerializer.BuildInvokeArgumentsが JSON 配列から型付きの位置引数配列を構築し(REST の invoke 経路と、保存された呼び出しを再生する側で共有)、ExposedObjectHandle.ResolveFunctionが、プロパティパス経由で到達する関数(メンバー上のネストされた関数)を、REST 層がネストしたメンバーを解決するのと同じ方式で解決します。 ExposedProperty.TryGetValue<T>/TrySetValue<T>: Source Generator が生成する型付きデリゲート(Func<object,T>/Action<object,T>)を通じて、値型のメンバーをボックス化せずに読み書きします。ShowIf/HideIfが複数の条件を受け取れるようになり、メソッドにも付けられるようになりました。条件はvisibilityConditions配列としてシリアライズされ、クライアント側で AND 評価されるため、メンバー(や関数ボタン)を複数の条件で同時にゲートできます。条件が 1 つの場合は従来どおりvisibilityフィールドを出力するため、型の出力はバイト単位で互換のままです。ImagePreviewAttribute: サーバー相対の画像 URL をポーリングして表示する読み取り専用コントロールです(キャプチャーのカメラプレビューを支えます)。ExposedObjectRegistry.keyedCollectionVersionとNotifyKeyedCollectionChanged()を追加しました。キーつきコレクションの要素が再構築されたときに、キャッシュ済みのプロパティ解決を無効化して、古いキーへ書き込みが着地するのを止められます。
Changed
- 公開プロパティのパイプラインから GC を排除しました。
bool/enumのゲッターはBoxedValues経由で正規のボックス化済みインスタンスを返し、購読者がいないときはoldValueの読み取りとプロパティ変更イベントをスキップします。FindProperty/GetPropertyは span ベース(name.ToString()なし)になり、配列要素のディスクリプタはキャッシュされ(ExposedPropertyType.GetArrayElement)、要素の探索はコレクションを 1 度だけ読んで一致した要素のみを構築します。PropertyPathのスラッシュ変換とAppendIndexはアロケーションフリーになり、ExposedUnityObject.componentsは LINQ のフィルタを 2 パスの手書きに置き換えました。REST のレスポンスとscene.jsonは変更ありません。
[0.24.3] - 2026-07-09
Added
AssetSelectorコントロール: アセット参照のフィールドがアセット GUID としてシリアライズされます(AssetRegistry。OnValidateで焼き込み、ランタイムで登録)。GET /api/asset?guid=が GUID をアセットの名前と型に解決するため、クライアントが選択肢にラベルを付けられます。サブアセットはguid:localIdの複合キーを使い、任意のrefPropertyがスキーム接頭辞つきの外部キー(例:file:<path>#<sub>)を運びます。外部キーの対象はまだ読み込まれていない場合があるため、AssetRegistryに名前フォールバックと表示名の解決が追加され、未読み込みのキーにもラベルを付けられます。GET /exposed/orphansとPOST /exposed/orphans/remove: 保存済みシーンが持っているが、今のセッションにはオブジェクトが存在しないルート参照を、削除可能な「missing」項目として提示します(パスを持つ子のオーバーライドは、アセットが非同期に読み込まれればバインドされるため除外されます)。削除すると、その id をルートに持つキュー済みのエントリがすべて破棄されるため、次のシーン保存でそれが再出力されることはありません。RemoteErrorNotifierが、Unity のエラー(Error / Exception / Assert)を、接続中のすべてのリモートアプリへエラー通知として転送します。ブロードキャストはメインスレッドへマーシャリングされ(サーバーレジストリはそこでのみ変更されます)、同一のメッセージは重複排除され、バーストには上限が設けられるため、毎フレームの例外がアプリを溢れさせることはありません。既定で有効です(RemoteErrorNotifier.enabled)。
Changed
- ライブシーンのシリアライザが、永続化ファイルのエントリに対して曖昧な復元の警告を出さなくなりました。それらは
@nameを持たず型でも一意に特定できないため、ユーザーが対処できない警告を出す代わりに、未解決のままリモートアプリが提示・削除できるようにします。
[0.24.2] - 2026-07-05
Changed
- 破壊的変更:
GET /exposed/objectsとGET /exposed/object/{id}が、既定で 深さ 1 のオブジェクトを返すようになりました。ネストしたインライン(未登録)の複合子要素は、完全に展開する代わりに切り詰めスタブ{ "@type": ..., "@truncated": true }として出力され、オブジェクトグラフが大きくなってもペイロードを小さくスケーラブルに保ちます。配列は深さを消費しないため、要素数と要素ごとの型は引き続き見え、登録済みの子要素は@ref形式のままです。?nested(または?nested=true)を渡すと従来の無制限展開に戻せます。プロパティ読み取り(GET /exposed/object/{id}/{path})、SSE ブロードキャスト、PUT レスポンス、永続化(scene / project / preset)は変更なく、常に完全展開されるため保存ファイルはバイト単位で同一に保たれます。オブジェクト一覧からネスト値をたどっていたクライアントは、?nestedを渡すか、切り詰められた各子要素をプロパティ GET で遅延取得する必要があります。
Added
POST /exposed/batchエンドポイントを追加しました。複数のオブジェクト/プロパティ/関数の操作を 1 リクエストで適用し、項目ごとに continue-on-error します(各項目のステータスとボディが順番に返されます)。公開 REST API はDocumentation~/openapi.ymlにドキュメント化されました。- メンバー単位の永続化スコープ:
[ExposedField]/[ExposedProperty]にpersistScope(Scene 既定 / Project)が追加され、シリアライザがライブシーンの状態とクラスごとのライブプロジェクト設定を分離できます。 - 公開キーパスアドレッシング: プロパティパスが、安定した
[ExposedKey]値で配列要素を指定できるようになりました(例:expressions[Joy].weight)。汎用のSetPropertyActionの「キーへバインド」フローを支えます。 [Collapsed]属性: リモートアプリが配列やネストした構造体を既定で折りたたんで表示するヒントです(展開トグルは残ります)。プロパティコントローラーとは独立した単独のcollapsedフラグとして出力されます。- 多態的な配列プロパティに対して
elementTypeOptionsを出力し、リモートアプリが「…型の要素を追加」を提示できるようにしました。 - 専用の
GET /api/performanceエンドポイントを追加しました。アプリの現在の FPS とプロセスメモリを返します(プレイヤーループからPerformanceMonitorでサンプリングし、メモリは Windows のネイティブProcessMemoryReader経由で読み取ります)。リモートアプリは、オーバーレイ表示中のみこのエンドポイントをポーリングでき、他のリクエストに相乗りする必要がなくなります。
Fixed
ExposedPropertyUtility.CreateDefaultElementが、抽象型/インターフェース型の要素に対して最初の具象[ExposedClass]サブタイプにフォールバックするようにしました。これにより、多態的な[SerializeReference]配列への要素追加がMissingMethodExceptionを投げなくなります。ExposedObjectHandleの構築時に、既に適用済みのオーバーライド値をダーティ検出のベースラインに焼き込まないようにしました。コンストラクタは無条件のSetDefaultの代わりにEnsureDefaultsCaptured(未設定のときだけキャプチャし、既存のベースラインは保持)を使うようになり、ハンドル登録前にライブシーンの復元でキャプチャされたオーバーライドが、現在の(適用済みの)値で上書きされて保存時に失われることがなくなりました。
[0.23.6] - 2026-06-23
Changed
[StringSelector]を、プロパティ・フィールドに加えてメソッドの引数にも付けられるようにしました。これを付けたExposedFunctionの引数は、所有オブジェクトのsourcePropertyNameプロパティを選択肢とするドロップダウンとして表示され、制約された文字列引数(例: 名前付きのステージマークの選択)を取れるようになります。
[0.23.4] - 2026-06-22
Fixed
?type=Xクエリが、GameObject が汎用ラッパーハンドル経由でも公開されている場合に、ファーストクラスの[ExposedClass]コンポーネント(例: 「Avatar」として公開されたAvatarController)を取りこぼさないようにしました。コンポーネントとラッパーは別個の公開アイデンティティです。従来の GameObject アイデンティティによる重複排除では、アバターがExposedGameObjectWithTransformとして公開されると RemoteApp のページが「利用可能なアバターがありません」と表示していました。同一ターゲットの本当の重複は引き続きFindByTargetで統合されます。- ライブシーンのシリアライズが、ベースシーンの再読み込み中に「実在したが破棄された(fake-null)」Unity 参照のみをスキップし、最初から裏付けの参照を持たない純粋なプロキシは引き続きシリアライズするようにしました。
[0.23.2] - 2026-06-21
Fixed
FrameRate.AsFrameNumberが、floatを経由してキャストする代わりにdoubleで計算するようにしました。2^24(60fps で約 3.2 日)を超えるとfloatは整数精度を失い、連続するフレーム番号が同じ値に潰れて Studio の再生バッファのルーティングがカクついていました。doubleは 2^53 までのフレーム整数を正確に表せます。
[0.23.0] - 2026-06-21
Added
[ExposedExternalEnum(typeof(T))]アセンブリ属性を追加しました。型を変更せずに、外部 enum や組み込み enum(例:HumanBodyBones)をリモートアプリのドロップダウンとして登録できます。excludeNamesに対応し、登録はReset()をまたいで保持されます。ExposedObjectSnapshotで、公開オブジェクトの既定状態をキャプチャし、それとの差分を計算できるようになりました(CaptureDefaults/CaptureDelta)。プロッププリセットの基盤となります。- ライブシーンの保存時にリモートアプリへ通知するようにしました。
Fixed
- 解決できない
@typeを参照する保存済みライブシーンのエントリが、null の穴を残さずきれいに消えるようにしました。 - ライブシーンの復元時に、シーンの表示状態とアクティブ状態を起動時に正しく再適用するようにしました。
[0.22.1] - 2026-06-14
Fixed
- ライブシーンの保存時に、
baseSceneNameとしてビルド設定に含まれるシーンのみを記録するようにしました。追加読み込みした.scene.lsbのワールド(buildIndex -1)がアクティブシーンの状態で保存すると、従来はそのワールド名が書き込まれ、ワールドを含まないビルドでそのファイルを読み込むとアクティブシーンが変更扱いになり、未保存の変更ダイアログでグレースフルな終了がブロックされていました。名前はアクティブシーンがビルドシーンの場合のみそれを採用し、そうでない場合は最初に読み込まれたビルドシーン(ワールドはその上にキャリブレーションされる)を採用するようにし、保存とHasUnsavedChangesの基準の両方に適用しました。
[0.21.2] - 2026-06-11
Fixed
ExposedObjectUtility.InstanceIDToObjectが、内部 API のUnityEngine.Object.FindObjectFromInstanceIDをリフレクションで呼ぶ代わりに、公開 API のResources.InstanceIDToObjectを呼ぶようにしました。Unity 6.1 (6000.3) で内部 API の引数がintからEntityId構造体へ変わり動作しなくなっていた問題に対応しています。公開 API は Unity 2021.3〜6000.3 を通じてint引数を維持します。
[0.21.0] - 2026-06-08
Changed
- メンバーアクセスをソース生成アクセサー方式に変更し、
Unity.Propertiesへの実行時依存を排除してリフレクションのオーバーヘッドを削減しました。 ExposedObjectを readonly struct 化し、ExposedObjectHandleに改名しました。プロパティアクセス時の一時ビューのアロケーションを排除しています。
Fixed
- 静的な
[ExposedClass]型が、全ゲームアセンブリの読み込み前に静的コンストラクターが走ると公開対象から消えてしまう問題を修正しました(アセンブリ読み込み後に属性を再スキャン)。 - 継承した
[ExposedClass]メンバーを宣言順で登録するようにしました。
[0.20.12] - 2026-06-04
Added
- LiveScene の読み込みに
switchSceneOnLoadオプションを追加しました。シーンを読み込んだときにそのシーンへ切り替えるかどうかを制御できます。
[0.20.11] - 2026-06-03
Added
- シーン保存時に live scene ファイルのパスをログ出力するようにしました。
[0.20.10] - 2026-06-02
- 機能的な変更はありません(モノレポのリリースに合わせてバージョンを同期)。
[0.20.9] - 2026-06-02
Added
- サーバーを全ネットワークインターフェースにバインドする
allowExternalConnectionsオプションを追加しました。
Changed
- ハンドラーをインスタンス単位で登録し、ルートをコンストラクターで宣言するようにしました。