0
0

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言語でシューティングゲーム8日目

Last updated at Posted at 2019-03-18

#これまでのあらすじ

  • 敵の管理部分ができた
  • 複数の敵を描画した

7日目はここです。

#8日目
弾幕シューティング開発8日目です。前回で敵の管理部分ができたので、今日はそこにさらに機能を追加していきます。

とその前に...

#謝罪
おそらくC言語を普段から使っている人や、C言語をちゃんと勉強している人なら7日目に追加した部分に重大な欠陥があることに気づくかと思います。はい、やらかしていました。mallocした後のメモリ解放処理をすっかり忘れていました。

この記事をはじめから見ていて実際にコードを真似して実行している方がいましたら、本当にごめんなさい。前回のプログラムで、敵をだしまくっていると、おそらくパソコンの動作に何らかのよくない影響が出ます。

これからは、ここに投稿する前にもっと他人が真似して実行しても問題がないようなソースを公開できるよう努力しますが、もしまた何かしらのよくないコードになっていたら、どうかそのプログラムを動かさないようにしてください。

また、もし私の書いたプログラムをコピペして使う場合は必ずどのような流れでどのような処理が実行されるのか理解したうえでコピペしてください。どうか、何も考えず脳死コピペからの実行はしないようお願いします。

free処理が抜けていたくらいで大げさな、と思った方がいましたら、今すぐその考えを改めましょう。最近は確かに自動で確保したメモリを解放してくれる言語が増えましたが、C言語やC++ではmalloc等で確保したメモリは勝手に解放されません。PCに挙動に悪影響が出てからでは遅いので、どんなに小さい量でもしっかり解放処理を書きましょう。

それでは、7日目のプログラムを改良していきます。

##7日目のプログラムの改良
ここでは、freeglut.hで定義されるglutCloseFunc関数を使用します。この関数に登録した関数は、プログラム終了時に自動で呼び出されます。なので、終了時にスタックに残っている敵をすべて消す処理を追加し、ゲームの終了時に呼び出される関数の中で呼び出します。

game.c
//以下を追記
#include <GL/freeglut.h>
#include "enemy/enemyManager.h"

//追記
void glInit(int *argc, char **argv);
void gameLoop();
void close();//これ

//追記
void glInit(int *argc, char **argv){
  //略
  glutCloseFunc(close);//これ
}

//追記
void close(){
  enemyManagerClean();
}
enemy/enemyManager.h
//追記
void enemyManagerClean();
enemy/enemyManager.c
//追記
void enemyManagerClean(){
  enemyNode_t **epp = &enemyList;
  enemyNode_t *temp;
  while (*epp != NULL) {
    temp = (*epp)->m_next;
    free(*epp);
    *epp = temp;
  }
}

以上を追記して実行すると、ゲーム終了時にスタックに追加したメモリは全て解放されます。

#あらためて8日目
それでは改めて今回も頑張って開発していきます。今回は敵の行動の制御と敵が画面外に出たらスタックから消す処理を作ります。

ゲーム内で敵がとる行動はたくさんあったほうが面白いです。しかし、行動を追加するごとに書き換える部分が多いと不便なので、行動の管理もなるべく簡単にできるように実装していきます。

また、もうでてこない敵をスタック内にいるまでも入れておいても無駄なので、出てきてから画面外に出たら消すようにします。

#今日の目標

  • 敵の行動を容易に追加する
  • 画面外の敵を消す

#敵の行動処理
前回のプログラムでは、敵の行動はenemyManagerUpdate関数の中で呼ばれるenemyUpdate関数の中で行っていました。この関数内ではすべての敵に共通した処理をするため、頻繁に書き換えたくありません。そのような意識で実装していきます。

##敵の行動処理の実装
敵の行動処理は新たなファイルに実装します。

追加するファイル
src/
 +- ...
 +- enemy/
     +- ...
     +- enemyMove.h
     +- enemyMove.c
enemyMove.h
#ifndef ___HEADER_ENEMYMOVE

#define ___HEADER_ENEMYMOVE

#include "enemyManager.h"
void enemyMove(enemy_t *enemy);


#endif
enemyMove.c
#include "enemyMove.h"

#include <math.h>

enum eMovePattern {
  movePattern0,
  movePatternMax
};

void pattern0(enemy_t *enemy);

const static void (*moveFunc[movePatternMax])(enemy_t *) = {
  pattern0
};

void enemyMove(enemy_t *enemy){
  moveFunc[enemy->m_movePattern](enemy);
  enemy->m_x += cos(enemy->m_angle) * enemy->m_speed;
  enemy->m_y += sin(enemy->m_angle) * enemy->m_speed;
}

void pattern0(enemy_t *enemy){
  const int t = enemy->m_count;
  if (t == 0) {
    enemy->m_speed = 3.0f;
    enemy->m_angle = M_PI / 2.0f;
  }
  if (t == 40)
    enemy->m_speed = 0.0f;
  if (t == (40 + 600)) {
    enemy->m_speed = 3.0f;
    enemy->m_angle = - M_PI / 2.0f;
  }
}

敵管理部分には、敵にどのような動き方があるのかを知らせる必要がない設計にしました。敵に行動パターンは関数ポインタの配列に格納されて、enemyMove関数の中で、敵が持っているパラメータの1つである行動パターンの番号に対応する行動関数を呼びます。その後で、実際に敵を動かします。

この設計にともなって、敵の管理部分を以下のように変更します。

enemyManager.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;//ここ
} enemy_t;
enemyManager.c
//追記
void enemyManagerInit(){
  enemy_t enemy1 = {enemyType1, 1, 100.0f, -100.0f, 2.0f, M_PI / 2.0f, 0, 0};//行動パターンを0に
  enemy_t enemy2 = {enemyType2, 1, 200.0f, 100.0f, 0.0f, 0.0f, 0, 0};//行動パターンを0に
  //略
}

