1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

glutとC言語でシューティングゲーム9日目

Last updated at Posted at 2019-03-22

#これまでのあらすじ

  • 敵が動く
  • 敵が画面外に出ると消える

8日目はここです。

#9日目
弾幕シューティング開発9日目です。前回の最後に、次は敵の登録処理を追加する、と書きましたが、それはまた次に後回しです笑。そして代わりに今回は何をするかというと、敵に弾を撃たせます!これがないと弾幕シューティングといえませんからね。

ただ、今回は変更点と追加がたくさんあります。おそらく前回からのすべての変更点はここに書ききることはできない(どう頑張っても書き忘れがあると思われる)ので、詳しくはgitのほうのソースで比べてみてください。

それでは頑張っていきましょう。

#今日の目標

  • ファイルの整理
  • 敵の弾幕と弾の管理
  • がりがり実装

#ファイルの整理
まず初めに、前回までに作成した敵関連のソースファイルを一度整理します。今enemyディレクトリの中にあるファイルは以下のようになっています。

src/
 +- enemy/
     +- enemyAppearance.h
     +- enemyType1.c
     +- enemyType2.c ここまで敵の見た目関係
     +- enemyMove.h
     +- enemyMove.c ここまで敵の動作関係
     +- enemyManager.h
     +- enemyManager.c 敵全体の管理

今回は、敵の弾幕の管理と弾の管理の機能を追加するため、このままではenemyディレクトリの中がさらにわかりづらくなります。

そこで、以下のように変更します。

変更後
src/
 +- enemy/
     +- types/
     |   +- enemyAppearance.h
     |   +- enemyType1.c
     |   +- enemyType2.c
     |
     +- moves/
     |   +- enemyMove.h
     |   +- enemyMove.c
     |
     +- enemyManager.h
     +- enemyManager.c

そして、ヘッダの読み込み先、つまりそのオブジェクトでどこに何を公開するか、といったことを考えてenemyManagerから構造体や敵管理用スタックといった情報を別の場所に定義することにします。このようにすることで、敵そのものの情報が外部から参照できないようになりました。一応オブジェクト意識ということで。(自機管理の部分やシーン管理の部分も直せと言われそうですが、勘弁してください)

さらに変更後
 +- enemy/
     +- types/
     |   +- enemyAppearance.h
     |   +- enemyType1.c
     |   +- enemyType2.c
     |
     +- moves/
     |   +- enemyMove.h
     |   +- enemyMove.c
     |
     +- enemyManager.h
     +- enemyManager.c
     +- enemyInfo.h
     +- enemyInfo.c

enemyInfoでは敵や敵が持つ弾幕や弾の構造体やスタックの定義、スタック操作処理を持ちます。ファイルの名前がわかりづらいのは本当にごめんなさい。名前付けほんと難しい。

enemyInfo.h
#ifndef ___HEADER_ENEMYINFO

#define ___HEADER_ENEMYINFO


enum eEnemyType {
  enemyType1,
  enemyType2,
  enemyType3,
  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;
  int m_movePattern;
} enemy_t;

typedef struct enemyNode_ {
  enemy_t m_enemyData;
  struct enemyNode_ *m_next; 
} enemyNode_t;


int enemyNodeAppend(enemyNode_t **epp, const enemy_t* const enemy);
void enemyNodeFree(enemyNode_t **epp);


#endif
enemyInfo.c
#include "enemyInfo.h"

#include <stdlib.h>

