vol.2_2 リアルタイム入力・フィールドを実装しよう!

はじめに

今回からいよいよプログラミングに着手していくのですが、ゲームを作るにあたってリアルタイム入力はなくてはならない存在です。
通例のscanf関数などでは入力終了の度にエンターキーを押さないといけないので、これでは楽しく遊べませんね。
しかも、入力が完了するまでは他のプログラムが全く動かないので、そもそもとして成立さえしません。
今回はこの状況を打開する策を提示し、実際に少しプログラムを書いていきたいと思います。

kbhit関数(キーバックヒット関数)

この関数はキーを押しているか押していないかを判断してくれる関数です。
さらっと言っていますが中々すごいことをしてくれていて、今までは入力待ちの状態ではプログラムはそこで止まってしまいます。
これを何もしなかったら何もしなかったと判断してくれるわけです。素晴らしい。
これさえあれば入力していない間に他の動作をすることができます。
今回作りたいでは、回転・横移動・高速移動の操作をプレイヤーがすることになりますが、これらの操作の有無にかかわらず組ぷよは自由落下しています。これを実装するのにkbhit関数が大いに役に立ちます。
この便利なkbhit関数ですが、残念ながら今回の環境には組み込まれていません。なので自作する必要があります。
自作するのですが、中身は相当難しいので理解する必要はありません。(私も理解できていない。)
以下、kbhit関数
(今後の事を考えてヘッダも作成しておきます。)

____game.h____

#include <stdio.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>

#define TRUE_BOOL 1
#define FALSE_BOOL 0

int kbhit(void);

____kbhit.c____

#include "game.h"

int kbhit(void)
{
    struct termios before, after;
    int key, bef;
    
    tcgetattr(STDIN_FILENO, &before);
    after = before;
    after.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(STDIN_FILENO, TCSANOW, &after);
    bef = fcntl(STDIN_FILENO, F_GETFL, 0);
    fcntl(STDIN_FILENO, F_SETFL, bef | O_NONBLOCK);
    
    key = getchar();
    
    tcsetattr(STDIN_FILENO, TCSANOW, &before);
    fcntl(STDIN_FILENO, F_SETFL, bef);
    
    if(key != EOF){
        ungetc(key, stdin);
        return TRUE_BOOL;
    }
    
    return FALSE_BOOL;
}

試しに、入力したらその入力した文字が何だったかを返してくれるプログラムを書いてみましょう。
____sampleMain.c____

#include "game.h"

int main(void)
{
    while(1){
        if(kbhit() == TRUE_BOOL){
            printf("%cが入力されました\n", getchar());
        }
    }
    
    return 0;
}

実行結果:

~$ gcc -c kbhit.c
~$ gcc -c sampleMain.c
~$ gcc -o run_sampleMain kbhit.o sampleMain.o
~$ ./run_sampleMain
aが入力されました
sが入力されました
dが入力されました
fが入力されました
jが入力されました
kが入力されました
lが入力されました
;が入力されました

~$

※プログラムを終了するときは、CtrlキーとCキーを同時押ししてください。
gcc -c "ファイル名".c とすることで分割された.cファイルをまとめてコンパイルするためのオブジェクトファイルを生成します。
gcc -o "実行名" "ファイル名".o とすることで上記で生成したオブジェクトファイルを統合して"実行名"で実行できるようにします。
./"実行名" で実行されます。

ここで着目してほしいのは、kbhit()を実行したあとにgetchar()を実行するとエンターキーを入力せずそのまま結果が反映されることです。
これがゲームの操作性を大きく向上させてくれます。
いちいちエンターキーで入力を完了させる必要もなく、入力していない間は別の動作をすることができます。

フィールドの作成

フィールドは縦12マス×横6マスですが、壁・床・上方向の画面外も加味して全フィールド縦14マス×横8マスの構成になります。
ところが、文字のアスペクト比を考えると縦:横=2:1になっているので、このままではやたら縦長いフィールドになってしまいます。
そこで、半角のマスで考えた時に全フィールドを縦14マス×横15マスで表現することで調整することとしましょう。
※フィールドを実装するにあたって、ターミナル(黒い画面のこと)の左上の座標が(1,1)で定義されていることに注意!
配列と違う点が少々厄介。

今回は壁・床をエスケープシーケンスを用いて白色で表現していきます。
エスケープシーケンスとは画面制御のための文字列のことで、printf関数と合わせて使うことによって様々な画面制御を実現します。
今回なら、指定した座標のマスを白くするエスケープシーケンスを記述していくことになります。

game.hに

#include <stdlib.h>

#define FIELD_HEIGHT 14 //縦14マス
#define FIELD_WIDTH 15 //横15マス

void putWhile(int, int);

を追加して、
____game.h____

#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>

#define TRUE_BOOL 1
#define FALSE_BOOL 0
#define FIELD_HEIGHT 14
#define FIELD_WIDTH 15

int kbhit(void);
void putWhile(int, int);

新しくputWhile関数を記述して、
____putColor.c____

#include <stdio.h>

void putWhite(int y, int x)
{
    //y行x列にカーソルを移動
    printf("\x1b[%d;%dH", y, x);
    //背景色を白に変更
    printf("\x1b[47m");
    //白の領域を描画
    printf("  ");
    //背景色をデフォルトに戻す
    printf("\x1b[49m");
}

フィールドを描画していきましょう!
____puyo.c____

#include "game.h"

int main(void)
{
    system("clear"); //画面のクリア
    
    //フィールドの描画
    for(int y = 1; y <= FIELD_HEIGHT; y++){
        for(int x = 1; x <= FIELD_WIDTH; x++){
            if((y != 1) && (x == 1 || x == FIELD_WIDTH || y == FIELD_HEIGHT))
                putWhite(y, x); //壁・床の描画
        }
        printf("\n");
    }
    
    return 0;
}

実行結果(コマンド):

~$ gcc -c putColor.c
~$ gcc -c puyo.c
~$ gcc -o run putColor.o puyo.o
~$./run

実行結果(フィールド):
f:id:sion2000114:20191211010424p:plain

フィールドに組みぷよを設置

フィールドが完成したので、そこに組みぷよを置いてみます。
初期位置が(7,2)、(7,3)なので、そこに設置してみたいと思います。
今後のことも考えて組みぷよは色と座標の情報を保持してくれるとありがたいので、構造体を用いて組みぷよを実現します。

game.hに組みぷよの情報を扱うための構造体 twinPuyoes_t、
フィールドの状態(空、壁、ぷよ(各色))、
組みぷよの初期位置、
これらを定義していきたいと思います。

#define INIT_ROLL_PUYO_X 7
#define INIT_ROLL_PUYO_Y 2
#define INIT_AXIS_PUYO_X 7
#define INIT_AXIS_PUYO_Y 3

#define CELL_TYPE_EMPTY 0
#define CELL_TYPE_WALL 1
#define CELL_TYPE_RED 2
#define CELL_TYPE_BLUE 3
#define CELL_TYPE_GREEN 4
#define CELL_TYPE_YELLOW 5
#define CELL_TYPE_PURPLE 6
#define CELL_TYPE_OJAMA 7

typedef struct {
    int rx, ry; //軸でないぷよ(生成時は上側のぷよ)の座標
    int ax, ay; //軸となるぷよ(生成時は下側のぷよ)の座標
    int rollPuyoColor; //軸でないぷよの色情報
    int axisPuyoColor; //軸となるぷよの色情報
} twinPuyoes_t;

これらの定義を基に早速組みぷよを初期位置に配置していきます!
色の割り当てはまた今度にするとして、今回は


という組みぷよを配置することにします。

まずはputWhite関数を踏襲して他の色も塗れるようにします。
____game.h____

#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <fcntl.h>

#define TRUE_BOOL 1
#define FALSE_BOOL 0
#define FIELD_HEIGHT 14
#define FIELD_WIDTH 15

#define INIT_ROLL_PUYO_X 7
#define INIT_ROLL_PUYO_Y 2
#define INIT_AXIS_PUYO_X 7
#define INIT_AXIS_PUYO_Y 3

#define CELL_TYPE_EMPTY 0
#define CELL_TYPE_WALL 1
#define CELL_TYPE_RED 2
#define CELL_TYPE_BLUE 3
#define CELL_TYPE_GREEN 4
#define CELL_TYPE_YELLOW 5
#define CELL_TYPE_PURPLE 6
#define CELL_TYPE_OJAMA 7

typedef struct {
    int rx, ry; //軸でないぷよ(生成時は上側のぷよ)の座標
    int ax, ay; //軸となるぷよ(生成時は下側のぷよ)の座標
    int rollPuyoColor; //軸でないぷよの色情報
    int axisPuyoColor; //軸となるぷよの色情報
} twinPuyoes_t;

