🎮

【ゲームプログラミングC++】Chapter1課題を解いてみた

2024/09/24に公開

記事の目的

最近、ゲームプログラミングC++という書籍を購入しまして、その中にある実装課題を解いていたのですが、書籍の中にもネット上にも模範解答が見当たらないんですよね。
なので、自分なりに実装したモノを記事で紹介することで、記事を見た方からのアドバイスを頂きつつ、今後この書籍で勉強する人が挫折せず続けられる一助となればと思った次第です。

そんなわけで、この記事ではどうやって課題を解いたかを中心に解説します。
書籍を読んでない方向けに、Chapterごとの簡単な説明はするかもしれませんが、書籍本文の内容は解説しませんので、悪しからず。

課題1.1

サンプルコードで実装されているPongに対して、1つしかないパドルを2つに増やし、Pongを2人プレイに対応させる

1.1は基本、既に実装されているパドルのコードを参考にすれば、自力でもそこまで苦戦しないはずです。

Game.h

2つ目のパドル(以下パドル2)に関するクラス変数を定義するだけでOK。

Game.h
private:
    //...省略

    // Pong specific
    // Direction of paddle
    int mPaddleDir;
    // Position of paddle
    Vector2 mPaddlePos;

+   // Direction of paddle
+   int mPaddleDir2;
+   // Position of paddle
+   Vector2 mPaddlePos2;

Game.cpp

Initalize()で定義したパドル2を初期化。
元のパドルは左端から10px離れた位置にあるため、パドル2のx値は右端から10px + パドルの幅(thickness)に指定。

Game.cpp
bool Game::Initialize()
{
    //.略

    mPaddlePos.x = 10.0f;
    mPaddlePos.y = 768.0f/2.0f;
+   mPaddlePos2.x = 1024.0f - 10.0f - thickness;
+   mPaddlePos2.y = 768.0f / 2.0f;

    return true;
}

ProcessInput()では、IキーとKキーによる上下移動をサポートさせる。

Game.cpp
void Game::ProcessInput()
{
    //略	

    // Update paddle direction based on W/S keys
    mPaddleDir = 0;
    if (state[SDL_SCANCODE_W])
    {
        mPaddleDir -= 1;
    }
    if (state[SDL_SCANCODE_S])
    {
        mPaddleDir += 1;
    }
    
+    mPaddleDir2 = 0;
+    if (state[SDL_SCANCODE_I])
+    {
+        mPaddleDir2 -= 1;
+    }
+    if (state[SDL_SCANCODE_K])
+    {
+        mPaddleDir2 += 1;
+    }
}

UpdateGame()には、パドル2の位置を更新する処理と、ホールがパドル2に当たった時のx速度反転処理を追加。パドル2用のdiff2も定義する。
画面右端に当たったらゲームを終了する処理も追加する。

Game.cpp
void Game::UpdateGame()
{
    //略

    // Update paddle position based on direction
    if (mPaddleDir != 0)
    {
        mPaddlePos.y += mPaddleDir * 300.0f * deltaTime;
        // Make sure paddle doesn't move off screen!
        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;
        }
    }
    
+   if (mPaddleDir2 != 0)
+   {
+       mPaddlePos2.y += mPaddleDir2 * 300.0f * deltaTime;
+       // Make sure paddle doesn't move off screen!
+       if (mPaddlePos2.y < (paddleH / 2.0f + thickness))
+       {
+           mPaddlePos2.y = paddleH / 2.0f + thickness;
+       }
+       else if (mPaddlePos2.y > (768.0f - paddleH / 2.0f - thickness))
+       {
+           mPaddlePos2.y = 768.0f - paddleH / 2.0f - thickness;
+       }
+   }

    //略

    // Bounce if needed
    // Did we intersect with the paddle?
    float diff = mPaddlePos.y - ball.position.y;
+   float diff2 = mPaddlePos2.y - ball.position.y;
    // Take absolute value of difference
    diff = (diff > 0.0f) ? diff : -diff;
+   diff2 = (diff2 > 0.0f) ? diff2 : -diff2;
    if (
        (//左パドル
            // Our y-difference is small enough
            diff <= paddleH / 2.0f &&
            // We are in the correct x-position
            ball.position.x <= 25.0f && ball.position.x >= 20.0f && //25は左paddleの右辺, 20は反射の有効範囲?
            // The ball is moving to the left
            ball.velocity.x < 0.0f)
+        ||
+       (//右パドル
+           // Our y-difference is small enough
+           diff2 <= paddleH / 2.0f &&
+           // We are in the correct x-position
+           ball.position.x <= mPaddlePos2.x + 5 && ball.position.x >= mPaddlePos2.x && //  mPaddlePos2.xは右paddleの左辺、mPaddlePos2.x + 5は反射の有効範囲
+           // The ball is moving to the left
+           ball.velocity.x > 0.0f)
+       )
    {
        ball.velocity.x *= -1.0f;
    }
    // Did the ball go off the screen? (if so, end game)
    else if (ball.position.x <= 0.0f)
    {
        mIsRunning = false;
    }
    // Did the ball collide with the right wall?
+   else if (ball.position.x >= (1024.0f - thickness) && ball.position.x > 0.0f)
+   {
+       mIsRunning = false;
+   }

    //略

}

(記事にしてて思いましたが、右パドル(パドル2)の反射処理、ballの幅・thicknessを考慮し忘れてる気がしますね)


最後にGenearteOutpt()にパドル2の描画処理を書けばOK。
右壁の描画処理は消しちゃいましょう。

