[Go to “teaching” in Morimoto Lab]

Game by ball 4

単位ベクトルとクラスの使い方について学びます。

今日の流れ:
 マウスを追いかけるボールを作る
 その動きをベクトルで制御する
 最終的に複数のボールの動きに発展させる [イメージ]


1. 単位ベクトル

単位ベクトルは長さが1のベクトルです。
「単位ベクトル=方向だけを取り出したもの」
→どんなに遠くても、同じ“向き”なら同じ値になります
→距離の影響を消して「向きだけ」にするのが目的

「単位ベクトル=ベクトル/ベクトルの大きさ(長さ)」です。

unitvector

どんな時に使うか?
例えば、マウスの描画からベクトルを取得したとき、そのベクトルを「ボールを動かす」処理に反映させたいとしましょう。
描画の長短はわからないので、そのまま使うと想定よりもボールが動きすぎたり、逆に動かなさすぎたりする可能性があります。
しかし単位ベクトルにしてしまえば必ず大きさが1になるので、そのあと指定した数を乗算するなどで、大きさを揃えることができます。

1.1 単位ベクトルを自分で計算しよう (1)

以下のコードは画面中央からマウスの位置への線を描画します。

Alt text

void setup()
{
size(800, 800);
strokeWeight(5);
stroke(255);
}
void draw()
{
background(0);
line(width/2, height/2, mouseX, mouseY);
}

上記を、以下のように一定の長さの線にしてください。

Alt text

画面中央からマウスへの単位ベクトルを使いましょう。
以下のように、画面中央からマウスへの距離を使って単位ベクトルを計算できます。

以下をsetup()の前など(つまりグローバル変数)に書く:

float len = 200; //length of vector

以下をdraw()内に書く:

//cxとcyは画面中央の座標です。自分で定義してください。
//ベクトル
float dx = mouseX - cx;
float dy = mouseY - cy;
//長さ(これはベクトルの視点と終点の2点間の距離です)
float dist = sqrt(dx*dx + dy*dy);
//単位ベクトル = (ベクトル / 長さ)
float vecx = dx / dist;
float vecy = dy / dist;
line(cx, cy, cx + vecx*len, cy + vecy*len);
//単位ベクトルを使えば上記のように掛け算で簡単に長さを調整できます
//※この計算は「方向だけ取り出して(単位ベクトル化して)、あとから長さを掛け直す」処理です

[sqrt() in the official web]
[sq() in the official web]

注意1 widthはsize関数の後に使おう:
widthやheightをグローバル変数の初期値などで使う場合、setup内のsize関数の前にそれらを使うことになります。
widthやheightはsize関数で初めて値が指定されるので、size関数を使う前にそれらを使うと、変な値になってしまうので注意です。

注意2 ベクトルを使ったlineの終点:「ベクトルをそのまま終点としない!」
「終点=始点+ベクトル*長さ」 となります。
ベクトルは原点(0,0)中心のものなので、画面中心などにする場合はその分足さないといけません。


Ex A. 単位ベクトルを使って、画面中央から伸びる一定の長さの線が常にマウスの方を向くようにしなさい。

Alt text


1.2 単位ベクトルを自分で計算しよう (2)

以下のコードはマウスの位置にボールを描画します。

Alt text

float bx = 1;
float by = 1;
float bs = 100;
//boolean bFollow=false;
void setup() {
size(800, 800);
bFollow = false;
}
void draw() {
background(0);
ellipse(bx, by, bs, bs);
// if (bFollow) {
bx = mouseX;
by = mouseY;
// }
}
void mouseClicked()
{
// if (bFollow==false)bFollow = true;
// else bFollow = false;
////bFollow = !bFollow;//this line causes the same result with the above two lines.
}

Note: boolean
booleanはintやfloatなどのようなデータ型です。
値はtrueかfalseを持っていて、if文ではそれ単体で真・偽(true・false)を表すことができます
true, falseは1, 0だとイメージして扱うとよいです。

Note: booleanをスイッチにする
ここではbFollowはスイッチとして機能します。
//でコメントアウトしてある部分を使うと、マウスクリックでボールを止めたり動かしたりすることができる。

Note: ビット演算
mouseClicked()の関数内の////の行はその上の2行と同じ挙動になります。
この部分はビット演算をしています。
論理否定(論理演算)はProcessingやC言語では!で表される。
!は0を1に、1を0にするので、trueだったらfalse、falseだったらtrueに反転させる。


ボールが消えてしまう!「0除算問題」

mouseX,Yの初期値は0なので、bx,byの初期値を0にすると、この後の課題でマウスとボールの距離を求める際、距離が0になる。
0で割り算した結果は未定義(NaNやInfinity)となるため、これをボールの位置などに代入すると、ボールが消えてしまう。
こうならないように

if(dist < 0.0001) return;

等を入れて回避しよう
これで、0や0に近い時は除算をしない、という風にできる。

