Nodachisoft Nodachi Sword Icon
  
@あまじ✎ 2021年8月1日に更新

第4章4 C言語コンソールで横スクロールシューティングゲーム(1/2)

イチからゲーム作りで覚えるC言語
第4章3 C言語コンソール上で迷路脱出プログラム・迷路の自動生成 : PREV
NEXT : 第4章5 C言語コンソールで横スクロールシューティングゲーム(2/2) :

概要

一つ前のページでは、以前に用意した、自前ゲームライブラリを使って、限られた時間の中でプレイヤーを操作して迷路から脱出する簡単なコンソールゲームを作成しました。

今回は横スクロールシューティングをコンソールゲーム上で作成すべく、ゲームの基礎的な部分を作っていきましょう!

まずは基本的な部分を先に作成して、プレイヤを動かしていきます。

ライブラリを含むこのページのソースコードはココをクリックで圧縮された zip 形式でダウンロードできます。

このページで作成するゲームの範囲

まずは下のように基本的な部分を作成し、シューティングゲームとして ”それっぽく" 動く部分を作成していきましょう。

今回作成していくプログラムには下のような機能を組み込んでいきます。

組み込んでいく機能

シンプルで最低限の横スクロールシューティングの機能をあげてみます。

  • プレイヤーはキーボードの十字キーで操作できる
  • 敵キャラクタが時間経過で出現する
  • プレイヤーはスペースキーで攻撃魔法をうてる
  • 敵キャラクタはプレイヤーの弾にあたると消える
  • プレイヤーは敵キャラクタの弾にあたると消える

プログラムの実行イメージ

このページで作成していくシューティングゲームの基礎を動かした時のイメージです。

迷路からの脱出成功パターン

プログラムの流れ

プログラムを起動してから終了するまでの流れをフローチャート図にして頭を整理しておきます。 大きな流れは前のページで作成した、迷路脱出ゲームと同じです。

ゲームのフローチャート

プログラムが main 関数からスタートして、 まず最初に、ゲームデータを準備したり、必要なメモリを確保したりします。 ここではプレイヤーの動くスピードや、敵キャラクタの配置、敵キャラクタの体力や動くスピード、 プレイヤーの打つ魔法(弾)を定義しています。

その後、ゲームの処理を行うためのループ(メインループ)に入ります。

メインループでは、下のような処理を行います。

  1. キーボード入力を確認 … プレイヤの操作を確認します。
  2. ゲームの状況を進行 … リアルタイムにゲームの状況を更新していきます。
  3. 画面バッファに書き込み … 画面に表示する内容を、事前にバッファに書き込みます。
  4. 画面の表示を更新 … バッファ内容を画面に表示し、更新します。

ソースコード

ゲームの初期化から終了まで、以下のソースコードで実現しています。

ファイル 内容
entry.c ゲーム本体のプログラムを記述
gamedata.h ゲームで使用する構造体や定数の定義
ndcgamelib.h コンソールゲーム用ライブラリ(ヘッダ)
ndcgamelib.c コンソールゲーム用ライブラリ(プログラム)

ゲーム本体のプログラム(entry.c)はちょうど 200 行程度です。

"ndcgamelib.h" には、様々なゲームでよくつかうような機能をまとめていますので、 詳細は前ページ「コンソール・ゲーム用ライブラリ」をご参照ください。

 
entry.c
#define _CRT_SECURE_NO_WARNINGS

#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
#include "ndcgamelib.h"
#include "gamedata.h"

// ==================================================
// 関数の定義
// ==================================================
// 画面に描画する内容を定義
void drawScreen(MapData *map) {
    clearScreenBuffer();
    drawText(0, 0, " ゲーム終了:ESCキー  魔法の弾:スペースキー", (RgbColor) { 200, 100, 200 });
    // 敵キャラクタをバッファに描画
    for (int i = 0; i < ENEMY_LIMIT; i++) {
        if (map->enemy[i].isAlive == FALSE) continue;
        drawText(map->enemy[i].posx, map->enemy[i].posy, "<:=", (RgbColor) { 150, 150, 200 });
    }
    // プレイヤ弾をバッファに描画
    for (int i = 0; i < PLAYER_FIRE_LIMIT ; i++) {
        if (map->pfire[i].isAlive == FALSE) continue;
        drawText(map->pfire[i].posx, map->pfire[i].posy, "-", (RgbColor) { 250, 250, 250 });
    }
    drawText(map->player.posx, map->player.posy, "nk", (RgbColor) { 255, 255, 255 });
}

