Skip to content

第07回 C++振り返り / オブジェクト指向入門

前回: 第06回 2Dシューティングゲーム入門 3 / 状態管理とゲームオーバー / 次回: 第08回 映像以外の構成要素 / BGM、効果音、エフェクト

前回の振り返り

シューティングゲームの基本形を作りました

  • プレイヤー、敵、弾を GameObject としてまとめました
  • 配列と for 文を使い、弾を複数扱えるようにしました
  • IsHit で弾と敵、プレイヤーと敵の判定を行いました
  • 敵を左右に動かし、命中したら再出現させました
  • enum GameStatePlayingGameOverGameClear の状態を定義し ゲーム内の状態によって更新処理と表示内容を切り替えました
  • 今回は、ここまで使ってきたC++の書き方を振り返り、 コードを役割ごとに整理する考え方へ進みます

今回の目的

より大きなプログラムを扱うための技法に触れる

  • 第01〜06回で使ったC++の要素を整理する
  • GameObject 構造体と、それを使ったコードの役割を振り返る
  • 長くなった処理を関数に分ける考え方を確認する
  • WinMain の中にある変数と処理を Game クラスへ移す
  • データと処理を近くに置く class の考え方に触れる
  • 複雑さを増していくコードを整理して ゲーム制作をスケールさせる方法を学ぶ

今回の授業内容

C++振り返り / オブジェクト指向入門

  • 第01〜06回の制作内容を、プログラムの部品として振り返る
  • 変数、関数、構造体、配列、enum の役割を再確認する
  • 既存コードを「入力」「更新」「判定」「描画」に分けて見る
  • 長くなった処理に名前を付け、関数として分ける
  • 関数化のために、変数の置き場所を Game クラスへ移す
  • structclass の違いに触れる
  • ゲーム全体の情報と処理を、1つのクラスにまとめる入口を扱う
  • 次回以降の制作で直しやすい形へ、少しずつコードを整える

C++ 振り返り

ここまで使ってきた 言語要素を再確認

復習はだいじ

第01〜06回での制作内容

ゲームに必要な要素を少しずつ積み上げてきました

  • 画面に文字や画像を表示する
  • プレイヤーをキー入力で動かす
  • 弾を発射し、複数の弾を同時に扱う
  • 弾の更新や描画を関数に分ける
  • 弾と敵、プレイヤーと敵の当たり判定を行う
  • 敵を動かし、倒したあとに再出現させる
  • ゲームオーバーやゲームクリアを表示する
  • ゲームとして遊べる形に近づいてきましたが 完成度を上げるためにはさらなる要素の追加が必要です

C++ の要素を役割別に確認

変数、定数、条件分岐、関数…

  • 値を持つ: 変数、定数、enum
  • 処理を分ける: 条件分岐、繰り返し
  • 処理に名前を付ける: 関数
  • データをまとめる: 構造体、配列
  • 高度なゲームを作るためには、これらの要素を より有機的に組み合わせて使いこなす必要があります

コードが長くなると何が困るか

複雑さが手に負えなくなる

  • プレイヤーの位置、入力、移動、描画といった処理が離れていると それぞれの機能の関係性を追いかけづらくなります
  • 敵や弾が増えると、似たような処理が何度も出てきます コピペで書いていると、修正漏れやミスが混入しやすくなります
  • あちこちに条件分岐が散らばると 条件同士の関係性を把握できなくなります
  • 整理されないままのコードが積み重なっていくと 機能の追加もエラーの修正も難しくなってしまいます

コードを整理する

まとまった処理を関数に分ける

  • 最初は多くの処理を縦にならべて 一直線に書いていました
  • 一部の処理は関数に分けてきましたが、 だんだんコードが長くなってきて、 全体を見通すのが難しくなってきました
  • 長くなった処理は、役割ごとで関数に分ける ことで、コードの見通しが良くなります
cpp
// 関数名の例

// 弾を発射する処理
ShootBullet();

// 弾を更新して、敵との命中も調べる処理
UpdateBullets();

// 弾を描画する処理
DrawBullets();

処理に名前を付けるメリット

