#はじめに
前回:https://qiita.com/murati111/items/3ccfdacd2ffe4e39470a
Githubに作成したコードを公開しています:
https://github.com/murati111/PracticeGameProgramCpp
IDE:VisualStudio2019
##1.6ゲームの更新
###1.6.2 「デルタタイムの関数」としてのロジック
昔は敵の位置更新などはフレームレートが固定なので、一定でよかった。
//x位置を5ピクセル移動
enemy.mPosition.x+=5;
しかし、フレームレートが変動するのは当たり前なので、
このコードだとフレームが倍になれば移動も倍になる。
この問題を解決するのが__デルタタイム(delta time)__
最後のフレームから今までに経過した時間の長さを意味する。
1フレームに何ピクセルではなく、1秒間に何ピクセル動くかに変更する。
これはUE4などでも必要ですね。
//x位置を150ピクセル/秒で更新
enemy.mPosition.x += 150*deltaTime;
SDLではこのような実装になる。
void Game::UpdateGame()
{
float deltaTime=(SDL_GetTicks()-mTicksCount)/1000.0f;
//時刻を更新
mTicksCount=SDL_GetTicks();
}
SDL_GetTicks()ではミリ秒数がでるので1000.0fで割ることで秒単位のデルタタイムを得られた!
しかし、物理シミュレーションではフレームレートによって異なる振る舞いとなる。
解決策として、__フレーム制限__をかける。
SDLでのフレーム制限のかけ方は以下のコードをUpdateGameの先頭に追加する。
while(!SDL_TICKS_PASSED(SDL_GetTicks(),mTicksCount + 16));
デルタタイムを大きくなりすぎる問題を、最大値(たとえば0.05f)以下に制限する。
if(deltaTime>0.05f)
{
deltaTime=0.05f;
}
最終形がこうなった
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)をセットする。
mPaddleDir = 0;
if (state[SDL_SCANCODE_W])
{
mPaddleDir -= 1;
}
if (state[SDL_SCANCODE_S])
{
mPaddleDir += 1;
}
次にパドルの位置を更新する。
ここでは毎秒300.0fピクセルというスピードでy座標を更新。
if (mPaddleDir != 0)
{
mPaddlePos.y += mPaddleDir * 300.0f * deltaTime;
}
パドルが画面外から出ないようにする。
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に追加する。
mBallPos.x += mBallVel.x * deltaTime;
mBallPos.y += mBallVel.y * deltaTime;
ボールが上の壁に衝突し、ボールが右上向きに動いていたら、右下向きに跳ね返るようにする。
上または下の壁に跳ね返る場合は、速度のy成分を反転。
右側または左側のパドルの場合は、速度のx成分を反転する。
//上の壁の場合
if(mBallPos.y <= thickness)
{
mBallVel.y *= -1;
}
しかしこのコードには問題点がある、
ボールがフレームAで、上の壁と衝突したとする。
その場合y速度が反転して、下向きに動き始める。
次のフレームBで、ボールは壁から離れようとするが、まだ十分に動いていない。
まだボールと壁は衝突している、なのでまたy速度が反転し、ボールは上向きになる。
これが続き、ボールが壁から永久に離れなくなる。
この問題を解消するためには、y速度を反転するとき、上の壁かつ、ボールが上向きに動いている時である必要がある。
if(mBallPos.y <= thickness && mBallVel.y < 0.0f)
{
mBallVel.y *= -1;
}
パドルの衝突は、ボールのy位置とパドルのy位置の差を計算し、その絶対値を求める。
もし差の絶対値がパドルの高さの半分よりも大きければ、ボールの位置がパドルより高いか低くなっている。
これとx位置が重なっているか、そして離れようとしないことをチェックする。
if(//yの差が十分に小さければ
diff <= paddleH / 2.0f
//ボールが正しいx位置にあれば
&& mBallPos.x <= 25.0f && mBallPos.x >= 20.0f
//ボールが左向きであれば
&& mBallVel.x *= -1.0f)
{
mBallVel.x *= -1.0f;
}
##1.D 練習問題とカスタマイズ
せっかくなのでこのゲームをカスタマイズした。
カスタマイズした点
1.2Pの追加(IキーとKキーで移動)
2.マルチボール(壁に跳ね返ったら速度などが違うボールを生成するシステム)
3.スコア機能(コンソールコマンドで表示にした)
###1.D.2 2Pの追加
メンバー変数mPaddleDirとmPaddlePosを要素数2の配列にした。
SDL_Rectで2個のパドルを生成した。
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()に追加。
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を追加した。
struct Ball
{
Vector2 pos;
Vector2 vel;
};
std::vector<Ball> mBall;
跳ね返った際、ボールの初期値(位置・速度)を設定し、mBallにpush_back
そして、GenerateBall関数を呼び出す。
しかし、なぜか座標がバグるので最後の行に強制的に座標を入れている。
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したら座標がバグるのでそれを強制的に戻す
}
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を追加。
ボールが相手のゴールに入れば得点を加算する。
//ボールが範囲外にいったときポイント加算
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秒待ってからもう一度初期からスタートする。
//ボールがなくなったら初期化
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();
}
#おわりに
とりあえずChapter1終了!
時間がかかりすぎたので、カスタマイズはほどほどにしておきます。
次回はChapter2から・・・!