int kbhit(void);
void putColor(int, int, int);

____putColor.c____

#include "game.h"

void putColor(int y, int x, int color)
{
    //(x,y))にカーソルを移動
    printf("\x1b[%d;%dH", y, x);
    
    //背景色を変更
    switch(color){
        case CELL_TYPE_WALL:
            printf("\x1b[47m");
            break;
        
        case CELL_TYPE_RED:
            printf("\x1b[41m");
            break;
        
        case CELL_TYPE_BLUE:
            printf("\x1b[44m");
            break;
        
        case CELL_TYPE_GREEN:
            printf("\x1b[42m");
            break;
        
        case CELL_TYPE_YELLOW:
            printf("\x1b[43m");
            break;
        
        case CELL_TYPE_PURPLE:
            printf("\x1b[45m");
            break;
        
        case CELL_TYPE_OJAMA:
            printf("\x1b[100m");
            break;
            
        default: break;
    }
    
    //描画
    printf("  ");
    //背景色をデフォルトに戻す
    printf("\x1b[49m");    
}

putWhite関数は新しくputColor関数として生まれ変わったので、フィールドの描画の方も関数を変更しておきます。
____puyo.c____

#include "game.h"

int main(void)
{
    system("clear"); //画面のクリア
    
    //フィールドの描画
    for(int y = 1; y <= FIELD_HEIGHT; y++){
        for(int x = 1; x <= FIELD_WIDTH; x++){
            if((y != 1) && (x == 1 || x == FIELD_WIDTH || y == FIELD_HEIGHT))
                putColor(y, x, CELL_TYPE_WALL); //壁・床の描画
        }
        printf("\n");
    }
    
    return 0;
}

では、初期位置に組みぷよを配置します。
___puyo.c____

#include "game.h"

int main(void)
{
    system("clear"); //画面のクリア
    
    //フィールドの描画
    for(int y = 1; y <= FIELD_HEIGHT; y++){
        for(int x = 1; x <= FIELD_WIDTH; x++){
            if((y != 1) && (x == 1 || x == FIELD_WIDTH || y == FIELD_HEIGHT))
                putColor(y, x, CELL_TYPE_WALL); //壁・床の描画
        }
        printf("\n");
    }
    
    //組みぷよの生成
    twinPuyoes_t tsumo = {
        INIT_ROLL_PUYO_X, INIT_ROLL_PUYO_Y,
        INIT_AXIS_PUYO_X, INIT_AXIS_PUYO_Y,
        CELL_TYPE_GREEN,
        CELL_TYPE_RED
    };
    
    //初期位置に設置
    putColor(tsumo.ry, tsumo.rx, tsumo.rollPuyoColor);
    putColor(tsumo.ay, tsumo.ax, tsumo.axisPuyoColor);
    
    return 0;
}

実行結果(コマンド):

~$ gcc -c kbhit.c
~$ gcc -c putColor.c
~$ gcc -c puyo.c
~$ gcc -o run kbhit.o putColor.o puyo.o
~$ ./run

実行結果(フィールド):
f:id:sion2000114:20191211230236p:plain
※$マークはプログラムの終了時に紛れ込んでしまったものなので、気になさらず

おわりに

今回は描画を主にしてきました。
いよいよ次回から本格的なぷよぷよの機能の実装に入っていきます。
作ったkbhit関数は次回以降使っていきます。
では、お疲れ様でした。

vol.2_1 ぷよぷよを作ってみよう!(準備編)

はじめに

今回は、前回紹介したPaizaCloudを作ってみたいと思います。
VisualStudioの時よりも実装するには少々不都合の多い環境なのですが、こういうところだからこそ頑張りたいものです!
開発の前に、ぷよぷよの仕様について記述していきます。
プログラムを記述していく前には、必ず見通しを立てておく必要があります。

ぷよぷよのゲームシステム

ぷよぷよを実装するにあたって必要となってくる情報を列挙的にまとめます。
これを書くにあたって
_________________________________

ぷよぷよ - Wikipedia

おじゃまぷよ算とは (オジャマプヨザンとは) [単語記事] - ニコニコ大百科
_________________________________

こちらの記事を参考にさせていただきました。ありがとうございます。
それでは早速まとめていきましょう!

ぷよぷよの主なゲームシステム

・フィールドは縦12マス×横6マスの格子で構成
 格子の1マスにつき1つのぷよを置くことができる。
 但し、上方向には画面外に1マス分だけぷよを置くことができる。
 全フィールドの左上のマスの座標を(1,1)と定義する。

・(4,2),(4,3)のマスを始点としてぷよが2つ1組で落下してくる。
 この2つ1組のことを組ぷよと呼ぶ。
 ぷよは3〜5種類+おじゃまぷよ(後述)で構成
 プレイヤーは落下する組ぷよに対して回転、横移動、高速落下のいずれかの操作を行う。

・次に落下する組ぷよ、その次に落下する組ぷよがフィールドの外枠に表示される。
 それらのことをそれぞれネクストぷよ、ネクネクぷよと呼ぶ。

・落下してきたぷよ組がフィールドの床や他のぷよに接地→設置すると、その位置に固定される。
 但し、組ぷよを横にして置いたりなどしてぷよに1マス分でも下方向に空白があるなら、そのぷよだけ強制的に落下させる。

・同種のぷよ(おじゃまぷよは除く)が周囲4方向にある場合、それらはくっついているという。
 4つ以上のぷよがくっついているとき、それらのぷよは消滅し得点となる。

・ぷよの消滅により、上にあったぷよは落下する。その際に再びぷよが4つ以上くっつくと消滅し、2以上の連鎖が発生する。
 但し、最初のぷよの消滅も1連鎖として見做す。
 複数種を同時に消滅させたり、同種を別箇所で消滅させた場合も1連鎖扱いとなる。

・ぷよを消滅させたときに入る得点は、消したぷよの数に、設定された連鎖倍率をかけることで得られる。

・(4,2)が埋まった状態でネクストぷよが落下態勢に入ろうとすると、窒息してゲームオーバーとなる。

・対戦形式の場合、
・連鎖による得点に比例した量のおじゃまぷよが相手のフィールドに降る。
 その個数は、連鎖による得点をおじゃまレートで割ることで得られる。
   おじゃまレートはマージンタイムという制限時間を超えると徐々に減少していく(つまり、時間経過とともに送られるおじゃまぷよの個数が増加していく)。

・おじゃまぷよは送られるたびにフィールド上部に予告ぷよとして表示される。
   おじゃまぷよが降ってくるのは、相手の連鎖が終了し、おじゃまぷよの個数が確定したあとに、出現している最初のぷよが接地し、ネクストぷよが落下態勢に入る直前とする。
 一度に降るおじゃまぷよの最大数は30個で(=5列分)で、それ以上のおじゃまぷよが送られている場合、複数回にわたって送られる。

・予告ぷよが表示されている間にこちらが連鎖することで、こちらが送るはずのおじゃまぷよが相殺として使われ、予告ぷよの個数をその分減少させる。

・全消しをすると、ボーナスが発生する。

各要素毎の細かい設定

回転

 組ぷよが落下態勢に入った際の下側のぷよが回転の軸となり、右回転・左回転を実現する。
 但し、回転先の一部分が侵入不可領域(フィールドの壁、床、他のぷよ)に入る場合、組ぷよを平行移動させることによって整合性を保つ。
 また、回転先の全部分が侵入不可領域に入る場合(縦を向いた組ぷよの両側が侵入不可領域である場合)、操作は実現しないが結果として180度回転となる際には回転が実行される。

落下速度

 自然落下時は2.4マス/s、高速落下時は24マス/sを基準とする。

接地

 ぷよがフィールドの床もしくは他のぷよと触ることを接地という。
 接地許容時間は合計32/60秒である。

設置

 設置許容時間(合計32/60秒)を超えるか設置を8回以上行った段階でぷよの位置を決定し、プレイヤーがそれ以上そのぷよに対して操作できなくなることを設置という。
 

得点等の計算式

おじゃまぷよの数=得点÷レート

レート:
時間(秒)    レート
0〜95     70
96〜111   52(マージンタイム)
112〜127  34
128〜143  25
144〜159  16
160〜175  12
176〜191  8
192〜207  6
208〜223  4
224〜239  3
240〜255  2
256〜     1

得点=(基本点+おじゃま点)×倍率+ボーナス

基本点=消したぷよの数×10

おじゃま点=Σ(おじゃま基本点×個数)

おじゃま基本点:
おじゃまぷよ・・・0点
得点ぷよ・・・50点

倍率=連鎖ごとの倍率+同時消し+連結