コードの役割を理解しやすくなる

  • 処理を関数にまとめると「何をしているか」を 名前で判別できるようになります
  • 例えば ShootBullet() と書いてあれば、 「ここは弾を発射する処理だな」とすぐにわかります
  • 機能を追加したり、エラーを修正したりするときも、 関数の単位でコードを追うことができるようになります
  • 処理に適切な名前を付けることは、プログラムを扱う上での基本にして奥義です ぜひこだわってみてください

さらにコードを整理する

データと処理をまとめて扱う

  • 関数に分けることで、処理を役割ごとにまとめることができました
  • ところで、プレイヤーの座標や大きさと、 プレイヤーを動かす処理は強く関係しています
  • 関係の深い値と処理を近くに置けば、 どのデータをどの処理が使うのかをより追いやすくなります
  • C++ には、データと処理をまとめて扱うための機能があります 今回は、値をまとめる struct から一歩進んで、 値と処理をまとめて扱う class を学びます

構造体でできていること

関連する値をまとめて扱う

  • GameObject は、位置、大きさ、有効状態を まとめるための構造体です
  • 第06回では プレイヤー、敵、弾を 同じ形のデータとして扱いましたが、 移動、判定、描画の処理は GameObject から 離れた場所に書いていました
  • 構造体が必要な処理を構造体の中にまとめる ことができれば、データと処理が近くなり コードの見通しがさらに良くなります
cpp
// 位置、大きさ、有効状態をまとめた構造体
struct GameObject
{
    int x;
    int y;
    int width;
    int height;
    bool isActive;
};

C++ の「クラス」

値と処理をまとめた新しい型

  • class は、struct と同じく 自分で新しい型を作る書き方の1つです
  • クラスの中には、値だけでなく 処理も一緒に書くことができます
  • クラスの中にある変数を メンバー変数、 関数を メンバー関数 と呼びます
  • この例では xy などがメンバー変数、 DrawDebugBox() がメンバー関数です
cpp
// 位置、大きさ、描画処理をまとめたクラス
class GameObject
{
public:
    int x;
    int y;
    int width;
    int height;
    bool isActive;

    // 動作確認用の矩形を描画するメンバー関数
    void DrawDebugBox(int color)
    {
        if (isActive)
        {
            // こちらはDXライブラリの関数
            DrawBox(x, y, x + width, y + height, color, TRUE);
        }
    }
};

オブジェクトとは

クラスから作った実体

cpp
GameObject player;
GameObject enemy;
  • GameObject は、自分で作ったです
  • playerenemy は、GameObject 型から作られた変数です
  • C++ では、このように型から作られた実体を オブジェクト と呼びます
  • クラスから作られたオブジェクトは、 特に インスタンス と呼ばれることもあります

オブジェクトの内側で処理を完結させる

メンバー関数でオブジェクトに処理を任せる

cpp
GameObject player;
player.x = 320;
player.y = 400;
player.DrawDebugBox(GetColor(0, 200, 255));
  • player.DrawDebugBox の中では、 同じ player が持つ xywidthheight を参照できます
  • player.DrawDebugBox(color); と書くことで、 player の中にある DrawDebugBox が実行されます
  • 呼び出し側はオブジェクトの内側での細かい処理内容を 知らなくて済むようになり、それぞれが自分の役割に集中できるようになります

クラスを使うメリット

役割ごとにコードを追いやすくなる

  • 位置や大きさなど、共通する値は GameObject にまとめられます 移動や描画など、共通する処理も GameObject にまとめられます
  • GameObject の内部事情を隠すことで、 他のコードは「自分が何をするか」に集中できるようになります
  • GameObject に問題が起きたり、機能を追加したりするときも、 GameObject の中を見るだけで済むようになります
  • 役割ごとに機能の置き場所を決めることで、 コードの見通しが格段に良くなります

public と private

必要のない部分を隠すことで コードの見通しを良くする

  • public は、クラスの外から使える部分です private は、クラスの外には見えない部分です
  • private にした値は、 外側から直接読み書きできなくなります その代わり Activate()Deactivate() の ようなメンバー関数を通してアクセスします
  • オブジェクトの内部状態を隠蔽して、アクセス 方法を制限する手法を カプセル化 と呼びます
