Unity 向け C# ランタイムを unity/com.afjk.loom に追加しています。
Unity 版 Loomlet は JavaScript 版と同じ JSON グラフを評価します。
グラフ DSL は Unity 側では直接扱わず、DSL をパースした後の JSON グラフを入力とします。
Graph DSL → JSON graph → Web Loomlet / Unity Loomlet で評価
IL2CPP 互換を想定しており、Roslyn・動的コンパイル・外部 NuGet 依存は不要です。
unity/com.afjk.loom/
package.json
Runtime/
Loom.asmdef
Loom/
LoomEngine.cs — メインエンジン
LoomGraph.cs — グラフモデル + JSON パーサ
LoomNode.cs — ノード型定義・ヘルパー
LoomError.cs — LoomException
LoomExpressionDsl.cs — filter.predicate 式 DSL
LoomSceneSyncAdapter.cs — SceneSync メッセージ処理
Nodes/
CoreNodes.cs — clock, constant, sine, add, multiply
EventNodes.cs — filter, sample, merge, pointerClick, pointerPosition, keyDown, keyUp
SceneSyncNodes.cs — serverClock, sceneSetPosition, sceneSetRotation, ...
Unity/
ILoomTargetResolver.cs
LoomUnityTargetResolver.cs
LoomSceneSyncBehaviour.cs
Tests/
EditMode/
Loom.Tests.asmdef
LoomEngineTests.cs
LoomExpressionDslTests.cs
LoomSceneSyncAdapterTests.cs
LoomUnityTargetResolverTests.cs
Unity Package Manager を開き、「+ > Add package from disk…」 で unity/com.afjk.loom/package.json を選択します。
Packages/manifest.json に以下を追加します。
{
"dependencies": {
"com.afjk.loom": "https://github.com/afjk/loomlet.git?path=unity/com.afjk.loom"
}
}
LoomSceneSyncBehaviour コンポーネントを追加します。scene-graph-* メッセージを受信したら HandleJsonMessage(json) を呼び出します。// WebSocket メッセージ受信コールバックの例
void OnWebSocketMessage(string json)
{
GetComponent<LoomSceneSyncBehaviour>().HandleJsonMessage(json);
}
Update() は MonoBehaviour のライフサイクルで自動的に呼ばれます。
手動で呼び出す必要はありません。
以下の JSON を HandleJsonMessage に渡すと、”Cube” という名前の GameObject の X 位置が正弦波で動きます。
{
"type": "scene-graph-set",
"scope": "scene",
"graph": {
"nodes": [
{ "id": "clock", "type": "serverClock" },
{ "id": "wave", "type": "sine", "params": { "freq": 1, "amplitude": 2 } },
{ "id": "move", "type": "sceneSetPosition", "params": { "target": "Cube" } }
],
"edges": [
{ "from": "clock.t", "to": "wave.t" },
{ "from": "wave.out", "to": "move.x" }
]
}
}
scope を { "object": "targetId" } にすると、特定オブジェクトのみに適用されるグラフを管理できます。
{
"type": "scene-graph-set",
"scope": { "object": "Cube1" },
"graph": {
"nodes": [
{ "id": "clock", "type": "serverClock" },
{ "id": "wave", "type": "sine", "params": { "freq": 0.5 } },
{ "id": "move", "type": "sceneSetPosition", "params": { "target": "Cube1" } }
],
"edges": [
{ "from": "clock.t", "to": "wave.t" },
{ "from": "wave.out", "to": "move.y" }
]
}
}
| ノード | 入力 | 出力 | 説明 |
|---|---|---|---|
clock |
— | t |
現在の engine 時刻(秒) |
constant |
— | out |
固定値(params.value) |
sine |
t, freq, amplitude, phase, offset |
out |
amplitude * sin(t * freq * 2π + phase) + offset |
add |
a, b |
out |
a + b |
multiply |
a, b |
out |
a * b |
| ノード | 説明 |
|---|---|
filter |
predicate を満たすイベントのみ通過 |
sample |
trigger 発火時に value の現在値を出力 |
merge |
2 つのイベントストリームを合流 |
pointerClick |
クリックイベント入力ノード |
pointerPosition |
ポインター位置 behavior ノード(pos 出力:{x, y})。初期値 {x: 0, y: 0}。LoomEngine.SetPointerPosition(x, y) で更新可能。 |
keyDown |
キー押下イベント入力ノード |
keyUp |
キー離しイベント入力ノード |
pointerPosition の利用例// 毎フレーム入力システムからポインター位置を更新する
void Update()
{
var mousePos = Input.mousePosition;
sceneSyncBehaviour.Engine.SetPointerPosition(mousePos.x, mousePos.y);
}
Note:
value.zは JavaScript Phase 1 仕様に含まれていないため、式 DSL では使用できません。value.x/value.yのみサポートします。使用するとEXPRESSION_PARSE_ERRORがスローされます。
| ノード | 説明 |
|---|---|
serverClock |
getServerTime() を t として出力 |
sceneSetPosition |
Transform.position を更新(target GameObject) |
sceneSetRotation |
Transform.localEulerAngles を更新(ラジアン入力 → 度変換) |
sceneSetScale |
Transform.localScale を更新 |
sceneSetColor |
Renderer.material.color を更新(alpha 維持) |
sceneSetVisible |
GameObject.SetActive(bool) を呼び出す |
sceneSetRotation の入力値はラジアンです(JavaScript 版との互換性のため)。
Unity の Transform.localEulerAngles は度数法なので、内部で自動変換されます。
Unity degrees = radians × (180 / π)
LoomUnityTargetResolverLoomUnityTargetResolver は以下の動作をします。
RegisterTarget(id, gameObject) で登録されたオブジェクトは、inactive でも確実に解決できます。GameObject.Find() を呼ぶコストを回避します。Resources.FindObjectsOfTypeAll<GameObject>() で検索します(inactive なオブジェクトも対象)。var resolver = new LoomUnityTargetResolver();
// 明示的に登録(inactive オブジェクトも解決可能)
resolver.RegisterTarget("Cube", gameObject);
// 登録を削除
resolver.UnregisterTarget("Cube");
// すべての登録・キャッシュを削除
resolver.ClearCache();
sceneSetVisible との連携sceneSetVisible(false) で非表示にしたオブジェクトを再び true に戻す場合、
RegisterTarget() で事前登録しておくと確実に解決できます。
// シーン読み込み時に登録
resolver.RegisterTarget("Cube", cubeGameObject);
// 非表示にしても再表示できる
// sceneSetVisible(false) → sceneSetVisible(true) が正常動作する
Note: Unity は GameObject 名の一意性を保証しません。同名オブジェクトが複数ある場合は最初に見つかった方が返されます。一意な解決が必要な場合は
RegisterTarget()による明示的登録か、独自のILoomTargetResolver実装を推奨します。
ILoomTargetResolver を実装することで、独自の解決ロジックを使用できます。
public sealed class MyCustomResolver : ILoomTargetResolver
{
public object ResolveTarget(string targetId)
{
// 独自の解決ロジック
return MyObjectRegistry.Find(targetId);
}
}
LoomSceneSyncAdapter のコンストラクタにカスタム resolver を渡します。
var adapter = new LoomSceneSyncAdapter(
send: _ => true,
getServerTime: () => (double)Time.time,
targetResolver: new MyCustomResolver()
);
Objects controlled by Loom sink nodes should generally be excluded from normal Scene Sync Transform synchronization. Loom motion is synchronized by sharing the graph and evaluating it locally on each client. Synchronizing both the graph and the resulting Transform can cause competing writes, jitter, or last-write-wins behavior.
Loom の sink ノード(sceneSetPosition / sceneSetRotation / sceneSetScale / sceneSetColor / sceneSetVisible)が書き込むオブジェクトプロパティは、通常の Scene Sync Transform 同期から除外してください。
Tests/EditMode/ に Unity Test Runner の EditMode テストを用意しています。
LoomEngineTests.cs — コアノード・イベントノード・pointerPosition(テスト 1–14、29–30)LoomExpressionDslTests.cs — 式 DSL(テスト 15–18 + 追加テスト)LoomSceneSyncAdapterTests.cs — SceneSync アダプタ・Unity ノード(テスト 19–28)LoomUnityTargetResolverTests.cs — target resolver 登録・キャッシュ・非表示オブジェクト解決テスト 1–19、25–30 および ExtraTest_* は純 C# で Unity なしでも dotnet で検証済みです(37 テスト全て pass)。
テスト 20–24(sceneSetPosition 等)と LoomUnityTargetResolverTests の Unity テストは Unity Test Runner が必要です。
以下は現バージョンでは対応していません。
HandleJsonMessage に JSON を直接渡す形で動作確認可)scene-graph-input メッセージの処理(Phase 2 以降)