感染シミュレーションの2
まあいつものことだけど、何か作ろうと思いたつと、頭の中はもうそのことばかり。 だから仕事をしないってことはないんだけど、仕事の最中でも、ちょっとした休憩で頭に浮かぶのはこっちだったりする。
とはいえ。
一定の領域の中で動かして、当たり判定をして、判定結果に応じて表示を変える。
つまるところこれだけなので、仕様面であまり考えることはない。
一応シミュレーションなので、動かした後は眺めるだけ。 なので、初期値として設定するパラメーターぐらいは変えられるようにしよう。 具体的には以下の項目。
- 人の数。 枠の中に何人を配置するか。
- 感染者の数。 配置した人のうちの何人が最初から感染していたか。
- 感染率。 接触した時に感染してしまう割合。
こんな感じのコントローラーがあればいいだろう。
あとは接触の定義。 接触した時に感染するのだが、この接触をどう扱うか。 もう少し具体的に言うと、丸を人と見立てるわけだが、この丸と人との物理的な関係性をどう定義するか。
すぐに思いつくのは、大雑把に以下の2つ。
-
丸が人の外枠と同じ。 つまり服や肌など。
この場合、丸同士は重なることなく、ぶつかれば跳ね返ることになるだろう。
-
丸が人の吐き出す飛沫を漂わせた空気と同じ。
この場合、丸同士が重なっても、動きを変えることなく素通りでいいだろう。
まあどっちにしても極端なモデル化ではあるが、現実に近いのは後者な気がするので、ここでは後者を採用する。
そして実装。
これまたいつものことだが、JavaScriptで作る。
丸を描くのも動かすのもcanvasでやるとして、動かすには座標の管理が、当たり判定やはみ出し判定には領域の管理が必要になる。 こうした基礎的なものから作っていく。
まずは座標、いや、ベクトルか。
位置を ( x, y ) として表現するのはもちろんだが、矩形領域の幅と高さも、速度も、同じ形式で表現できる。 この形式で表現できる何かのためのデータ構造なら、名前も座標よりはベクトルの方がしっくりくるだろう。
class Vct {
constructor( x, y ) {
this.x = x;
this.y = y;
}
add( vct ) {
this.x += vct.x;
this.y += vct.y;
}
}
位置も速度も同じくベクトルで表現して、移動は、現在の位置に速度を足して次の位置とする。 この足し算に使う想定で add メソッドを用意している。
これが感染シミュレーションじゃなかったら、不変オブジェクトを意識して、こんな実装にしたかもしれない。
class Vct {
#x;
#y;
constructor( x, y ) {
this.#x = x;
this.#y = y;
}
get x() { return this.#x }
get y() { return this.#y }
add( vct ) {
return new Vct(
this.x + vct.x,
this.y + vct.y,
)
}
}
でも今回のような、つまり短い周期で連続して動かすような場合、移動の度に新しいインスタンスを生成するのは、流石にちょっと躊躇ってしまう。 今作ったインスタンスは、一瞬(60fpsのディスプレイなら0.017秒後)でゴミになるからね。 マシンスペック的には問題無いだろうけど、なんか勿体なくて。
ということで、メモリ効率を優先して、クラスのメンバーは全て public で通す。
次に矩形領域。
class Rct {
constructor( pt, sz ) {
this.pt = pt; // ::Vct
this.sz = sz; // ::Vct
}
get xl() { return this.pt.x } // X Left
get yt() { return this.pt.y } // Y Top
get xr() { return this.pt.x + this.sz.x } // X Right
get yb() { return this.pt.y + this.sz.y } // Y Bottom
isXOverflow( rct ) { // rctが左右方向で自分からはみ出しているか
return ( rct.xl < this.xl || this.xr < rct.xr );
}
isYOverflow( rct ) { // rctが上下方向で自分からはみ出しているか
return ( rct.yt < this.yt || this.yb < rct.yb );
}
isOverlap( rct ) { // rctが自分と重なっているか
return !( this.xr < rct.xl || rct.xr < this.xl ) && !( this.yb < rct.yt || rct.yb < this.yt )
}
}
動き回る丸も、その丸が動き回れる領域も、この矩形領域クラスを継承して作る想定。
コンストラクタに渡しているのは、左上の座標となるベクトルと、幅と高さを成分とするベクトル。 これらを元に、矩形領域の左右端のX座標と上下端のY座標を返すメソッドも作っておいた。
シミュレーションで直接使う想定のメソッドは、はみ出し判定と当たり判定。
はみ出し判定は、丸が領域の端に来た時に跳ね返すため。 跳ね返りは、左右の壁からはみ出したら速度のX成分を、上下の壁からはみ出したら速度のY成分を、反転させることで実現する。 なのではみ出し判定も、左右方向と上下方向のそれぞれで用意している。
当たり判定は、丸同士の重なり合いの判定のため。 丸同士の重なり合いなので、ちゃんとやるなら中心間の距離と半径の和との比較だが、今回は丸同士が衝突しても跳ね返らないからね。 丸が表すのも周囲の空気というふわっとしたものだし、これでいいのだ。
基底とするクラスができたので、今日はここまで。