cpp
class GameObject
{
// private の部分は、クラスの外からは見えない
private:
  bool isActive = true;

// public の部分は、クラスの外からも見える
public:
  void Activate()
  {
    isActive = true;
  }

  void Deactivate()
  {
    isActive = false;
  }

  bool IsActive()
  {
    return isActive;
  }
};

カプセル化の利点

プログラムを作る側と使う側を分け、コードの影響範囲を限定する

  • プログラムの内側の処理内容をなんでもかんでも外に見せると 内部の細かい部分を気にしながら利用しなければならなくなります
  • ですが利用者が必要な部分だけを公開するようにすれば、 利用する側は内部の詳細を気にせずに済みます
  • クラスの内部を隠すことで、コードの影響範囲を限定し、 変更に強いコードを書くことができます
  • 公開すべき情報と隠すべき情報を切り分けて整理するカプセル化は 重要なプログラミング技法の1つです

private 化は少しずつ

今回は考え方だけでも OK

  • isActiveprivate にすると、 bullet.isActive = true; といったコードがコンパイルエラーになります
  • private 化した場合は、弾の発射処理や当たり判定の処理も Activate()Deactivate()IsActive() を使う形に 置き換える必要があります
  • カプセル化は重要な概念ですが、今までに書いたコードへの影響が大きいため 今回の実習では isActivepublic のままでも構いません
  • private 化は、クラスに慣れてから少しずつ進めましょう

struct と class の関係

C++ では本質的に同じもの

  • C++ の structclass は、 実はほぼ同じものです
  • 構造体とクラスのどちらも メンバー変数やメンバー関数を持てます
  • 大きな違いは、publicprivate を 書かなかった際のデフォルトの公開範囲です
  • struct は最初から publicclass は最初から private として扱われます
cpp
// 構造体の例
struct PlayerData
{
  //  指定がない場合は public
  int x;
  int y;

  // 構造体にもメンバー関数を書ける
  void Move(int dx, int dy)
  {
    x += dx;
    y += dy;
  }
};

// ↑の構造体と同じ内容のクラス
class Player
{
  // 指定がない場合は private
public:
  // public と書くと構造体と同じになる
  int x;
  int y;

  // もちろんクラスにもメンバー関数を書ける
  void Move(int dx, int dy)
  {
    x += dx;
    y += dy;
  }
};

関数に分ける前に考えること

その関数はどの変数を使うのか

  • 第06回では、複数の関数から使う変数を WinMain の外に置いていました
  • 外に置いた変数は複数の関数から扱えますが、 数が増えると管理しにくくなります
  • 関数に分けるためには、処理だけでなく 変数の置き場所 も整理する必要があります
  • 今回は、ゲーム中ずっと使う変数を Game クラスの中へ移していきます
cpp
void ShootBullet()
{
    // 関数の中で player, bullets,
    // shotTimer, shotInterval を使いたい
}

変数の置き場所を決める

ゲーム全体で使うものは Game に集める

置き場所入れるもの
Game クラスプレイヤー、敵、弾、スコア、状態、画像ハンドル
Game のメンバー関数入力、更新、発射、命中判定、描画
WinMainDXライブラリの初期化、メインループ、終了処理
  • Game クラスは、ゲーム本体の置き場所です
  • 変数を Game のメンバー変数にすると、 Game のメンバー関数から扱いやすくなります
  • WinMain は、ゲームを開始して毎フレーム呼び出す入口として 最小限の処理だけを置きます

Game クラスへ移す手順

少しずつ、着実に移行しましょう

  1. class Game を作る
  2. WinMain の外にあった共有変数を、Game のメンバー変数へ移す
  3. 初期化処理を Initialize() へ移す
  4. 毎フレームの更新処理を Update() へ、描画処理を Draw() へ移す
  5. 弾の発射、弾の更新などを小さなメンバー関数に分ける
  6. WinMain から game.Initialize()game.MainLoop() を呼ぶ

リファクタリング