// プレイヤの弾と敵キャラの衝突判定
void checkCollisionPlayerBullet(MapData* map, GameObject* bullet) {
    // enemy;
    for (int i = 0; i < ENEMY_LIMIT; i++) {
        GameObject* enemy = &map->enemy[i];
        if (enemy->isAlive == TRUE) {
            double bx = bullet->posx;
            double by = round( bullet->posy); // 縦方向は整数ぴったりでないとイライラする
            double br = bullet->weapon.collision_param1;
            double ex = enemy->posx;
            double ey = round(enemy->posy); // 縦方向は整数ぴったりでないとイライラする
            double er = ENE_SQUID.collisionRadius;

            // 弾の円と敵キャラの円の衝突判定(円が触れ合っている)
            if (isCollisionCircle(bx,by,br, ex,ey,er)) {
                bullet->isAlive = FALSE;    // 弾は消失
                enemy->hitpoint -= bullet->weapon.damage;   // ダメージを与える
                if (enemy->hitpoint <= 0) {
                    enemy->isAlive = FALSE; // 敵を撃破
                }
                return;
            }
        }
    }
}

// プレイヤの弾の動きを定義
void actionPlayerBullet(MapData* map, long deltaTime ) {
    MagicType weapon = map->player.weapon;
    for (int i = 0; i < PLAYER_FIRE_LIMIT ; i++) {
        GameObject* bullet = &map->pfire[i];
        if (bullet->isAlive == TRUE) {
            bullet->posx += (double)deltaTime * weapon.speed / 1000.0;
            checkCollisionPlayerBullet(map, bullet);
            if (bullet->posx > map->screensize_x - 1) { // 画面外なので削除する
                bullet->isAlive = FALSE;
            }
        }
    }
}

// 敵キャラクタの動きを定義
void actionEnemy(MapData* map, long deltaTime) {
    for (int i = 0; i < ENEMY_LIMIT; i++) {
        GameObject* enemy = &map->enemy[i];
        if (enemy->isAlive == TRUE) {
            double speed = ENE_SQUID.speed; //  eData[enemy->eneType].speed;
            enemy->posx -= (double)deltaTime * speed / 1000.0;            
            if (enemy->posx < 0) { // 画面外なので削除する
                enemy->isAlive = FALSE;
            }
        }
    }
}

// プレイヤーの弾を追加
void addPfire(MapData* map) {
    MagicType weapon = map->player.weapon;
    clock_t now = clock();
    if (now - map->player.weaponUsedTime < weapon.interval) {
        // 弾を装填する次の時間までは、何もしない
        return;
    }
    map->player.weaponUsedTime = now;
    GameObject pfire;
    pfire.posx = map->player.posx+2;
    pfire.posy = map->player.posy;
    pfire.isAlive = TRUE;
    pfire.weapon = weapon;      // 弾の種類を記録

    // 弾を出現
    for (int i = 0; i < PLAYER_FIRE_LIMIT; i++) {
        GameObject temp = map->pfire[i];
        if (temp.isAlive == FALSE) {
            map->pfire[i] = pfire;
            return;
        }
    }
}

// 敵キャラを追加する
void addEnemy(MapData* map, const int posy) {
    GameObject enemy;
    enemy.posx = map->screensize_x;
    enemy.posy = posy;
    enemy.isAlive = TRUE;
    enemy.hitpoint = ENE_SQUID.hitpoint;
    for (int i = 0; i < ENEMY_LIMIT ; i++ ) {
        GameObject tempEnemy = map->enemy[i];
        if (tempEnemy.isAlive == FALSE) {
            map->enemy[i] = enemy;
            return;
        }
    }
}

void readInput(MapData* map, long deltaTime) {
    double charSpeed = deltaTime * 8.0 / 1000.0;
    if (isKeyDown(VK_LEFT)) { // ←キーが押されている状態
        if (map->player.posx > 0) { // 座標 x は 0 以下にならない
            map->player.posx -= charSpeed;
        }
    }
    if (isKeyDown(VK_RIGHT)) { // ←キーが押されている状態
        if (map->player.posx < map->screensize_x - 2 ) {
            map->player.posx += charSpeed;
        }
    }
    if (isKeyDown(VK_UP)) { // ↑キーが押されている状態
        if (map->player.posy > 1) {
            map->player.posy -= charSpeed;
        }
    }
    if (isKeyDown(VK_DOWN)) { // ↓キーが押されている状態
        if (map->player.posy < map->screensize_y - 6) {
            map->player.posy += charSpeed;
        }
    }
    if (isKeyDown(VK_SPACE)) { // スペースキーが押されている状態
        // プレイヤ弾の生成
        addPfire(map);
    }
}