void enemyCopy(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;
  e2->m_movePattern = e1->m_movePattern;
}

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;

  enemyCopy(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 enemyNodeFree(enemyNode_t **epp){
  enemyNode_t *temp;
  while (*epp != NULL) {
    temp = (*epp)->m_next;
    free(*epp);
    *epp = temp;
  }
}

これでenemyManagerから敵の管理に直接関係ないものは取り除けました。

ここから敵に弾幕を撃たせるための準備をしていきます。

#敵の弾幕と弾の管理
##弾幕と弾について
ここでこのプログラムの弾幕と弾の定義をします。

弾とは、座標や角度、速さなどを持っていて弾1つ1つが別々の動きをするものとします。弾は弾幕からのみ登録されて、登録された弾はその弾が持つ情報だけで移動を計算され、描画されることにします。

弾幕とは、弾の集合であり、ある規則を持って弾を登録する機能のこととします。弾幕は1体の敵からのみ登録されて、登録された弾幕はスタックに保持し、まとめて弾幕ごとに計算します。弾幕はそれぞれで弾のスタックを保持し、弾幕を計算することで、弾幕が持つ弾のスタックに登録されたすべての弾を計算します。弾幕は、その弾幕のパターンを表す値や登録した敵のID、弾のスタックを持ちます。

##敵の弾幕の管理と弾の管理方法
これで、敵に弾幕を撃たせるために実装しなければいけないものが決まりました。次に、それらをどのようにして管理するか決めます。

今回もスタックで管理するため、それぞれの管理方法は敵の管理方法と似たようなものになります。弾幕の管理部分では、弾幕のスタックを保持し、登録されている弾幕分だけ更新処理を行い、計算対象の弾幕のみ描画する、といった流れです。弾幕ごとの更新処理で、弾幕の種類ごとに弾を登録し、登録された弾の移動を計算します。弾幕ごとの描画処理では、弾幕がもつ計算対象のすべての弾を描画します。

ここで問題になるのは、敵の管理部分と敵の弾幕管理部分を別々にすることで、敵が弾幕を登録する処理が簡単に実装できなくなることです。具体的には、敵の管理部分で敵がしかるべきタイミングで弾幕を登録するのですが、そのタイミングで別の場所にある弾幕管理用スタックに弾幕を登録する必要がありますが、それぞれの管理用スタックはできるだけ別の場所に見せたくありません。

そこで、それぞれの管理スタックのポインタを持つ部分を新たに作ることにしました。敵の管理部分から敵の弾幕管理部分のスタックを操作するには、それぞれの管理スタックのポインタを持つ部分が提供する操作を実行することで実現します。このようにすることで、お互いの管理部分が提供する処理を見せることなく、お互いが管理するスタックを必要最低限だけ操作できるようにします。

それではあとはがりがり実装していきます。

#弾幕管理部分の実装
##弾幕と弾の構造体の定義
まずは必要なデータ構造とそれらの操作処理を作ります。

enemyInfo.h
//以下追加
typedef struct enemyBullet_ {
  float m_x;
  float m_y;
  float m_angle;
  float m_speed;
  int m_count;
  int m_flag;
} enemyBullet_t;

typedef struct enemyBulletNode_ {
  enemyBullet_t m_bulletData;
  struct enemyBulletNode_ *m_next;
} enemyBulletNode_t;

typedef struct enemyShot_ {
  int m_pattern;
  int m_enemyId;
  int m_count;
  int m_status;
  enemyBulletNode_t *m_bulletList;
} enemyShot_t;

typedef struct enemyShotNode_ {
  enemyShot_t m_shotData;
  struct enemyShotNode_ *m_next;
} enemyShotNode_t;

//以下追加
int enemyShotNodeAppend(enemyShotNode_t **epp, const enemyShot_t* const shot);
void enemyShotNodeFree(enemyShotNode_t **epp);
int enemyBulletNodeAppend(enemyBulletNode_t **epp, const enemyBullet_t* const bullet);
void enemyBulletNodeFree(enemyBulletNode_t **epp);
enemyInfo.c
//以下追加
void enemyShotCopy(const enemyShot_t const *s1, enemyShot_t *s2){
  s2->m_pattern = s1->m_pattern;
  s2->m_enemyId = s1->m_enemyId;
  s2->m_count = s1->m_count;
  s2->m_status = s1->m_status;
  s2->m_bulletList = NULL;
}

enemyShotNode_t *enemyShotNodeNew(const enemyShot_t const *shot, enemyShotNode_t *next){
  enemyShotNode_t *ep;
  ep = (enemyShotNode_t *)malloc(sizeof(enemyShotNode_t));
  if (ep == NULL)
    return NULL;

  enemyShotCopy(shot, &(ep->m_shotData));
  ep->m_next = next;
  
  return ep;
}

int enemyShotNodeAppend(enemyShotNode_t **epp, const enemyShot_t* const shot){
  enemyShotNode_t *ep;
  ep = enemyShotNodeNew(shot, NULL);
  if (ep == NULL) return 1;

  while (*epp != NULL) {
    epp = &((*epp)->m_next);
  }

  *epp = ep;
  return 0;
}

void enemyBulletCopy(const enemyBullet_t* const b1, enemyBullet_t *b2){
  b2->m_x = b1->m_x;
  b2->m_y = b1->m_y;
  b2->m_angle = b1->m_angle;
  b2->m_speed = b1->m_speed;
  b2->m_count = b1->m_count;
  b2->m_flag = b1->m_flag;
}

enemyBulletNode_t *enemyBulletNodeNew(const enemyBullet_t* const bullet, enemyBulletNode_t *next){
  enemyBulletNode_t *ep;
  ep = (enemyBulletNode_t *)malloc(sizeof(enemyBulletNode_t));
  if (ep == NULL)
    return NULL;

  enemyBulletCopy(bullet, &(ep->m_bulletData));
  ep->m_next = next;
  
  return ep;
}

int enemyBulletNodeAppend(enemyBulletNode_t **epp, const enemyBullet_t* const bullet){
  enemyBulletNode_t *ep;
  ep = enemyBulletNodeNew(bullet, NULL);
  if (ep == NULL) return 1;

  while (*epp != NULL) {
    epp = &((*epp)->m_next);
  }

  *epp = ep;
  return 0;
}

void enemyNodeFree(enemyNode_t **epp){
  enemyNode_t *temp;
  while (*epp != NULL) {
    temp = (*epp)->m_next;
    free(*epp);
    *epp = temp;
  }
}

void enemyBulletNodeFree(enemyBulletNode_t **epp){
  enemyBulletNode_t *temp;
  while (*epp != NULL) {
    temp = (*epp)->m_next;
    free(*epp);
    *epp = temp;
  }
}

void enemyShotNodeFree(enemyShotNode_t **epp){
  enemyShotNode_t *temp;
  while (*epp != NULL) {
    enemyBulletNodeFree(&((*epp)->m_shotData.m_bulletList));
    temp = (*epp)->m_next;
    free(*epp);
    *epp = temp;
  }
}

弾幕はshot、弾はbulletと表記しています。弾幕の構造体が弾のスタックを持つようにしています。操作関数は、敵管理スタックとほぼほぼ同じ内容です。

##共有スタック管理機能の実装
敵を管理する部分から弾幕を登録できないことにはどうしようもないので、複数の管理機構で情報を共有する仕組みを作ります。機能は最低限しか見せない、を意識して実装していきます。

追加するファイル
src/
 +- enemy/
     +- share/
         +- enemyShare.h
         +- enemyShare.c
enemyShare.h
#ifndef ___HEADER_ENEMYSHARE

#define ___HEADER_ENEMYSHARE


#include "../enemyInfo.h"

void enemyShareSetEnemyStack(enemyNode_t **mainEnemyList);
void enemyShareSetEnemyShotStack(enemyShotNode_t **mainEnemyShotList);

void enemyShareGetEnemyPosition(float p[2], int id);
int enemyShareGetEnemyFlag(int id);
void enemyShareEnterEnemyShot(const enemy_t* const enemy);

#endif
enemyShare.c
#include "enemyShare.h"

#include "../enemyInfo.h"
#include <stdio.h>

static enemyNode_t **copyEnemyList;
static enemyShotNode_t **copyEnemyShotList;

void enemyShareSetEnemyStack(enemyNode_t **mainEnemyList){
  copyEnemyList = mainEnemyList;
}

void enemyShareSetEnemyShotStack(enemyShotNode_t **mainEnemyShotList){
  copyEnemyShotList = mainEnemyShotList;
}

void enemyShareGetEnemyPosition(float p[2], int id){
  enemyNode_t **epp = copyEnemyList;
  while (*epp != NULL) {
    if ((*epp)->m_enemyData.m_id == id) {
      p[0] = (*epp)->m_enemyData.m_x;
      p[1] = (*epp)->m_enemyData.m_y;
      return;
    }
    epp = &((*epp)->m_next);
  }
  p[0] = -1.0f;
  return;
}

int enemyShareGetEnemyFlag(int id){
  enemyNode_t **epp = copyEnemyList;
  while (*epp != NULL) {
    if ((*epp)->m_enemyData.m_id == id) {
      return (*epp)->m_enemyData.m_flag;
    }
    epp = &((*epp)->m_next);
  }
  return 0;
}

void enemyShareEnterEnemyShot(const enemy_t* const enemy){
  enemyShot_t s = {
    enemy->m_shotPattern,
    enemy->m_id,
    0,
    1};
  enemyShotNodeAppend(copyEnemyShotList, &s);
}

敵のスタックと敵の弾幕スタックのコピーを保持し、外部にはその用途ごとの処理のみ提供しています。今回は、敵の管理部分の外から敵のフラグや敵の座標を取得する機能を実装しています。また、敵の弾幕管理部分以外の場所から弾幕を登録する処理も追加しています。それに伴って、敵には固有の値であるidというメンバ変数を追加しました。ついでに、敵の弾幕開始時間を表すメンバ変数も追加しています。

enemyInfo.h
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;
  int m_movePattern;
  int m_shotPattern;
  int m_id;            //ここと
  int m_shotStartCount;//ここ
} enemy_t;

