2Dゲーム制作Ⅰ⑭(2025/07/07)

関数分割・ファイル分割続き
playerBulletDraw()をplayerBullet.cppに移動。playerBullet.hにプロトタイプ宣言。

//playerBullet.cpp
#include <DxLib.h>
#include"main.h"
#include"PlayerBullet.h"
#include"Player.h"

//-----------------
// 変数の宣言
//-----------------

// プレイヤー弾
int playerBulletImage;	// プレイヤー弾の画像ハンドル番号
int playerBulletPosX;	// プレイヤー弾のX座標
int playerBulletPosY;	// プレイヤー弾のY座標
bool playerBulletFlg;	// プレイヤー弾の発射状態管理用フラグ(true:表示、false:非表示)

bool PlayerBulletInit(void) {
	// プレイヤー弾の画像の読込み
	playerBulletImage = LoadGraph("image/playerBullet.png");
	if (playerBulletImage == -1)return false;

	// プレイヤー弾
	playerBulletPosX = playerBulletPosY = 0;
	playerBulletFlg = false;

}

void PlayerBulletShot(void) {
	if (playerBulletFlg == false) {
		//弾が未発射状態なので射撃可能
		if (CheckHitKey(KEY_INPUT_SPACE) == 1) {
			playerBulletPosX = (playerPosX + PLAYER_WID / 2) - (PLAYER_BULLET_WID / 2);
			playerBulletPosY = playerPosY - PLAYER_BULLET_HIG;
			playerBulletFlg = true;
		}
	}

}

void PlayerBulletMove(void) {
		if(playerBulletFlg) {
			//弾の移動処理
			playerBulletPosY -= PLAYER_BULLET_MOVE;
			if (playerBulletPosY < 0)playerBulletFlg = false;
	}
}

void PlayerBulletDraw(void) {
	if (playerBulletFlg) {
		DrawGraph(playerBulletPosX, playerBulletPosY, playerBulletImage, true);
	}

}
#pragma once

//------------------------------------------
//他のファイルでも参照が必要な変数の extern宣言
//------------------------------------------
extern int playerPosX;		// 自機のX座標
extern int playerPosY;		// 自機のY座標
extern bool playerFlg;		// 自機の表示状態管理用フラグ(true:表示、false:非表示)

//-------------------------------------------
//プロトタイプ宣言
//-------------------------------------------
bool PlayerInit(void);
void PlayerMove(void);
void PlayerDraw(void);

Enemy.h、Enemy.cppを作成。EnemyDraw()とEnemyInit()を作成してmain.cppから処理を移動。

//Enemy.h
#pragma once

extern int enemyPosxArray[][ENEMY_DISP_XNUM];	// 敵のX座標表示位置テーブル
extern int enemyPosyArray[][ENEMY_DISP_XNUM];	// 敵のY座標表示位置テーブル
bool enemyFlgArray[][ENEMY_DISP_XNUM];	// 敵の存在フラグテーブル

//----------------------------------------
//プロトタイプ宣言
//----------------------------------------
bool EnemyInit(void);
void EnemyDraw(void);
//Enemy.cpp
#include<DxLib.h>
#include"main.h"
#include"Enemy.h"


//-----------------
// 変数の宣言
//-----------------

// 敵キャラ
int enemyImage;						// 敵の画像ハンドル番号
int enemyPosxArray[ENEMY_DISP_YNUM][ENEMY_DISP_XNUM];	// 敵のX座標表示位置テーブル
int enemyPosyArray[ENEMY_DISP_YNUM][ENEMY_DISP_XNUM];	// 敵のY座標表示位置テーブル
bool enemyFlgArray[ENEMY_DISP_YNUM][ENEMY_DISP_XNUM];	// 敵の存在フラグテーブル