0だけでなく、0に極めて近い時も計算結果が不安定になるので避ける。
これは、浮動小数点数の丸め誤差によりベクトルの値にわずかなズレが含まれ、それを非常に小さい値で割ることで、その誤差が相対的に大きくなるためである。
このような状態を「計算が不安定」と呼ぶ。
※実際にはこのような「小さすぎる値を避ける処理」は、物理シミュレーションやゲームでもよく使われる。
特に、距離がほぼ0のときは「方向」がほとんど定義できなくなるため、結果が不安定になる。
つまり、「距離がほぼ0のときは方向が定義できない」と考えるとよい。

returnの公式ページリンク [return]

Ex B. 速度の単位ベクトルを計算して、一定の速度でボールがマウスを追うようにしてください。

Alt text

ボールからマウスへの単位ベクトルを計算し、それを使ってボールの速度を計算しましょう。
ボールを速度で移動するやり方はgame by ball 1の2.3などでやりましたね。

速度は以下の式で求めます。

速度=単位ベクトル・速度係数(調整係数)
また、以前講義でやった
位置=位置+速度
と組み合わせて使います。

Ex C. ボールからマウスに向かって線(矢印)を描画しましょう。

以下が例です。

Alt text

注意 ボールがプルプルして止まらない?
ボールがマウスに追い付いても、プルプルして止まってくれません。
これはあなたのマウスを持つ手が微妙に震えているとかいうわけではなく、プログラム的にボールは必ず少しずつ動くので、静止しません。
割とちゃんとした物理シミュレーションでもこれは起きるので、摩擦力などの理由で速度を0にしていくとか、もっと強制的な方法だとマウスと十分近づいたら動くのをやめるとかのような、+αの処理をしてあげると止まります。
※この課題では上記のような+αの処理は求めません。


1.3 PVectorを使って単位ベクトルを計算しよう

クラスは「データと処理をまとめたもの」です。
クラスもintやfloatと同じデータの「型」です。
今回はクラスの理解の第一歩として、クラスの使い方を学びます。

PVectorは「座標+計算」をまとめたクラスです。
[PVector in the official web]
int → 数値1個
float → 小数1個
PVector → xとyをまとめたデータ(+計算機能付き)

Note:
PVectorはProcessingに用意されているクラスというものの一つです。
クラス変数や関数を持つことができます。とても便利です。

PVectorというクラス:x,yなどの変数、add, subなどの関数など、クラスでは関係するデータや関数をまとめて持つことができます。PVectorはベクトルを扱うクラスなので、それ関係のものが詰まっています。

Note 2:
クラスを使って変数を定義する場合、newを使います。newをせずにクラスの変数を使うとエラーが起こります。これはintやfloatなどとは少し違いますね。使い方の詳細は上記のリンクを見てください。newはメモリを確保するために必要ですが、ここでは詳細は割愛します。

↓PVectorのサンプルコード(公式ページより)

PVector v1, v2;//←ボールの位置などにする場合、意味のわかるような変数名にしましょう!
void setup() {
noLoop();//←ボールを動かす場合はこれは削除しないと、drawが一度しか呼ばれなくなります!!!
v1 = new PVector(40, 20);
v2 = new PVector(25, 50);
}
void draw() {
ellipse(v1.x, v1.y, 12, 12);
ellipse(v2.x, v2.y, 12, 12);
v2.add(v1);
ellipse(v2.x, v2.y, 24, 24);
}

Ex D. PVectorでEx Cのプログラムを書き換えてください。

ボールの位置や速度などをPVectorで表すなどしてください。
PVectorを使うとコードがどう簡単になるか確認しましょう。
また、PVectorの変数をどこで定義し、どこでnew(生成)するか考えましょう。

Ex E. PVectorのメソッドを使って単位ベクトルを計算するよう、Ex Cのプログラムを書き換えてください。

E-1. まずはadd(), mult(), div(), などを使って書き換えてください。

足し算、掛け算、割り算を行うメソッドです。
メソッドを使って、より“ベクトルらしい書き方”にしましょう(x,y成分をまとめ、数式に少し近いまとまった記述ができます)

使い方はPVectorの説明から、更にリンクで飛んだ先にあります。
例えばadd()はPVector同士の足し算ができます。

※引数の数によって挙動が変わりますが、とりあえず引数が一つだけの使い方を試してみましょう。

他の変数もPVectorで定義すれば、PVector同士での計算ができます。

注意:PVectorのaddやsubなどが元の変数の値を書き換えてしまう。
⚠️ 重要:PVectorの破壊的操作(値を書き換える)についてです。
PVectorの変数、v1, v2, v3 があったときに、

v1 = v2.sub(v3); //v2変化あり(破壊的)
v1= PVector.sub(v2, v3); //v2変化なし👍

となります。前者は
v2 = v2 - v3
という意味になり、v1だけでなくv2の値も変化してしまいます(壊れる)。

