シェルスクリプトマガジン

香川大学SLPからお届け!(Vol.62掲載)

筆者:宇野 光純

 前回に引き続き、Windowsアプリケーションとして動く簡単な2Dゲームの開発を紹介します。汎用プログラミング言語の「C++」と、オープンソースのパソコンゲーム開発用ライブラリの「DXライブラリ」を組み合わせることで、時間と労力は必要ですが、Unityなどのゲームエンジンよりも自由度の高いゲーム開発ができます。

シェルスクリプトマガジン Vol.62は以下のリンク先でご購入できます。

図2 「Shot.h」ファイルに記述するコード

#pragma once
#include "Object.h"

class Shot : public Object {
private:
  bool flag;
  bool image;
  // true なら敵機の弾、false なら自機の弾
  static int image1; // 画像ハンドル1
  static int image2; // 画像ハンドル2
  void SetImage(); // 画像関連設定用の関数

public:
  double xv, yv; // X、Y 方向の移動量
  Shot();
  void Update(); // 更新
  void Draw(); // 描画
  // 座標、速度、画像を指定し発射する
  void Shoot(double nx, double ny,
             double nxv, double nvy, bool fimg);
};

図3 「Shot.cpp」ファイルに記述するコード

#include "DxLib.h"
#include "Shot.h"
#include "Info.h"
int Shot::image1 = -1;
int Shot::image2 = -1;

Shot::Shot() {
  x = y = 0.0; xv = yv = 0.0; flag = true; image = false;
  SetImage();
}
void Shot::Update() {
  if (flag) {
    x += xv; y += yv;
    // 画面外に出た場合、無効にする
    if (x < 0 || GetWidth() < x || y < 0 || GetHeight() < y) flag = false;
  }
}
void Shot::Draw() {
  if (flag) {
    if (image) DrawGraph((int)(x - size / 2), (int)(y - size / 2), image1, TRUE);
    else DrawGraph((int)(x - size / 2), (int)(y - size / 2), image2, TRUE);
  }
}
void Shot::SetImage() {
  size = 16;
  if (image1 == -1) image1 = LoadGraph("./images/shot1.png");
  if (image2 == -1) image2 = LoadGraph("./images/shot2.png");
}
// 座標、速度、画像を指定し発射する
void Shot::Shoot(double nx, double ny, double nxv, double nyv, bool fimg) {
  x = nx; y = ny; xv = nxv; yv = nyv;
  image = fimg; flag = true;
}

図4 「Object.h」ファイルに追加するコード

class Object {
public:
  bool flag; // 有効無効を示すフラグ
};

図5 「Player.h」ファイルに追加するコード

#include "Shot.h"
class Player : public Object {
private:
  int shot_num; // 現在の弾配列の添字
  int shot_span; // 弾の発射間隔
  void SetShot(); // 弾関連の設定用関数
  void ShotFire(); // 弾発射用の関数
public:
  static const int shot_max = 20; // 弾配列の要素数
  Shot shot[shot_max]; // 弾配列
};

図6 「Player.cpp」ファイルに追加するコード

Player::Player() {
  flag = true; // 有効フラグを設定立てる
  this->SetShot(); // 自機の弾関連の設定
}
void Player::Update() {
  this->ShotFire(); // 自機の弾の発射
  for (int i = 0; i < shot_max; i++) shot[i].Update();
}
void Player::Draw() {
  for (int i = 0; i < shot_max; i++) shot[i].Draw();
}
void Player::SetShot() {
  shot_num = 0; shot_span = 0;
  for (int i = 0; i < shot_max; i++) shot[i] = Shot();
}
void Player::ShotFire() {
  if (GetKey(KEY_INPUT_Z)) {
    // 発射間隔shot_span が4 以上になったとき
    if (shot_span++ >= 4) {
      // 自機位置から弾を発射する
      shot[shot_num++].Shoot(x, y, 0, -8, false);
      // 配列の添字が要素数以上になったときは0 にする
      if (shot_num >= shot_max) { shot_num = 0; }
      // 発射間隔のリセット
      shot_span = 0;
    }
  }
}

図7 「MainScene.h」ファイルに記述するコード

#include "Shot.h"
#include <vector>

class MainScene {
private:
  int enemy_span; // 弾の発射間隔
  double enemy_shot_base; // 発射角度
  std::vector<Shot> enemy_shot; // 敵機の弾配列

public:
  void StageInitialize(); // パラメータ初期化用関数
  void StageUpdate(); // 敵機の弾発射を実装する関数
};

図8 「MainScene.cpp」ファイルに追加するコード