連鎖ごとの倍率:
連鎖     倍率
1・・・・・ 0
2・・・・・ 8
3・・・・・ 16
4・・・・・ 32
5・・・・・ 64
6・・・・・ 96
7・・・・・ 128
8・・・・・ 160
9・・・・・ 192
10・・・・ 224
11・・・・ 256
12・・・・ 288
13・・・・ 320
14・・・・ 352
15・・・・ 384
16・・・・ 416
17・・・・ 448
18・・・・ 480
19・・・・ 512

同時消し:
1色・・・・・0
2色・・・・・3
3色・・・・・6
4色・・・・・12
5色・・・・・24

連結:
4つ・・・・・0
5つ・・・・・2
6つ・・・・・3
7つ・・・・・4
8つ・・・・・5
9つ・・・・・6
10つ・・・・7
11つ~・・・10

ボーナス=落下ボーナス+全消しボーナス

落下ボーナス=高速落下させた段数

全消しボーナス={
0 (残しあり)
2100(全消し)

おわりに

今回は実装していきたい内容を記述していきました。
これらを基に次回からプログラムを書き始めていきましょう。
では、お疲れ様でした。

新環境・PaizaCloudの紹介

はじめに

お久しぶりです。プログラミング再開することにしました!
今回からは心機一転、環境を変えての再開となります。

クラウド開発環境として
paiza.cloud
こちらのPaizaCloudを利用していきたいと思います。
電子メールのアカウントさえあれば誰でもブラウザ上ですぐに利用可能なので、前回までのVisualStudioよりは扱いやすくなるはず、、、です。
今回はこのPaizaCloudの利便性、及び実際の使い方等の説明をしたいと思います。

PaizaCloudとは?

PaizaCloudとは、クラウドIDE統合開発環境)のことです。
ここだけでプログラムの記述、コンパイルが全て行えます。便利ですね~

PaizaCloudの利点は?

なんといっても気軽に使えること!!!

プログラミングを始めるにあたって最初に立ちはだかる「準備」の壁は相当に険しいものです。(ここで挫折する人も多くいるとか・・・)
でも、PaizaCloudなら電子メールのアカウントを用いて新規登録するだけで「誰でも」「簡単に」プログラミングを始めることができます!

そしてもう一つは、どこでも使えること!!

例えば外出先でプログラミングをしていて、家に帰ってもそのまま続けてやりたいなんてことがあったとき、PaizaCloudは活躍します。
だって、クラウド上にあるんですから、インターネットに繋がっていればどこからでもアクセスできますしね。うん、良い。

注意点

これだけ素晴らしいサービスを提供してくださるPaizaCloudですが、サービス内容がプランによって異なります。
内訳としては、
__________________________________________
〇無料プラン   ・・・ 月額費用:無料
             利用可能時間:24時間 
             連続起動時間:4時間

〇ライトプラン  ・・・ 月額費用:980円
             利用可能時間:無制限
             連続起動時間:4時間

〇ベーシックプラン・・・ 月額費用:1980円~
             利用可能時間:無制限
             連続起動時間:無制限
__________________________________________
このようになっています。
有料のものを選べば、それだけ快適に作業することはできますが、十分無料プランでも使えます。(そもそも、私は無料プランを使っているので・・・)
新規登録する際には無料プランになっているので、「いきなりお金、取られるかも!」とかは安心してくださいw

使い方

新規登録自体はメールアドレス等の情報を入力して、届いたメールからURLを踏んでもらえればすぐにできるので、

サーバの設立プログラムの記述実際にコンパイル

このあたりを説明していきます。

① サーバの設立

なんかすごく難しそうな響きですけども、やることは単純です。
ログインしたら中央に「新規サーバ作成」と出てきます。
そちらをクリックしていただくと、次のような画面になると思います。

f:id:sion2000114:20191206101823p:plain

(サーバ名をすでに記述してしまっていたので、編集して隠してあります。)

サーバ名はつけたいようにつけてもらえればいいです。後で覚えていないと使えない!というわけでもないので、そこは気楽に気楽にー
サーバ名は数字とアルファべットが混合していて、ほかにPaizaCloudを使っている人とサーバ名が被らなければ問題ありません。
打てたらそのまま「新規サーバ作成」を押してもらえれば完了です。

②プログラムの記述

プログラムは(C言語なら).cファイルに記述していきます。
サーバが設立できると次のような画面になっていると思います。

f:id:sion2000114:20191206102812p:plain

(右上の名前は個人情報の保護のため、黒で塗りつぶしてあります。)

左上にある「新規ファイル」を選択していただいて、
ファイル名として「〇〇〇.c」のように記述してください。(〇〇〇はお好みの名前で)
その後「エンターキー」を押していただくか、「作成」をクリックしていただくと無事にファイルを作成することができます。

ファイルが作成されると自動でそのファイルは開かれます。ここにプログラムを書けばいいということですね。
試しに、

#include<stdio.h>

int main(void)
{
    printf("hello world!\n");
    
    return 0;
}

このように書いておきましょうか。
書き終わったら「保存」を押しましょう。
ここで隣にある「自動保存」にチェックを入れておくと適宜保存してくれるので便利です。

③ 実際にコンパイル

コンパイルをするにはまずは左側にある「ターミナル」をクリックしてください。
すると黒い画面が出てくると思います。
その黒い画面に次のようなコマンドを入力します:

gcc 〇〇〇.c
./a.out

(〇〇〇はつけた名前)

もしプログラムに不備があると

gcc 〇〇〇.c

の段階でいろいろエラーが返ってきます。
無事にコマンドを打ち終えられると結果として、

hello world!

と出力されると思います。

データの保存について

前述の通り、無料プランだと利用可能時間が24時間となっています。
24時間を超えてしまうと今まで書いてきたプログラムが消えてしまうので、どこか別のところに保存しておく必要があります。
私の場合ですと、GoogleDriveを利用してデータを保存しています。
(保存とは言うもののやってることはコピペです。。。)
PaizaCloudで作成したプログラムコードと同様の物をGoogleDrive内にも作成して、また次回新しいサーバでプログラミングを再開するときに保存したものをコピペで再生させています。
このあたりは皆さんの環境にお任せします。

おわりに

今回はPaizaCloudについて書いていきました。
次回からはこの環境でゲームを作っていけたらなーと思います。
では、今回はこのあたりで。
お疲れ様でした。

vol.14 敵の移動・戦闘(ひとまず終了)

目次

はじめに

前回からの続きで、まずは敵がプレイヤーに近づく機構を作成していきたいと思います。
近づけたら敵と戦闘を行えるようにしていきます。細かい仕様等は各々記述していきますね。

敵の移動(2)

プレイヤーを中心とした7*7マスの中に敵がいた場合、敵はプレイヤーに近づこうとします。移動方向は縦か横かしかないので、どの方向を優先的に取るのかを決める必要があります。今回は、縦・横のより大きな成分の方向に移動することにします。成分が同じ場合は横方向を優先することにしましょう。
正直この定義だと敵AIとしては不成立と言ってもいいくらいですが、まあ今は目を瞑りましょう。

以下、enemyAct関数

void enemyAct(int *enemyX, int *enemyY, int enemyCount, int cursorX, int cursorY)
{
	int x ,y;
	int flag = 0;

	flag = 0; //フラグ初期化
	for (y = cursorY - 3; y <= cursorY + 3; y++) {
		for (x = cursorX - 3; x <= cursorX + 3; x++) {
			//フィールドの範囲外探索を回避
			if (x < 0 || y < 0 || x >= FIELD_WIDTH || y >= FIELD_HEIGHT)
				continue;

			//範囲内に敵がいれば
			if (x == *enemyX && y == *enemyY && flag != 1) {
				flag = 1;
				//プレイヤーに近づく処理をここに記述//
				int gapX = cursorX - *enemyX; //敵から見たプレイヤーの相対x座標
				int gapY = cursorY - *enemyY; //敵から見たプレイヤーの相対y座標

				//上へ
				if (gapY < 0 && abs(gapY) > abs(gapX)) {
					//壁・階段・プレイヤーには侵入しない
					if (CELL_TYPE_WALL <= field[*enemyY - 1][*enemyX] && field[*enemyY - 1][*enemyX] <= CELL_TYPE_PLAYER) break;

					field[*enemyY - 1][*enemyX] = 5; //移動先を敵アイコン(cellSymbolの5番目の配列)に変更
					field[*enemyY][*enemyX] = CELL_TYPE_NONE; //敵が元いた場所は空気に変更
					*enemyY -= 1; //敵は1つ上へ
				}
				//下へ
				else if (gapY > 0 && abs(gapY) > abs(gapX)) {
					//壁・階段・プレイヤーには侵入しない
					if (CELL_TYPE_WALL <= field[*enemyY + 1][*enemyX] && field[*enemyY + 1][*enemyX] <= CELL_TYPE_PLAYER) break;

					field[*enemyY + 1][*enemyX] = 5; //移動先を敵アイコン(cellSymbolの5番目の配列)に変更
					field[*enemyY][*enemyX] = CELL_TYPE_NONE; //敵が元いた場所は空気に変更
					*enemyY += 1; //敵は1つ下へ
				}
				//左へ
				else if (gapX < 0 && abs(gapY) <= abs(gapX)) {
					//壁・階段・プレイヤーには侵入しない
					if (CELL_TYPE_WALL <= field[*enemyY][*enemyX - 1] && field[*enemyY][*enemyX - 1] <= CELL_TYPE_PLAYER) break;

					field[*enemyY][*enemyX - 1] = 5; //移動先を敵アイコン(cellSymbolの5番目の配列)に変更
					field[*enemyY][*enemyX] = CELL_TYPE_NONE; //敵が元いた場所は空気に変更
					*enemyX -= 1; //敵は1つ左へ
				}
				//右へ
				else {
					//壁・階段・プレイヤーには侵入しない
					if (CELL_TYPE_WALL <= field[*enemyY][*enemyX + 1] && field[*enemyY][*enemyX + 1] <= CELL_TYPE_PLAYER) break;

					field[*enemyY][*enemyX + 1] = 5; //移動先を敵アイコン(cellSymbolの5番目の配列)に変更
					field[*enemyY][*enemyX] = CELL_TYPE_NONE; //敵が元いた場所は空気に変更
					*enemyX += 1; //敵は1つ右へ
				}
				
			}
		}
	}
		
	//範囲内に敵がいなければ
	if (flag == 0) { ... }
}