これをしたくない場合は後者のようにするとよいです。 この書き方だとv2の値は変更されません。

E-2. 次にnormalize()を使って書き換えてください。

今まで距離を計算し、ベクトルを割ることで自力で正規化(normalize)していました。
normalize()を使うと、それを一行でできます(内部でベクトルを長さで割る処理をしています)。

正規化(normalize)の色々な意味
Drawing & Pattern 0のExDで「正規化と補間」について解説していますが、今回の正規化は単位ベクトル化のことです。

以下はwikipediaより↓
「正規化(せいきか、英語: normalization)とは、データ等々を一定のルール(規則)に基づいて変形し、利用しやすくすること。別の言い方をするならば、正規形でないものを正規形(比較・演算などの操作のために望ましい性質を持った一定の形)に変形することをいう。」

場合によって正規化の意味が変わってくるので注意しましょう。

Ex F. 配列で増やして連結しよう

F-1. ボールの数を配列で増やそう

次の回で扱う動的配列の前に、
ぜひ静的配列でPVectorを扱ってみてほしい。
ボールの位置だけ増やして大きさは同じでまずはやってみよう。
ただし、ボールの初期位置は少しずらしてあげるのがポイント。

速度のデータが一つだとボールは全て同じ動きになるので、
その後、各ボールの速度の配列なども用意しよう。

Note:
配列を作るときnewが必要でした。PVectorを作るときもnewが必要です。なのでPVectorの配列を作るときは “new”が二度必要です。

以下はPVectorクラスを用いた配列の作成例。

//1回目のnew。配列を作ります。
PVector [] positions = new PVector[10];
//2回目のnew。インスタンスを作ります。配列で作った全てのデータをnewするのでforを使っています。
for(int i=0 ; i < 10; i++){//10の代わりにpositions.lengthを使ってもOK
positions[i] = new PVector();//初期値の指定はここでできる
}

※ただしこの実装だとボールはそのうちほとんど一か所に固まってしまい、少し残念な感じ(下図左)
もっといい感じにするには速度の調整係数をそれぞれ別にするなどの方法があるが、これは速度や調整係数をそれぞれ配列で用意しないといけないので、面倒である。
のちに出てくるクラスを使うと、このようなことがしやすくなる。

Alt text Alt text
右図は2022年度Kawasakiさんのアレンジしたプログラム。

F-2. 増やしたボールを線で繋いで、前のボールを追いかけよう
「他のオブジェクトを参照する」というのはゲームやシミュレーションで非常に重要です。
まずは増やしたボールを線でつなぎましょう。

次に、
1番目のボールはマウスを追いかけ、
2番目のボールは1番目のボールを追いかけ…、
と言う風にしてください。

このとき、ボールとボールの距離が一定以下にならないようにすれば縮まないし、この後のBoidsにも繋がります。

PVectorというクラスを使うことによって、x,y成分を別々に定義しなくてよくなったが、それでも位置、速度、などの配列を別々に用意しないといけないのは面倒だ。
次回、クラスを自作することによって、さらにこの問題を解決する。

Alt textAlt text


余裕がある人へ. フロッキングアルゴリズムのBoids理論の実装

フロッキングアルゴリズムとは、群れ(flock)の動きをシミュレートするアルゴリズムです。
魚や動物の群れの動きをよりリアルに表現することができます。
その中でも非常に有名で簡単なBoids(ボイズ)と呼ばれるもののアルゴリズムを見てみましょう。
「鳥もどき(bird-oid)」からboidsと名付けられた、人工生命シミュレーションプログラムで、鳥や魚の動きに使われるほか、チームラボのエフェクトなどにも使われていたと思います。

基本的なルールは三つ。
1. 分離 separation:近づきすぎたら離れる →距離チェック
2. 整列 alignment:仲間と同じ方向、同じ速度に合わせて動く →速度の平均
3. 結束 cohesion:群れの中心に向かって動く →中心への単位ベクトル

3Dにも適用できます。 (Processingでは3Dへの拡張が手軽です)

参考URL:
Processingによる実装記事配列が増えるのが少し大変ですが、今までの知識+αくらいで理解可能です!?GameByBall2のボールをたくさん増やす課題が少し似ています。
enter image description here
Wikipedia ボイド(人工生命)
Coding challenge “124: Flocking Simulation”:こちらのページからもBoidsのProcessing実装に関する講義動画の閲覧やコードの取得が可能です!英語です!クラスも使ってます
群衆シミュレーション:こちらもフロッキングアルゴリズムと似ていますが、ボーンのあるような3Dキャラクターに適用するイメージ?Unreal Engineの例。
マルチエージェントシステム:ゲームキャラクタの動きの自動決定などに使われます。スクエニのAIエンジニアさんのスライドをこちらにリンクしておきます。ちょっと難解かも。。こっちが入門記事でわかりやすいかもです!