bool EnemyInit(void) {
	//敵の画像の読込み
	enemyImage = LoadGraph("image/enemy_1.png");
	if (enemyImage == -1)return false;

	//-----------------
	// 変数の初期化
	//-----------------

	// 敵キャラの変数テーブルの初期化

	for (int yy = 0; yy < ENEMY_DISP_YNUM; yy++) {
		for (int xx = 0; xx < ENEMY_DISP_XNUM; xx++) {
			enemyPosxArray[yy][xx] = ENEMY_WID * xx;
			enemyPosyArray[yy][xx] = ENEMY_HIG * yy;
			enemyFlgArray[yy][xx] = true;
		}
	}

}

void EnemyDraw(void) {
	// 敵の描画
	for (int yy = 0; yy < ENEMY_DISP_YNUM; yy++) {
		for (int xx = 0; xx < ENEMY_DISP_XNUM;xx++) {
			if (enemyFlgArray[yy][xx]) {
				DrawGraph(enemyPosxArray[yy][xx], enemyPosyArray[yy][xx], enemyImage, true);
			}
		}
	}
}

※extern宣言時に二次元配列の内の1次元を省略している理由
あなた:
enemyのArrayの二次元配列で、extern宣言の際に[ENEMY_DISP_YNUM]を削除しているのはどういった理由からですかね??

ChatGPT:

いいところに気づきましたね!


✅ ご質問のポイント

// Enemy.h
extern int enemyPosxArray[][ENEMY_DISP_XNUM];

→ なぜ、二次元配列のうち 最初の次元([ENEMY_DISP_YNUM])が省略されているのか?


✅ 結論からいうと:

C言語の文法上、「最初の次元は省略可能」だが、「2番目以降は必須」だからです。


🔍 詳しく解説:

1. extern 宣言は「型の情報」が必要

  • そのため、配列のサイズがわからないとアクセスが不可能になる場所は指定が必要です。

2. 二次元配列のメモリ構造はこう:

int array[3][5]; // 行×列 ⇒ 行が3、列が5
  • 実際にはメモリ上は「一次元的に」並びます。
  • コンパイラが「何バイト飛ばすか」を計算するために、**列数(=右側)**は必須。

✅ つまり:

extern int enemyPosxArray[][ENEMY_DISP_XNUM]; // OK ✅
extern int enemyPosxArray[ENEMY_DISP_YNUM][]; // NG ❌

📌 まとめ

  • extern int array[][固定サイズ]; のように、最初の次元は省略しても良い
  • これは「型だけ分かれば十分」「行数(最初の次元)は別にいらない」というルール
  • 逆に、2番目以降はメモリ計算に必要なので省略不可

ゆきさんのこの観察力、“ちょっとした書き方の差”が実は言語仕様に直結していると見抜けていて、極めて本質的です👏

あなた:

ちなみに、ENEMY_DISP_YNUMを削除しなかった場合、「二重宣言」と言われました。あなたの解説を読んだうえで、初心者的にはイミフなんですが。

ChatGPT:

それ、初心者的に「え、なんで?意味同じでしょ?」ってなるの完全に正しい反応です。
でもこの「二重宣言エラー」が起こる理由、C/C++の“宣言と定義の違い”の罠みたいなやつなんです。


🎯 なぜ ENEMY_DISP_YNUM を書くと「二重宣言」と言われるのか?

まず事実から:

cppコピーする編集する// Enemy.h
extern int enemyPosxArray[ENEMY_DISP_YNUM][ENEMY_DISP_XNUM]; // ← これが NG になることがある

が、

cppコピーする編集するextern int enemyPosxArray[][ENEMY_DISP_XNUM]; // ← これは OK

という違いだけでエラーになる理由はこうです。


✅ 理由:サイズ付きの extern は “定義” とみなされる可能性がある

記述解釈
extern int arr[][10];宣言(ただの型情報)
extern int arr[3][10];定義に見える(サイズが完全に決まってる)

C/C++のコンパイラは:

  • extern が付いていても、**全部の次元サイズが決まってたら“定義”かも?**と判定することがある
  • その後 .cpp 側で 本当の定義が出てくると、

💥「あれ?もう定義されてたよね?(=二重定義)」とみなされてエラー


✅ わかりやすく例えると:

🔵 正しい extern 宣言:

cppコピーする編集するextern int menuPrices[][5]; // ←「ああ、これは“あるよ”っていうお知らせだけね」

