【C++】ゲームプログラミングC++を読書しながらゲームをつくる#2

#はじめに
前回：https://qiita.com/murati111/items/3ccfdacd2ffe4e39470a
Githubに作成したコードを公開しています：
https://github.com/murati111/PracticeGameProgramCpp
IDE:VisualStudio2019

##1.6ゲームの更新
###1.6.2　「デルタタイムの関数」としてのロジック
昔は敵の位置更新などはフレームレートが固定なので、一定でよかった。

```cpp
//x位置を5ピクセル移動
enemy.mPosition.x+=5;
```
しかし、フレームレートが変動するのは当たり前なので、
このコードだとフレームが倍になれば移動も倍になる。

この問題を解決するのが__デルタタイム(delta time)__
最後のフレームから今までに経過した時間の長さを意味する。
1フレームに何ピクセルではなく、1秒間に何ピクセル動くかに変更する。
これはUE4などでも必要ですね。
```cpp
//x位置を150ピクセル/秒で更新
enemy.mPosition.x += 150*deltaTime;
```

SDLではこのような実装になる。

```cpp
void Game::UpdateGame()
{
    float deltaTime=(SDL_GetTicks()-mTicksCount)/1000.0f;
    //時刻を更新
　　mTicksCount=SDL_GetTicks();
}
```
SDL_GetTicks()ではミリ秒数がでるので1000.0fで割ることで秒単位のデルタタイムを得られた！

しかし、物理シミュレーションではフレームレートによって異なる振る舞いとなる。
解決策として、__フレーム制限__をかける。
SDLでのフレーム制限のかけ方は以下のコードをUpdateGameの先頭に追加する。

```cpp
while(!SDL_TICKS_PASSED(SDL_GetTicks(),mTicksCount + 16));
```

デルタタイムを大きくなりすぎる問題を、最大値（たとえば0.05f）以下に制限する。

```cpp
if(deltaTime>0.05f)
{
   deltaTime=0.05f;
}
```

最終形がこうなった

```cpp
void Game::UpdateGame()
{
    //前フレームから16msが経過するまで待つ
    while(!SDL_TICKS_PASSED(SDL_GetTicks(),mTicksCount + 16));
    float deltaTime=(SDL_GetTicks()-mTicksCount)/1000.0f;
    //時刻を更新
    mTicksCount=SDL_GetTicks();

    //デルタタイムを最大値で制限する
    if(deltaTime>0.05f)
    {
       deltaTime=0.05f;
    }
    //TODO
}
```

###1.6.3 パドルの位置を更新する
Pongでは、[W]キーで上に移動、[S]キーで下に移動とする。
mPaddleDirというint型の変数を使う。
パドルが動かないなら0を、上なら-1(負のy)を、下なら1(正のy)をセットする。

```cpp
	mPaddleDir = 0;
	if (state[SDL_SCANCODE_W])
	{
		mPaddleDir -= 1;
	}
	if (state[SDL_SCANCODE_S])
	{
		mPaddleDir += 1;
	}
```
次にパドルの位置を更新する。
ここでは毎秒300.0fピクセルというスピードでy座標を更新。

```cpp
	if (mPaddleDir != 0)
	{
		mPaddlePos.y += mPaddleDir * 300.0f * deltaTime;
    }
```

パドルが画面外から出ないようにする。

```cpp
if (mPaddleDir != 0)
{
    mPaddlePos.y += mPaddleDir * 300.0f * deltaTime;
    //パドルを画面から出ないようにする
    if(mPaddlePos.y<(paddleH/2.0f + thickness))
    {
        mPaddlePos.y=paddleH/2.0f + thickness;
    }
    else if(mPaddlePos.y>(768.0f - paddleH/2.0f - thickness))
    {
        mPaddlePos.y=768.0f - paddleH/2.0f - thickness;
    }  
}
```
###1.6.4 ボールの位置を更新する
ボールは速さだけでなく、__速度(velocity)__が必要であり、
ボールと壁との衝突判定も必要である。