int main( void ) {
    hideCursor();
    setConsoleWindowTitle("ぷらんく猫シューティング");

    // プレイヤの初期化
    GameObject player;
    player.posx = 5;
    player.posy = 10;
    player.weapon = BULLET_PLAYER;
    player.weaponUsedTime = 0L;

    // ゲームデータの初期化
    MapData map;
    map.player = player;
    map.screensize_x = 60;
    map.screensize_y = 21;

    // 敵キャラデータの初期化と設置
    for (int i = 0; i < ENEMY_LIMIT ; i++) {
        GameObject enemy;
        enemy.isAlive = FALSE;
        map.enemy[i] = enemy;
    }
    addEnemy(&map, 2);
    addEnemy(&map, 6);
    addEnemy(&map, 10);

    // プレイヤ弾初期化
    for (int i = 0; i < PLAYER_FIRE_LIMIT; i++) {
        GameObject pfire;
        pfire.isAlive = FALSE;
        map.pfire[i] = pfire;
    }

    long deltaTime;
	initNdcGameLib(map.screensize_x, map.screensize_y);
    // ゲームのループ処理
    while ( !isKeyPushed(VK_ESCAPE) ) {
        deltaTime = getDeltaTime();
        drawScreen(&map);
        readInput(&map, deltaTime);
        actionEnemy(&map, deltaTime);
        actionPlayerBullet(&map, deltaTime);
        Sleep(1);
        flushScreen();
    }
    freeNdcGameLib();
    fflush(stdout);
}

それでは、ゲームを実行する順番と合わせて内容を確認していきます。

ゲームの起動・初期化パート

main 関数からプログラムが動き出したら、 画面の初期設定や、必要なゲームデータの作成を行っています。

画面の初期設定としては、以下を実施しています。

  1. カーソルの点滅は必要ないので、表示されないように設定します。
  2. ウィンドウのタイトルバーを変更します。ゲームの名前に設定しておきます。
  3. プレイヤーの初期位置、攻撃に使用する弾(BULLET_PLAYER)を定義します。
  4. ゲームデータ全体を格納する MapData の初期化を行います。
  5. 3つの敵キャラを配置します。
  6. プレイヤの弾(攻撃用の魔法)を初期化します。
  7. 自作ライブラリの初期化を行います。コンソールのウィンドウサイズがここで変更されます。