//追記
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;
  e2->m_movePattern = e1->m_movePattern;//ここ
}

//変更
void enemyUpdate(enemy_t *enemy){
  enemyMove(enemy);
  enemy->m_count++;
}

敵に行動パターンの番号を持たせるように変更し、敵の管理部分で敵を更新する処理がより簡素なものになりました。

そして、実際に敵の行動パターンを1つ追加しました。それがpattern0関数です。この行動パターンは、敵を毎フレーム3pxで40フレームだけ下方向に移動します。その後、600フレームその場に待機し、その後上方向に毎フレーム3pxの速さで移動する、というパターンです。

後はMakefileに追記してmake、実行すると、2種類の敵が0番目の行動をするようになりました。

#画面外の敵を消す処理
これで敵の行動パターンを比較的簡単に追加し、敵に行動パターンの番号を渡すだけで勝手に行動してくれるようになりました。敵のパターンの多くは、最終的に画面の外に消えていく、というものになると思います。画面の外の敵は計算対象から外すことで計算はされなくなりますが、スタックの中に入れておいてもメリットがないので消します。というか、敵を配列ではなくスタックで管理するメリットがこれです。

敵が画面の外に出たことを判定するには、大雑把に画面外の座標のボーダーを設定してもいいですが、無駄な計算はしたくないので、敵の種類ごとにサイズを取得し、その敵のサイズで完全に画面から見えなくなっているかどうかを判定します。なので、敵の種類ごとでサイズを取得する関数を用意し、敵の更新処理の最後に敵が見える場所にいるかどうかを判定します。

それでは実装していきます。

##敵のサイズを取得する関数

enemyAppearance.h
//追記
void enemyType1Size(float size[2]);
void enemyType2Size(float size[2]);
enemyType1.c
//追記
void enemyType1Size(float size[2]){
  size[0] = size[1] = ENEMY_TYPE1_SIZE * sqrt(2.0);
}
enemyType2.c
//追記
void enemyType1Size(float size[2]){
  size[0] = size[1] = ENEMY_TYPE2_SIZE * sqrt(2.0);
}

これで各種敵のサイズがわかります。あとは、敵が画面の外かどうかの判定処理を追加します。

##敵の場所を判定する処理

enemyManager.c
//追加
const static void (*enemySize[enemyTypeMax])(float size[2]) = {
  enemyType1Size,
  enemyType2Size,
};

//追加
int isInside(enemy_t *enemy);

//追記
void enemyManagerUpdate(){
  enemyNode_t **epp = &enemyList;
  while (*epp != NULL) {
    if ((*epp)->m_enemyData.m_flag) {
      enemyUpdate(&((*epp)->m_enemyData));
    } else {
      if ((*epp)->m_next != NULL) {
        enemyNode_t *temp = (*epp)->m_next;
        free(*epp);
        *epp = temp;
      } else {
        free(*epp);
        enemyList = NULL;
        break;
      }
    }
    epp = &((*epp)->m_next);
  }
}

//デバッグ用に追記
void enemyManagerDraw(){
  enemyNode_t **epp = &enemyList;
  int num = 0;
  while (*epp != NULL) {
    if ((*epp)->m_enemyData.m_flag) {
      enemyDraw[(*epp)->m_enemyData.m_type](&((*epp)->m_enemyData));
      num++;
    }
    epp = &((*epp)->m_next);
  }
  printf("enemyNum:%d\n", num);
}

//追記
void enemyUpdate(enemy_t *enemy){
  enemyMove(enemy);
  enemy->m_count++;

  //ここ
  if (isInside(enemy))
    enemy->m_flag = 0;
}

//追加
int isInside(enemy_t *enemy){
  if (enemy->m_count < 60)
    return 0;

  float size[2];
  enemySize[enemy->m_type](size);
  if (enemy->m_x < FIELD_START_X - size[0] / 2.0f ||
      enemy->m_x > FIELD_START_X + FIELD_SIZE_X + size[0] / 2.0f ||
      enemy->m_y < FIELD_START_Y - size[1] / 2.0f ||
      enemy->m_y > FIELD_START_Y + FIELD_SIZE_Y + size[1] / 2.0f)
    return 1;

  return 0;
}

判定処理の中では、そもそも敵が画面の外から現れる場合を考慮して、60フレーム以上経過しないと判定しないようにしています。

敵の更新部分では、計算対象から外れた敵をスタックから消しています。また、スタック内の敵の数がわかるように描画関数の中で敵の数を標準出力に出力しています。

それでは再度makeして実行すると、敵が画面外から現れて待機した後画面の外に出ていき、敵が画面から見えなくなるとスタックにある敵の数が減ります。

これで敵が画面外に出たらスタックから消す処理ができました。

#8日目まとめ

追加したファイル
src/
 +- enemy/
     +- enemyMove.h
     +- enemyMove.c
  • 敵の行動パターンが管理しやすくなった
  • 敵が画面から見えなくなるとスタックから消える

今回は前回の謝罪から始まりました。いやぁ恥ずかしいミスでした。今後は人に見せてよいコードかどうか確認してから記事に書こうと思います。それでもよくない部分があったときは、各自修正してから使っていただけたらと思います。

次は、敵の登録を一度にできるようにします。まだ2体しかでてこないですからね笑。あとは、敵の種類と行動パターンをのんびり追加していこうと思います。

追加は特に記事にはしない予定です。変更点はgithubから見れるかと思うのでそちらを見てください。一応リンクはここです。

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

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?