動作を変えずにコードを整理する

  • 動作を変えずにコードの構造を改善することをリファクタリングといいます
  • コードを整理することで、機能の追加やエラーの修正がしやすくなりますが 一緒に動作も変わってしまうと、どこかでエラーが混入しても 原因を特定するのは難しくなります
  • そのため、コードを移すときは、動作を変えないように 少しずつ、着実に移していくことが大事です
  • 今回の場合、まずは大きく InitializeUpdateDraw に分けて移動、 その後で細かい関数へ処理を分割していくのが良いでしょう

Game クラスの形

ゲーム全体を管理するクラスを作る

  • public は、外側から呼び出す処理です WinMain からは Initialize()MainLoop() を呼びます
  • private は、Game クラスの内側だけで使う 補助的な処理です
  • 弾の発射や更新はゲーム内部の処理なので、 まずは private に置いておきます
cpp
class Game
{
public:
    void Initialize();
    void MainLoop();
    void Update();
    void Draw();

private:
    // 判定と再出現
    bool IsHit(GameObject a, GameObject b);
    GameObject RespawnEnemy(GameObject enemy);

    // 更新処理
    void ShootBullet();
    void UpdatePlayerAndEnemy();
    void UpdateBullets();

    // 描画処理
    void DrawBullets();
};

宣言と定義を分ける

先に名前を、処理の中身は後で書く

  • クラスの中に void Initialize(); と書くと、 「このクラスには Initialize という関数がある」と伝えられます
  • このように、関数の名前、戻り値、引数だけを 先に書くことを 関数の宣言 と呼びます プロトタイプ宣言 と呼ばれることもあります
  • 実際の処理の中身を書く部分は 関数の定義 と 呼び、こちらが関数の実体になります
cpp
class Game
{
public:
    // 関数のプロトタイプ宣言
    void Initialize();
};

// 関数の定義
// 「Game クラスの Initialize という関数」の意味
void Game::Initialize()
{
    // 初期化処理をここに書く
}

メンバー変数へ移す

外に置いていた共有変数を Game クラスに置く

  • ゲーム中ずっと必要な値は、 Game のメンバー変数にします
  • bullets の配列数に使うため、bulletMaxstatic const int として定義しています
  • 画像ハンドルは、まだ読み込んでいない状態を -1 で表しておきます
cpp
class Game
{
private:
    // 関数の宣言は省略

    // 弾数、発射間隔、移動速度、クリア条件
    static const int bulletMax = 10;
    const int shotInterval = 10;
    const int moveSpeed = 4;
    const int bulletSpeed = 8;
    const int clearScore = 3;

    // プレイヤーと敵の状態
    GameObject player = { 320, 400, 32, 32, true };
    GameObject enemy = { 320, 80, 48, 48, true };

    // 弾の管理配列
    GameObject bullets[bulletMax] = {};

    // ゲーム中に変化する値
    int shotTimer = 0;
    int score = 0;
    int enemySpeed = 2;
    GameState gameState = Playing;

    // 画像ハンドル
    int playerHandle = -1;
    int enemyHandle = -1;
};

static const の意味

配列の個数として使うための定数

  • 配列の個数は、プログラムを実行する前に 決まっている必要があります
  • static const int と書くと、 クラス全体で共有される変更しない整数として扱えます
  • そのため、bullets[bulletMax] のように 配列の個数として使うことができます
  • 今回は「クラスの中で配列数を決める定数」として この形を使う、と覚えておけば大丈夫です
cpp
class Game
{
private:
    static const int bulletMax = 10;
    GameObject bullets[bulletMax] = {};
};

初期化処理を移す

DxLib_Init の後で画像を読み込む

  • LoadGraph は DXライブラリの関数ですので DXライブラリの初期化後に呼びます
  • そのため、画像読み込みは Game を作った 瞬間ではなく、Initialize() の中で行います
  • 初期化処理がまとまっているとリトライ処理を 作るときも再利用しやすくなります
cpp
void Game::Initialize()
{
    // プレイヤー画像と敵画像を読み込む
    playerHandle = LoadGraph(L"Images/Player.png");
    enemyHandle = LoadGraph(L"Images/Enemy.png");

    // ゲーム開始時の状態に戻す
    player = { 320, 400, 32, 32, true };
    enemy = { 320, 80, 48, 48, true };

    for (int i = 0; i < bulletMax; i++)
    {
        bullets[i].isActive = false;
    }

    shotTimer = 0;
    score = 0;
    enemySpeed = 2;
    gameState = Playing;
}