ボールの速度をVector2型のmBallVelという変数を作り、(200.0f,235.0f)で初期化する。
そしてボールの位置を速度に応じて動かすため、UpdateGameに追加する。

```cpp
mBallPos.x += mBallVel.x * deltaTime;
mBallPos.y += mBallVel.y * deltaTime;
```

ボールが上の壁に衝突し、ボールが右上向きに動いていたら、右下向きに跳ね返るようにする。
上または下の壁に跳ね返る場合は、速度のy成分を反転。
右側または左側のパドルの場合は、速度のx成分を反転する。

```cpp
//上の壁の場合
if(mBallPos.y <= thickness)
{
    mBallVel.y *= -1;
}
```
しかしこのコードには問題点がある、
ボールがフレームAで、上の壁と衝突したとする。
その場合y速度が反転して、下向きに動き始める。
次のフレームBで、ボールは壁から離れようとするが、まだ十分に動いていない。
まだボールと壁は衝突している、なのでまたy速度が反転し、ボールは上向きになる。
これが続き、ボールが壁から永久に離れなくなる。
この問題を解消するためには、y速度を反転するとき、上の壁かつ、ボールが上向きに動いている時である必要がある。

```cpp
if(mBallPos.y <= thickness && mBallVel.y < 0.0f)
{
    mBallVel.y *= -1;
}
```

パドルの衝突は、ボールのy位置とパドルのy位置の差を計算し、その絶対値を求める。
もし差の絶対値がパドルの高さの半分よりも大きければ、ボールの位置がパドルより高いか低くなっている。
これとx位置が重なっているか、そして離れようとしないことをチェックする。

```cpp
if(//yの差が十分に小さければ
   diff <= paddleH / 2.0f 
   //ボールが正しいx位置にあれば
   && mBallPos.x <= 25.0f && mBallPos.x >= 20.0f 
   //ボールが左向きであれば
   && mBallVel.x *= -1.0f)
{
    mBallVel.x *= -1.0f;
}
```
これにてPongゲームの単純バージョン完成！
![gamecpp.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398871/e72da4ee-376b-adab-bd35-d7cc6e14c3c2.gif)


##1.D 練習問題とカスタマイズ
せっかくなのでこのゲームをカスタマイズした。
__カスタマイズした点__
1.2Pの追加（IキーとKキーで移動）
2.マルチボール（壁に跳ね返ったら速度などが違うボールを生成するシステム）
3.スコア機能（コンソールコマンドで表示にした）

###1.D.2　2Pの追加
メンバー変数mPaddleDirとmPaddlePosを要素数2の配列にした。
SDL_Rectで2個のパドルを生成した。

```cpp
	for (int i = 0; i < 2; i++) {
		SDL_Rect paddle{
			static_cast<int>(mPaddlePos[i].x),
			static_cast<int>(mPaddlePos[i].y - paddleH / 2),
			thickness,
			static_cast<int>(paddleH)
		};
		SDL_RenderFillRect(mRenderer, &paddle);
	}
```
また、[I]キーと[K]キーの処理をProcessInput()に追加。

```cpp
	mPaddleDir[1] = 0;
	if (state[SDL_SCANCODE_I])
	{
		mPaddleDir[1] -= 1;
	}
	if (state[SDL_SCANCODE_K])
	{
		mPaddleDir[1] += 1;
	}
```

###1.D.3　マルチボール
正直これが一番大変だった。。。
壁に跳ね返ったら速度などが違うボールを生成するというシステムを思いつき作ってみた。
ただのマルチボールだと2Pの追加と同様に配列にして、初期化すれば終了だが、
これはゲームの途中でボールを増やさなければならない。

まずBall構造体とメンバー変数にvector型のmBallを追加した。