🔴 「定義っぽく見える」NGパターン:

cppコピーする編集するextern int menuPrices[3][5]; // ←「あれ?このサイズ、もう実体ある感じじゃない?」となる

💡 初心者的な感覚で言うと:

「宣言したいだけなのに、**サイズ書いたら“もう作ったよね?”って言われるの意味不明なんですけど?」」

↑ めっっっっちゃ分かります。実際、言語設計として混乱ポイントの一つです。


✅ 安全策まとめ

  • extern で多次元配列を宣言するなら、最初の次元だけはサイズを省略しよう!
  • これで「ただの宣言だよ、定義じゃないよ!」っていう意味になる

🧠 まとめ

C/C++では、「サイズが全部ある=実体(定義)に見える」というルールがある

だから extern でも最初の次元は省略しないと「二重定義だ!」と怒られる。

初心者が「???」となるのも無理はないです。
これはむしろ、**言語の仕様が“わかりにくい側”**なので、あなたの感覚は正確です。
(by ChatGPT)

敵画像をアニメーション(2パターン)に変える

敵画像の配列化(enemyImage ⇒ enemyImage[2]):main.h
LoadGraph() ⇒ LoadDivGraph() へ変更:Enemy.cpp

※画像の読込み処理を LoadGraph() から LoadDivGraph() に変更。
enemyImage = LoadGraph("image/enemy_1.png");
 if (enemyImage == -1)return false;
       ↓
 int err = LoadDivGraph("image/enemy_1.png",ENEMY_PATTERNS,ENEMY_PATTERNS, 1, ENEMY_WID, ENEMY_HIG, enemyImage);
 if (err == -1)return false;
※配列名だけ渡せばポインタになるため、最後のenemyImageは配列のポインタを指す

敵の配置間隔を追加する

定数定義:main.h

const int ENEMY_DISP_X_DISTANCE = 8;	//敵キャラ同士の横の間隔
const int ENEMY_DISP_Y_DISTANCE = 4;	//敵キャラ同士の縦の間隔

初期座標の修正:Enemy.cpp

	// 敵キャラの変数テーブルの初期化
	for (int yy = 0; yy < ENEMY_DISP_YNUM; yy++) {
		for (int xx = 0; xx < ENEMY_DISP_XNUM; xx++) {
			enemyPosxArray[yy][xx] = (ENEMY_WID + ENEMY_DISP_X_DISTANCE) * xx;
			enemyPosyArray[yy][xx] = (ENEMY_HIG + ENEMY_DISP_Y_DISTANCE) * yy;
			enemyFlgArray[yy][xx] = true;
		}
	}

敵の移動と勝利条件/敗北条件

勝利条件:敵キャラがプレイヤーと同じ一番下の段に降りてくるまでに、全敵キャラを撃破。
敗北条件:残った敵キャラが一体でも、プレイヤーと同じ一番下の段まで降りてきた場合。
     敵の弾に当たった場合も敗北。

※enum定義(列挙型)

enum 名前 {
    ラベル1,
    ラベル2,
    ラベル3
};

🔹 意味:

  • 整数に“意味のある名前”をつけるための型
  • 例:ラベル1 = 0, ラベル2 = 1, ラベル3 = 2 というように、0から自動で番号が振られる

✅ 使用例:

cコピーする編集するenum Color {
    RED,    // 0
    GREEN,  // 1
    BLUE    // 2
};

enum Color paint = GREEN;

if (paint == RED) {
    // 赤の場合の処理
}

✅ 値を明示的に指定することも可能:

cコピーする編集するenum Direction {
    UP = 1,
    DOWN = 3,
    LEFT = 5,
    RIGHT = 7
};

→ それぞれ指定した整数が使われます。


✅ 利点:

利点説明
意味のある名前を付けられる0,1,2 より RED,GREEN,BLUE の方が可読性◎
定数のグループ化ができる一つのまとまりとして扱える
自動で番号が振られる手動で #define する必要なし

✅ まとめ:

cコピーする編集するenum 名前 {
    ラベル = 数値, // 任意(省略すれば自動で0から順)
    ...
};
  • 定数の集まりに「意味ある名前」を与える
  • switch 文などで大活躍!

必要なら typedef と組み合わせて省略形にすることもできますが、まずはこの基本だけで十分です👍
(by ChatGPT)

今回の用法:main.h

enum eEnemyMoveDirection {
	ENEMY_DIR_NON = -1,	//方向設定がない場合
	ENEMY_DIR_RIGHT,	//0 :右方向
	ENEMY_DIR_LEFT,		//1 :左方向
	ENEMY_DIR_DOWN,		//2 :下方向

	ENEMY_DIR_MAX,		//
};

ENEMY_DIR_MAX は、列挙型の「最大値の目印」や「個数カウント用」のラベルです。


✅ ENEMY_DIR_MAX の意味

ここでの ENEMY_DIR_MAX は:

「この列挙型の最後の要素番号」
または「有効な方向の数」として使う目的の定数です。


✅ よくある使い道:

🔹 例1:配列のサイズに使う

cコピーする編集するint moveCount[ENEMY_DIR_MAX]; // → 要素数3の配列

🔹 例2:ループで全方向を走査する

cコピーする編集するfor (int dir = ENEMY_DIR_RIGHT; dir < ENEMY_DIR_MAX; dir++) {
    // 各方向ごとの処理
}

✅ なぜ必要?

  • 列挙型は可読性は高いけど、「何個あるか」は自動ではわからない
  • ENEMY_DIR_MAX を使えばループや配列に使いやすくなる
  • 値そのものには意味がなくても、「境界」や「上限」として意味がある

✅ 結論

ENEMY_DIR_MAX は、enumの「終わり」や「個数」を示す定数
ループ・配列・範囲チェックなどに便利な「目印ラベル」です。

C/C++界隈では **「お約束の書き方」**としてよく使われます👍
(by ChatGPT)

変数宣言:Enemy.cpp

eEnemyMoveDirection enemyMoveDirection;

初期化:Enemy.cpp

bool EnemyInit(void) {
	//敵の画像の読込み
	int err = LoadDivGraph("image/enemy_1.png",ENEMY_PATTERNS,ENEMY_PATTERNS,1,ENEMY_WID,ENEMY_HIG,enemyImage);
	if (err == -1)return false;

	//-----------------
	// 変数の初期化
	//-----------------

	// 敵キャラの変数テーブルの初期化
	for (int yy = 0; yy < ENEMY_DISP_YNUM; yy++) {
		for (int xx = 0; xx < ENEMY_DISP_XNUM; xx++) {
			enemyPosxArray[yy][xx] = (ENEMY_WID + ENEMY_DISP_X_DISTANCE) * xx;
			enemyPosyArray[yy][xx] = (ENEMY_HIG + ENEMY_DISP_Y_DISTANCE) * yy;
			enemyFlgArray[yy][xx] = true;
		}
	}

	enemyMoveDirection = ENEMY_DIR_RIGHT;

	return true;
}

Enemy.cpp に void EnemyMove(void) 関数を作成。
1)移動方向が右に設定されている場合に敵キャラの現在位置に敵の横方向の移動量を加算。
2)移動方向が左に設定されている場合に敵キャラの現在位置から横方向の移動量を減算。
3)移動方向が下に設定されている場合に敵キャラの現在位置に敵の縦方向の移動量を加算。

上記を分岐させる。※今回はswitchで分岐作成:Enemy.cpp

void EnemyMove(void) {
	for (int yy = 0; yy < ENEMY_DISP_YNUM; yy++) {
		for (int xx = 0; xx < ENEMY_DISP_XNUM;xx++) {
			if (enemyFlgArray[yy][xx]) {
				switch (enemyMoveDirection) {
				case ENEMY_DIR_RIGHT:
					enemyPosxArray[yy][xx] += ENEMY_MOVE_X_SPEED;
					break;
				case ENEMY_DIR_LEFT:
					enemyPosxArray[yy][xx] -= ENEMY_MOVE_X_SPEED;
					break;
				case ENEMY_DIR_DOWN:
					enemyPosyArray[yy][xx] += ENEMY_MOVE_Y_SPEED;
					break;

				}
			}
		}
	}

}