更新処理を移動する

Playing の間だけゲームを動かす

  • Update() は、毎フレームの更新処理をまとめる関数です
  • 最初は第06回の Playing の中身を そのまま Update() に移して構いません
  • 慣れてきたら、プレイヤー、敵、弾、判定のように さらに小さなメンバー関数へ分けています
cpp
void Game::Update()
{
  switch (gameState)
  {
  case Playing:
    // プレイ中だけゲームの更新処理を行う
    UpdatePlayerAndEnemy();

    // 連射制御用タイマーを減らす
    if (shotTimer > 0)
      shotTimer--;

    ShootBullet();
    UpdateBullets();

    // プレイヤーが敵に触れたらゲームオーバー
    if (IsHit(player, enemy))
      gameState = GameOver;

    break;
  }
}

描画処理を移動する

処理を Draw にまとめる

  • Draw() は現在のゲーム状態を描画する関数です
  • ClearDrawScreen()ScreenFlip() は、 描画に必要な処理ですが、描画そのものでは ないのでMainLoop() 側で呼んでいます
  • 弾の描画は長くなりやすいので、 DrawBullets() に分割しています
cpp
void Game::Draw()
{
  DrawGraph(player.x, player.y, playerHandle, TRUE);

  // 敵が有効なときだけ描画する
  if (enemy.isActive)
    DrawGraph(enemy.x, enemy.y, enemyHandle, TRUE);

  DrawBullets();

  DrawFormatString(10, 10, GetColor(255, 255, 255),
    L"SCORE %d / %d", score, clearScore);

  // 状態に応じて結果を表示する
  if (gameState == GameOver)
  {
    DrawString(260, 220, L"GAME OVER",
      GetColor(255, 255, 255));
  }

  if (gameState == GameClear)
  {
    DrawString(260, 220, L"GAME CLEAR",
      GetColor(255, 255, 255));
  }
}

MainLoop を作る

毎フレームの処理を Game にまとめる

  • MainLoop() は、ゲーム中に毎フレーム繰り返す処理です
  • Update()Draw() の呼び出しも Game クラスの中にまとめて、ゲームループの 処理を一元化しています
cpp
void Game::MainLoop()
{
  while (ProcessMessage() == 0 &&
    CheckHitKey(KEY_INPUT_ESCAPE) == 0)
  {
    Update();

    // 1フレーム分の描画を行う
    ClearDrawScreen();
    Draw();
    ScreenFlip();
  }
}

WinMain を整理

Game の MainLoop を呼ぶ

  • ゲームの機能は Game クラスの中に集約され、 WinMain は、DXライブラリの準備と終了、 Game の作成と開始を担当するだけで 十分になりました
  • 以降、ゲームに関係する変更は Game クラスの 中で完結することになります
cpp
int WINAPI WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
  ChangeWindowMode(TRUE);
  if (DxLib_Init() == -1)
    return -1;

  SetDrawScreen(DX_SCREEN_BACK);

  Game game;
  game.Initialize();
  game.MainLoop();

  DxLib_End();
  return 0;
}

サンプルコード(1/4)

弾を発射する

  • ShootBullet()Game のメンバー関数です playerbulletsshotTimer をそのまま 使えます
  • 第06回では関数として外にあった処理を ほぼ同じ形で流用しています
  • 空いている弾に位置と大きさを入れて、 使用中にします
  • 発射できたら待ち時間を入れて break でループを抜けています
cpp
void Game::ShootBullet()
{
    // Zキーが押されていない、または待ち時間中なら発射しない
    if (CheckHitKey(KEY_INPUT_Z) == false || shotTimer > 0)
        return;

    for (int i = 0; i < bulletMax; i++)
    {
        // 使っていない弾を見つけたら発射する
        if (bullets[i].isActive == false)
        {
            bullets[i].x = player.x + 12;
            bullets[i].y = player.y;
            bullets[i].width = 8;
            bullets[i].height = 16;
            bullets[i].isActive = true;
            shotTimer = shotInterval;
            break;
        }
    }
}