```cpp
struct Ball
{
    Vector2 pos;
    Vector2 vel;
};
std::vector<Ball> mBall;
```

跳ね返った際、ボールの初期値（位置・速度）を設定し、mBallにpush_back
そして、GenerateBall関数を呼び出す。
しかし、なぜか座標がバグるので最後の行に強制的に座標を入れている。

```cpp
	if (diff[i] <= paddleH / 2.0f &&
			b.pos.x <= 1024.0f - 20.0f && b.pos.x >= 1024.0f - 25.0f &&
			b.vel.x > 0.0f)
		{
			b.vel.x *= -1.0f;
			if (NumBall <= maxBallNum) {
				Ball IniBall = {
				b.pos,
				{b.vel.x * GetRandom(0.7f, 1.5f), GetRandom(1.0f, 5.0f)}
				};
				mBall.push_back(IniBall);
				GenerateBall(mBall[mBall.size() - 1], IniBall.pos, IniBall.vel);
				NumBall++;
				b.pos = IniBall.pos; //push_backしたら座標がバグるのでそれを強制的に戻す
		}
```

```cpp
void Game::GenerateBall(Ball b, Vector2 pos, Vector2 vel)
{

	SDL_Rect ball{
		static_cast<int>(b.pos.x - thickness / 2),	//static_cast<int>はfloatからint整数に変換する
		static_cast<int>(b.pos.y - thickness / 2),
		thickness,
		thickness
	};
	SDL_RenderFillRect(mRenderer, &ball);
}
```

###1.6.3　スコア機能
これは簡単で、メンバー変数にScore[2]とボールの数を数えるNumBallを追加。
ボールが相手のゴールに入れば得点を加算する。

```cpp
	//ボールが範囲外にいったときポイント加算
	if (b.pos.x < 0.0f)
	{
		Score[1]++;
		std::cout << Score[0] << "VS" << Score[1] << std::endl;
		NumBall--;
		if (mBall.size() > 0) /*vectorのメモリ領域を超えてしまわないように*/ {
			mBall.erase(mBall.begin() + count);
		}
		}
	//ボールが範囲外にいったときポイント加算
	else if (b.pos.x > 1024.0f)
	{
		Score[0]++;
		std::cout << Score[0] << "VS" << Score[1] << std::endl;
		NumBall--;
		if (mBall.size() > 0)/*vectorのメモリ領域を超えてしまわないように*/ {
			mBall.erase(mBall.begin() + count);
		}
	}
```

次にフィールドにあるボールの数が0個になったら、リザルトを表示し、
2秒待ってからもう一度初期からスタートする。

```cpp
	//ボールがなくなったら初期化
	if (NumBall <= 0)
	{
		//Result確認
		std::cout << std::endl << "Result:" << Score[0] << "VS" << Score[1] << std::endl;
		if (Score[0] > Score[1])
		{
			std::cout << "Player0 Win : Player1 Lose" << std::endl;
		}
		else if (Score[0] < Score[1])
		{
			std::cout << "Player0 Lose : Player1 Win" << std::endl;
		}
		else
		{
			std::cout << "Draw" << std::endl;
		}
		Sleep(2 * 1000);
		SDL_DestroyRenderer(mRenderer);
		mBall.emplace_back();
		mBall[0].pos = mBallInitialPos;
		mBall[0].vel = mBallInitialVel;
		NumBall = 1;
		for (int i = 0; i < 2; i++) Score[i] = 0;
		mRenderer = SDL_CreateRenderer(
			mWindow, // Window to create renderer for
			-1,		 // Usually -1
			SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC
		);
		GenerateOutput();
	}
```

これでカスタムPong完成！
![gamecpp2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/398871/93d35fdb-52ff-cb75-39ae-b3e82df1b9a2.gif)

#おわりに
とりあえずChapter1終了！
時間がかかりすぎたので、カスタマイズはほどほどにしておきます。
次回はChapter2から・・・！