敵AIの構成はゲームの質を大きく変化させる要素なので、もっと研究していくべきでしょう。(まあここではやりませんが。)

敵のステータス追加

さて、敵と戦闘を行いたいのですが、そもそも敵にステータスが無ければ話になりませんね。敵の種類に応じてステータスを割り振るenemyStatusQuota関数を作成します。

//敵の種類に応じたステータス割り振り
void enemyStatusQuota(int enemyID, PERSON_t *enemy)
{
	switch (enemyID) {
	case CELL_TYPE_ENEMY__SLIME:
		(enemy->ATK) = 1, (enemy->DEF) = 1, (enemy->EXP) = 1, (enemy->GOLD) = 2, (enemy->HP) = 5, (enemy->LV) = 1, (enemy->MHP) = 5;
		break;
	case CELL_TYPE_ENEMY__DRAKEE:
		(enemy->ATK) = 2, (enemy->DEF) = 2, (enemy->EXP) = 2, (enemy->GOLD) = 4, (enemy->HP) = 8, (enemy->LV) = 1, (enemy->MHP) = 8;
		break;
	case CELL_TYPE_ENEMY__GHOST:
		(enemy->ATK) = 3, (enemy->DEF) = 4, (enemy->EXP) = 4, (enemy->GOLD) = 10, (enemy->HP) = 10, (enemy->LV) = 1, (enemy->MHP) = 10;
		break;
	case CELL_TYPE_ENEMY__MONSTER_TOALSTOOL:
		(enemy->ATK) = 5, (enemy->DEF) = 3, (enemy->EXP) = 10, (enemy->GOLD) = 15, (enemy->HP) = 12, (enemy->LV) = 1, (enemy->MHP) = 12;
		break;
	default: break;
	}
}

まあ、このあたりの数値は適当です。w
この関数をmain関数の、敵を生成するときに起動してやります。
main関数内:

//敵の初期生成
int enemyCount = random(5, 7);
ITEM_t enemy[FIELD_ITEM_COUNT]; //敵の識別
PERSON_t enemyInfo[FIELD_ITEM_COUNT]; //敵のステータス

for (i = 0; i < enemyCount; i++) {
	enemy[i].ID = random(CELL_TYPE_ENEMY__SLIME, CELL_TYPE_ENEMY_END - 1);
	setRandomIcon(enemy[i].ID, &enemy[i].x, &enemy[i].y);

	enemyStatusQuota(enemy[i].ID, &enemyInfo[i]);
}

64ターン経過の際にも敵は生成されるので忘れず起動しましょう。

//フィールドの描画
while (1) {
	//ターン経過
	turnNum++;
	//敵追加
	if (turnNum == 64 && enemyCount < FIELD_ENEMY_COUNT) {
		enemy[enemyCount].ID = random(CELL_TYPE_ENEMY__SLIME, CELL_TYPE_ENEMY_END - 1);
		setRandomIcon(enemy[enemyCount].ID, &enemy[enemyCount].x, &enemy[enemyCount].y);
		enemyStatusQuota(enemy[enemyCount].ID, &enemyInfo[enemyCount]);
		enemyCount++;
		turnNum = 0; //ターン数初期化
        }
        ...省略...
}

戦闘の概念

プレイヤーが移動したりメニュー画面を開いたりする箇所(main関数のswitch文)に機能を追加します。o(オー)キー(Offensive:攻撃)を入力することで隣接する敵に攻撃を仕掛ける機構を実装しましょう。
本来であればプレイヤーを中心とした全てのマスに存在する敵に対して個別に攻撃できるようにするところなのですが、分岐等が少々面倒なので「斜め攻撃なし」「プレイヤーの攻撃は全体攻撃(回し切り)」ということにしておきましょう。
また、ちゃんと戦闘終了処理等もここで行っていきます。敵を倒せたら経験値とゴールドを取得しフィールドから存在を消去し、プレイヤーは敗北すればゲームを終了するようにしましょう。
以下、main関数 case 'o':

case 'o': {
	//全ての敵について
	for (i = 0; i < FIELD_ENEMY_COUNT; i++) {
                //死んでいたら処理しない
		if (enemyInfo[i].HP <= 0) continue;
		//プレイヤーの目の前に敵がいたら
		if ((enemy[i].x - 1 == cursorX && enemy[i].y == cursorY) || (enemy[i].x + 1 == cursorX && enemy[i].y == cursorY)
		    || (enemy[i].x == cursorX && enemy[i].y - 1 == cursorY) || (enemy[i].x == cursorX && enemy[i].y + 1 == cursorY)) {
			//プレイヤーの攻撃宣言
			printf("プレイヤーの攻撃!\n");
			_getch();
			break; //攻撃宣言は一度だけ(まとめて周囲に攻撃できることにした)
		}
	}
	//全ての敵について
	for (i = 0; i < FIELD_ENEMY_COUNT; i++) {
                //死んでいたら処理しない
		if (enemyInfo[i].HP <= 0) continue;
		//プレイヤーの目の前に敵がいたら
		if ((enemy[i].x - 1 == cursorX && enemy[i].y == cursorY) || (enemy[i].x + 1 == cursorX && enemy[i].y == cursorY)
		    || (enemy[i].x == cursorX && enemy[i].y - 1 == cursorY) || (enemy[i].x == cursorX && enemy[i].y + 1 == cursorY)) {
			//プレイヤーの攻撃
			int damageEnemy = (player.ATK) - (enemyInfo[i].DEF);
			printf("プレイヤーは%s%dポイントのダメージを与えた!\n", idToName(enemy[i].ID), damageEnemy);
			enemyInfo[i].HP -= damageEnemy;

			//敵を倒したとき
			if (enemyInfo[i].HP <= 0) {
				printf("%sを倒した!\n", idToName(enemy[i].ID));
				printf("経験値%dポイント獲得!\n", enemyInfo[i].EXP);
				printf("%dゴールド入手した!\n", enemyInfo[i].GOLD);
				//経験値・ゴールドの取得
				player.EXP += enemyInfo[i].EXP;
				player.GOLD += enemyInfo[i].GOLD;
				//ifledの情報の書き換え(敵の死亡)
				field[enemy[i].y][enemy[i].x] = CELL_TYPE_NONE;
			}
		}
	}
	_getch();
	//全ての敵について
	for (i = 0; i < FIELD_ENEMY_COUNT; i++) {
		//死んでいたら処理しない
		if (enemyInfo[i].HP <= 0) continue;
		//プレイヤーの目の前に敵がいたら
		if ((enemy[i].x - 1 == cursorX && enemy[i].y == cursorY) || (enemy[i].x + 1 == cursorX && enemy[i].y == cursorY)
		    || (enemy[i].x == cursorX && enemy[i].y - 1 == cursorY) || (enemy[i].x == cursorX && enemy[i].y + 1 == cursorY)) {
			//敵の攻撃
			printf("%sの攻撃!\n", idToName(enemy[i].ID));
                        _getch();
			int damagePlayer = enemyInfo[i].ATK - player.DEF;
			printf("プレイヤーは%dポイントのダメージを受けた!\n", damagePlayer);
			player.HP -= damagePlayer;
			_getch();

			//体力は最小でも0なので
			if (player.HP < 0) {
				player.HP = 0;
				break;
			}
		}
	}
	break;
}