そして、敵管理部分の変更は1か所です。

enemyManager.c
void enemyUpdate(enemy_t *enemy){
  enemyMove(enemy);
  enemy->m_count++;
  //ここ追加
  if (enemy->m_count == enemy->m_shotStartCount)
    enemyShareEnterEnemyShot(enemy);
  //ここまで
  if (isInside(enemy))
    enemy->m_flag = 0;
}

これであとは弾幕管理部分を作るだけです。といってもまだまだあるのでもう少しついてきてください笑。

##弾幕管理部分の実装
まだまだやることは残っているので、がりがり実装していきます。まずは、弾幕全体を管理する部分です。

追加するファイル
src/
 +- enemy/
     +- shot/
         +- enemyShotManager.h
         +- enemyShotManager.c
enemyShotManager.h
#ifndef ___HEADER_ENEMYSHOTMANAGER

#define ___HEADER_ENEMYSHOTMANAGER


void enemyShotManagerInit();
void enemyShotManagerUpdate();
void enemyShotManagerDraw();
void enemyShotManagerClean();


#endif

これらの関数は敵管理部分と同じく、ゲームシーン管理部分から呼ばれます。なので、構成もそっくりだと思います。初期化、更新、描画、後片付け、の4つですね。

enemyShotManager.c
#include "enemyShotManager.h"