#define _USE_MATH_DEFINES
#include <math.h>

MainScene::MainScene() {
  StageInitialize(); // 追加したパラメータの初期化
}
void MainScene::Update() {
  for (auto itr = enemy_shot.begin(); itr != enemy_shot.end();) {
    if (!(*itr).flag) itr = enemy_shot.erase(itr);
    else { (*itr).Update(); itr++; }
  }
  StageUpdate(); // ステージの更新
}
void MainScene::Draw() {
  for (auto itr = enemy_shot.begin(); itr != enemy_shot.end(); ++itr)
    (*itr).Draw(); // 敵機の弾の描画
}
void MainScene::StageInitialize() {
  enemy_span = 0; enemy_shot_base = 0;
}
void MainScene::StageUpdate() {
  if (enemy_span++ >= 50) {
    double shot_v = 2.0; int shot_num = 36;
    for (int i = 0; i < shot_num; i++) {
      double angle = enemy_shot_base + M_PI / 18 * i; // 発射角度
      enemy_shot.push_back(Shot()); // インスタンスを末尾に追加
      enemy_shot.back().Shoot(enemy.x, enemy.y, shot_v * cos(angle),
                              shot_v * sin(angle), true); // 発射
    }
    enemy_shot_base += 0.1; // 基準の角度を更新
    enemy_span = 0; // 発射間隔を初期化
  }
}

図10 「Info.h」ファイルに追加するコード

#include "Object.h"

// 2 オブジェクトの当たり判定用関数
void Collision(Object *obj1, Object *obj2);

図11 「Info.cpp」ファイルに追加するコード

#include <math.h>
void Collision(Object *obj1, Object *obj2) {
  double dx = obj1->x - obj2->x; // X 座標の差
  double dy = obj1->y - obj2->y; // Y 座標の差
  double ds = obj1->hit_size + obj2->hit_size; // 半径の合計
  // 有効フラグが立っているかどうかの確認
  if (!obj1->flag || !obj2->flag) return;
  // 三平方の定理を使用
  if (pow(dx, 2) + pow(dy, 2) <= pow(ds, 2)) {
    // 当たり判定後の処理
    obj1->CollisionResult();
    obj2->CollisionResult();
  }
}

図12 「Object.h」ファイルに追加するコード

class Object {
public:
  int hit_size; // 当たり判定エリアの半径
  // 当たり判定後の処理用関数
  virtual void CollisionResult() {}
};

図13 「Player.h」ファイルに追加するコード

class Player : public Object {
public:
  int hp_now, hp_max; // 体力の現在値、最大値
  void CollisionResult(); // 当たり判定後の処理用関数
};

図14 「Player.cpp」ファイルに追加するコード

Player::Player() {
  hp_now = hp_max = 3; // 体力の初期化
}
void Player::SetImage() {
  hit_size = 8;
}
void Player::CollisionResult() {
  if (hp_now-- < 0) flag = false;
}

図15 「Enemy.h」ファイルに追加するコード

class Enemy : public Object {
public:
  int hp_now, hp_max;
  void CollisionResult();
};

図16 「Enemy.cpp」ファイルに追加するコード

Enemy::Enemy() {
  hp_now = hp_max = 100; // 体力
  flag = true; // 有効フラグを立てる
}
void Enemy::SetImage() {
  hit_size = 32;
}
void Enemy::CollisionResult() {
  if (hp_now-- < 0) flag = false;
}

図17 「Shot.h」ファイルに追加するコード

class Shot : public Object {
public:
  // 当たり判定後の処理用関数
  void CollisionResult();
};

図18 「Shot.cpp」ファイルに追加するコード

void Shot::SetImage() {
  hit_size = 8;
}
void Shot::CollisionResult() { flag = false; }

図19 「MainScene.cpp」ファイルに追加するコード

void MainScene::Update() {
  // 自機の弾と敵機の当たり判定処理
  for (int i = 0; i < player.shot_max; i++)
    Collision(static_cast<Object*>(&player.shot[i]), static_cast<Object*>(&enemy));
  // 自機と敵機の弾の当たり判定処理
  for (auto itr = enemy_shot.begin(); itr != enemy_shot.end(); itr++)
    Collision(static_cast<Object*>(&player), static_cast<Object*>(&(*itr)));
}
void MainScene::Draw() {
  DrawFormatString(0, 0, GetColor(255, 255, 255), "Player : %d", player.hp_now);
  DrawFormatString(GetWidth() - 120, 0, GetColor(255, 255, 255), "Enemy : %d", enemy.hp_now);
}

図20 「Info.h」ファイルに追加する コード

