GameDev #3 ベクトルと物理法則 [Devlog #016]

Table of Contents

ゲーム開発者の教科書:Game Programming in C++ を読んで理解したことについてを要約します(内容の転載を避け、詳しく説明しすぎないように配慮します)

ゲームプログラミング in C++


ベクトルと物理法則

ベクトル

オブジェクトの前方ベクトルは、オブジェクトの進む方向を表現するベクトルである

このコードではMath.hヘッダファイルに含まれる独自のベクトルライブラリを使う

Vector2 myVector;
myVector.x = 5;
myVector.y = 10;

2点間のベクトルを作る(減算)

Vector2 a, b;
Vector2 result = a - b;

ベクトルのスケーリング(スカラ乗算)

Vector2 a;
Vector2 result = 5.0f * a;

ベクトルを組み合わせる(加算)

Vector2 a, b;
Vector2 result = a + b;

距離を求める(長さ)

ベクトルは大きさと向きの両方を表現する

平方根の処理はCPU処理において多くのクロック数を必要とするため、大小の比較だけの場合はベクトルの長さの2乗を比較するべきである

ベクトルの長さと長さの2乗は以下のように求める

Vector2 a;
float length = a.Length();
float length_squared = a.LengthSquared();

向きを求める(単位ベクトルと正規化)

単位ベクトルは、長さが1のベクトルである

非単位長のベクトルから単位ベクトルへの変換を正規化という

ゼロ除算を考慮する必要があるほか、ベクトルを正規化すれば大きさの情報は失われるため、タイミングを考慮する必要がある

正規化は前方ベクトルや上向きベクトルに対して用いられる

Math.h

指定されたベクトルをその場で正規化するNormalize()関数と、引数として渡されたベクトルを正規化してその正規化されたベクトルを変えるNormalize()静的関数がある

Vector2 a;
a.Normalize();
Vector2 result = Vector2::Normalize(a);

角度を前方ベクトルに変換する

Actor.h

Actor::GetForward()関数は、アクターの角度を前方ベクトルに直接変換する

Vector2 GetForward() const {
	return Vector2(Math::Cos(mRotation), -Math::Sin(mRotation));
}

前方ベクトルを角度に変換する

宇宙船アクターshipを小惑星アクターasteroidに向けるとすると

Vector2 shipToAsteroid = asteroid->GetPosition() - ship->GetPosition();
shipToAsteroid.Normalize();
float angle = Math::Atan2(-shipToAsteroid.y, shipToAsteroid.x);
ship->SetRotation(angle);

2Dゲームではアークタンジェントのアプローチが適しており、3Dゲームではドット積のアプローチが適している

2つのベクトルを成す角を求める(ドット積)

余弦定理に基づいて、ドット積からベクトルの成す角度を求める

ドット積が0であれば直角、1であれば平行、-1であれば逆平行となる

アークコサイン関数が返す角度の範囲は[0,π]であるため、回転の時計回りor反時計回りの情報は無視される

Vector2::Dot()静的関数を用いて、origForwardnewForwardの成す角度は以下のように求まる

float dotResult = Vector2::Dot(origForward, newForward);
float angle = Math::Acos(dotResult);

法線を計算する(外積)

クロス積(外積)を求めることで、平行でない2つの3Dベクトルを含む平面に垂直なベクトルがわかる

値が0のz成分を加えることで、2Dベクトルにもクロス積を使うことができる

クロス積の結果のベクトルが表裏どちらを向くのかを見極めるには座標系に対応する法則を用いる

OpenGLの座標系は左手形であるため、このコードでは左手の法則を用いる

クロス積の成分ごとの計算は以下のようになる

$$ c_x = a_y b_z - a_z b_y $$ $$ c_y = a_z b_x - a_x b_z $$ $$ c_z = a_x b_y - a_y b_x $$

Vector3::Cross()関数は、2つのベクトルのクロス積を計算する

運動の基本

ゲームワールドの中でアクターを動かすMoveComponentクラスを作る(動きの振る舞いをカプセル化する)

MoveComponentクラスの派生クラスInputComponentは、キーボード入力を直接受け取る

基礎となるMoveComponentクラスの作成

MoveComponent.h

アクターを速度に基づいて前進、回転させるMoveComponentクラスはComponentクラスの派生クラスとして実装する

コンストラクタはコンポーネントの更新順序updateOrderに10を指定している(デフォルトは100)

Update()関数をオーバーライドして、アクターを動かすコードを加える

回転と前進の2つの速度を取得・設定するゲッタ関数がGetAngularSpeed()GetForwardSpeed()であり、セッタ関数がSetAngularSpeed()SetForwardSpeed()である

MoveComponent.cpp
MoveComponent::MoveComponent()
MoveComponent::Update()

親クラスのComponentクラスはメンバ変数のmOwnerを介して、自分を所有するアクターにアクセスできるため、このmOwnerポインタを使って所有アクターの位置、向き、前方ベクトルを取得する

Math::NearZero()関数は、引数の値がゼロに近いかどうかを判定する(引数の絶対値と微小な値(epsilon)とを比較して判定)

画面のラッピング(画面の左端と右端で瞬間移動させる仕組み)のコードも実装する(アステロイドゲームであるため)