敵が死亡したことをfieldに変更してもenemyAct関数で再び書き換えられてしまうので、enemyAct関数を呼び出す際にそもそも敵が死亡していたらcontinueするようにすれば書き換えられることはなくなります。
main関数の下の方

//敵AI
for (i = 0; i < enemyCount; i++)
    if (enemyInfo[i].HP <= 0) continue;
    enemyAct(&enemy[i].x, &enemy[i].y, enemyCount, cursorX, cursorY);
}

そういえばidToName関数に敵の情報を追加していなかったので追加しておきましょう。まだアイテムの情報しか入ってませんからね。

char *idToName(int itemID)
{
	switch (itemID) {
	case CELL_TYPE_BREAD:
		return "パン";
	case CELL_TYPE_BIG_BREAD:
		return "大きなパン";
	case CELL_TYPE_ENEMY__SLIME:
		return "スライム";
	case CELL_TYPE_ENEMY__GHOST:
		return "ゴースト";
	case CELL_TYPE_ENEMY__DRAKEE:
		return "ドラキー";
	case CELL_TYPE_ENEMY__MONSTER_TOALSTOOL:
		return "おばけきのこ";
	default: break;
	}
}

※今回の戦闘におけるダメージ計算は、簡略化のために攻撃力から防御力を引いた値を実際のダメージとしています。他にもダメージ計算には種類があるのでよかったら別の方法でも試してみてくださいな。

おわりに

脈絡もなく今までプログラムを書いてきたので良い見通しが立っておらず、プログラムが煩雑になってしまっているのは申し訳ないです。ひとまず戦闘まで実装することができました。
もうあらかたゲームに必要なものはそろってきていると思いますので、続きはどうしようか、また考えておきます。
では、お疲れ様でした。

追伸:
とりあえずアスキーアートで表現する、C言語でのゲームプログラミングはここで終了したいと思います。何かほかにやってほしいことがあったらコメントください。即時実現・説明入れていきます。
このシリーズは終了しますが、新しく新環境でゲームプログラミングしていきたいと思います!
言語は変わらずC言語で(ひとまずは)続けようと思いますので、乞うご期待!

vol.13 敵の実装

目次

はじめに

今回は敵を実装していきます。ローグライクゲームはRPGにありがちなランダムエンカウント(フィールドを歩いていると見えない敵と遭遇して戦闘が開始)ではないので、敵と戦闘するのは敵と対面したときになります。まずはアイテムを生成した時と同様に敵を設置して、その設置された敵と戦闘できるようにしましょう。

敵の生成

まずはmain関数のアイテム生成と同じ箇所で初期の敵の生成を行います。敵はフロア生成段階でアイテムと同様に5~7体生成されます。そこで冒険していくにあたって64ターンに1回敵が1体増えます。ダンジョン内に敵は20体までしかいられないとして話を進めていきましょう。
さて、敵の種類と場所を特定するために敵はアイテムと同じ扱いでいきましょう。(ITEM_tはIDと座標の情報を持っているから扱いが楽。) 敵の各IDはenumで管理してやります。今回は敵として
スライム(CELL_TYPE_ENEMY__SLIME)
ゴースト(CELL_TYPE_ENEMY__GHOST)
ドラキー(CELL_TYPE_ENEMY__DRAKEE)
おばけキノコ(CELL_TYPE_ENEMY__MONSTER_TOALSTOOL)
の4種類を追加するとしましょう。
また、IDの範囲を選択するにあたってアイテムがどこまでか、敵がどこまでかを明確にしておきましょう。アイテムIDの最後をCELL_TYPE_ITEM_END、敵IDの最後をCELL_TYPE_ENEMY_ENDとしましょう。

enum ID{
	CELL_TYPE_NONE, //0
	CELL_TYPE_WALL, //1
	CELL_TYPE_PLAYER, //2
	CELL_TYPE_STAIRS, //3

	CELL_TYPE_BREAD, //パン
	CELL_TYPE_BIG_BREAD, //大きなパン
	CELL_TYPE_ITEM_END,

	CELL_TYPE_ENEMY__SLIME, //スライム
	CELL_TYPE_ENEMY__GHOST, //ゴースト
	CELL_TYPE_ENEMY__DRAKEE, //ドラキー
	CELL_TYPE_ENEMY__MONSTER_TOALSTOOL //おばけキノコ
	CELL_TYPE_ENEMY_END
};

setRndomIcon関数を使うにあたってcellSymbolに敵のアスキーアートを追加します。"※"で表現することにします。

#define CELL_COUNT 6

char cellSymbol[CELL_COUNT][3] = {
	"・", //CELL_TYPE_NONE(0)番目の配列
	"■", //CELL_TYPE_WALL(1)番目の配列
	"@", //CELL_TYPE_PLAYER(2)番目の配列
	"巛", //CELL_TYPE_STAIRS(3)番目の配列
	"ア", //4番目の配列にアイテムアイコン
	"※"  //5番目の配列に敵アイコン  
};

では、setRandomIcon関数に手を加えます。まだアイテムにしか対応していないので敵にも対応させます。

void setRandomIcon(int CELL_TYPE, int *x, int *y)
{
	int roomNum = random(0, areaCount - 1); //どのルーム(エリアとみなしても同値)に置くか
	*x = random(areas[roomNum].room.x, areas[roomNum].room.x + areas[roomNum].room.w - 1);
	*y = random(areas[roomNum].room.y, areas[roomNum].room.y + areas[roomNum].room.h - 1);

	if (CELL_TYPE_BREAD <= CELL_TYPE && CELL_TYPE <= CELL_TYPE_ITEM_END - 1) //CELL_TYPEがアイテムなら
		CELL_TYPE = 4; //cellSymbolの4番目の配列用
	else if (CELL_TYPE_ENEMY__SLIME <= CELL_TYPE && CELL_TYPE <= CELL_TYPE_ENEMY_END - 1) //CELL_TYPEが敵なら
		CELL_TYPE = 5; //5番目の配列用
	
	field[*y][*x] = CELL_TYPE;
}

以下main関数(アイテムの生成~敵の生成)

//アイテムの生成
int itemCount = random(5, 7);
ITEM_t item[FIELD_ITEM_COUNT]; //アイテムの識別

for (i = 0; i < itemCount; i++) {
	item[i].ID = random(CELL_TYPE_BREAD, CELL_TYPE_ITEM_END - 1); //IDの範囲を指定している
	setRandomIcon(item[i].ID, &item[i].x, &item[i].y);
}

//敵の初期生成
int enemyCount = random(5, 7);
ITEM_t enemy[FIELD_ITEM_COUNT]; //敵の識別

for (i = 0; i < enemyCount; i++) {
	enemy[i].ID = random(CELL_TYPE_ENEMY__SLIME, CELL_TYPE_ENEMY_END - 1);
	setRandomIcon(enemy[i].ID, &enemy[i].x, &enemy[i].y);
}

・・・省略・・・

switch(_getch()) {
case 'w':
	if (field[cursorY - 1][cursorX] == CELL_TYPE_WALL) break;
		
	if (field[cursorY - 1][cursorX] == 4) {
		・・・省略・・・
	}
	・・・省略・・・
case 's':
	if (field[cursorY + 1][cursorX] == CELL_TYPE_WALL) break;
	if (field[cursorY + 1][cursorX] == 4) {
		・・・省略・・・
	}
	・・・省略・・・
case 'a':
	if (field[cursorY][cursorX - 1] == CELL_TYPE_WALL) break;
	if (field[cursorY][cursorX - 1] == 4) {
		・・・省略・・・
	}
	・・・省略・・・
case 'd':
	if (field[cursorY][cursorX + 1] == CELL_TYPE_WALL) break;
	if (field[cursorY][cursorX + 1] == 4) {
		・・・省略・・・
	}
	・・・省略・・・
・・・省略・・・
}

次は64ターン経過毎に新たにモンスターを追加します。上限は20体として作成していきます。

#define FIELD_ENEMY_COUNT 20

以下main関数、フィールド描画部分

int turnNum = 0; //ターン経過数

//フィールドの描画
while (1) {
	//ターン経過
	turnNum++;
	//敵追加
	if (turnNum == 64 && enemyCount < FIELD_ENEMY_COUNT) {
		enemy[enemyCount].ID = random(CELL_TYPE_ENEMY__SLIME, CELL_TYPE_ENEMY_END - 1);
		setRandomIcon(enemy[enemyCount].ID, &enemy[enemyCount].x, &enemy[enemyCount].y);
		enemyCount++;
		turnNum = 0; //ターン数初期化
	}

	system("cls");

	・・・省略・・・
}

敵の移動(1)

