#これまでのあらすじ
- ゲームシーンの見た目がよくなった
- 自機がフィールドから出ることがなくなった
6日目はここです。
#7日目
弾幕シューティング開発7日目です。今回は敵の導入と描画までを実装したいと思います。
敵がゲーム中にするべき行動は意外と多く、制御も大変です。そのため、先に設計をある程度考えておかないと、後で修正しなければいけない箇所がたくさん出てきて大変なことになります。個人的な経験ですが、プログラムが複雑になってくると設計が破綻し途端に開発のモチベーションがなくなるなんてことが多々ありました笑。この弾幕シューティング開発は絶対に最後までやりきりたいので、しっかり設計からやっていきます。
ただ、今まであきらめてきたことが多いことから設計に慣れていなくて、かなり時間がかかっています。そのため、この先も記事の更新が遅れるか内容が少ない記事になるかもしれませんが、どうか見守っていただけたらと思います。
それでは今回も頑張っていきます。
#今日の目標
- 敵の管理部分の実装
- 敵の描画
#敵の管理部分の実装
##敵の管理方法
敵の管理はenemyManager.hで定義される初期化処理、更新処理、描画処理をそれぞれゲームシーンから呼ぶことで実現させることにします。enemyManagerで敵のインスタンスを保持し、更新処理ですべての敵を更新し、描画処理ですべての敵の描画を処理します。
敵はenemy_tという構造体で表し、enemyManagerはenemy_tを連結リストで保持することにします。enemy_t型の配列を用意して配列で管理するより、連結リストで管理するほうが楽です。あとは、リストに入っている計算すべき敵をenemyManagerの更新処理で更新し、描画処理で実際に描画するだけです。
何がしたいかというと、enemyManagerは一回作ってしまえば、あとはリストに登録された敵を勝手に更新し描画してくれるようにしたいです。シーンの管理部分を作ったときと似たような設計にしたつもりです。
それでは実装します。
##敵の管理部分の実装
まずは追加するファイルです。
src/
+- ...
+- enemy/
+- enemyManager.h
+- enemyManager.c 敵を管理する処理
enemyManager.hには、敵の情報を表すenemy_t構造体と初期化関数、更新関数、描画関数を定義します。
#ifndef ___HEADER_ENEMYMANAGER
#define ___HEADER_ENEMYMANAGER
enum eEnemyType {
enemyType1,
enemyTypeMax
}
typedef struct enemy_ {
enum eEnemyType m_type; //敵のタイプ
int m_flag; //計算対象か
float m_x;
float m_y; //座標
float m_speed; //敵の速さ
float m_angle; //敵の向き
int m_count; //カウンタ
} enemy_t;
void enemyManagerInit();
void enemyManagerUpdate();
void enemyManagerDraw();
#endif
敵の種類ごとに描画関数を作成しようと思うので、敵の種類を表すeEnemyTypeを定義します。敵の種類が増えるたびに、ここに追記していきます。
次に上で定義した関数の実態と、敵情報の連結リストを作っていきます。
#include "enemyManager.h"
#include <GL/glut.h>
#include <math.h>
#include <stdlib.h>
typedef struct enemyNode_ {
enemy_t m_enemyData;
struct enemyNode_ *m_next;
} enemyNode_t;
const static void (*enemyDraw[enemyTypeMax])(const enemy_t *) = {
};
static enemyNode_t *enemyList;
void enemyInit(const enemy_t const *e1, enemy_t *e2);
enemyNode_t *enemyNodeNew(const enemy_t const *enemyData, enemyNode_t *next);
int enemyNodeAppend(enemyNode_t **epp, const enemy_t const *enemy);
void enemyUpdate(enemy_t *enemy);
void enemyManagerInit(){
}
void enemyManagerUpdate(){
enemyNode_t **epp = &enemyList;
while (*epp != NULL) {
if ((*epp)->m_enemyData.m_flag) {
enemyUpdate(&((*epp)->m_enemyData));
}
epp = &((*epp)->m_next);
}
}
void enemyManagerDraw(){
enemyNode_t **epp = &enemyList;
while (*epp != NULL) {
if ((*epp)->m_enemyData.m_flag) {
enemyDraw[(*epp)->m_enemyData.m_type](&((*epp)->m_enemyData));
}
epp = &((*epp)->m_next);
}
}
void enemyInit(const enemy_t const *e1, enemy_t *e2){
e2->m_type = e1->m_type;
e2->m_flag = e1->m_flag;
e2->m_x = e1->m_x;
e2->m_y = e1->m_y;
e2->m_speed = e1->m_speed;
e2->m_angle = e1->m_angle;
e2->m_count = e1->m_count;
}
enemyNode_t *enemyNodeNew(const enemy_t const *enemyData, enemyNode_t *next){
enemyNode_t *ep;
ep = (enemyNode_t *)malloc(sizeof(enemyNode_t));
if (ep == NULL)
return NULL;
enemyInit(enemyData, &(ep->m_enemyData));
ep->m_next = next;
return ep;;
}
int enemyNodeAppend(enemyNode_t **epp, const enemy_t const *enemy){
enemyNode_t *ep;
ep = enemyNodeNew(enemy, NULL);
if (ep == NULL) return 1;
while (*epp != NULL) {
epp = &((*epp)->m_next);
}
*epp = ep;
return 0;
}
void enemyUpdate(enemy_t *enemy){
enemy->m_count++;
enemy->m_x += cos(enemy->m_angle) * enemy->m_speed;
enemy->m_y += sin(enemy->m_angle) * enemy->m_speed;
}
連結リストの説明は省略します。連結リストのノードはenemyNode_tという構造体で表します。敵のデータと次のノードへのポインタのみ持ちます。そして、リストの先頭はenemyListという変数とします。リストを操作する関数はenemyNodeNewとenemyNodeAppendの2つです。enemyNodeNew関数は、新たにノードを作成します。引数のenemyDataで表される敵情報が新たに作成されるノードに格納され、nextで次のノードへのポインタを指定します。enemyNodeAppend関数は、リストの末尾に新たなノードを追加する関数です。引数のeppでリストの先頭を指すポインタを指定し、enemyで追加する敵の情報を指定します。enemyInit関数は敵の情報をコピーする関数です。enemyUpdate関数で、実際に敵の更新を行います。
これらの機能を使って、実際に敵を管理する機能を作ります。enemyManageInit関数では、敵をリストに登録します。enemyManageUpdate関数では、リストに登録された敵で計算対象ならばその敵を更新します。enemyManageDraw関数では、リストに登録された敵で計算対象ならばその敵のタイプの描画関数を呼んで実際に描画します。
敵の移動については、三角関数と速さを用いて計算します。この部分は、敵の移動パターンを複数用意して選択できるように、あとで変更します。今回はとりあえずこうしておきます。
#敵の描画
##敵の種類と描画処理の追加
敵の描画部分は、敵のタイプごとに実装します。今回は2種類の敵を作ってみます。
//追記
enum eEnemyType {
enemyType1,
enemyType2,
enemyTypeMax
}
//追記
#include "enemyAppearance.h"
//追記
const static void (*enemyDraw[enemyTypeMax])(const enemy_t *) = {
enemyType1Draw,
enemyType2Draw
};
#ifndef ___HEADER_ENEMYAPPEARANCE
#define ___HEADER_ENEMYAPPEARANCE
#include "enemyManager.h"
void enemyType1Draw(const enemy_t *enemy);
void enemyType2Draw(const enemy_t *enemy);
#endif
敵の種類(見た目)を増やすには、以上の個所にどんどん追記していきます。
それでは実際に敵を描画する処理を実装します。
##敵を描画する際の注意点
敵の描画範囲は自機移動可能範囲と同じです。そのため、敵を描画する際は描画範囲を全体ではなく自機移動可能範囲に狭める必要があります。そのようにしておかなければ、敵が周囲の枠にはみ出してしまいます。敵の描画範囲から外れた部分を描画してはいけないような描画処理をしなければいけません。
描画範囲を変更するには、glViewport関数を使います。この関数を使うことで、描画範囲を画面全体から画面の一部に変更することができます。しかし、この関数を使うだけだと描画範囲を一部の範囲に圧縮して描画されてしまいます。そのため、描画するモノを圧縮された割合だけ拡大して、圧縮後でも元の大きさで映るようにします。拡大にはglScalef関数を使います。
以上に挙げた関数を使う必要があるため、矩形描画処理と円描画処理を一部変更します。glPushMatrix関数とglPopMatrix関数を任意のタイミングで呼べるようにします。ここら辺はglutの描画処理を説明しないとわからないと思いますが、ここで説明はしません。
#ifndef ___HEADER_RECT
#define ___HEADER_RECT
void rectBegin();
void rectEnd();
void rectDraw(float x,
float y,
float width,
float height,
float angle,
unsigned char red,
unsigned char green,
unsigned char blue);
#endif
#include "rect.h"
#include <GL/glut.h>
void rectBegin(){
glPushMatrix();
}
void rectEnd(){
glPopMatrix();
}
void rectDraw(float x, float y, float width, float height, float angle, unsigned char red, unsigned char green, unsigned char blue){
glTranslatef(x, y, 0.0f);
glColor3ub(red, green, blue);
glRotatef(angle, 0.0f, 0.0f, 1.0f);
glRectf(-width / 2.0f, -height / 2.0f, width / 2.0f, height / 2.0f);
}
#ifndef ___HEADER_BALL
#define ___HEADER_BALL
void ballBegin();
void ballEnd();
void ballDraw(float x,
float y,
float r,
unsigned char red,
unsigned char green,
unsigned char blue);
#endif
#include "ball.h"
#include <GL/glut.h>
void ballBegin(){
glPushMatrix();
}
void ballEnd(){
glPopMatrix();
}
void ballDraw(float x, float y, float r, unsigned char red, unsigned char green, unsigned char blue){
glTranslatef(x, y, 0.0f);
glColor3ub(red, green, blue);
glScalef(r, r, 0.0f);
glutSolidSphere(1, 16, 16);
}
rectBegin関数とballBegin関数の中ではglPushMatrix関数を、rectEnd関数とballEnd関数の中ではglPopMatrix関数を呼んでいるだけです。
それでは修正した図形描画機能を使って敵の描画を実装します。
##種類ごとの描画処理の実装
#include "enemyAppearance.h"
#include "../shape/rect.h"
#include <GL/glut.h>
#include "../scene/game/gameField.h"
#include "../define.h"
const static float ENEMY_TYPE1_SIZE = 25.0f;
const static unsigned char ENEMY_TYPE1_COLOR[3] = {250, 250, 250};
void enemyType1Draw(const enemy_t *enemy){
rectBegin();
glViewport(
(int)FIELD_START_X,
(int)FIELD_START_Y,
(int)FIELD_SIZE_X,
(int)FIELD_SIZE_Y);
glScalef(
(float)WINDOW_WIDTH / FIELD_SIZE_X,
(float)WINDOW_HEIGHT / FIELD_SIZE_Y,
1.0f);
//敵描画
rectDraw(
enemy->m_x,
enemy->m_y,
ENEMY_TYPE1_SIZE,
ENEMY_TYPE1_SIZE,
(float)(enemy->m_count) * 2.0f,
ENEMY_TYPE1_COLOR[0],
ENEMY_TYPE1_COLOR[1],
ENEMY_TYPE1_COLOR[2]);
glViewport(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
rectEnd();
}
#include "enemyAppearance.h"
#include "../shape/rect.h"
#include <GL/glut.h>
#include "../scene/game/gameField.h"
#include "../define.h"
const static float ENEMY_TYPE2_SIZE = 45.0f;
const static unsigned char ENEMY_TYPE2_COLOR[3] = {250, 150, 250};
void enemyType2Draw(const enemy_t *enemy){
rectBegin();
glViewport(
(int)FIELD_START_X,
(int)FIELD_START_Y,
(int)FIELD_SIZE_X,
(int)FIELD_SIZE_Y);
glScalef(
(float)WINDOW_WIDTH / FIELD_SIZE_X,
(float)WINDOW_HEIGHT / FIELD_SIZE_Y,
1.0f);
//敵描画
rectDraw(
enemy->m_x,
enemy->m_y,
ENEMY_TYPE2_SIZE,
ENEMY_TYPE2_SIZE,
(float)(enemy->m_count) * 2.0f,
ENEMY_TYPE2_COLOR[0],
ENEMY_TYPE2_COLOR[1],
ENEMY_TYPE2_COLOR[2]);
glViewport(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
rectEnd();
}
パターン1の敵は白くて自機と同じくらいの大きさです。正方形が常に回転しています。パターン2の敵はピンク色で大きめです。
それでは実際にこれらの敵を登録して描画させたいと思います。
//追記
void enemyManagerInit(){
enemy_t enemy1 = {enemyType1, 1, 100.0f, -100.0f, 2.0f, M_PI / 2.0f, 0};
enemy_t enemy2 = {enemyType2, 1, 200.0f, 100.0f, 0.0f, 0.0f, 0};
enemyNodeAppend(&enemyList, &enemy1);
enemyNodeAppend(&enemyList, &enemy2);
}
敵の登録は敵管理の初期化処理で行います。リストに連結させているだけです。
パターン1の見た目の敵は、真下の方向に毎フレーム2pxの速さで移動します。パターン2の見た目の敵は初めから速さ0なのでその場にとどまり続けます。
それでは敵の管理処理をゲームシーンから呼びます。
//追記
#include "../../enemy/enemyManager.h"
//追記
void sceneGameInit(unsigned char *p){
for (int i = 0; i < SCENE_PARAMETER_MAX; i++)
param[i] = p[i];
playerInit(&player);
enemyManagerInit();//ここ
glClearColor(0.15f, 0.15f, 0.4f, 1.0f);
count = 0;
}
//追記
void sceneGameUpdate(void (*changeSceneFunc)(enum eScene, unsigned char *, int)){
//update
count++;
playerUpdate(&player);
enemyManagerUpdate();//ここ
glutPostRedisplay();
}
//追記
void sceneGameDispaly(){
glClear(GL_COLOR_BUFFER_BIT);
drawBoard();
playerDraw(&player);
enemyManagerDraw();//ここ
glutSwapBuffers();
}
それではMakefileにさくっと書くこと書いて、make、実行してみます。
動きました!白いやつは描画範囲外から下に動いてきます。右上の数値は、ただのデバッグ用です。
#7日目まとめ
追加したファイル
src/
+- ...
+- enemy/
+- enemyManager.h
+- enemyManager.c
+- enemyAppearance.h
+- enemyType1.c
+- enemyType2.c
- 敵の管理部分ができた
- 敵の種類ごとに描画できるようになった
今回でついに敵が見えるようになりました。まあ、敵といっても正方形を描画しているだけですが笑。細かい見た目と敵の種類については、のんびり作っていくことにします。
実は敵に関する処理はもっとたくさんあります。この後の予定では、敵の登録方法の追加と敵の行動パターンの追加です。特に、敵の登録方法についてはしっかり作るつもりです。敵を100体登録するために、100行ソースコードに追加処理を書くなんてやってられません笑。
今回は、連結リストを導入したことでwhileループを使って登録された敵すべての処理を実現しました。実装中に、whileループの中のリストのポインタの更新処理を間違えて消してしまい、それに気づかずに発生個所がわからない無限ループに苦しみました笑。無限ループ、怖いなぁ。
それでは今回はここまで。