筆者:宇野 光純
前回に引き続き、Windowsアプリケーションとして動く簡単な2Dゲームの開発を紹介します。汎用プログラミング言語の「C++」と、オープンソースのパソコンゲーム開発用ライブラリの「DXライブラリ」を組み合わせることで、時間と労力は必要ですが、Unityなどのゲームエンジンよりも自由度の高いゲーム開発ができます。
シェルスクリプトマガジン Vol.62は以下のリンク先でご購入できます。
図2 「Shot.h」ファイルに記述するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#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」ファイルに記述するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#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」ファイルに追加するコード
1 2 3 4 |
class Object { public: bool flag; // 有効無効を示すフラグ }; |
図5 「Player.h」ファイルに追加するコード
1 2 3 4 5 6 7 8 9 10 11 |
#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」ファイルに追加するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
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」ファイルに記述するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#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」ファイルに追加するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#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」ファイルに追加するコード
1 2 3 4 |
#include "Object.h" // 2 オブジェクトの当たり判定用関数 void Collision(Object *obj1, Object *obj2); |
図11 「Info.cpp」ファイルに追加するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#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」ファイルに追加するコード
1 2 3 4 5 6 |
class Object { public: int hit_size; // 当たり判定エリアの半径 // 当たり判定後の処理用関数 virtual void CollisionResult() {} }; |
図13 「Player.h」ファイルに追加するコード
1 2 3 4 5 |
class Player : public Object { public: int hp_now, hp_max; // 体力の現在値、最大値 void CollisionResult(); // 当たり判定後の処理用関数 }; |
図14 「Player.cpp」ファイルに追加するコード
1 2 3 4 5 6 7 8 9 |
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」ファイルに追加するコード
1 2 3 4 5 |
class Enemy : public Object { public: int hp_now, hp_max; void CollisionResult(); }; |
図16 「Enemy.cpp」ファイルに追加するコード
1 2 3 4 5 6 7 8 9 10 |
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」ファイルに追加するコード
1 2 3 4 5 |
class Shot : public Object { public: // 当たり判定後の処理用関数 void CollisionResult(); }; |
図18 「Shot.cpp」ファイルに追加するコード
1 2 3 4 |
void Shot::SetImage() { hit_size = 8; } void Shot::CollisionResult() { flag = false; } |
図19 「MainScene.cpp」ファイルに追加するコード
1 2 3 4 5 6 7 8 9 10 11 12 |
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」ファイルに追加する コード
1 2 3 4 |
// ゲームシーンの取得用関数 int GetGameScene(); // ゲームシーンの設定用関数 void SetGameScene(int val); |
図21 「Info.cpp」ファイルに追加するコード
1 2 3 |
int g_GameScene; // シーン管理用変数 int GetGameScene() { return g_GameScene; } void SetGameScene(int val) { g_GameScene = val; } |
図22 「TitleScene.h」ファイルに 記述するコード
1 2 3 4 5 6 7 8 9 |
#pragma once class TitleScene { private: int wait; // 待ち時間 public: TitleScene(); bool Update(); void Draw(); }; |
図23 「TitleScene.cpp」ファイルに記述するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#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」ファイルに記述するコード
1 2 3 4 5 6 |
#pragma once class ResultScene { public: bool Update(); void Draw(); }; |
図26 「ResultScene.cpp」ファイルに記述するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
#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」ファイルに追加するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#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」ファイルに追加 するコード
1 2 3 4 5 |
class MainScene { public: // void Update(); 行は削除 int Update(); }; |
図31 「MainScene.cpp」ファイルに追加するコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
// 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 ); } } } } |