#include "../enemyInfo.h"
#include "../share/enemyShare.h"
#include <stdio.h>
#include <stdlib.h>
#include <math.h>

static enemyShotNode_t *enemyShotList;

void updateShot(enemyShot_t *shot);
void drawShot(enemyShot_t *shot);

void enemyShotManagerInit(){
  enemyShareSetEnemyShotStack(&enemyShotList);
}

void enemyShotManagerUpdate(){
  enemyShotNode_t **epp = &enemyShotList;
  while (*epp != NULL) {
    if ((*epp)->m_shotData.m_status > 0) {
      updateShot(&((*epp)->m_shotData));
    } else {
      if ((*epp)->m_shotData.m_bulletList != NULL) 
        enemyBulletNodeFree(&((*epp)->m_shotData.m_bulletList));

      if ((*epp)->m_next != NULL) {
        enemyShotNode_t *temp = (*epp)->m_next;
        free(*epp);
        *epp = temp;
      } else {
        free(*epp);
        enemyShotList = NULL;
        break;
      }
    }
    epp = &((*epp)->m_next);
  }
}

void enemyShotManagerDraw(){
  enemyShotNode_t **epp = &enemyShotList;
  while (*epp != NULL) {
    if ((*epp)->m_shotData.m_status > 0) {
      drawShot(&((*epp)->m_shotData));
    }
    epp = &((*epp)->m_next);
  }
}

void enemyShotManagerClean(){
  enemyShotNodeFree(&enemyShotList);
}

void updateShot(enemyShot_t *shot){
}

void drawShot(enemyShot_t *shot){
}

はい、中身も敵の管理部分とほぼ同じです。初期化処理では弾幕管理スタックを共有情報管理部分に渡します。後片付けでは、弾幕管理スタックの中身をすべてfreeします。更新処理では、弾幕の状態に応じて計算するかスタックから消すかの処理に分岐します。