敵はプレイヤーが近くにいるとプレイヤーを追いかけます。そして敵がプレイヤーのいるルームにいない場合は、移動可能なルームへ敵は移動していきます。本来は別ルームへ移動する際にA*(エースター)アルゴリズムというものを用いて実装するのですが、ここでは面倒なので実装するのはやめます。敵がプレイヤーの近くにいない場合は完全にランダムに移動することにします。
プレイヤーを中心とした7*7マスの中に敵がいた場合、敵がプレイヤーの方に移動するようにしたいですが、それはまた次回にしましょう。

enum direction {
	UP,
	DOWN,
	LEFT,
	RIGHT
};

以下、enemyAct関数(概形)

void enemyAct(int *enemyX, int *enemyY, int enemyCount, int cursorX, int cursorY)
{
	int x ,y;
	int flag = 0;

	flag = 0; //フラグ初期化
	for (y = cursorY - 3; y <= cursorY + 3; y++) {
		for (x = cursorX - 3; x <= cursorX + 3; x++) {
			//フィールドの範囲外探索を回避
			if (x < 0 || y < 0 || x >= FIELD_WIDTH || y >= FIELD_HEIGHT)
				continue;

			//範囲内に敵がいれば
			if (x == *enemyX && y == *enemyY) {
				flag = 1;
				//プレイヤーに近づく処理をここに記述//
			}
		}
	}
		
	//範囲内に敵がいなければ
	if (flag == 0) {
		//ランダムに移動する処理をここに記述//
		int randomDirection = random(UP, RIGHT); //ランダムな方向
		switch (randomDirection) {
		case UP:
			if (field[*enemyY - 1][*enemyX] == CELL_TYPE_WALL) break;

			field[*enemyY - 1][*enemyX] = 5; //移動先を敵アイコン(cellSymbolの5番目の配列)に変更
			field[*enemyY][*enemyX] = CELL_TYPE_NONE; //敵が元いた場所は空気に変更
			*enemyY -= 1; //敵は1つ上へ
			break;
		case DOWN:
			if (field[*enemyY + 1][*enemyX] == CELL_TYPE_WALL) break;

			field[*enemyY + 1][*enemyX] = 5; //移動先を敵アイコン(cellSymbolの5番目の配列)に変更
			field[*enemyY][*enemyX] = CELL_TYPE_NONE; //敵が元いた場所は空気に変更
			*enemyY += 1; //敵は1つ下へ
			break;
		case LEFT:
			if (field[*enemyY][*enemyX - 1] == CELL_TYPE_WALL) break;

			field[*enemyY][*enemyX - 1] = 5; //移動先を敵アイコン(cellSymbolの5番目の配列)に変更
			field[*enemyY][*enemyX] = CELL_TYPE_NONE; //敵が元いた場所は空気に変更
			*enemyX -= 1; //敵は1つ左へ
			break;
		case RIGHT:
			if (field[*enemyY][*enemyX + 1] == CELL_TYPE_WALL) break;

			field[*enemyY][*enemyX + 1] = 5; //移動先を敵アイコン(cellSymbolの5番目の配列)に変更
			field[*enemyY][*enemyX] = CELL_TYPE_NONE; //敵が元いた場所は空気に変更
			*enemyX += 1; //敵は1つ右へ
			break;
		default: break;
		}
	}
}

main関数ではswitch文の後で行います。
以下、main関数(switch文付近)

int main(void)
{
	・・・省略・・・
	switch(_getch()) { ... }
	
	//敵AI
	for (i = 0; i < enemyCount; i++)
		enemyAct(&enemy[i].x, &enemy[i].y, enemyCount, cursorX, cursorY);
	
	・・・省略・・・
}

おわりに

今回は敵の移動の途中まで実装しました。次回はプレイヤーが近くにいるときにプレイヤーに近づいていくアルゴリズムを作成していきましょう。では、お疲れ様でした。

vol.12-2 アイテムの取得、使用

目次

はじめに

分割が終われば次はいよいよメニュー画面を作成していきます。メニュー画面では所持アイテム、装備品を確認できるようにして、アイテムも使用できるようにしましょう。また、ダンジョン内に落ちているアイテムの上に乗った時、そのアイテムを取得するかどうかを選択させるようにし、実際にそのアイテムを取得できるようにしましょう。

メニュー画面の作成

mキーを入力した時にメニュー画面を表示できるようにしましょう。メニュー画面ではいろいろなことを行えるようにしたいのでコードが長くなる可能性があります。そこで最初からメニュー画面に関する内容は関数として記述してやることにします。

void menuScreen(void);

でプロトタイプ宣言してから定義してやります。
メニュー画面をこのような設計にします
|――――――|
|1. アイテム |
|――――――|
|2. ステータス|
|――――――|

void menuScreen(void)
{
	printf("|――――――|\n");
	printf("|1.アイテム |\n");
	printf("|――――――|\n");
	printf("|2.ステータス|\n");
	printf("|――――――|\n");

	switch (_getch()) {
	case 'm':
		return;
	}

	return;
}

あとから色々機能を追加するので今はこれくらいです。一応mキーを押すとメニュー画面が閉じるようにはしました。

アイテムの取得

アイテムを取得するためにはそもそもプレイヤーがアイテムを持つという概念を追加しなければいけません。ローグライクゲームでは基本プレイヤーは20個しかアイテムは持てません。更に、重複して持つことも一部のアイテムを除き不可能です。限られたアイテムの中でゲームを攻略していくという意図があります。
今回はリスト構造というものを使って実装していきたいと思います。これはvol.12-1に挙がっているので細かいことはそっちを参考にして下さいねー。
セルをこのように宣言します。

typedef struct possession{
	int ID;
	struct possession *next;
}POSSESSION_t; //所持品

少し今までと違った書き方ですが、これはPOSSESSION_tの中に次のPOSSESSION_tのポインタを実装する際に今まで通りの書き方だとエラーが発生してしまうので、このようになります。
さて、実際にアイテムを取得するために準備をしていきましょう。vol.12-1に書かれたcreate関数を参考にして書いていきます。

POSSESSION_t *createData(int itemID)
{
	POSSESSION_t *pouch;
	pouch = (POSSESSION_t *)malloc(sizeof(POSSESSION_t)); //メモリの確保、ここが準備の中核
	(pouch->ID) = itemID, (pouch->next) = NULL;

	return pouch;
}

これをmain関数の最初にダミーセルとして

POSSESSION_t *itemPouch = createData(-1); //アイテムポーチの作成

このように準備してやります。

ちょっとだけ補足。
メモリの確保のためにmalloc関数というものを使っているのですが、これは配列などで予めメモリを確保しておくのではなく、状況に応じて必要分のメモリを動的に確保してくれるものです。
sizeof演算子は、カッコ内の変数やデータ型がどれくらいのメモリサイズを持っているかを教えてくれるものです。
(POSSESSION_t *)はただのキャストです。
なのでpouch = ...の記述は、POSSESSION_tのメモリ分だけpouchのメモリを確保しましょう、という意味になります。

次に、アイテムをポーチに追加するaddData関数(insert関数を参考に)を書いていきます。

//k番目にアイテムデータを追加
void addData(POSSESSION_t *list, int k, int itemID)
{
	POSSESSION_t *p;

	if (k > 1) addData(list->next, k - 1, itemID);
	else {
		p = createData(itemID);
		(p->next) = (list->next);
		(list->next) = p;
	}
}

次に、実際にアイテムを消費する関数consumeItem関数(delete関数を参考に)を書いていきます。(ここではまだアイテムの効果は発揮させません。)

//k番目のアイテムを消費
void consumeItem(POSSESSION_t *list, int k)
{
	POSSESSION_t *p;

	if (k > 1) useItem(list->next, k - 1);
	else {
		p = (list->next);
		(list->next) = (list->next->next); //listの次にあるデータを1つ飛ばした
		free(p); //メモリの開放、pは飛ばされたいらないやつなので
	}
}

ここではとりあえずアイテムを消費したということにして消しただけですので、実際にpの効果を発動させないといけません。そこで、pの内容に応じて効果を発動させる新たな関数activateItem関数を作成しましょう。

//アイテム発動
void activateItem(int itemID, PERSON_t *person)
{
	switch (itemID) {
	case CELL_TYPE_BREAD:
		(person->HUNGER) += 500; //最大満腹度1000の50%
		if ((person->HUNGER) >= 1000) (person->HUNGER) = 1000; //最大満腹度1000は越えてはいけない
		break;
	case CELL_TYPE_BIG_BREAD:
		(person->HUNGER) = 1000; //満腹度全回復
		break;
	}
}

