ClusterScript作例
CCK2.0以降で対応
Scriptable Item
これをItemに付ける
ファイルもしくは直接記述、20KBまで
リファレンス
サンプル記事
ファイルサイズのチェック
UseDecimalでKiB→KB表示にできる
インタープリター
基礎
Hello Cluster!
Hello Clusterとコンソールに出力だけのコード
code:js
$.log("Hello Cluster!"); // Hello Clusterとコンソールに出力
コメント
code:js
// 1行コメント
/* 複数行コメント/* */で囲まれた範囲がコメントになる
コメント1
*/
定数/変数の定義
code:js
const a = 123; // 定数
const text = "Hello Cluster!"; // 文字列
let b = 123; // ローカル変数、{}内でのみアクセスできる。(書き換え可能)
b = 456; // bに456が入る(代入)
代入操作
変数に値を入れたり、計算結果を変数に保存するときに使用する。
code:js
let a = 12;
a = 123; // aに123を入れる。
a = a + 4; // a+4の結果がaに入る
// 左辺 = 右辺;
右辺の式が計算(評価)されその結果が左辺の変数に入る。
小数の書き方
code:js
123.4
0.4
// 以下のような片側の0を省略した書き方もできる
.4 // 0.4
4. // 4.0
123e2 // 12300 (123 x 10の2乗)
123e-2 // 1.23 (123 x 10の-2乗)
code:js
code a = 123e2;
文字列の書き方
ダブルコーテーション「"」か シングルコーテーション「'」で囲む
code:js
"文字列"
'文字列'
code:js
const text = '文字列';
ブロック
code:js
{
// いろんな処理
// 複数の処理をまとめる時に使う
// ここで定義したlet/const変数はこのブロック内でのみ使える
}
ログ
code:js
const a = 123;
$.log(a); // 123とコンソールに出力
セミコロン ;
code:js
// 命令の区切り,終わりを示す
cost a = 123; $.log("ほげ"); // ;で区切れば1行にまとめられる
Javascriptにおいては改行は区切り扱いにならない。末尾に;があるのはこのせい。
関数/メソッド
処理をまとめたり、共通化したいときに使う
引数:パラメータ/入力値
code:js
const 関数名 = (引数1,引数2) => { // 引数がない場合は()内は空でも良い
// 何かの処理
return 返り値; // returnは無くても良い
};
code:js
const add = (a, b) => { return a + b; }; // 足し算するだけ
const answer = add(1, 3); // 引数aに1、bに3
const multiply = (a,b=2) => { return a + b; }; // 引数にデフォルト値を入れられる(右側の引数から設定)
const log = (text) => { $.log(${text}); }; // $.logを実行するだけの関数
テンプレートリテラル
文字列をフォーマットできる
code:js
$.log(a:${a}); // テンプレートリテラルで囲むとフォーマットできる。変数は${}で囲む。
const add = (a, b) => { return a+b; };
$.log(a:${add(a,3)}); // a:126 ${}内には処理も直接記述できる(値を返す関数なら返り値が埋め込まれる)
数値の四則演算
code:js
let a = 10; // aに10を入れる(代入)
a = a + 5: // 足し算
a = a * 3; // 掛け算
a = a / 2; // 割り算
a = a % 3; // 余り 3で割った余り
a = a * a; // 二乗
a = a ** 3; // 3乗 (指数)
code:js
a = (a + 2) * 3; // ()内が優先される
短縮して記述
「+=」といった形式
code:js
a += 2; // a = a + 2;と同じ
インクリメント/デクリメント
+1/-1したい時に使う
code:js
i++; // i=i+1;と同じ
i--: // i=i-1;と同じ
ブーリアン/boolean
true/falseの2値
code:js
const isON = true;
条件分岐
code:js
if ( 条件 ) {
// 条件がtrueなら実行
} else {
// 条件がfalseなら実行
}
code:js
if ( 条件1 ){
// 条件1がtrueなら実行
} else if( 条件2 ) {
// 条件1がfalse、条件2がtrueなら実行
} else {
// 条件1がfalse、条件2がfalseなら実行
}
条件式の書き方
code:js
a == 1 // 値が1ならtrue
a != 1 // 1でないならtrue
a < 1 // 1より小さいならtrue
a > 1 // 1より大きいならtrue
a <= 1 // 1以下ならtrue
a >= 1 // 1以上ならtrue
code:js
if (a==1) { // aが1
}
else { // aが1でない
}
AND(&&)条件OR(||)条件
code:js
// ANDは&&
1 <= a && a < 10 // 1以上10より小さいならtrue
// ORは||
a < -1 || 1 < a // -1より小さい、または1より大きい場合true
// 計算式も入れられる
a % 2 == 0 // aの余りが0、偶数の場合true
// ()で優先度も
(1<a && a<10) || (20<a && a<30) // 1 < a < 10または 20 < a < 30ならtrue
code:js
// 使用例
if(1 <= a && a < 10) {
}
==は実は厳密な比較でない。1=="1"などがtrueになる。
配列/リスト
「[]」で囲んで定義
code:js
const b = arr0; // 0から順番に数字でアクセス const empty = []; // 空の配列
arr.length; // 配列の長さ
[]内の添え字は0から始まることに注意
forループ
code:js
for(let i=0; i<10; i++) {
// i=0,1,....9 まで10回繰り返す
}
code:js
for(1.初期化処理; 2.継続条件; 3.{}内の処理後に繰り返す処理) {
}
// 1が実行される
// 2がtrueなら{}内を実行-> 3も実行 -> 2の判定を繰り返す
// 2がfalseなら即終了
break - ループを抜ける
code:js
for(let i=0; i<10; i++) {
if(i==6) break; // breakで即ループを終了させる
}
continue - 次のループへ行く
code:js
for(let i=0; i<10; i++) {
if(i==5) continue; // continueで次の繰り返しへ(i++ した後 i<10の繰り返し判定に行く)
}
forループで配列にアクセス
code:js
for(let i=0; i < arr.legnth; i++) { // arr.legnthは5、i=0->1->2->3->4になる
}
for-of
添え字なしでアクセスもできる
code:js
for(const a of arr) {
$.log(a);
}
TBD:Vector3,Quaternion
型
table:type
型名 値 値の例 使い道
boolean 真偽値 true, false 条件判定、ON/OFFフラグ
number 整数、小数 123, 123.4
string 文字列 "Hello" 文字列
Vector2 2次元ベクトル (3, 4) 位置座標、回転角度
Vector3 3次元ベクトル (0,1,2) 位置座標、回転角度
Quaternion 四元数(4次元ベクトル) 回転情報
state
値の保存用、スクリプトは定期的にリセットされるのでstateで値を保存しよう。
code:js
$.state.変数名 = 値
code:js
$.state.angle = 60;
保存できる値の種類
code:js
boolean | number | string | Vector2 | Vector3 | Quaternion | null
??演算子(Null 合体演算子)
stateの初期化で使う。未定義の変数を初期化できる。
code:js
$.state.hoge = $.state.hoge ?? 0; // $.state.hogeが未定義(undefined/null)なら0を入れる。
$.state.hoge ??= 0; // これでもOK
未定義の変数はundefinedとなるため、それを利用
stateのVector3などの挙動
通常のVector3変数だとaddなどで中身が書き換わるがstateだと更新されない
code:js
$.state.vec3.add(addVec3); // これだとstateは保存されない
$.state.vec3 = $.state.vec3.add(addVec3); // 代入が必要
添え字でのアクセス
code:js
$.state"hoge" = $.state"hoge" ?? 0; // $.state.hogeが未定義(undefined/null)なら0を入れる。 $.state"hoge" ??= 0; // これでもOK Javascriptでいろいろ
clamp
code:js
const clamp = (n, a, b) => {
return Math.min(b, Math.max(a, n));
};
range
a<= n <bの数例を生成
(遅い)
code:js
const range = (a, b) => {
return Array(b - a).fill().map((_, i) => i + a);
};
(速い)
code:js
// a ~ b-1 までの数列を返す
const range = (a, b) => {
const arr = new Array(b-a);
for (let i = a; i < b; i++) arri-a = i; return arr;
};
// 使用例
シャッフル
code:js
const shuffle = (array, N=3) => {
for (let i = 0; i < N; i++) array.sort(() => Math.random() - 0.5);
return array;
};
// 使用例
shuffle(range(10)); // range()と組み合わせ
1回だけだと偏りが出ることがあるので複数回シャッフル(Nで調整)
例外
sendの上限エラーなどでエラー(例外)が発生する。
エラー発生時は以降の処理が中断するが、try,catchで囲むとcatch以降も実行できる。
code:bash
try {
// 処理
// エラー発生時は発生時点で{}内の処理は中断される
}
catch (err) {
// エラー発生時にこの処理が実行される
}
ClusterScriptでいろいろ
setStateCompat
トリガー送信
code:ks
$.setStateCompat("this", "foo", true);
シグナル送信
code:js
$.sendSignalCompat("this", "Signal1");
getStateCompat
取得
code:txt
"signal" | "boolean" | "float" | "double" | "integer" | "vector2" | "vector3"
code:txt
$.getStateCompat("this", "foo", "boolean");
Signalを取得する際、parameterTypeがsignalだとDateオブジェクトになる。(signalだと小数点以下切り捨て)
stateの初期化
code:js
const initialize = () => {
if($.state.initialized) return; // 1回だけ実行
$.state.initialized = true;
// ここで初期化
};
$.onUpdate((deltaTime) => {
initialize();
});
トグルスイッチ
押されるたびにON/OFF
code:js
$.onInteract(() => {
$.state.isON = !$.state.isON ?? true; // isONの値を切り替え、最初に押されたときはtrue
$.setStateCompat("this", "isON", $.state.isON); // isONトリガーに設定
});
回転
固定軸回転
code:js
const target = $; // 回転させる対象、子オブジェクトは$.subNode("Cube")などで指定
const ROTATE_SPEED = 40; // 回転速度(度/秒)
const AXIS = new Vector3(1, 1, 0).normalize(); // 回転軸、Y軸(0,1,0)
const newRotation = new Quaternion(); // 最終的な回転角度格納用
$.onUpdate((deltaTime) => {
const rotateSpeed = ROTATE_SPEED * deltaTime; // この1フレームで回転させる角度
let angle = $.state.angle ?? 0; // stateに保存されていればその値を取得。そうでなければ初期値0
angle += rotateSpeed; // 現在の角度に加算
$.state.angle = angle; // stateに現在の角度を保存
newRotation.setFromAxisAngle(AXIS, angle); // 回転軸周りにangle度の角度を示すQuaternionを計算
target.setRotation(newRotation); // 回転を設定
});
(2022.1.5 $.getRotation().apply()方式だと微小角度の回転ができないのでstate方式に書き換え)
※quaternionを外部でnewして使いまわしてるのは毎フレームnewでのメモリリークを考慮
XYZ軸、それぞれで回転
code:js
const target = $; // 回転させる対象、子オブジェクトは$.subNode("Cube")などで指定
const ROTATE_SPEED = new Vector3(0, 40, 90); // XYZ軸、回転速度
const rotateSpeed = new Vector3(); // 回転角速度の格納用
const newRotation = new Quaternion(); // 最終的な回転角度の格納用
$.onUpdate((deltaTime) => {
rotateSpeed.set(ROTATE_SPEED.x, ROTATE_SPEED.y, ROTATE_SPEED.z);
let angle = $.state.angle ?? new Vector3(); // stateに保存されていればその値を取得。そうでなければ初期値0
angle.add(rotateSpeed.multiplyScalar(deltaTime)); // 現在の角度に加算
$.state.angle = angle; // stateに現在の角度を保存
newRotation.setFromEulerAngles(angle); // 各軸の回転からQuaternionを計算
target.setRotation(newRotation); // 回転を設定
});
上下にアニメーション
code:js
const target = $.subNode("Cube"); // 動かす対象
const period = 6; // 周期(1以上)
const move = 0.2; // 上下の移動量
const offset = new Vector3(0,0,0); // オフセット(位置調整用)
const pos = new Vector3();
$.onUpdate((deltaTime) => {
$.state.time = ($.state.time ?? 0) + deltaTime;
const t = ($.state.time % period) / period;
const y = move * Math.sin(t * Math.PI * 2);
target.setPosition(pos.set(0, y, 0).add(offset));
});
ばねで上下
sin使わないのでたぶんこっちのが軽い
code:js
const target = $.subNode("Cube"); // 動かす対象
const period = 6; // 周期
const length = 0.2; // 上下の移動量
const offset = new Vector3(0, 0, 0); // オフセット(位置調整用)
const pos = new Vector3();
const k = ((2 * Math.PI) / period) ** 2;
$.onUpdate((deltaTime) => {
$.state.len ??= length;
$.state.v ??= 0;
const at = k * $.state.len * deltaTime;
$.state.v -= at;
$.state.len += $.state.v * deltaTime;
target.setPosition(pos.set(0, $.state.len, 0).add(offset));
});
ただし、速度と位置をstateで持っているので、ホットリロードで周期とかのパラメータ更新すると変な適用になる。更新時は新しいサーバを立てた方が良い。
ふわふわ回転
上の2つの組み合わせ
code:js
const target = $.subNode("Cube"); // 動かす対象
const period = 6; // 周期(1以上)
const move = 0.2; // 上下の移動量
const offset = new Vector3(0,0,0); // オフセット(位置調整用)
const rotateSpeed = 20; // 回転速度
const axis = new Vector3(0.5, 1, 0); // 回転軸、Y軸(0,1,0)
const pos = new Vector3();
const quaternion = new Quaternion();
$.onUpdate((deltaTime) => {
$.state.time = ($.state.time ?? 0) + deltaTime;
const t = ($.state.time % period) / period;
const y = move * Math.sin(t * Math.PI * 2);
target.setPosition(pos.set(0, y, 0).add(offset));
let angle = $.state.angle ?? 0;
angle += rotateSpeed * deltaTime;
$.state.angle = angle % 360; // 360越えたら余り
target.setRotation(quaternion.setFromAxisAngle(axis, $.state.angle));
});
jsonでstate保存
文字列が保存できるでjson形式で保存すれば連想配列(辞書)も保存できる。
code:js
$.state.hoge = $.state.hoge ?? "{}";
const hogeData = JSON.parse($.state.hoge); // 読み込み(JSONをパース)
//
// hogeDataを変更
//
$.state.hoge = JSON.stringify(hogeData); // JSON文字列にして保存
文字列で配列保存
0~65535までの整数を文字列で保存
code:js
const arr2str = (arr) => {
return String.fromCharCode(...arr);
};
const str2arr = (dest, src) => {
for (let i = 0; i < src.length; i++) desti = src.charCodeAt(i); return dest;
};
// 使用例
const array = Array(10).fill().map((_, i) => i); // 配列データ
$.state.arrayStr = arr2str(array); // 文字列化
str2arr(array, $.state.arrayStr); // 読み込み
シグナル検出
トリガーに保存する版
code:js
// シグナル検出
const detectSignal = (target, name) => {
const signal = $.getStateCompat(target, name, "double");
const keyName = ${target}_${name};
const lastSignal = $.getStateCompat("this", keyName, "double");
if (signal > lastSignal) {
$.setStateCompat("this", keyName, signal);
return true;
}
return false;
};
$.onUpdate((deltaTime) => {
if (detectSignal("this", "SignalName")) {
$.log("Signal Name Detect!");
}
});
json使う版
code:js
const detectSignal = (target, name) => {
$.state.signals = $.state.signals ?? "{}";
const signals = JSON.parse($.state.signals);
const lastSignal = signalsname ?? 0; const signal = $.getStateCompat(target, name, "double");
if (signal > lastSignal) {
$.state.signals = JSON.stringify(signals);
return true;
}
return false;
};
// 使用例
$.onUpdate((deltaTime) => {
if (detectSignal("this", "SignalName")) {
$.log("Signal Name Detect!");
}
});
stateを使う方式に修正(2022/10/24)
時刻付きログ
code:js
const pad = (n, l = 2) => {
return n.toString().padStart(l, "0"); // 左側を0詰めする
};
const now = () => {
const d = new Date(); // 現在時刻
const time = `${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(
d.getSeconds()
)}.${pad(d.getMilliseconds(), 3)}`; // hh:mm:ss.fffでフォーマット
return time;
};
const log = (...msg) => $.log(${now()} ${msg.join()}); // 時刻付きログを出力
使用例
code:js
log("hoge");
log("hoge", 123, value1);
持ってるアイテムの移動距離を計算
code:js
$.onGrab((isGrab) => {
$.state.isGrab = isGrab;
if (isGrab) $.state.initialPosition = $.getPosition(); // 掴んだ時点のアイテムの位置を保存
});
$.onUpdate(() => {
if (!$.state.initialPosition || !$.state.isGrab) { // 掴んでなければdistanceトリガーの値をを0に
$.setStateCompat("this", "distance", 0);
return;
}
const pos = $.getPosition();
const distVec = pos.sub($.state.initialPosition); // 現在位置と掴んだ時点の位置の差分ベクトル
const distance = distVec.length(); // 差分ベクトルのの長さ
$.setStateCompat("this", "distance", distance); // distanceトリガーに距離を設定
});
持っているアイテムの速度とるやつ
code:js
$.onGrab((isGrab) => {
$.state.isGrab = isGrab;
if (isGrab) $.state.lastPosition = $.getPosition(); // 掴んだら前回の位置として保存
});
$.onUpdate((deltaTime) => {
if (!$.state.lastPosition || !$.state.isGrab) { // アイテムを掴んでなければvelocityトリガーの値を0にする。
$.setStateCompat("this", "velocity", 0);
return;
}
const pos = $.getPosition();
const diff = pos.sub($.state.lastPosition); // 現在の位置座標と前回の位置座標の差分ベクトル
$.state.lastPosition = $.getPosition(); // 位置座標を記録
const velocity = diff.length() / deltaTime; // 差分ベクトルの長さ/経過時刻で現在の移動速度(m/s)を計算
$.setStateCompat("this", "velocity", velocity); // veolocityトリガーに値を設定
});
アイテム間の距離を取得 2022/10/22修正
code:js
const target = $.subNode("Target"); // 子オブジェクトTargetを別アイテムにコンストレイントで括りつける
$.onUpdate((deltaTime) => {
const distance = target.getPosition().length(); // ローカル座標
$.setStateCompat("this", "distance", distance);
});
一定時間ごとに実行
code:js
const callInterval = (name, deltaTime, interval, func, ...args) => {
const key = callInterval_${name}};
$.statekey = ($.statekey ?? interval) + deltaTime; if ($.statekey < interval) return; func(...args);
};
使用例
code:js
const func1 = (param1, param2) => {
// なんか処理
};
$.onUpdate((deltatime) => {
callInterval("Task1", deltaTime, 2, func1, 1, 2); // 2秒ごと呼び出し
callInterval("Debug1", deltaTime, 4, log, "Hello Cluster"); // 4秒ごとログ出力
});
BPM計測
手にしたアイテムのUse間隔からBPMを求める
code:bash
const LAST_SEC = "lastSec";
const BPM = "bpm";
const AVG_BPM = "avgBpm";
const DELTA_SEC = "deltaSec";
const AVG_DELTA_SEC = "avgDeltaSec";
const ARR_SEC_LENGTH = "arrSecLength";
$.onGrab((isGrab) => {
if (!isGrab) return;
$.setStateCompat("this", LAST_SEC, -1);
arrSec.length = 0;
});
const arrSec = [];
const MAX_STACK = 20;
const sec2bpm = (s) => {
return Math.round(60 / s);
};
$.onUse((isDown) => {
if (!isDown) return;
const currentSec = new Date().getTime() / 1000;
const lastSec = $.getStateCompat("this", LAST_SEC, "double");
if (lastSec > 0.0) {
const detalSec = currentSec - lastSec;
const bpm = sec2bpm(detalSec);
$.setStateCompat("this", BPM, bpm);
$.setStateCompat("this", DELTA_SEC, detalSec);
// 平均方式
if (arrSec.length >= MAX_STACK) arrSec.shift();
arrSec.push(detalSec);
// 平均値でBPMを計算
const total = arrSec.reduce((s, v) => s + v, 0);
const avgSec = total / arrSec.length;
const avgBpm = sec2bpm(avgSec);
$.setStateCompat("this", AVG_BPM, avgBpm);
$.setStateCompat("this", AVG_DELTA_SEC, avgSec);
$.setStateCompat("this", ARR_SEC_LENGTH, arrSec.length);
}
$.setStateCompat("this", LAST_SEC, currentSec);
});
FPS計測
code:js
const arrSec = [];
const MAX_STACK = 20;
$.onUpdate((deltaTime) => {
if (arrSec.length >= MAX_STACK) arrSec.shift();
arrSec.push(deltaTime);
const total = arrSec.reduce((s, v) => s + v, 0);
const avgDelta = total / arrSec.length;
const fps = 1 / avgDelta;
$.setStateCompat("this", "fps", fps);
});
ItemのOwnerで計算される(結果はOwnerの値になる)
注意点
グローバル変数のリセット
グローバル変数は定期的にリセット(スクリプトがリロード)される。Ownerが変わってもリセット。
永続化したい状態はstateに保存する
正確なリセット時間は不明、5分くらい?
配列の最大サイズ
512まで
stateの数値保存精度
float 32bit (IEE754-FP32)
numberやらsignal(double)の値を保存しようとすると精度が足りなくて正確に保存できない。
精度が必要な場合はsetStateCompatを使う。
整数値の精度限界はおそらく 2^24 - 1=16777215
この値を越えて計算すると結果がおかしくなる?
ItemTriggerとの同時併用
onInteract() + InteractItem:NG
onGrab(isGrab) + OnGrabItemTrigger/OnReleaseItemTrigger:OK
onUse(isDown) + UseItemTrigger:NG
onRide(isGetOn)+OnGetOnItem/OnGetOffItem:OK
単純な無限ループは危険
while(true){}など
FPSが1になるのでやるべきではない
非アクティブ
非アクティブのアイテムでもスクリプトは動き続ける
デバッグ方法
ログを常時出力させながら部分アップロード
OnInterat
OnInteract関数を使っていれば、InteractItemTriggerを付けなくてもInteractできる。
位置の同期
SetPosition/SetRotationでも同期されるが、MovableItemを付けた方が動きが滑らか
onUpdateの呼び出し
FPSに依存する
スマホなら30fps上限
PCなら90fps
その他
evalは使える
Reflect.defineProperty()は使えない?
アップロード時の注意
アップロード時に20KBを超えるスクリプトエラーが出たときは、ScriptableItemのSourceCodeを一回消さないとエラー出続ける。ファイルだけ編集しても解消されない。