ここで弾幕の状態について簡単に説明します。弾幕展開中に敵がやられた場合、弾幕はこれ以上弾を描画しないような状態にならなければいけません。しかし、画面上にまだその弾幕の弾がある場合、それらの弾まで消すのは不自然です。なので、弾幕自体に状態を持たせています。とりあえず、1以上の値で弾幕を計算するようにしています。また、弾幕を消すときは、弾幕が持つ弾をすべて消してから弾幕自体をスタックから消します。そうしないと、アクセスできないデータが残ることになって困ります。

弾幕の描画についても、弾幕の状態に応じて描画します。

これで弾幕を管理する部分ができたので、あとは実際に弾幕を更新する部分や弾幕を描画する部分。実際の弾幕そのものをこれから作っていきます。

その前に、ゲームシーンや後片付け処理登録を変更しておきます。

game.c
//変更
void close(){
  sceneGameClean();
}
sceneGame.h
//追加
void sceneGameClean();
sceneGame.c
void sceneGameInit(unsigned char *p){
  for (int i = 0; i < SCENE_PARAMETER_MAX; i++)
    param[i] = p[i];
  playerInit(&player);
  enemyManagerInit();
  enemyShotManagerInit();//追加
  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();
  enemyShotManagerUpdate();//追加
  glutPostRedisplay();
}

void sceneGameDispaly(){
  glClear(GL_COLOR_BUFFER_BIT);
  
  drawBoard();
  playerDraw(&player);
  enemyManagerDraw();
  enemyShotManagerDraw();//追加
//以下略
}

//追加
void sceneGameClean(){
  enemyShotManagerClean();
  enemyManagerClean();
}

シーン全体で1つの後片付け用関数を作成しました。

##弾幕更新処理の実装
それでは登録された弾幕を更新する処理を実装します。

enemyShotManager.c
//追加
#include "../../scene/game/gameField.h"

//追加
void updateShot(enemyShot_t *shot){
  int eflag = enemyShareGetEnemyFlag(shot->m_enemyId);
  
  if (eflag != 1)
    shot->m_status = 2;

  enemyBulletNode_t **epp = &(shot->m_bulletList);
  while (*epp != NULL) {
    if ((*epp)->m_bulletData.m_flag) {
      (*epp)->m_bulletData.m_x +=
        cos((*epp)->m_bulletData.m_angle) * (*epp)->m_bulletData.m_speed;
      (*epp)->m_bulletData.m_y +=
        sin((*epp)->m_bulletData.m_angle) * (*epp)->m_bulletData.m_speed;
      (*epp)->m_bulletData.m_count++;
    } else {
      if ((*epp)->m_next != NULL) {
        enemyBulletNode_t *temp = (*epp)->m_next;
        free(*epp);
        *epp = temp;
      } else {
        free(*epp);
        *epp = NULL;
        break;
      }
    }
    //outside judge
    if ((*epp)->m_bulletData.m_x < FIELD_START_X - 20.0f ||
        (*epp)->m_bulletData.m_y < FIELD_START_Y - 20.0f ||
        (*epp)->m_bulletData.m_x > FIELD_START_X + FIELD_SIZE_X + 20.0f ||
        (*epp)->m_bulletData.m_y > FIELD_START_Y + FIELD_SIZE_Y + 20.0f)
      (*epp)->m_bulletData.m_flag = 0;

    epp = &((*epp)->m_next);
  }

  if (*epp == NULL && eflag != 1) {
    shot->m_status = 0;
    return;
  }

  shot->m_count++;
}

弾幕の計算では、まずその弾幕を使った敵の状態を取得し、敵がすでにいなかったらこれ以上弾を登録しないが弾幕の計算は続ける、といった状態にします。その後で、実際に弾幕が保持するすべての弾を調べて、その弾が計算対象なら角度と速さから座標を計算します。そして、計算対象でない弾はスタックから消します。弾を移動させた後は、画面内にあるかどうかを判定し、画面外にあるなら弾を計算対象から外します。最後に、弾を持っていなくてさらに弾幕が続行しない状態であるなら弾幕そのものを計算対象から外します。