アイテムはまだ2つしかないのでこんな感じになります。アイテムが増えてき次第この関数は更新していきます。
では、activateItem関数をconsumeItem関数に組み込んでいきましょう。ここで、consumeItem関数の中からPERSON_tのものを呼び出すことになるので、consumeItem関数の引数にPERSON_tを追加します。

//k番目のアイテムを消費
void consumeItem(PERSON_t *person, POSSESSION_t *list, int k)
{
	POSSESSION_t *p;

	if (k > 1) useItem(list->next, k - 1);
	else {
		p = (list->next);
		activateItem(p->ID, person);
		(list->next) = (list->next->next); //listの次にあるデータを1つ飛ばした
		free(p); //メモリの開放、pは飛ばされたいらないやつなので
	}
}

アイテムが使えるようになったということでようやくアイテムを取得する段階になりました。プレイヤーがアイテムの上に立った時に拾うか拾わないかを選択し、拾う場合アイテムデータとして追加してやりましょう。アイテムの上に立った後で行う処理なので、main関数のswitch文内で記述してやりましょう。
(注:int pouchItemCount=0; をmain関数の最初に記述してあります。)

int searchItemID; //落ちていたアイテムのIDを検索し、結果をここに代入

switch (_getch()) {
case 'w':
	if (field[cursorY - 1][cursorX] == CELL_TYPE_WALL) break;
		
	if (field[cursorY - 1][cursorX] >= CELL_TYPE_BREAD) {
		printf("このアイテムを拾いますか?\n");
		printf("1:はい 2(またはその他のキー):いいえ\n");
		if (_getch() != '1') break;
		else {
			if (pouchItemCount == 20) {
				printf("これ以上アイテムは持てません。");
				_getch();
				break;
			}
			//落ちているアイテムのIDを検索
			for (i = 0; i < itemCount; i++)
				if (item[i].x == cursorX && item[i].y == cursorY - 1) {
					searchItemID = item[i].ID;
					break;
				}

			addData(itemPouch, pouchItemCount, searchItemID);
			pouchItemCount++;
			printf("アイテムを入手した!\n");
			_getch();
		}
	}

	field[cursorY][cursorX] = CELL_TYPE_NONE;
	cursorY--;
	field[cursorY][cursorX] = CELL_TYPE_PLAYER;
	player.HUNGER--;
	break;
case 's':
	if (field[cursorY + 1][cursorX] == CELL_TYPE_WALL) break;
	if (field[cursorY + 1][cursorX] >= CELL_TYPE_BREAD) {
		printf("このアイテムを拾いますか?\n");
		printf("1:はい 2(またはその他のキー):いいえ\n");
		if (_getch() != '1') break;
		else {
			if (pouchItemCount == 20) {
				printf("これ以上アイテムは持てません。");
				_getch();
				break;
			}
				//落ちているアイテムのIDを検索
			for (i = 0; i < itemCount; i++)
				if (item[i].x == cursorX && item[i].y == cursorY + 1) {
					searchItemID = item[i].ID;
					break;
				}

			addData(itemPouch, pouchItemCount, searchItemID);
			pouchItemCount++;
			printf("アイテムを入手した!\n");
			_getch();
		}
	}

	field[cursorY][cursorX] = CELL_TYPE_NONE;
	cursorY++;
	field[cursorY][cursorX] = CELL_TYPE_PLAYER;
	player.HUNGER--;
	break;
case 'a':
	if (field[cursorY][cursorX - 1] == CELL_TYPE_WALL) break;
	if (field[cursorY][cursorX - 1] >= CELL_TYPE_BREAD) {
		printf("このアイテムを拾いますか?\n");
		printf("1:はい 2(またはその他のキー):いいえ\n");
		if (_getch() != '1') break;
		else {
			if (pouchItemCount == 20) {
				printf("これ以上アイテムは持てません。");
				_getch();
				break;
			}
			//落ちているアイテムのIDを検索
			for (i = 0; i < itemCount; i++)
				if (item[i].x == cursorX - 1 && item[i].y == cursorY) {
					searchItemID = item[i].ID;
					break;
				}

			addData(itemPouch, pouchItemCount, searchItemID);
			pouchItemCount++;
			printf("アイテムを入手した!\n");
			_getch();
		}
	}

	field[cursorY][cursorX] = CELL_TYPE_NONE;
	cursorX--;
	field[cursorY][cursorX] = CELL_TYPE_PLAYER;
	player.HUNGER--;
	break;
case 'd':
	if (field[cursorY][cursorX + 1] == CELL_TYPE_WALL) break;
	if (field[cursorY][cursorX + 1] >= CELL_TYPE_BREAD) {
		printf("このアイテムを拾いますか?\n");
		printf("1:はい 2(またはその他のキー):いいえ\n");
		if (_getch() != '1') break;
		else {
			if (pouchItemCount == 20) {
				printf("これ以上アイテムは持てません。");
				_getch();
				break;
			}
			//落ちているアイテムのIDを検索
			for (i = 0; i < itemCount; i++)
				if (item[i].x == cursorX + 1 && item[i].y == cursorY) {
					searchItemID = item[i].ID;
					break;
				}

			addData(itemPouch, pouchItemCount, searchItemID);
			pouchItemCount++;
			printf("アイテムを入手した!\n");
			_getch();
		}
	}

	field[cursorY][cursorX] = CELL_TYPE_NONE;
	cursorX++;
	field[cursorY][cursorX] = CELL_TYPE_PLAYER;
	player.HUNGER--;
	break;
case 'm':
	menuScreen(itemPouch);
	break;
default:break;
}

メニュー画面の改良

アイテムを取得できるようになったので、実際に何を持っているのか確認でき、使えるようにしていきます。これはアイテムの取得の確認も兼ねています。メニュー画面で'1'を入力したら所持しているアイテムを全て表示するようにします。これは過去のノートに載っているprint関数を参考にしましょう。
問題は、何を表示するかです。今アイテムIDを取得することはできましたが、肝心の名前が定められていません。IDに応じてアイテム名を返すidToName関数をまずは作成します。

char *idToName(int itemID)
{
	switch (itemID) {
	case CELL_TYPE_BREAD:
		return "パン";
	case CELL_TYPE_BIG_BREAD:
		return "大きなパン";
	}
}

アイテムが増えて行く度にここを更新していきましょう。

さあ、いよいよ所持アイテムの表示になります。menu画面で'1'を入力したときに所持しているアイテムを全て表示するようにします。もしアイテムを持っていなかった場合、0を返すようにします。以前のノートにあるprint関数を参考にしてshowPouchItem関数も同時に作成して使います。

int showPouchItem(POSSESSION_t *list, int *count)
{
	list = list->next;
	if(list == NULL) return 0;
	while (list != NULL) {
		*count += 1;
		printf("%d.%s\n", *count, idToName(list->ID));
		list = list->next;
	}
	return 1;
}

そして最後に、使えるようにしましょう。所持アイテムを表示した後で再び入力を待機させ、使うかどうかを選択させます。使うなら効果を発動させましょう!
(他にも本来ならありますが今回はこれだけにしておきましょう。)

所持アイテムの選択時に、例えば、
1.大きなパン
2.パン
3.大きなパン
とあったとき、パンを選びたいときは2と選択するようにしたいのですが、所持アイテムが増えた時、所持アイテム数が二桁になることがあります。ここではscanf_s関数を用いて入力しますが、scanf_s関数は数字だけでなく他の文字も入力できてしまうので、数字だけ入力する機構を作成することによってコマンド入力をします。

int selectItem; //アイテムの選択用
char buf[64]; //scanf_s関数の後処理用
while(1){
	if(scanf_s("%d",&selectItem) == 1) break;
	printf("数字で入力してください。\n");
	scanf_s("%s",buf,63); //数字以外が入力された場合、入力した文字をここで処理する
}

このような機構で数字のみを入力します。
補足:scanf_s関数は入力に成功した個数を返してくれるので、もし%dに対して文字列が入力されればそれは入力失敗と判定され、戻り値のカウントには入りません。この時scanf_s関数の特性で、入力に失敗した場合その入力した文字はプログラムの中で漂ってしまいます。なので、char buf[64];を準備してそこに吸収してあげたわけです。(別にchar型ならなんでもOK)
以下、case 1:の内容

case 1:{
	int count = 0; //所持アイテムの番号
	int selectItem; //アイテムの選択用
	char buf[64]; //scanf_s関数の後処理用

	if (showPouchItem(list, &count) == 0) {
		printf("アイテムを持っていません。\n");
		_getch();
		return;
	}
		
	//数字しか入力を受け付けない機構
	while (1) {
		if (scanf_s("%d", &selectItem) == 1) break;
		printf("数字で入力してください。\n");
		scanf_s("%s", buf, 63); //数字以外が入力された場合、入力された文字をここで処理する
		}
		
		if (1 <= selectItem && selectItem <= count) {
			printf("%d番のアイテムを使いますか?\n", selectItem);
			printf("1:はい 2(またはその他のキー):いいえ\n");
			if (_getch() != '1') break;
			else {
			printf("アイテムを使用した!\n");
			consumeItem(person, list, selectItem);
			_getch();
			}
		}
		break;
	}
}

