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()
静的関数を用いて、origForward
とnewForward
の成す角度は以下のように求まる
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
クラスの派生クラス
コンストラクタの中で、MoveComponent
とSpriteComponent
を構築する
また、コンストラクタでは最初に小惑星のランダムな位置と向きを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個のCircleComponent
をAsteroid
オブジェクトに追加する
// 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型のメンバ変数mLaserCooldown
を0.0f
で初期化し、“スペースキーが押されている"かつ”mLaserCooldown
が0
以下"であるかによってレーザーの発射を実行する
条件が満たされている場合、レーザーを作り、その位置と無機を宇宙船に合わせて、mLaserCooldown
を0.5f
に設定する
Ship::UpdateActor()
mLaserCooldown
をデルタタイムだけ減らす
Laser
は一定時間後に強制的に消去する