ここで重要なのは、弾幕が持つ弾の動作はここで実現するわけではない、という設計になっている点です。弾1つ1つの動きは弾幕のパターンとして他で定義しておいて、この計算処理より前に弾幕の弾制御を実行します。このようにすることで、弾幕そのもののパターンを追加しやすくなります。

ちなみに、画面外にあるかどうかの判定は適当です。おそらくこの先で変更しますが、今はこんなもんで。見えなくなったタイミングでメモリが解放されればそれでいいんです。

##弾幕描画処理
###弾の描画処理の実装
弾幕を描画するには、弾を描画する機能をまず作る必要があります。といっても、何回も言っているように、使う図形は矩形か円のみです。なので、このゲームの弾は全て同じ大きさの矩形とします。大玉みたいな円や、レーザーは出てきません。蝶みたいな弾も出ません。それでは実装します。

追加するファイル
src/
 +- enemy/
     +- shot/
         +- enemyBullet.h
         +- enemyBullet.c
enemyBullet.h
#ifndef ___HEADER_ENEMYBULLET

#define ___HEADER_ENEMYBULLET


#include "../enemyInfo.h"

void drawBullet(enemyBullet_t *bullet);


#endif
enemyBullet.c
#include "enemyBullet.h"

#include "../../shape/rect.h"
#include "../../scene/game/gameField.h"
#include "../../define.h"
#include <GL/glut.h>

static const float ENEMY_BULLET_WIDTH = 8.0f;
static const float ENEMY_BULLET_HEIGHT = 10.0f;
static const unsigned char ENEMY_BULLET_COLOR[3] = {240, 240, 150};