Game.cpp
void Game::GenerateOutput()
{
    //略

-   // Draw right wall
-   wall.x = 1024 - thickness;
-   wall.y = 0;
-   wall.w = thickness;
-   wall.h = 1024;
-   SDL_RenderFillRect(mRenderer, &wall);

    // Draw paddle
    SDL_Rect paddle{
        static_cast<int>(mPaddlePos.x),
        static_cast<int>(mPaddlePos.y - paddleH / 2),
        thickness,
        static_cast<int>(paddleH)
    };
    SDL_RenderFillRect(mRenderer, &paddle);

+   SDL_Rect paddle2 = paddle;
+   paddle2.x = mPaddlePos2.x;
+   paddle2.y = mPaddlePos2.y;
+   SDL_RenderFillRect(mRenderer, &paddle2);    
}

以上で課題1.1は完了です。
今後余裕があれば課題ごとの実行動画を載せたいですね。

課題1.2

引き続き実装済みのPongに対して、1つしかないパドルを2つに増やし、Pongを2人プレイに対応させる

更新を楽にするため、ボールはvectorでまとめて管理するようにしました。
(時間に余裕のある人は、パドルもvectorで管理するようにしてみてください)

Game.h

vectorで管理するために、Ball構造体を作成しています。

Game.h
+struct Ball
+{
+    Vector2 position;
+    Vector2 velocity;
+};

class Game
{
private:

    //略

+   vector<Ball> mBalls;
}

Game.cpp

Initalize()で、定義した2つのボールをmBallsに突っ込みます

Game.cpp
bool Game::Initialize()
{
    //略

+   Ball ball1;
+   ball1.position.x = 1024.0f / 2.0f;
+   ball1.position.y = 768.0f / 2.0f;
+   ball1.velocity.x = -200.0f;
+   ball1.velocity.y = 235.0f;
+   Ball ball2;
+   ball2.position.x = 1024.0f / 2.0f;
+   ball2.position.y = 768.0f / 2.0f;
+   ball2.velocity.x = 130.0f;
+   ball2.velocity.y = -335.0f;
+   mBalls = { ball1, ball2 };

    return true;
}

UpdateGame()では、mBallsからボールを一つずつ取り出して処理するようにする。
(1.1で実装したのと合わせて、関連するコードを全部提示します)

Game.cpp
void Game::UpdateGame()
{
    //略

    // Update ball position based on ball velocity
    for (Ball& ball : mBalls)
    {
        ball.position.x += ball.velocity.x * deltaTime;
        ball.position.y += ball.velocity.y * deltaTime;
    
        // Bounce if needed
        // Did we intersect with the paddle?
        float diff = mPaddlePos.y - ball.position.y;
        float diff2 = mPaddlePos2.y - ball.position.y;
        // Take absolute value of difference
        diff = (diff > 0.0f) ? diff : -diff;
        diff2 = (diff2 > 0.0f) ? diff2 : -diff2;
        if (
            (//左パドル
                // Our y-difference is small enough
                diff <= paddleH / 2.0f &&
                // We are in the correct x-position
                ball.position.x <= 25.0f && ball.position.x >= 20.0f &&
                // The ball is moving to the left
                ball.velocity.x < 0.0f)
            ||
            (//右パドル
                // Our y-difference is small enough
                diff2 <= paddleH / 2.0f &&
                // We are in the correct x-position
                ball.position.x <= mPaddlePos2.x + 5 && ball.position.x >= mPaddlePos2.x &&
                // The ball is moving to the left
                ball.velocity.x > 0.0f)
            )
        {
            ball.velocity.x *= -1.0f;
        }
        // Did the ball go off the screen? (if so, end game)
        else if (ball.position.x <= 0.0f)
        {
            mIsRunning = false;
        }
        // Did the ball collide with the right wall?
        else if (ball.position.x >= (1024.0f - thickness) && ball.position.x > 0.0f)
        {
            mIsRunning = false;
        }
    
        // Did the ball collide with the top wall?
        if (ball.position.y <= thickness && ball.velocity.y < 0.0f)
        {
            ball.velocity.y *= -1;
        }
        // Did the ball collide with the bottom wall?
        else if (ball.position.y >= (768 - thickness) &&
            ball.velocity.y > 0.0f)
        {
            ball.velocity.y *= -1;
        }
    }
}

GenerateOutput()のball描画処理をmBallsのループ処理に変更

Game.cpp
void Game::GenerateOutput()
{
    // Draw ball
-    SDL_Rect ball{	
-       static_cast<int>(mBallPos.x - thickness/2),
-       static_cast<int>(mBallPos.y - thickness/2),
-       thickness,
-       thickness
-   };
-   SDL_RenderFillRect(mRenderer, &ball);
    
+   for (const Ball& const ball : mBalls)
+   {
+       SDL_Rect b{
+           static_cast<int>(ball.position.x - thickness / 2),
+           static_cast<int>(ball.position.y - thickness / 2),
+           thickness,
+           thickness
+       };
+       SDL_RenderFillRect(mRenderer, &b);
+   }

}

以上で課題1.2は完了です。

あとがき

こういった記事を書くのは初めてなので読みにくい点があったら申し訳ないです。
Chapter2も既に実装しているため、なるべく早くアップしたいと思います。
記事の構成はガラッと変わるかもしれないですがご了承を。

余談(エラーハンドリング)

書籍のサンプルプロジェクトクローン後、Chapter1(Pong)をデバッグ起動しようとしたら、エラーで起動できない問題が起きました。
調べたところ、おそらくローカルのSDKとプロジェクトのSDKのズレ?が原因みたいでした。

visual studioのプロジェクト(P)>ソリューションの再ターゲットでそのままOKを押せば、ズレが治り、Pongを実行できましたので、類似の問題が起きている方は参考にしていただければ幸いです。

画像はChapter3のもの。また、プロジェクトを新しく開いた時とは表示が微妙に異なります

Discussion