// ゲームシーンの取得用関数
int GetGameScene();
// ゲームシーンの設定用関数
void SetGameScene(int val);

図21 「Info.cpp」ファイルに追加するコード

int g_GameScene; // シーン管理用変数
int GetGameScene() { return g_GameScene; }
void SetGameScene(int val) { g_GameScene = val; }

図22 「TitleScene.h」ファイルに 記述するコード

#pragma once
class TitleScene {
private:
  int wait; // 待ち時間
public:
  TitleScene();
  bool Update();
  void Draw();
};

図23 「TitleScene.cpp」ファイルに記述するコード

#include "DxLib.h"
#include "TitleScene.h"
#include "Info.h"

TitleScene::TitleScene() {
  SetBackgroundColor(100, 100, 100); // 背景を灰色に
  wait = 30;
}
bool TitleScene::Update() {
  // スペースキーを押したら、true を返す
  if (wait-- < 0 && GetKey(KEY_INPUT_SPACE)) return true;
  return false;
}
void TitleScene::Draw() {
  DrawFormatString(200, 200, GetColor(255, 255, 255), " タイトルです.");
  DrawFormatString(200, 400, GetColor(255, 255, 255), " スペースキーを押してください.");
}

図25 「ResultScene.h」ファイルに記述するコード

#pragma once
class ResultScene {
public:
  bool Update();
  void Draw();
};

図26 「ResultScene.cpp」ファイルに記述するコード

#include "DxLib.h"
#include "ResultScene.h"
#include "Info.h"

bool ResultScene::Update() {
  if (GetKey(KEY_INPUT_SPACE)) return true; // メインシーンに遷移
  return false;
}
void ResultScene::Draw() {
  // ゲームクリア時
  if (GetGameScene() == 2) {
    SetBackgroundColor(255, 255, 255); // 背景を白色に
    DrawFormatString(300, 200, GetColor(0, 0, 0), " ゲームクリア !!");
  }
  // ゲームオーバー時
  else {
    SetBackgroundColor(255, 100, 100); // 背景を赤色に
    DrawFormatString(300, 200, GetColor(0, 0, 0), " ゲームオーバー...");
  }
  DrawFormatString(300, 360, GetColor(0, 0, 0), " スペースキーを押すと");
  DrawFormatString(300, 380, GetColor(0, 0, 0), " タイトルに戻ります.");
  DrawFormatString(300, 420, GetColor(0, 0, 0), " 終了には、ESC キー.");
}

図29 「Main.cpp」ファイルに追加するコード

#include "TitleScene.h"
#include "ResultScene.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
         LPSTR lpCmdLine, int nCmdShow)
{
  TitleScene ts = TitleScene(); 
  ResultScene rs = ResultScene();
  while (ProcessMessage() == 0 && ScreenFlip() == 0 &&
      ClearDrawScreen() == 0) {
    // ms.Update(); ms.Draw(); は消してその部分に以下を追加
    int t = 0;
    switch (GetGameScene()) {
      case 0:
        if (ts.Update()) { ms = MainScene(); SetGameScene(1); break; }
        ts.Draw();
        break;
      case 1:
        if((t = ms.Update()) != 0) { SetGameScene(t); break; }
        ms.Draw();
        break;
      default:
        if (rs.Update()) { ts = TitleScene(); SetGameScene(0); break; }
        rs.Draw();
        break;
    }
  }
}

図30 「MainScene.h」ファイルに追加 するコード

class MainScene {
public:
  // void Update(); 行は削除
  int Update();
};

図31 「MainScene.cpp」ファイルに追加するコード

// void MainScene:Update() { 行を次の行に置き換えてから太字部分をブロック末尾に追加
int MainScene::Update() {
  if (player.hp_now <= 0) { return 3; }
  if (enemy.hp_now <= 0) { return 2; }
  return 0;
}
void MainScene::StageUpdate() {
  if (enemy_span++ >= 50) {
    // 体力が半分より大きいとき
    if (enemy.hp_now > enemy.hp_max / 2) {
        // このブロックに既存のコードを挿入
    }
    // 体力が半分以下のとき、ランダムな角度に多数の弾を発射
    else {
      double shot_v = 1.0 + GetRand(40) / 10.0;
      int shot_num = 2;
      for (int i = 0; i < shot_num; i++) {
        double angle = M_PI * (GetRand(3600) / 10.0) / 180;
        enemy_shot.push_back(Shot());
        enemy_shot.back().Shoot(
          enemy.x, enemy.y, shot_v * cos(angle), shot_v * sin(angle), true
        );
      }
    }
  }
}