void drawBullet(enemyBullet_t *bullet){
  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(
      bullet->m_x,
      bullet->m_y,
      ENEMY_BULLET_WIDTH,
      ENEMY_BULLET_HEIGHT,
      bullet->m_angle,
      ENEMY_BULLET_COLOR[0],
      ENEMY_BULLET_COLOR[1],
      ENEMY_BULLET_COLOR[2]);

  glViewport(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
  rectEnd();
}

敵の描画処理とほぼ同じですね。

###弾幕描画処理の実装
それでは、弾幕の描画処理を追加します。

enemyShotManager.c
//追加
#include "enemyBullet.h"

//追加
void drawShot(enemyShot_t *shot){
  enemyBulletNode_t **epp = &(shot->m_bulletList);
  while (*epp != NULL) {
    if ((*epp)->m_bulletData.m_flag) {
      drawBullet(&((*epp)->m_bulletData));
    }
    epp = &((*epp)->m_next);
  }
}

弾幕更新処理と違ってシンプルです。計算対象の弾をすべて描画しているだけです。

これで弾幕管理部分のすべてが実装できました!

#弾幕の実装
後は弾幕を追加するだけです。先にも言ったように、弾幕はそれぞれの弾の制御処理といえます。具体的には、どのタイミングで弾の角度を変える、どのタイミングで複数の弾を特定の角度で特定のスピードで登録する、みたいな処理といえます。

まずは弾幕を計算する仕組みを作って、最後に1つ弾幕を追加してみます。

追加するファイル
src/
 +- enemy/
     +- shot/
         +- danmaku.h
         +- danmaku.c

安直な名前ですね笑。

danmaku.h
#ifndef ___HEADER_DANMAKU

#define ___HEADER_DANMAKU


#include "../enemyInfo.h"
void danmaku(enemyShot_t *shot);


#endif
danmaku.c
#include "danmaku.h"

#include "../share/enemyShare.h"
#include <math.h>
/*
  float m_x;
  float m_y;
  float m_angle;
  float m_speed;
  int m_count;
  int m_flag;
  */

enum edanmakuPattern {
  pattern0,
  pattern1,
  patternMax
};

void danmakuPattern0(enemyShot_t *shot);
void danmakuPattern1(enemyShot_t *shot);

const static void (*danmakuFunc[patternMax])(enemyShot_t *) = {
  danmakuPattern0,
  danmakuPattern1,
};

void danmaku(enemyShot_t *shot){
  danmakuFunc[shot->m_pattern](shot);
}

void danmakuPattern0(enemyShot_t *shot){
}

void danmakuPattern1(enemyShot_t *shot){
}

弾幕は関数ポインタを使って管理します。まあ、今までにも同じようなことをしてきましたからね。

後は弾幕管理部分で、今追加したdanmaku関数を呼び出します。

enemyShotManager.c
void enemyShotManagerUpdate(){
  enemyShotNode_t **epp = &enemyShotList;
  while (*epp != NULL) {
    if ((*epp)->m_shotData.m_status > 0) {
      danmaku(&((*epp)->m_shotData));//追加
      updateShot(&((*epp)->m_shotData));
    } else {
//略
    }
    epp = &((*epp)->m_next);
  }
}

これであとは弾幕の種類を増やしていけばいいだけです。

##弾幕のパターン追加
とりあえず2パターン作ってみました。

danmaku.c

void danmakuPattern0(enemyShot_t *shot){
  if (shot->m_count == 0) {
    float p[2];
    enemyShareGetEnemyPosition(p, shot->m_enemyId);
    if (p[0] < 0.0f) return;
    enemyBullet_t b = {
      p[0],
      p[1],
      M_PI / 2.0f,
      3.0f,
      0,
      1};
    enemyBulletNode_t **epp = &(shot->m_bulletList);
    enemyBulletNodeAppend(epp, &b);
  }
}

まずはこれです。この弾幕?はスタートと同時に真下方向に1つ弾を飛ばすだけです。ただの弾ですね笑。

danmaku.c
void danmakuPattern1(enemyShot_t *shot){
  if (shot->m_count <= 200 && shot->m_count % 5 == 0) {
    float p[2];
    enemyShareGetEnemyPosition(p, shot->m_enemyId);
    if (p[0] < 0.0f) return;
    enemyBullet_t b = {
      p[0],
      p[1],
      M_PI / 2.0f,
      5.0f,
      0,
      1};
    enemyBulletNode_t **epp = &(shot->m_bulletList);
    enemyBulletNodeAppend(epp, &b);
  }
}

次は弾幕です笑。これは弾幕開始から200フレーム経つまで、5フレーム間隔で真下に弾を撃ち続けます。

どちらも見ごたえが全くないですが、これものんびり増やしていこうかなと。

##敵の登録
最後に今までの変更から、初めに敵に登録する情報も増えているので、敵を登録していきます。

enemyManager.c
void enemyManagerInit(){
  enemyShareSetEnemyStack(&enemyList);
  enemy_t enemy1 = {enemyType1, 1, 100.0f, -100.0f, 2.0f, M_PI / 2.0f, 0, 0, 1, 0, 100};
  enemy_t enemy2 = {enemyType2, 1, 200.0f, 100.0f, 0.0f, 0.0f, 0, 0, 0, 1, 200};
//略
}

今回は、小さい敵を画面外から降らせ、敵が行動開始後100フレーム経ってから弾幕パターン1で弾幕を撃ちます。大きい敵は初めから画面内で行動開始後200フレーム経ってから弾幕パターン0で弾を撃ちます。

#実行
それでは今までの変更をすべてMakefileに書いてビルド、実行します。
day9.JPG

できました!

#9日目まとめ

ファイル
src/
 +- enemy/
     +- types/
     |   +- enemyAppearance.h
     |   +- enemyType1.c
     |   +- enemyType2.c
     |
     +- moves/
     |   +- enemyMove.h
     |   +- enemyMove.c
     |
     +- share/
     |   +- enemyShare.h
     |   +- enemyShare.c
     |
     +- shot/
     |   +- enemyShotManager.h
     |   +- enemyShotManager.c
     |   +- enemyBullet.h
     |   +- enemyBullet.c
     |   +- danmaku.h
     |   +- danmaku.c
     |
     +- enemyManager.h
     +- enemyManager.c
     +- enemyInfo.h
     +- enemyInfo.c
  • 敵が弾幕を撃てるようになった

今回はいままでで一番変更点が多かった気がします。とにかく書き起こすのがたいへんでした笑。でも、これでようやく弾幕シューティングを作ってきたといえるようになりました。やったー。

あとは、投稿間隔があいてごめんなさい。研究忙しくてこっちに避ける時間があんましないです。

でもなんとか完成までは持っていくので、気長に待っていただけたらと思います。

それでは今回はここまで。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?