Grimoire.js をより便利に、効果的に扱うため、あるいはバグの対処のために覚えておくといいことを7個紹介する。
少し踏み入った内容になるので、是非Grimoire.jsって何?って方は、とりあえず**こちら**を参照していただきたい。
フロントインターフェース編
ノード・コンポーネントの登録のタイミングに気をつけよ
Grimoire.jsのタグツリーが構築されると、gr(function(){})
によって登録したイベントハンドラが呼び出される。
Grimoire.jsを扱ったことがある方なら、以下で表示されたタイミングから1秒後にメッシュの色が赤くなることがわかるだろう。
(以下、gomlのscriptタグにid="main"
が付いていると仮定する)
gr(function(){
gr("#main")("mesh").setAttribute("color","red");
});
この、grが呼び出されるのは、必ずGrimoire.jsのロード時に存在したHTML上のtype="text/goml"
が指定されたscriptタグにより指定されたGOMLのツリーが構築された直後である。
したがって、コンポーネントやノード登録の処理を以下のように書くのは無効である。
gr(function(){
gr.registerComponent("SomeComponent",ComponentConstructor);
gr.registerNode("some-node",[...],{...});
gr("#main")("mesh").setAttribute("color","red");
});
なぜなら、この関数が実行されるのはツリー構築後なのだ。もしも、GOML内にこれらを使うコードがあるなら例外を出すことになってしまうだろう。したがって、これは以下のようにgrの外に書くべきだ。
gr.registerComponent("SomeComponent",ComponentConstructor);
gr.registerNode("some-node",[...],{...});
gr(function(){
gr("#main")("mesh").setAttribute("color","red");
});
first()よりもsingle()を使え
特定のノードの特定のコンポーネントをフロントインターフェース側で取得したいケースが稀に存在する。
このような場合、以下のように書けば良い。
const c = gr("#main")("mesh").first().getComponent("コンポーネント名");
gr("#main")("mesh").first()
によって、見つかった中で一番最初のノードを返してくれる。このノードに対してgetComponent
をするわけだ。
しかし、同じノード名はどうしたって存在するし、思ったようにクエリは動いていないかもしれない。
本当は一つだけを絞っているつもりが複数個取れてしまってもバグになる。
もし、これが0個しかとれないならば、nullになるので例外ですぐにわかることであろう。しかし、first()
を使ってしまうと、思ったものが2つ存在して、片方だけ実行しても問題は全くない。
しかし、もし取れるものが必ず一つだと思う場合はsingle()
を代わりに使うといいだろう。もしもこの時2つ以上存在すると例外をだす。
const c = gr("#main")("mesh").single().getComponent("コンポーネント名");
クラス参照の取得を活用せよ
Grimoire.jsの開発中では、各tsファイルは別々に記述されており、内部的にはimport
やrequire
でそれぞれの参照を取得している。
Grimoire.jsを利用する場合はスクリプトタグあるいはnpmを用いて利用することができるが、そのどちらをとっても、ほぼ全てのGrimoire.jsでの内部のファイルのデフォルトのエクスポートへアクセスすることができる。
取得の対象は、取得する対象がコアの中に定義されているものか、grimoireのプラグインの中に定義されているものかによって少し取得方法が異なる。
grimoirejsのコアのそれぞれのファイルのデフォルトエクスポートへのアクセス
例えば、Grimoire.js内でsrc/Node/Component
に定義されている、Component
クラスへは以下のような形式でアクセスできる。
ES6での取得例
import Component from "grimoirejs/ref/Node/Component";
scriptタグからの取得例
const Component = gr.Node.Component;
grimoirejsのプラグインのそれぞれのファイルのデフォルトエクスポートへのアクセス
Grimoire.jsではベクトル型でさえもプラグインとして登録されている。普段、grimoirejs-preset-basic
という、最低限のプラグインなどが含まれているパッケージを利用する方が多いと思うが、この中に含まれているgrimoirejs-math
こそがベクトルなどのクラスを管理しているパッケージである。
例えば、3次元ベクトルを管理するクラスであるsrc/Vector3
に定義されているVector3
は以下のように取得することができる。
ES6での取得例
import Vector3 from "grimoirejs-math/ref/Vector3";
scriptタグからの取得例
const Vector3 = gr.lib.math.Vector3;
コンポーネント系
attributeの変数とのバインドは$awakeで行え
もし、attributeの中の変数を、コンポーネント内にバインドするなら$mount
で行うか、$awake
で行うか悩む場合がある。
しかし、もし、コンポーネント自身にバインドするなら、それがどのノードに属しているかに寄らず、コンポーネントの変数に代入してほしいはずである。
あるコンポーネントに最初に呼ばれるメッセージ$awake
は、再度マウントされたりしても2度と呼ばれることはない。最初の一度だけ呼ばれることが保証されるメッセージである。
したがって、ある変数をバインドするなら、以下のようになるだろう。
gr.registerComponent("ABC",
attributes:{
someAttribute:{
converter:"String",
default:""
}
},
$awake:function(){
this.getAttributeRaw("someAttribute").boundTo("_someAttribute");
}
});
これ以降は、このコンポーネントの中ではthis._someAttribute
によりアクセスできるので、いちいちthis.getAttribute
を呼ぶ必要はない。
これは、属性の検索や、コンバーターの呼び出し回数の削減につながるので多くの場合、コンポーネントの最適化にもつながる。
DOMのイベントハンドラのadd,deleteには気をつけよ
もし、HTMLのDOM要素などに対して、addEventListener
などをする場合、参照カウント式GCをもつjavascriptの特性から、破棄したはずのコンポーネントが生き残ってしまう可能性がある。
例えば、以下のようなコンポーネントを作成したとしよう。
gr.registerComponent("ElementClickObserver",
attributes:{
elemQuery:{
converter:"String",
default:"div"
}
},
$mount:function(){
const elemQuery = this.getAttribute("elemQuery");
document.getElementsByQuery(elemQuery).item(0).addEventListener("mouseclick",()=>{
// elemQueryを含んだ処理など
});
}
});
このmountではイベントを登録しているが、仮にコンポーネントが消されたとしても、このunmountには何の処理も書いておらず、エレメントから、このコンポーネント内のハンドラまでの参照が残り続ける。また、このハンドラは何らかのコンポーネントの変数に参照を残しているとすれば、このハンドラがGCによって回収されない影響でコンポーネントも回収されない。
もし、頻繁にコンポーネントが追加されたり、削除されたりするなら、このようなGC上のメモリリークに気をつける必要がある。
イベントは、いらなくなったら消そう。
是非、$unmount
、$dispose
メッセージを活用してほしい。
コンストラクタ形式でのgetComponentを活用せよ
Unityを利用している方にとっては直感的だと思うが、コンポーネントはポリモーフィズムを持った活用方法をとることによってより一層の価値を発揮するようになる。
例えば、TransformComponentを継承したコンポーネントTransformExtended
を作成したとしよう。
gr.registerComponent("TransformExtended",
{
$update:function(){
}
},"Transform");
この場合、Transform
で定義された$update
を書き換えることができる。コンポーネントとして保つべきインターフェースを保てば、コンポーネントを参照する側は必要以上の実装を気にかけずに、中身の実装が異なるそれぞれのコンポーネントを取得できる。
const Transform = gr.lib.fundamental.Components.TransformComponent;
const transforms = this.node.getComponentsInChildren(Transform);
この、以下のtransformsの中にはTransform
を継承するコンポーネントならば全て入りうる。これにより、ポリモーフィズムを維持したコンポーネントの活用が可能だ。
もし、これがTypescriptならば、さらにジェネリクスによって、transforms
はTransform[]
であることも認識されてさらに便利である。
イベント目的で自分のノードにsendMessageは代わりにeventEmitterを用いよ
もし、自分のノードに対してイベント目的でsendMessage
を呼び出す、つまり何らかのタイミングで同じノードについている他のコンポーネントに対してsendMessage
を用いて通知したい場合、eventEmitter
を用いるべきだ。
sendMessage
はフロントインターフェース側からはハンドリングすることはできない。ノードのライフサイクル上でノード間の連携のために存在する機能である。もし、単にイベントを通知する目的でsendMessage
を用いるなら、eventEmitter
を用いることによって、以下の利点が得られる。
- 若干のパフォーマンスの向上
- フロントインターフェース側からのハンドリング
this.node.sendMessage("some-event",args);
これは、以下のように描き直すべきだ。
this.node.emit("some-event".args);