おわりに

なんとかアイテム関連を実装することができました。下準備が大変でしたね。次回は敵の概念を追加してみようと思います。お疲れ様でした。

vol.12-1 ファイルの分割、リスト構造

目次

はじめに

メニュー画面の作成等の前に、ソースコードが肥大化してきたのでファイルの分割を行いたいと思います。これは理解しなくても今後問題はないので変更点のみ押さえておいてくださいな。
そして、vol.12-2で用いるリスト構造についての簡単な解説の引用を貼っておきます。活用しましょう。

ヘッダファイルとCファイル、extern宣言

ヘッダファイルと言うとstdio.hやtime.hなどがありますが、自作することができます。main関数を記述する以外に新たにCファイルを作成することも可能です。今回のゲーム作成ではグローバル変数を多用しているので、ファイル間でグローバル変数を使えるようにしないといけません。そもそもグローバル変数は1つのファイル内のみ有効な変数になるので、それを拡張するために新たに作成したCファイルで「extern宣言」というものをしてやります。

書式

extern データ型 データ名;

extern int x;

ここではint型のxを「宣言」するだけなので、定義(初期化)は別のファイルで行います。(今回の場合ならmain関数で初期化してやる。)定義では、

int x=0;

のように記述してあげればよいです。extern宣言は各変数に対して一度だけするようにしましょう。
自作ヘッダ(今回はgame.hとした)では、まず最初に#pragma onceと入力してやります。その後に#include...とすることによって二重includeによるエラーを起こらないようにしてくれます。今回はincludeするものを全てgame.hで管理するようにするので、game.hに使用する全てのヘッダを記述してやります。
game.hに記述する内容は、include指令マクロ(#define)指令列挙体定数(enum)構造体(typedef struct)関数のプロトタイプ宣言です。ヘッダファイルはとにかく宣言です。細かいことは別のところに書くので放置!
もう一つのCファイル(今回はfunc_def.cとした)には、冒頭に

#include "game.h"

と記述するのを忘れないようにしましょう。ここで関数の定義をしていくので必ず必要になります。また上述のように、グローバル変数の宣言はここで行ってやります。
main関数では、#include "game.h"としてinclude指令を一度で済むようにしました。グローバル変数の定義はここで行うので必ず行いましょう。初期化する必要のないと思っているグローバル変数も、裏側で適当な値が代入されて初期化が行われているので、例えばint areaCount;とするだけでもこれは定義になります。あくまでもfunc_def.cではextern「宣言」をしているので、いつものようにint型で宣言するのは「定義」することにあたります。

以下、ファイル毎のソースコード(一部省略)

---game.h---

#pragma once

#include...

#define...

enum {...};

//ルームの座標・横幅・高さをひとまとめに管理するために構造体ROOM_tで定義
typedef struct {...}ROOM_t;

/*
エリアの座標・横幅・高さをひとまとめに管理するために構造体AREA_tで定義
また、エリア内にルームを作成するのでルームもここで管理します。
orderRoom関数用にチェック項目を追加
*/

typedef struct {...}AREA_t;

typedef struct {...}PERSON_t;

typedef struct {...}ITEM_t;

int random(int, int);
void spritArea(int);
void generateField(void);
void orderRoom(int);
void setRandomIcon(int, int *, int *);
---func_def.c---

#include "game.h"

extern AREA_t areas[AREA_MAX]; //各エリアの座標・横幅・高さの情報をしまっておける
extern int areaCount; //エリアの個数
extern int field[FIELD_HEIGHT][FIELD_WIDTH]; //座標毎のフィールドの状態
extern int areaNumber[FIELD_HEIGHT][FIELD_WIDTH]; //座標に対応するエリア番号をしまっておける
extern char cellSymbol[CELL_COUNT][3];
extern int order[AREA_MAX]; //ルームをつなげる順番
extern int orderIndex; //order[]の添え字
extern int connectErrorCount; //エラー発見用

int random(int start, int end){...}

//エリアを分割して、エリア毎に座標・横幅・高さを取得する関数
//oldAreaIndex : 分割前のエリア番号
//newAreaIndex : 分割後のエリア番号
void spritArea(int oldAreaIndex){...}

void generateField(void){...}

void orderRoom(int areaIndex){...}

void setRandomIcon(int CELL_TYPE, int *x, int *y){...}

---main.c---

#include "game.h"

AREA_t areas[AREA_MAX]; //各エリアの座標・横幅・高さの情報をしまっておける
int areaCount; //エリアの個数
int field[FIELD_HEIGHT][FIELD_WIDTH]; //座標毎のフィールドの状態
int areaNumber[FIELD_HEIGHT][FIELD_WIDTH]; //座標に対応するエリア番号をしまっておける
char cellSymbol[CELL_COUNT][3] = {
	"・", //CELL_TYPE_NONE(0)番目の配列
	"■", //CELL_TYPE_WALL(1)番目の配列
	"@", //CELL_TYPE_PLAYER(2)番目の配列
	"巛", //CELL_TYPE_STAIRS(3)番目の配列
	"ア"  //4番目の配列にアイテムアイコン
};

int order[AREA_MAX]; //ルームをつなげる順番
int orderIndex = 0; //order[]の添え字
int connectErrorCount = 0; //エラー発見用

int main(void){...}

リスト構造

~データ構造(リストについて)~
データ系列の管理
①データの参照
②データの追加
③データの削除

【配列】
参照:添え字を用いて簡単に実現できる
追加、削除:1つずつずらす作業が必要

【リスト】
参照:先頭からたどっていく必要がある
追加、削除:ずらす必要がない

―リストとはー
データの追加や削除を低コストで実現できるデータ構造
セルをポインタで繋げて実現する

セル:要素とポインタ(次の要素)を格納している箱→一つのセルで一つのデータを実現する

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<conio.h>

//セルを表す構造体
struct element{
 int data; //要素
 struct element *next; //次の要素
};
//セルの作成
struct element *create(int dat) {
 struct element *p;

 //セルの領域を確保
 p = (struct element *)malloc(sizeof(struct element));

 p->data = dat;
 p->next = NULL;
 return p;
};
//(リストlは0番目)k番目の要素を返す
int access(struct element *l, int k)
{
 if (k > 1)
  return access(l->next, k - 1);
 else {
  return (l->next->data);
 }
}

//(リストlを0番目とする)k番目に要素datを追加
void insert(struct element *l, int k, int dat)
{
 struct element *p;

 if (k > 1)
  insert(l->next, k - 1, dat);
 else {
  p = create(dat);
  p->next = l->next;
  l->next = p;
 }
}
//(リストは0番目)k番目のリストを削除
void delete(struct element *l, int k)
{
 struct element *p;
 if (k > 1)
  delete(l->next, k - 1);
 else {
  p = l->next;
  l->next = l->next->next;
  free(p);
 }
}
//リストlから全て表示する
void print(struct element *l)
{
 while (l != NULL) {
  printf("%d ", l->data);
  l = l->next;
 }
 printf("\n\n");
}
//リストにデータを入力
void Input_element(struct element *l, int n)
{
 int dat;
 for (int i = 1; i < n; i++) {
  printf("%d番目:", i);
  scanf("%d", &dat);
  insert(l, i, dat);
 }
}

int main(void)
{
 int n,a;
 struct element *head;
 head = create(-1); //ダミーのセル
    
 Input_element(head, 6);
 print(head->next);

 insert(head, 2, 100);
 printf("2番目に100を追加してみた\n");
 print(head->next);

 delete(head, 3);
 printf("3番目を削除してみた\n");
 print(head->next);

 printf("3番目だけ表示してみた\n");
 a = access(head, 3);
 printf("%d\n", a);
 
 
 _getch();
 return 0;
}

―ダミーのセルとはー
データなしを表す
・探そうとする要素、追加しようとする空間、削除しようとする要素に直前の要素があることが保証される。

データの参照:access
・リストをたどって目的の一つ前のセルまでたどる
・次のセルのデータが参照すべきデータ

追加と削除については図がないと説明できない(語彙力がない)ので省略
[引用:LINEグループ「ゲームプログラミング」内 LINEノート 2019/7/17 14:56]

上述にある追加と削除についての解説図も追加しておきます。
データの追加:insert
f:id:sion2000114:20190907152344p:plain
データの削除:delete
f:id:sion2000114:20190907152508p:plain

おわりに

ここは準備段階になるので、次のvol.12-2からが本文になります。
では、お疲れ様でした。