[forum知見] ThymeイベントAPI仕様
Algodoo Forum http://www.algodoo.com/forum/ の会話ログから、codex (GPT-5.6 Sol) に抜粋させた
結論
- 物理ロジックは
postStep、停止中も必要な UI ロジックはupdate、瞬間的な接触処理はonCollide/onLaserHit/onHitByLaserに分離する。 onSpawnは初期化・再バインドに便利だが、その場で自分自身を再生成すると同一フレーム内で無限連鎖し、Algodoo がハングする。eは Event。e.this/e.other/e.geom/e.laserに加え、イベントごとのe.pos、e.normal、e.dt、キー情報を持つ。"" + eでイベント構造を文字列化すると調査できる。- レーザーイベントは一回の画面フレーム中に複数回呼ばれることがあり、重い処理やトグルにはデバウンス/間引きが必要。
- 再利用可能な部品は、ローカル状態を
_name付きオブジェクト変数に、共有状態をscene.myに置く。エンティティ本体をscene.myに保存せず、ID をonSpawnで再取得する。 Scene.cloneEntityToはプロトタイプ複製に使える。保存・再読込でentityID/geomIDは変わるため、固定値の埋め込みは禁止。- デバッグの基本は Console/Logfile、
Console.print、TAB 補完、"" + e、最小イベントへの切り分け。再帰的プロパティや無制限 spawn はアプリごと落とす危険がある。
1. イベントと e.*
Algodoo 2.1.0 で onClick、onKey、onSpawn、onDie、postStep、update が追加された。公式説明は postStep = 各 simulation step 後、update = 各描画 frame である。(Post 66920)
| イベント | 発火と主な e |
実用メモ | 確度 |
|---|---|---|---|
onCollide |
幾何同士の衝突。e.this, e.other, e.pos, e.normal, e.soundGain |
e.pos は接触点。水など通常の geom でない相手は e.other 前提が崩れる可能性あり |
確認済み(値一覧: Post 86342、e.this/e.other: Post 86835) |
onLaserHit |
レーザー側のイベント。e.geom, e.laser, e.pos, e.normal |
当たった形状は e.geom。レーザー自身は e.laser |
確認済み(Post 68816、Post 86342) |
onHitByLaser |
形状側のイベント。内容は onLaserHit と同型 |
スクリプトを受光側へ置きたいときに使う | 確認済み(Post 68816、Post 86342) |
onSpawn |
エンティティ生成直後。追加値なし | 初期化、ID 再取得、部品登録向き。undo や clone/import でも走る前提で冪等にする | 確認済み(追加値: Post 86342、初期化例: Post 79188) |
onDie |
killer、timeToLive、remove 等で死亡時。追加値なし |
遅延実行や消滅時の複製に使える | 確認済み(公式: Post 66920、遅延: Post 77969) |
onKey |
押下/解放/リピート。e.pressed, e.keyCode, e.keyChar, e.handled, e.this |
GUI がキーを奪うため play mode が必要な場合あり | 確認済み(Algodoo 2.1: Post 72646、GUI 競合: Post 68918) |
postStep |
simulation step ごと。e.dt |
物理、制御、タイマーの標準置き場。停止中は走らない | 確認済み(公式: Post 66920、停止中: Post 86276) |
update |
描画 frame ごと。e.dt |
停止中にも動く UI/表示用。物理結果を frame rate に依存させない | 確認済み(停止中: Post 82903、update 必須例: Post 84699) |
水と onCollide は記録が矛盾する。2013 年には「水が geom の onCollide を発火するが e.other はない」とする投稿があり、2014 年には Kilinich が「水は onCollide を呼ばない」と回答している。(Post 71255、Post 73252) 版差または観測差の可能性があり 要検証。水粒子を通常 geom として e.other で扱う設計は避ける。
最小例
// onCollide フィールド
(e)=>{
e.other.color = e.this.color;
Console.print("hit=" + e.pos)
}
// レーザー側の onLaserHit フィールド
(e)=>{ e.geom.color = [1, 0, 0, 1] }
// 受光側で _lastLaser := 0 を一度宣言した後、onHitByLaser フィールド
(e)=>{ e.this._lastLaser = e.laser.entityID }
イベント欄には通常、onCollide = まで含めず、既存の (e)=>{} の中身を編集する。投稿内の説明でも、geom のイベント欄と Console のコードを混同したことがコンパイル失敗の原因になっている。(Post 68857)
e の中身を自分で確認する
(e)=>{ text = "" + e }
e を文字列へ変換すると ClassObject with children: 以下にイベント固有の構造が出る。この方法で e.dt、e.pos、e.normal 等が調査された。(Post 86360) ただし巨大な構造を text やカスタム変数に保持すると Script menu が重くなり得るため、調査後は消す。(Post 37285)
2. postStep と update の時間モデル
公式定義では、postStep はシミュレーション刻み、update は描画刻みである。60 Hz 描画に対し simulation frequency を 120 Hz にすると、同じ postStep の増分コードは画面更新までに 2 回走り、運動速度も 2 倍になったという実験がある。(Post 67555) よって tick ごとの固定増分ではなく e.dt を掛ける。
// postStep: 1 秒あたり 2 rad 増やす
(e)=>{ angle = angle + 2.0 * e.dt }
確度: 確認済み。なお Post 86276 は postStep を「once per scene frame」と表現するが、公式説明と 120 Hz 実験には合わないため、「各 simulation step」を採用する。
Algodoo 2.1 の既定 simulation frequency は 60 Hz。旧シーンは 100 Hz で読み込まれる互換処理があり、周波数は scene に保存される。(Post 67176、Post 67071) したがって、イベント回数を時間の代用にするコードはバージョン/シーン依存になる。
3. レーザーイベントの多重発火とデバウンス
onLaserHit は「1 frame に 1 回」とは限らず、値が 10 から 143 へ一気に増えたという再現報告がある。(Post 762、Post 768) 2010 年の実例では controllerAcc カウンタで 3 回に 1 回だけ重い処理を行うと大幅に高速化した。(Post 15248)
時間ベースの推奨形:
// geom に _nextLaser := 0 を宣言しておく
// laser.onLaserHit
(e)=>{
sim.time >= e.geom._nextLaser ? {
e.geom._nextLaser = sim.time + 0.10;
e.geom.color = e.geom.color == [1,1,1,1]
? [0,0,0,0]
: [1,1,1,1]
} : {}
}
確度: 有力。同じ考え方の _lastHit / system.time 例が Post 68816 にある。ただし同投稿は「古いレーザーは simulation time より realtime に近かった」と留保しているため、現行では sim.time と system.time のどちらを基準にするか要試験。物理再現性には sim.time、実時間 UI には system.time が自然。
controllerAcc は空いていればタイマー/カウンタに流用できるが、本来の Geometry Controller と衝突し得る。専用 _lastHit / _nextLaser の方が安全。(Post 68816)
controllerAcc を時刻レジスタにする旧来パターン
// onHitByLaser
(e)=>{
density = 1;
e.this.controllerAcc = sim.time
}
// postStep(元投稿は update)
(e)=>{
sim.time - controllerAcc >= 2 ? {density = 2} : {}
}
レーザー hit 時に sim.time を保存し、連続イベント側で経過時間を比較する方法が実例として示されている。(Post 71187) 確度: 確認済み。元例は update だが、停止中に期限を進めたくない物理用途では postStep が適する。Geometry Controller を使う部品では _triggerTime を新設する。
4. 初期化、遅延処理、無限連鎖
onSpawn で初期化・再バインド
// 部品オブジェクト
onSpawn = (e)=>{
scene.my.carControllerID = e.this.entityID;
scene.my.carReady == undefined ? { scene.my.carReady := false } : {}
}
Phunlet が必要な scene.my 変数を持ち込めない問題は、部品内の onSpawn で初期化する方法で解決確認されている。(Post 79188、動作確認 Post 79195) sim.time == 0 で scene start のみに限定し undo 時を除外する案もある。(Post 79206) ただし import/clone 時の初期化を逃すので、通常は「未定義なら初期化」の冪等ガードを優先する。
timeToLive + onDie を遅延キューとして使う
scene.addBox({
size := [0.001, 0.001];
collideSet := 0;
timeToLive := 0.5;
onDie := (e)=>{ scene.my.fire() }
})
確度: 確認済み。timeToLive=x の geom と onDie で x 秒後に処理する方法が明示されている。(Post 77969) ただしタイマー 1 個につき entity 1 個を作るので、大量用途は postStep のスケジューラ配列の方が軽い可能性がある。
危険な onSpawn 再帰
// 実行禁止: 同じ onSpawn を持つ box をその onSpawn 内で生成
(e)=>{ scene.addBox({onSpawn := onSpawn}) }
生成された box が同じ frame 内で次の box を生成し続け、画面更新前に Algodoo が応答不能になった。(Post 68857) 確度: 確認済み(投稿者が実行)。再生成は postStep で回数上限を付けるか、timeToLive/onDie で時間を分割する。
5. onKey と入力
Algodoo 2.1 では次が使える。
// onKey
(e)=>{
e.pressed && e.keyCode == "space" ? {
scene.my.fire()
} : {};
e.handled = true
}
// 連続状態を読む場合(postStep 等)
(e)=>{ keys.isDown("up") ? { e.this.vel = e.this.vel + [0, 3*e.dt] } : {} }
onKey は press/release の両方で呼ばれ、長押しはテキスト入力のように repeat する。e.pressed で押下と解放を区別する。e.handled=true は GUI に渡さない用途。(Post 72646) 文字入力では repeat による二重入力を避けるためクールダウンが必要。(Post 80954)
設計モードでは a、s などが GUI/tool に取られるため、play mode を使う。(Post 72646、Post 68923)
Keys.bind は scene 内で使わない。scene に保存されず、他ユーザーの config を汚染するため明確に非推奨とされている。(Post 69120、Post 69138) 確度: 確認済み。
6. 状態スコープ: scene.my、ローカル _name、ID
宣言と代入
:=は新規宣言、=は既存値への代入として扱う。- scene-scoped 変数は最初に
scene.my.speed := 0と宣言する。=だけでは宣言/保存されないことがある。(Post 12345、Post 12406) - オブジェクト固有で保存したい値は
_counterのように underscore を付ける。(Post 83996) scene.myは全体共有/通信に限定し、各マーブルのカウンタ等は各オブジェクトの_counterに置く。(Post 84137)
確度: 有力。ただし 2.1.0 では、オブジェクトを scene.my に保存した状態で scene を上書き保存すると _local が root/config に漏れ、再起動後にも残る再現報告がある。(Post 72453) プリミティブ値・配列・ID 以外を scene.my に入れない。
entity 本体を scene.my に保存しない
scene.my.box = entity のように ClassObject を入れると、保存時に全プロパティが root/config 側へ展開されることがあり、オブジェクト配列では crash も報告されている。(Post 82321) ID だけを保存し、必要時に解決する。
// controller.onSpawn
(e)=>{ scene.my.controllerID = e.this.entityID }
// 使用時
controller := scene.entityByID(scene.my.controllerID);
controller._tick();
controller = ""
entityID と geomID は保存・再読込ごとに変わる。(Post 69752) したがって固定 ID を scene.my に永続化するのではなく、毎回 onSpawn で再登録する。確度: 確認済み。
7. デバッグとエラー診断
基本手順
- Console を開く。古い版/資料では F11 または
~、新しい版では F10 の報告がある。(Post 10036、Post 85864) ホットキー差はバージョン/OS 依存として扱う。 Console.print("stage=" + value)で経路と値を追う。print(...)もConsole.printの別名として使われる。(Post 14322、Post 86801)scene.まで入力して TAB で利用可能な名前を列挙する。(Post 68888) Algodoo 1.8 から TAB 補完と/検索に値・既定値・説明が表示される。(Post 33482)- Console の内容を共有する場合は Documents/Algodoo の Logfile 末尾から取得する。(Post 13641)
- イベント引数を
"" + eでダンプし、想定した枝 (other,geom,laser) が存在するか確認する。(Post 86360)
注: 上記 F10/F11 のうち、抽出本文で確認できた新しい報告は「F10 だった」(Post 85864) であり、OS/バージョン別の正確な割当は 要検証。
Script menu は Algodoo 1.6.4 で multi-line function 対応、1.6.5 で改行が Shift+Enter となり、構文ミス時にコードを消去しないよう改善された。(Post 18847、Post 19243) 古い「Enter でコードが消えた」という報告を現行挙動として扱わない。
代表的なエラーの読み方
Cannot find identifier "x" in "my":scene.my.xが未宣言、または Phunlet 化で scene.my セクションを失った。(Post 41384)Not an integer/bound to bad type: int 属性へ bool/float/ClassObject 等を渡した。エラー後に前値へ戻される場合がある。(Post 41384)Wrong number of arguments: イベント値を(e)=>{}でなく{}にした古い 1.7.5 例。(Post 31626)Newline in string literal: 余分な"などで string が途中終了。Console が line number を出す。(Post 68605)- 値が
NaN/ 型違い: motorSpeed 等の属性コード自体が消えたり、entity が削除された古い報告がある。まず型と未定義値を print する。(Post 12255)
切り分け用テンプレート
(e)=>{
Console.print("enter");
Console.print("this=" + e.this);
scene.my.target == undefined ? {
Console.print("target undefined")
} : {
Console.print("target=" + scene.my.target)
}
}
一行ずつ戻し、イベント種別、変数宣言、型、対象参照の順に確認する。e.this は (e)=>{} のあるイベント内だけで使える。イベント引数のない code block では e 自体が存在しない。(Post 82313)
8. 無限ループ・再帰・クラッシュ
density = {density}のようにプロパティが自分自身を参照する reactive recursion はアプリ crash を起こす。開発者は Thyme 実行系の全面書換えが必要と回答しており、少なくとも 1.7.5 時点では未修正。(Post 31626、Post 31836) 確度: 確認済み・旧版依存。現行でも禁止パターンとして扱う。- 再帰関数は終了条件が必須。未定義値が見つかるまで再帰する例でも「見つからなければ stack overflow」と明記される。(Post 80839)
- 2010 年の
for/再帰には 40~48 程度の深さ制限報告がある。(Post 27560、Post 27625) 数値は版により揺れているので 要検証。大配列処理を 1 tick の深い再帰で行わない。 - collision/spawn で指数的に entity を増やすと数秒~十数秒で crash する。(Post 68857) 上限、TTL、kill plane、
Scene.removeEntityを必ず設ける。
9. 性能最適化
優先度順:
- 高頻度イベントを間引く。 特に laser の重い処理・collision layer 切替は
_nextRunで 0.05~0.2 秒に一度へ。(Post 15248) - 物理処理は
postStep、停止中不要ならupdateを避ける。updateは常時走る。(Post 86276) readable(...)と entity lookup を繰り返さない。readableは CPU-resource-heavy とされ、1 回だけローカルへキャッシュする。(Post 65554)
thyme
ownerGeom := scene.entityByGeomID((readable(e.this)).geom);
v := ownerGeom.vel;
// ownerGeom を同じ event 内で再利用
readable と reactive 属性内の owner scope を得る owner は Algodoo 2.0 RC5 で追加された。(Post 53404) それ以前の scene/Phun 互換を要求する場合は使えない。
- ClassObject を
scene.myに保存しない。 serialize 膨張、config 汚染、list では crash の報告。(Post 82321) - 巨大配列/ClassObject をオブジェクトの表示対象変数に置かない。 Script menu を開くだけで数分ハングした報告。(Post 37285)
- 生成物の寿命を管理する。 spawner の projectile/particle は
timeToLive、killer、destroy button 等で蓄積を防ぐ。(Post 10641) - イベント順序へ依存しない。 同一 step 内の laser/script 実行順は entityID 順と推測され、配置交換で Game of Life が壊れた。(Post 70942、Post 70946) これは公式確定でなく 有力。共有状態は一旦 next-state に書き、次の
postStepで commit する二相更新が安全。 - nested loop の一時参照を解放する。 2020 年の倍増型
xForは nested 実行で memory leak し、末尾でiteration = ""; exec = ""として参照を切ると解消した報告がある。(Post 87163) 単一実装の報告なので 有力。大反復は 1 tick に詰め込まず分割する。
10. clone、保存、undo、バージョン差
Scene.cloneEntityTo
clone := Scene.cloneEntityTo(e.this, [10, 10]);
clone.color = [1, 1, 1, 1]
2014 年以降に複数の動作報告があり、clone の返り値を編集できる。(Post 77540、Post 87220) 確度: 確認済み。プロトタイプを見えない場所へ置くより、背景固定・非衝突の sample object を用意し、onSpawn で ID を登録する。
保存・Phunlet
scene.myは scene とともに保存されるが、宣言は:=を使う。(Post 12406)- 2011 年の custom menu 例では、Algodoo 内の「Save as phunlet」で保存すると
scene.myセクションが消え、browser 側から phunlet folder へ保存する必要があった。(Post 41425) 旧版依存・要検証。 - ID は reload で変わるため、onSpawn 再バインドが必須。(Post 69752)
undo
- 2.1.0 beta 初期には
timeToLiveで死んだ entity が undo で戻らない bug があり、その直後の build で修正された。(Post 66923、Post 66928) App.AddUndoPointは game 等の save point 用 Thyme command として Algodoo 2.0 RC5 で追加された。(Post 53404) scene 内で意図的に undo 境界を作る場合の正規 API 候補だが、引数・現行挙動は 要検証。- 旧「New Method」は
app.step; app.undoで hidden 属性を refresh する hack だったが、undo を壊し、simulation を遅くし、版によって geomID attach に使えない。(Post 4669、Post 35076) 現代コードには採用しない。
バージョン境界
- entity events と
keys.isDownは 2.1.0 追加。2.0.x/Phun 向け資料には存在しない。(Post 66920) - 古い scene は simulation frequency 100 Hz を維持し、新規 2.1 scene は既定 60 Hz。tick-based code の挙動が変わる。(Post 67176)
- 1.7~1.9 の New Method、laser beacon、
Keys.bindscene hack は歴史資料として扱い、2.1 の event/onSpawn/postStep へ置換する。
11. 関数ライブラリとメニューの再利用パターン
A. scene 内ライブラリ
scene.my.clamp := (x, lo, hi)=>{ x < lo ? lo : {x > hi ? hi : x} };
scene.my.every := (now, next, dt)=>{ now >= next ? now + dt : next }
scene.my.fn は scene と一緒に持ち運ぶ用途。長いイベントコードを scene.my.explosion(pos) 等へ隠すと読みやすくなるという実践例がある。(Post 14796) 配列 element 書換えなど、組み込み不足を scene.my 関数で補う共有例も複数ある。(Post 86332、より単純な slice 版 Post 87161)
確度: 確認済み。ただし scene.my を持たない phunlet 単体 import では、controller object の onSpawn に library 初期化を持たせる。
B. controller object をライブラリ/疑似クラスにする
背景固定・非衝突の box に _init、_tick、_state を置き、onSpawn で ID を公開する。
// controller box
_state := 0;
_tick := (dt)=>{ _state = _state + dt };
onSpawn := (e)=>{ scene.my.controllerID = entityID };
// client
c := scene.entityByID(scene.my.controllerID);
c._tick(e.dt);
c = ""
新しい ClassObject は通常作れないため geom を namespace として使う案が提示されている。(Post 72334) 2020 年には ID のみを scene.my に保存し onSpawn 再登録する疑似クラス実装が提示された。(Post 87159) 確度: 有力。
2020 年の alloc を用いる独立 instance/map 実装もあるが、単一投稿で保存・reload の検証が不足するため 要検証。(Post 87160)
C. config 全体ライブラリ
scene.my 以外に作った関数は config に serialize され、Console または config.cfg から定義すれば永続化するという説明がある。(Post 71196) これは作者個人の開発環境用には便利だが、scene の閲覧者へは移らず、Keys.bind と同様に他人の config を汚染し得る。配布物には使わない。確度: 有力・環境依存。
D. データ駆動 custom menu
歴史的な user-menu phunlet は、[title, width, count, labels, actions] の配列を scene.my.menuOpen(menu, pos) に渡し、submenu も action から開く設計だった。(Post 39829) 条件付き label を関数で生成する dynamic menu も動作報告がある。(Post 39985)
scene.my.menu := [
"Car", 8, 2,
["Reset", "Close"],
[{scene.my.resetCar()}, {scene.my.menuClose}]
];
確度: 要検証(Algodoo 1.8 時代)。発想は有用だが、phunlet 保存方法、laser activation、collision group G への依存がある。現行 MCP 用 skill では、menu 自体より「actions を関数配列として分離する」設計だけ採用するのが堅い。
実装チェックリスト
- [ ] event field には
(e)=>{...}を入れ、Console 用コードと混ぜていない - [ ]
onCollideはe.other、laser はe.geom/e.laserを使い分けた - [ ] 高頻度 event に
_nextRun/_lastHitがある - [ ] 物理量の積分に
e.dtを掛けた - [ ]
updateを停止中にも走らせる必要が本当にある - [ ]
onSpawn初期化が二重実行されても壊れない - [ ]
onSpawnから同じonSpawnを持つ entity を即時再生成していない - [ ]
scene.myに entity/ClassObject を保存していない - [ ] reload で変わる entityID/geomID を固定値で埋めていない
- [ ] custom object state は
_name、共有状態だけscene.my - [ ] spawn 物に TTL/killer/remove の終了経路がある
- [ ] Console/Logfile に error がなく、
""+eと print で event 値を確認した - [ ] 旧 New Method /
Keys.bindscene hack を使っていない