以下は初期化を行っているソースコードの部分です。

 
main.c
int main( void ) {
    hideCursor();
    setConsoleWindowTitle("ぷらんく猫シューティング");

    // プレイヤの初期化
    GameObject player;
    player.posx = 5;
    player.posy = 10;
    player.weapon = BULLET_PLAYER;
    player.weaponUsedTime = 0L;

    // ゲームデータの初期化
    MapData map;
    map.player = player;
    map.screensize_x = 60;
    map.screensize_y = 21;

    // 敵キャラデータの初期化と設置
    for (int i = 0; i < ENEMY_LIMIT ; i++) {
        GameObject enemy;
        enemy.isAlive = FALSE;
        map.enemy[i] = enemy;
    }
    addEnemy(&map, 2);
    addEnemy(&map, 6);
    addEnemy(&map, 10);

    // プレイヤ弾初期化
    for (int i = 0; i < PLAYER_FIRE_LIMIT; i++) {
        GameObject pfire;
        pfire.isAlive = FALSE;
        map.pfire[i] = pfire;
    }

    long deltaTime;
	initNdcGameLib(map.screensize_x, map.screensize_y);

一つ一つ確認していきましょう! また、ゲームの実行に必要なデータを設定しています。

ゲームステージに登場するプレイヤーやアイテム、敵キャラクタなどは GameObject 構造体に収めるようにします。

GameObject 構造体は別途 gamedata.h の中で下のように定義しています。

gamedata.h
// ゲーム内で扱うオブジェクトの構造体を定義 
typedef struct {
    BOOL isAlive;  // 存在している(生存している)なら TRUE
    double posx;   // オブジェクトの位置(x成分)
    double posy;   // オブジェクトの位置(y成分)
    int eneType;   // 敵キャラクタの場合の種類
    int hitpoint;  // キャラクタの体力
    MagicType weapon; // 武器
    long weaponUsedTime; // 前回武器を使用したタイミング
} GameObject;

今回はプレイヤーや敵キャラクタ、プレイヤーの魔法(攻撃の弾)を GameObject 構造体を使って定義します。

最初にプレイヤーのデータを定義しています。

entry.c
    // プレイヤの初期化
    GameObject player;
    player.posx = 5;
    player.posy = 10;
    player.weapon = BULLET_PLAYER;
    player.weaponUsedTime = 0L;

最初に画面上でプレイヤーがいる位置について、 x 座標を posx、y 座標を posy で設定しておきます。

また、プレイヤーが攻撃するときに出る弾(魔法)を weapon に指定します。 この BULLET_PLAYER は別途 gamedata.h の中で下のように定義されています。

gamedata.h
// 攻撃魔法(弾)の定義
typedef struct {
    double speed;    // 弾のスピード(秒あたりに動くマス速度)
    long interval; // 次の弾を発射するまでの間隔(ミリ秒)
    int way;         // 弾の動き 0: 直進、1: 真上に進む、2:重量にしたがって落ちる
    int damage;      // ダメージ
    double collision_param1;    // 衝突判定用のパラメータ係数 1
    double collision_param2;    // 衝突判定用のパラメータ係数 2
    char disp[4];   // 表示するぐらいふぃっく
} MagicType;

static const MagicType BULLET_PLAYER = { // プレイヤーの魔法(武器の弾)設定
     50.0  // 弾のスピード(秒あたりに動くマス速度)
    ,250   // 次の弾を発射するまでの間隔(秒)
    ,1     // ダメージ
    ,1     // 衝突判定  0:NONE, 1:小さな円 
    ,0.5   // 衝突判定用のパラメータ係数 1(円の半径・x成分増幅)
    ,0.5   // 衝突判定用のパラメータ係数 2(円の半径・y成分増幅)
    ,"-"   // 弾の表現
};

BULLET_PLAYER で、プレイヤが打つ攻撃のための魔法(弾)が 動くスピードや、敵に当たった時のダメージ量、当たり判定の大きさ、 コンソール上で表示される弾の文字列("-")を定義しています。

続いて、ゲーム全体のデータを格納する先を MapData 構造体で管理します。 MapData 構造体の変数 map の中に、 先ほど作成したプレイヤーのデータや、敵キャラクタ、プレイヤの弾の状態、 ゲームに使用する画面サイズを定義しています。

entry.c
// ゲームデータの初期化
MapData map;
map.player = player;
map.screensize_x = 60;
map.screensize_y = 21;

// 敵キャラデータの初期化と設置
for (int i = 0; i < ENEMY_LIMIT ; i++) {
    GameObject enemy;
    enemy.isAlive = FALSE;
    map.enemy[i] = enemy;
}
addEnemy(&map, 2);
addEnemy(&map, 6);
addEnemy(&map, 10);

// プレイヤ弾初期化
for (int i = 0; i < PLAYER_FIRE_LIMIT; i++) {
    GameObject pfire;
    pfire.isAlive = FALSE;
    map.pfire[i] = pfire;
}

long deltaTime;
initNdcGameLib(map.screensize_x, map.screensize_y);

MapData 構造体は gamedata.h の中で定義されており、図にすると下のような構造になっています。

クラス図

構造体の変数(データの塊)を一つの四角で表しています。 MapData 構造体は内部に プレイヤーや敵キャラクタの構造体を持っており、その内容を 図の右側で記載しています。

このような図は一般にクラス図と呼ばれ、オブジェクト指向プログラミングと呼ばれる プログラミングの方法で使用されます。

今回のプログラムではオブジェクト指向プログラミングは使っていませんが、 どんなデータを使うのかを整理するために非常に有用なので、参考として掲載しています。

具体的に、MapData 構造体ソースコードでは下のように定義されています。

MapData 構造体の中に GameObject 構造体を持ち、プレイヤや敵キャラなどの ゲーム進行に必要なデータ一式を管理しています。

gamedata.h
// マップデータ
typedef struct {
    int screensize_x;   // 画面サイズ(X成分)
    int screensize_y;   // 画面サイズ(Y成分)
    long gametime;      // ゲームが開始されてからの時間(ミリ秒)
    GameObject player;
    GameObject enemy[ENEMY_LIMIT];    // 敵キャラクタデータ
    GameObject pfire[PLAYER_FIRE_LIMIT];  // プレイヤのミサイル
} MapData;

初期化の最後に、ライブラリの初期化を行うため、 initNdcGameLib を呼び出します。

 
entry.c
initNdcGameLib(map.screensize_x, map.screensize_y);

これを呼び出すことで、ゲームライブラリを使用できるようになり、 ウィンドウのサイズが変更されます。

ライブラリについては詳細は前ページ「コンソール・ゲーム用ライブラリ」をご参照ください。

ゲームのメインループ

続いて、ゲームのメインループの内容を確認していきます。

entry.c
// ゲームのループ処理
while ( !isKeyPushed(VK_ESCAPE) ) {   // ESC キーが押されるまでループ    deltaTime = getDeltaTime(); // ループ1周にかかった時間を取得
    drawScreen(&map);           // 画面のバッファへ書き込み
    readInput(&map, deltaTime); // キーボード入力&プレイヤ動作
    actionEnemy(&map, deltaTime);   // 敵の動作
    actionPlayerBullet(&map, deltaTime);    // プレイヤの弾の動作
    Sleep(1);       // CPUを占有しない
    flushScreen();  // 画面の表示を更新
}

メインループは while 文で繰り返し実行されます。

while 文を繰り返す条件として !isKeyPushed(VK_ESCAPE) が指定されています。

  • !isKeyPushed(VK_ESCAPE) は ESC キーが押されていない時、真(TRUE)です。

前のページの迷路脱出プログラムと同じで、ESC キーを押したら 即座にメインループ処理から抜ける、という意味になります。

続いて、メインループ処理でどんなことをしているか確認していきます。

ゲーム時間の進行具合を計算

引き続きメインループの中を確認していきます。 while 文のループの中でまず、ゲームライブラリ内で定義している getDeltaTime() 関数を使い、

entry.c
// ゲームのループ処理
while ( !isKeyPushed(VK_ESCAPE) ) {   // ESC キーが押されるまでループ
    deltaTime = getDeltaTime(); // ループ1周にかかった時間を取得    drawScreen(&map);           // 画面のバッファへ書き込み
    readInput(&map, deltaTime); // キーボード入力&プレイヤ動作
    actionEnemy(&map, deltaTime);   // 敵の動作
    actionPlayerBullet(&map, deltaTime);    // プレイヤの弾の動作
    Sleep(1);       // CPUを占有しない
    flushScreen();  // 画面の表示を更新
}

最初にミリ秒単位で、変数 deltaTime に取得することで、 ループ1周にかかった時間を取得し、 プレイヤーや、敵キャラ、プレイヤーの放った魔法(攻撃の弾)を 時間に応じて計算して動かします。

 
entry.c
deltaTime = getDeltaTime();  // ループ1周にかかった時間を取得

ゲームの内容を画面バッファに描画

続いてdrawScreen 関数で現在のゲームの状況を画面バッファに描画します

entry.c
// ゲームのループ処理
while ( !isKeyPushed(VK_ESCAPE) ) {   // ESC キーが押されるまでループ
    deltaTime = getDeltaTime(); // ループ1周にかかった時間を取得
    drawScreen(&map);           // 画面のバッファへ書き込み    readInput(&map, deltaTime); // キーボード入力&プレイヤ動作
    actionEnemy(&map, deltaTime);   // 敵の動作
    actionPlayerBullet(&map, deltaTime);    // プレイヤの弾の動作
    Sleep(1);       // CPUを占有しない
    flushScreen();  // 画面の表示を更新
}

drawScreen 関数の中身は下のようになっています。

entry.c
// 画面に描画する内容を定義
void drawScreen(MapData *map) {
    clearScreenBuffer();
    drawText(0, 0, " ゲーム終了:ESCキー  魔法の弾:スペースキー", (RgbColor) { 200, 100, 200 });
    // 敵キャラクタをバッファに描画
    for (int i = 0; i < ENEMY_LIMIT; i++) {
        if (map->enemy[i].isAlive == FALSE) continue;
        drawText(map->enemy[i].posx, map->enemy[i].posy, "<:=", (RgbColor) { 150, 150, 200 });
    }
    // プレイヤ弾をバッファに描画
    for (int i = 0; i < PLAYER_FIRE_LIMIT ; i++) {
        if (map->pfire[i].isAlive == FALSE) continue;
        drawText(map->pfire[i].posx, map->pfire[i].posy, "-", (RgbColor) { 250, 250, 250 });
    }
    drawText(map->player.posx, map->player.posy, "nk", (RgbColor) { 255, 255, 255 });
}

clearScreenBuffer() 関数でいままで書き込んでいた画面バッファの内容を消去します。 この関数はライブラリ(ndcgamelib.h、ndcgamelib.c)で定義されています

以下をバッファに描画しています。

  • 画面上部にキーボード操作を描画
  • 敵キャラクタを描画。(敵キャラクタは「<:=」で表現)
  • プレイヤの攻撃魔法(弾)を描画。(弾は「-」で表現)
  • プレイヤを描画(プレイヤは「nk」で表現)

プレイヤーの入力をチェックする

ゲームプレイヤーを操作するために、readInput 関数の中で キーボードからの入力を判定しています。

entry.c
// ゲームのループ処理
while ( !isKeyPushed(VK_ESCAPE) ) {   // ESC キーが押されるまでループ
    deltaTime = getDeltaTime(); // ループ1周にかかった時間を取得
    drawScreen(&map);           // 画面のバッファへ書き込み
    readInput(&map, deltaTime); // キーボード入力&プレイヤ動作    actionEnemy(&map, deltaTime);   // 敵の動作
    actionPlayerBullet(&map, deltaTime);    // プレイヤの弾の動作
    Sleep(1);       // CPUを占有しない
    flushScreen();  // 画面の表示を更新
}

readInput 関数は同じソースコード中で下の用に定義しています。

 
entry.c
void readInput(MapData* map, long deltaTime) {
    double charSpeed = deltaTime * 8.0 / 1000.0;
    if (isKeyDown(VK_LEFT)) { // ←キーが押されている状態
        if (map->player.posx > 0) { // 座標 x は 0 以下にならない
            map->player.posx -= charSpeed;
        }
    }
    if (isKeyDown(VK_RIGHT)) { // ←キーが押されている状態
        if (map->player.posx < map->screensize_x - 2 ) {
            map->player.posx += charSpeed;
        }
    }
    if (isKeyDown(VK_UP)) { // ↑キーが押されている状態
        if (map->player.posy > 1) {
            map->player.posy -= charSpeed;
        }
    }
    if (isKeyDown(VK_DOWN)) { // ↓キーが押されている状態
        if (map->player.posy < map->screensize_y - 6) {
            map->player.posy += charSpeed;
        }
    }
    if (isKeyDown(VK_SPACE)) { // スペースキーが押されている状態
        // プレイヤ弾の生成
        addPfire(map);
    }
}

readInput 関数の中で実施していることは、前回の迷路作成プログラムで作成したものとほぼ同じです。

キーボードの上下左右を押していれば、プレイヤはそのぶん移動することができます。

スペースキーが押されたときは addPfire(map) 関数を呼び出して、 プレイヤの攻撃魔法(弾)を追加する処理を行います。

プレイヤの攻撃魔法(弾)の追加

プレイヤがスペースキーを押したときに呼び出される addPfire(map) 関数の 弾を追加する処理を確認します。

 
entry.c
// プレイヤーの弾を追加
void addPfire(MapData* map) {
    MagicType weapon = map->player.weapon;
    clock_t now = clock();
    if (now - map->player.weaponUsedTime < weapon.interval) {
        // 弾を装填する次の時間までは、何もしない
        return;
    }
    map->player.weaponUsedTime = now;
    GameObject pfire;
    pfire.posx = map->player.posx+2;
    pfire.posy = map->player.posy;
    pfire.isAlive = TRUE;
    pfire.weapon = weapon;      // 弾の種類を記録

    // 弾を出現
    for (int i = 0; i < PLAYER_FIRE_LIMIT; i++) {
        GameObject temp = map->pfire[i];
        if (temp.isAlive == FALSE) {
            map->pfire[i] = pfire;
            return;
        }
    }
}

88 行~91行目では、前回プレイヤーが addPfire で攻撃魔法を打ったときからの 時間(map->player.weaponUsedTime)をミリ秒単位で比較して、一定の時間(weapon.interval)以上経過していないなら、 何もせずに関数から戻ります。

もし一定時間以上経過していたらプログラムを続けます。

その後、新しく、GameObject 構造体の変数 pfire を定義し、プレイヤの弾を作成します。 作成した変数 pfire は map->pfire 配列から未使用の添え字を見つけて保存します。

map->pfire 配列は使用中か、未使用かは isAlive をチェックして判別しており、 isAlive が FALSE なら、その配列の要素は未使用状態とみて、新しく定義した GameObject pfire を代入します。

敵キャラクタ

敵キャラクタの動きは actionEnemy 関数の中で定義しています。

 
entry.c
// 敵キャラクタの動きを定義
void actionEnemy(MapData* map, long deltaTime) {
    for (int i = 0; i < ENEMY_LIMIT; i++) {
        GameObject* enemy = &map->enemy[i];
        if (enemy->isAlive == TRUE) {
            double speed = ENE_SQUID.speed;
            enemy->posx -= (double)deltaTime * speed / 1000.0;            
            if (enemy->posx < 0) { // 画面外なので削除する
                enemy->isAlive = FALSE;
            }
        }
    }
}

map 変数の中には、敵キャラクタを格納している enemy 変数があります。 enemy 変数はGameObject 構造体の配列です。

配列のうち、敵キャラデータが有効(isAlive が TRUE )のもののみを対象に、 画面左方向に向かっての移動を行います。

また、敵キャラクタの位置が画面左端を超えて左側に移動した場合には キャラクタデータを削除(isAlive を FALSE )します。

プレイヤの弾の動きを定義

関数 actionPlayerBullet の中でプレイヤの弾の動きを定義しています。

entry.c
// ゲームのループ処理
while ( !isKeyPushed(VK_ESCAPE) ) {   // ESC キーが押されるまでループ
    deltaTime = getDeltaTime(); // ループ1周にかかった時間を取得
    drawScreen(&map);           // 画面のバッファへ書き込み
    readInput(&map, deltaTime); // キーボード入力&プレイヤ動作
    actionEnemy(&map, deltaTime);   // 敵の動作
    actionPlayerBullet(&map, deltaTime);    // プレイヤの弾の動作    Sleep(1);       // CPUを占有しない
    flushScreen();  // 画面の表示を更新
}

actionPlayerBullet 関数の中は下のように定義されています。

entry.c
// プレイヤの弾の動きを定義
void actionPlayerBullet(MapData* map, long deltaTime ) {
    MagicType weapon = map->player.weapon;
    for (int i = 0; i < PLAYER_FIRE_LIMIT ; i++) {
        GameObject* bullet = &map->pfire[i];
        if (bullet->isAlive == TRUE) {
            bullet->posx += (double)deltaTime * weapon.speed / 1000.0;
            checkCollisionPlayerBullet(map, bullet);
            if (bullet->posx > map->screensize_x - 1) { // 画面外なので削除する
                bullet->isAlive = FALSE;
            }
        }
    }
}

プレイヤの弾のデータが格納されている配列 map->pfire から 有効(isAlive が TURE)のデータを一つ一つ処理しています。

checkCollisionPlayerBullet 関数では、 プレイヤの弾と敵キャラクタが衝突しているかを判定する処理を呼び出しています。

また、プレイヤの弾も画面右端まで到着したら、未使用(isAlive を FALSE)にして無効なデータに変更しています。

プレイヤの弾と敵キャラクタの衝突判定

プレイヤの弾と敵キャラクタの衝突判定は以下のように関数内で処理しています。

entry.c
// プレイヤの弾と敵キャラの衝突判定
void checkCollisionPlayerBullet(MapData* map, GameObject* bullet) {

    for (int i = 0; i < ENEMY_LIMIT; i++) {
        GameObject* enemy = &map->enemy[i];
        if (enemy->isAlive == TRUE) {
            double bx = bullet->posx;
            double by = round( bullet->posy); // 縦方向は整数ぴったりでないとイライラする
            double br = bullet->weapon.collision_param1;
            double ex = enemy->posx;
            double ey = round(enemy->posy); // 縦方向は整数ぴったりでないとイライラする
            double er = ENE_SQUID.collisionRadius;

            // 弾の円と敵キャラの円の衝突判定(円が触れ合っている)
            if (isCollisionCircle(bx,by,br, ex,ey,er)) {
                bullet->isAlive = FALSE;    // 弾は消失
                enemy->hitpoint -= bullet->weapon.damage;   // ダメージを与える
                if (enemy->hitpoint <= 0) {
                    enemy->isAlive = FALSE; // 敵を撃破
                }
                return;
            }
        }
    }
}

有効な敵キャラクタデータの位置(bx, by)と当たり判定のある半径(br)を計算し、 プレイヤの打った攻撃の位置(ex, ey)と弾の当たり判定の半径(er)を取得し、

ライブラリの円同士の衝突判定関数である isCollisionCircle を呼び出して 衝突しているかを判定しています。

敵キャラクタとプレイヤの打った弾が衝突している場合、 弾は消滅し、弾のダメージのぶん、敵キャラクタの体力を減らします。 もし、敵キャラクタの体力が 0 以下なら、敵キャラクタを撃破できたものとして、 該当する敵キャラクタのデータを無効化します。

画面への出力・表示の更新

実際にディプレイ(コンソール画面上)へバッファの内容を表示するための処理は メインループ処理の中の flushScreen 関数で行っています。

 
main.c
// ゲームのループ処理
while ( !isKeyPushed(VK_ESCAPE) ) {   // ESC キーが押されるまでループ
    deltaTime = getDeltaTime(); // ループ1周にかかった時間を取得
    drawScreen(&map);           // 画面のバッファへ書き込み
    readInput(&map, deltaTime); // キーボード入力&プレイヤ動作
    actionEnemy(&map, deltaTime);   // 敵の動作
    actionPlayerBullet(&map, deltaTime);    // プレイヤの弾の動作
    Sleep(1);       // CPUを占有しない
    flushScreen();  // 画面の表示を更新}

ライブラリ(ndcgamelib)の中で管理されているバッファを画面に出力します。

補足

このページではシューティングゲームの基本的なロジック部分を作成していきました。 次回はもう少しゲーム性を拡張していき、最低限のゲームとして動くものを作成します。

参考文献

イチからゲーム作りで覚えるC言語
第4章3 C言語コンソール上で迷路脱出プログラム・迷路の自動生成 : PREV
NEXT : 第4章5 C言語コンソールで横スクロールシューティングゲーム(2/2) :
 
 
送信しました!

コメント、ありがとうございます。

なんかエラーでした

ごめんなさい。エラーでうまく送信できませんでした。ご迷惑をおかけします。しばらくおいてから再度送信を試していただくか、以下から DM などでご連絡頂ければと思います。

Twitter:@NodachiSoft_jp
お名前:
 
連絡先:
 
メッセージ:
 
戻る
内容の確認!

以下の内容でコメントを送信します。よろしければ、「送信」を押してください。修正する場合は「戻る」を押してください

お名前:
 
連絡先:
 
メッセージ:
 
Roboto からの操作ではないという確認のため確認キーを入れてください。
確認キー=95
戻る
 / 
送信確認へ
コメント欄
コメント送信確認へ

関連ありそうな記事(5件)です!

第1章01 Visual Studio Community 2019 のインストール手順

#C11仕様#C言語#ゲームプログラミング✎ 2021-08-08
C言語でプログラミングをするために、無料で使える Visual Studio Community を使った開発環境を揃えていく手順や注意点をお話しています。
目次
第4章4 C言語コンソールで横スクロールシューティングゲーム(1/2)
第4章4 C言語コンソールで横スクロールシューティングゲーム(1/2)
概要
概要
このページで作成するゲームの範囲
このページで作成するゲームの範囲
組み込んでいく機能
組み込んでいく機能
プログラムの実行イメージ
プログラムの実行イメージ
プログラムの流れ
プログラムの流れ
ソースコード
ソースコード
ゲームの起動・初期化パート
ゲームの起動・初期化パート
ゲームのメインループ
ゲームのメインループ
ゲーム時間の進行具合を計算
ゲーム時間の進行具合を計算
ゲームの内容を画面バッファに描画
ゲームの内容を画面バッファに描画
プレイヤーの入力をチェックする
プレイヤーの入力をチェックする
プレイヤの攻撃魔法(弾)の追加
プレイヤの攻撃魔法(弾)の追加
敵キャラクタ
敵キャラクタ
プレイヤの弾の動きを定義
プレイヤの弾の動きを定義
プレイヤの弾と敵キャラクタの衝突判定
プレイヤの弾と敵キャラクタの衝突判定
画面への出力・表示の更新
画面への出力・表示の更新
補足
補足
参考文献
参考文献
Nodachisoft © 2021