Asteroid.h
Asteroid.cpp

Actorクラスの派生クラス

コンストラクタの中で、MoveComponentSpriteComponentを構築する

また、コンストラクタでは最初に小惑星のランダムな位置と向きをRandom()関数で取得する

Game.cpp
Game::LoadData()

いくつかの小惑星(アクター)を作成する

InputComponentクラスの作成

Actorクラスの派生クラスかComponentクラスの派生クラスに入力処理を組み込む

Component.h

ProcessInput()仮想関数を追加する(デフォルトの実装は空)

Actor.h

ProcessInput()関数(仮想ではない)を追加する(すべてのコンポーネントで呼び出す)

ActorInput()仮想関数を追加する(アクターごとにオーバーライド可能な振る舞い)

Game.cpp
Game::ProcessInput()

全アクターのProcessInput()関数を呼び出す

ProcessInput()関数の内部で別のアクターを作る際は、mActors配列ではなく、mPendingActors配列に追加する必要があるので、mUpdatingActorsフラグをtrueにする

InputComponent.h

MoveComponentクラスの派生クラス

キー入力で所有アクターの前進・後退・回転を制御する

親クラスのMoveComponentクラスで定義された前進・回転の速度を、オーバーライドしたProcessInput()で制御する

InputComponent.cpp

キーボード入力に対する最大スピードを指定する

InputComponent::ProcessInput()

押されているキーに対応して前進速度を決定し、継承したSetForwardSpeed関数に渡す

押されているキーに対応して回転速度を決定し、継承したSetAngularSpeed関数に渡す

SpriteComponent.h
SpriteComponent.cpp

前回と同様にSpriteComponentを作成してテクスチャを割り当てる

前回の該当箇所

ニュートン物理学

このコードで扱うのは、ニュートン物理学の分野のうち、回転しない運動の質点の力学のみを考慮する

より複雑なニュートン物理学についての議論は参考文献:Game Physics Engine Developmentを参照

質点の力学の概要

質点の力学に必要不可欠な2つの要素が質量であり、それぞれベクトルとスカラーで表される

運動の第2法則に従うと、力は質量と加速度の積に等しい

$$ F = m\cdot a $$

ゲームに必要なのは、物体に力をかけて、その力から加速度を割り出すこと

これを離散的な時間間隔(デルタタイム)で計算し、数値積分(積分の近似)を行う

オイラー積分で位置を計算する

重力などはフレームに共通した一定の力だが、撃力(impulse)は1フレームだけにかかる力である

// 加速度 = 力の合計 / 質量
accelaration = sumOfForces / mass;

以下のように、力・加速度・速度・位置をすべてベクトルで計算することができる

// 半陰的オイラー積分(修正されたオイラー法)
velocity += acceleration * deltaTime;
position += velocity * deltaTime;

可変タイムステップの問題点

可変フレーム時間(タイムステップ)は、物理シミュレーションを使うゲームで問題を起こすことがある

フレームごとにフレームレートが変化すると、数値積分の精度も変化する

例えば、フレームレートが低いほど加速度の減少が遅れ、ジャンプできる距離が大きくなる

そのため、フレーム制限のアプローチや、長いタイムステップを複数の固定サイズのタイムステップに分割するアプローチによって対策する

円と円の交差

交差判定をするには、まず2つの円の中心を結ぶベクトルを作り、そのベクトルの大きさを算出する

次に、その距離を2つの円の半径の和と比較する

CircleComponent派生クラスを作る

CircleComponent.h

Component派生クラスとして衝突検知を行う

円の中心位置はこのコンポーネントを所有するアクターに依存しているので、メンバデータは半径だけである

CircleComponent.cpp
CircleComponent::CircleComponent()
Intersect()

2つのCircleComponentを参照の形で受け取るグローバルな関数

2つの円が交差するとtrueを返す

Asteroid.cpp
Asteroid::Asteroid()

1個のCircleComponentAsteroidオブジェクトに追加する

// Create a circle component (for collision)
	mCircle = new CircleComponent(this);
	mCircle->SetRadius(40.0f);
Game.h

Asteroidポインタ群の配列(std::vector)を追加

public:
	std::vector<class Asteroid*>& GetAsteroids() { return mAsteroids; }
private:
	std::vector<class Asteroid*> mAsteroids;
laser.h
laser.cpp
Laser::UpdateActor()

小惑星それぞれに対する交差テストを実行する

GetCircle()関数は、その小惑星のCircleComponentへのポイントを返すpublicなメンバ関数である

ゲームプロジェクト

小惑星と宇宙船との衝突

()

ニュートン物理学の実装

()

レーザーオブジェクトの作成処理

クールダウンを導入して、1/2秒に1回だけレーザーを発射できるようにする

Ship.h(Actorクラスを継承)
Ship.cpp
Ship::ActorInput()

float型のメンバ変数mLaserCooldown0.0fで初期化し、“スペースキーが押されている"かつ”mLaserCooldown0以下"であるかによってレーザーの発射を実行する

条件が満たされている場合、レーザーを作り、その位置と無機を宇宙船に合わせて、mLaserCooldown0.5fに設定する

Ship::UpdateActor()

mLaserCooldownをデルタタイムだけ減らす



Laserは一定時間後に強制的に消去する