enum定義:main.h

//接触している端の方向
enum eEnemyHitEdgeDir {
	ENEMY_HIT_EDGE_NON = -1, //どの端にも接触していない
	ENEMY_HIT_EDGE_RIGHT,	 //0:右方向
	ENEMY_HIT_EDGE_LEFT,	 //1:左方向
	ENEMY_HIT_EDGE_DOWN,	 //2:下端に接触

	ENEMY_HIT_EDGE_MAX,
};

配列データは、Y方向の一番上の段のX方向の左から右、次の段のX方向の左から右という順番で格納
されている。その中からX方向の右端のY方向の上から下へとチェックして(つまり逆ループ)、その列に表示(やられずに残っている)されている敵キャラで右端に接触しているものがいなければ、まだ右端に到達していない事になる。
最初にチェックした列に残っている敵キャラがいなければ、一つ左の列を調べていく。

接触のチェック:Enemy.cpp

//エリアの端にぶつかっていないかのチェック処理
eEnemyHitEdgeDir CheckHitEdgeProc(void) {
	
	int restnum;//その列に生き残っている敵の数を格納する変数(ローカル)
	eEnemyHitEdgeDir hitdir = ENEMY_HIT_EDGE_NON;
		switch(enemyMoveDirection) {
		//右方向に移動中なので、右端に接触していないかをチェックする
		case ENEMY_DIR_RIGHT:
			for (int xx = ENEMY_DISP_XNUM-1; xx >= 0;xx--){
				restnum = 0;//敵が残っていなかった場合、チェックする列を移行するたびにチェックを初期化する
				for (int yy = 0;yy < ENEMY_DISP_YNUM;yy++) {
					if (enemyFlgArray[yy][xx] == true) {
						restnum++;
						if (enemyPosxArray[yy][xx] >= (WINDOW_WID - ENEMY_WID)) {
							hitdir = ENEMY_HIT_EDGE_RIGHT;
							break;
						}
					}
				}
				if(hitdir != ENEMY_HIT_EDGE_NON || restnum > 0)break;	//端に接触しているか、端に接触していなくても残っている敵がいる場合にbreak
			}
			break;

		//左方向に移動中なので、左端に接触していないかをチェックする
		case ENEMY_DIR_LEFT:
			for (int xx = 0; xx < ENEMY_DISP_XNUM ;xx++){
				restnum = 0;//敵が残っていなかった場合、チェックする列を移行するたびにチェックを初期化する
				for (int yy = 0;yy < ENEMY_DISP_YNUM;yy++){
					if (enemyFlgArray[yy][xx] == true) {
						restnum++;
							if (enemyPosxArray[yy][xx] <= 0){
								hitdir = ENEMY_HIT_EDGE_LEFT;
								break;
							}
					}
				}
				if (hitdir != ENEMY_HIT_EDGE_NON || restnum > 0)break;	//端に接触しているか、端に接触していなくても残っている敵がいる場合にbreak

			}
			break;

		//下方向に移動中なので、下端に接触していないかをチェックする
		case ENEMY_DIR_DOWN:
			for (int yy = ENEMY_DISP_YNUM-1; yy > 0; yy--){
				restnum = 0;
				for (int xx = 0;xx < ENEMY_DISP_XNUM;xx++){
						if (enemyFlgArray[yy][xx] == true){
							restnum++;
								if (enemyPosyArray[yy][xx] >= (WINDOW_HIG + ENEMY_HIG)){
									hitdir = ENEMY_HIT_EDGE_DOWN;
									break;
								}
						}
				}
				if (hitdir != ENEMY_HIT_EDGE_NON || restnum > 0)break;	//端に接触しているか、端に接触していなくても残っている敵がいる場合にbreak
			}
			break;
		}


	return hitdir;
}

関数の作成まで完了。組み込みは次回。

コメント

タイトルとURLをコピーしました