LoginSignup
12
13

More than 3 years have passed since last update.

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

Posted at

はじめに

前回: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などでも必要ですね。
cpp
//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;
}

これにてPongゲームの単純バージョン完成!
gamecpp.gif

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();
    }

これでカスタムPong完成!
gamecpp2.gif

おわりに

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

12
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
13