サンプルコード(2/4)

弾を移動する

  • 弾の移動、画面外判定、敵との命中判定を UpdateBullets() にまとめます
  • enemyscoregameStateGame の メンバー変数なので、この関数から扱えます
  • まずは使用中の弾だけを上へ動かし、 画面外に出た弾を未使用に戻します
cpp
void Game::UpdateBullets()
{
    for (int i = 0; i < bulletMax; i++)
    {
        // 使用中ではない弾は更新しない
        if (bullets[i].isActive == false)
            continue;

        // 弾を上に動かす
        bullets[i].y -= bulletSpeed;

        // 画面外に出たら未使用に戻す
        if (bullets[i].y < -16)
            bullets[i].isActive = false;

サンプルコード(3/4)

敵との命中判定を行う

  • 弾が敵に当たったら、その弾を未使用に戻し、 スコアを増やします
  • スコアが足りなければ敵を再出現させ、 目標に達したら GameClear にします
cpp
        // 弾が敵に当たったらスコアを増やす
        if (IsHit(bullets[i], enemy))
        {
            bullets[i].isActive = false;
            score++;

            if (score >= clearScore)
            {
                enemy.isActive = false;
                gameState = GameClear;
            }
            else
            {
                // まだクリアでなければ敵を再出現させる
                enemy = RespawnEnemy(enemy);
            }
        }
    }
}

サンプルコード(4/4)

弾の描画も分ける

  • 弾の描画処理も同じように DrawBullets() にまとめます
  • 弾の描画もコード量が多いため、 関数化すると見通しが良くなります
  • この弾の処理のように分かりやすいところから 関数に分ける練習をしてみましょう
cpp
void Game::DrawBullets()
{
    for (int i = 0; i < bulletMax; i++)
    {
        if (bullets[i].isActive)
        {
            DrawBox(
                bullets[i].x, bullets[i].y,
                bullets[i].x + bullets[i].width,
                bullets[i].y + bullets[i].height,
                GetColor(255, 255, 0), TRUE);
        }
    }
}

何でもクラスにまとめたらいいの?

適切な粒度でコードを整理することが大事

  • 何でも1つのクラスに詰め込むと、 コード量が増えてかえって読みにくくなります
  • 逆に細かなクラスを増やしすぎても、追いかける場所が増えるだけで 読みづらいコードになってしまいます
  • バランスのとれたコードを書くためには設計と実装の経験が必要です 時間をかけて、試行錯誤しながら学んでいきましょう
  • 今回の Game クラスは、ゲーム全体を管理するクラスとしてまとめています 細分化でコードを改善できるか、考えてみてください

今まで書いたコードを改善する

動く状態を保ったまま少しずつ整理していく

  • まずは第06回のコードを Game クラスへ移し、 WinMain を短くするところから始めましょう
  • ここまでのコードを否定する必要はありません 今までのコード内容をよく理解した上で、 より整頓されたコードに置き換えていく練習をしてみましょう
  • いきなり大量に書き換えると混乱してしまうかもしれません 動作を確認しながら、小さな改善を積み重ねていきましょう

実習1: 既存コードの役割を分解する

どの処理が何のためにあるかを見分ける

  1. 第06回のコードを開く
  2. プレイヤー、敵、弾、状態管理に関係する行を探す
  3. それぞれの処理が、入力、更新、判定、描画のどれかをメモする
  4. どの変数を Game のメンバー変数にするかメモする
  5. どの処理を Initialize()Update()Draw() に入れるか考える
  • まずはコードの内容をしっかり理解することから始めましょう
  • 気が付いたところはメモしておきましょう コメントはコードの中に書いても、別のファイルに書いても構いません

実習2: Game クラスを作る

ゲーム全体の置き場所を用意する

  1. class Game を定義する
  2. public:Initialize()MainLoop()Update()Draw() の宣言を書く
  3. private: に補助関数の宣言を書く 例: ShootBullet()UpdateBullets()DrawBullets()
  4. クラス定義の最後に ; を付ける
  • まずは「ゲーム全体の入れ物」を作るところまで進めます
  • この時点では、関数の中身はまだ空でも構いません

実習3: メンバー変数を用意する

Game が必要な値を持てるようにする

  1. 第06回で WinMain の外に置いていた共有変数を Game のメンバー変数へ移す
  2. playerHandleenemyHandle もメンバー変数にする
  3. bulletMaxstatic const int として置く
  4. ここまでできたら、まずビルドできるか確認する
  • まだ処理の中身は移さなくても構いません
  • まずは Game が必要な値を持てる状態にします

実習4: Initialize を作る

ゲーム開始時の状態をまとめる

  1. クラスの外に Game::Initialize() を定義する
  2. 画像読み込みを Initialize() へ移す
  3. プレイヤー、敵、弾、スコア、状態の初期値を Initialize() で設定する
  4. WinMainGame game; を作り、game.Initialize(); を呼ぶ
  • ここまでできたら、まずビルドできるか確認します
  • 画像読み込みは DxLib_Init() の後に呼ばれているか確認しましょう

実習5: Update と Draw を作る

更新と描画の入口を分ける

  1. Playing 中の更新処理を Game::Update() へ移す
  2. 描画処理を Game::Draw() へ移す
  3. 弾の発射処理を ShootBullet() へ分ける
  4. 弾の更新と命中判定を UpdateBullets() へ分ける
  5. 弾の描画処理を DrawBullets() へ分ける
  • 1つ移したらビルドして、動きが変わっていないか確認します
  • エラーが出たら、使っている変数がメンバー変数か確認しましょう

実習6: MainLoop を作る

WinMain を短くする

  1. Game::MainLoop() を作り、Update()Draw() をその中から呼ぶ
  2. ClearDrawScreen()ScreenFlip()MainLoop() へ移す
  3. WinMain から game.MainLoop() を呼ぶ
  4. WinMain に残す処理を、DXライブラリの初期化と終了だけに近づける
  • 最後に第06回と同じように遊べることを確認します

よくあるつまずき 1

クラスの書き方

  • public: を書き忘れていないか
  • クラス定義の最後に ; を付けているか
  • Game::Update() のように、クラス名を付けて関数を定義しているか
  • クラスの中に書いた宣言と、外に書いた定義の名前が一致しているか
  • bullets[bulletMax] の個数に使う bulletMaxstatic const int になっているか

よくあるつまずき 2

処理を移したときの確認ポイント

  • メンバー関数の中で使う変数が、Game のメンバー変数になっているか
  • LoadGraphDxLib_Init() より前に呼んでいないか
  • WinMain に残す処理と Game に移す処理が混ざっていないか
  • MainLoop() の中で Update()Draw()ScreenFlip() を呼べているか
  • 一度に全部を移動せず、小さい単位で動作を確かめているか

発展課題: GameObject も整理する

難しければ後回しでも OK

  • Game クラスで全体を整理できたら、次は GameObject 側も整理してみましょう
  • たとえば Move()DrawImage() の追加で、 位置の変更や画像描画をオブジェクトに 任せられます
  • ただし一度にやりすぎると混乱しやすいので、 まずは Game クラスへの移行を優先しましょう
  • 余裕があれば挑戦してみてください
cpp
class GameObject
{
public:
    int x;
    int y;
    int width;
    int height;
    bool isActive;
    int imageHandle = -1;

    void Move(int dx, int dy)
    {
        x += dx;
        y += dy;
    }

    void LoadImage(const TCHAR* fileName)
    {
        imageHandle = LoadGraph(fileName);
    }

    void DrawImage()
    {
        if (isActive && imageHandle != -1)
        {
            DrawGraph(x, y, imageHandle, TRUE);
        }
    }
};

今回のまとめ

今日のポイント

  • 第01〜06回で、変数、関数、構造体、配列、enum を使って ゲームの基本形を作りました
  • クラスは構造体の機能に加えて、値と処理をまとめて扱うことができます 今回 Game クラスを作ることで、WinMain に集中していた 変数と処理の置き場所を整理できました
  • Initialize()Update()Draw() に分けることで、 初期化、更新、描画の役割を追いやすくなりました

おつかれさまでした!

次回予告 第08回 音とエフェクト

より派手に していきましょう