#0.はじめに
はじめてQiitaに投稿します、Bana7です。ロマンをもとめ、わざわざC言語でゲームを作りました。素人目で変なコードを書いてるかもしれませんが、ご理解の程宜しくお願いいたします。
また本記事はQiita Advent Calendar 2021の1日目です。
ニッチな話ではあるかもしれませんが、誰かの助けになれば光栄です
##注意点
本記事を読み進めるにあたり、以下の点に注意していただきたいです。
- バグが発生する可能性がある点
- 可読性があまり見られない点
- 最低限ゲームとして遊べることを目標に実装した点
- マップ生成のアルゴリズムと云々。の話ではない点
##ターゲット
また、本記事では以下の2点に当てはまる方に焦点をあて、論じていきます。
- ncursesで画面制御を行いたい方
- C言語でゲームを作ろうと思っている方
##作成動機
単純にやってみたかったというのが第一です。
また、今回は一度作ったソースコードを修正するにあたり、「ncursesを使おう。」ということになりました。
#1.完成結果
ソースコードはこちらからどうぞ
##動作環境
動作環境としては、Linux系OSやmacOS,Windows(WSL)等で動作します。
##特徴1:セーブ機能がある!
fscanfの仕様をいまいち理解していなかったため、結構手こずりました。
機能としては大それたものではありませんが、声を大にして言いたい。
##特徴2:入るたびにマップが変わる!
これがなかったらローグライクじゃねぇ!
てことで実装してみましたが、驚くほどシンプルです。詳細はのちほど。
#2.プログラム紹介
##2-1.ncursesによる画面制御
initscr();//端末制御開始のおまじない
//色の設定
start_color();
init_pair(1,COLOR_YELLOW,COLOR_BLACK);
init_pair(2,COLOR_WHITE,COLOR_BLACK);
init_pair(3,COLOR_BLUE,COLOR_BLACK);
init_pair(4,COLOR_MAGENTA,COLOR_BLACK);
init_pair(5,COLOR_RED,COLOR_BLACK);
init_pair(6,COLOR_BLACK,COLOR_BLACK);
init_pair(7,COLOR_BLACK,COLOR_RED);
bkgd(COLOR_PAIR(2));//デフォルトの色を設定
cbreak();//Enter キー不要のモード
attrset(COLOR_PAIR(3));//色3を使う
...
endwin();//画面制御終了のおまじない
return 0;
//move(行, 桁) : 指定した行、桁にカーソルが移動
//printw(書式,データ,データ,・・・) : ncurses版のprintf()
//mvprintw(行,桁,書式,データ,データ,・・・) : move() + printw()
mvprintw(1,8,"+--------+");
mvprintw(2,8,"|%-8s|",getName(getStates(0)));
加えて、
erase();//画面の表示内容を消去
refresh();//画面の表示
簡単な画面制御であれば、これらの関数で十分遊べると思います。
ncursesの詳しい使い方については最後に参考サイトとして挙げています。
##2-2.マップ生成
#define MAP(x,y) MAP[y * mapWidth + x]
extern int mapHeight,mapWidth,OBJECTS,ENEMYS,X_Player,Y_Player;
char* map(){
int x,y,z,count = 0;
char* MAP = NULL;
srand((unsigned int)time(NULL));
mapHeight = (rand() % (MAX_MAP_HEIGHT-MIN_MAP_HEIGHT)) + MIN_MAP_HEIGHT;
mapWidth = (rand() % (MAX_MAP_WIDTH-MIN_MAP_WIDTH)) + MIN_MAP_WIDTH;
MAP = (char*)malloc(mapHeight*mapWidth *sizeof(char));
if(MAP == NULL) exit(1);
OBJECTS = ((mapHeight*mapWidth) / 10);
ENEMYS = ((mapHeight*mapWidth) / 15);
//基本マップ生成 壁>"-","|"
//MAP[i][j] を *(MAP + i*mapwidth + j) で表す
for(int i=0;i<mapHeight;i++){
for(int j=0;j<mapWidth;j++){
if(i<=1||i>=mapHeight-2||j<=1||j>=mapWidth-2) MAP(j,i) = '#';
else MAP(j,i)= '.';
}
}
// //障害物生成 障害物>"#"
for(int i=0;i < OBJECTS;i++){
while(1){
x=(rand()%(mapWidth-4)) + 2;
y=(rand()%(mapHeight-4)) + 2;
if(MAP(x,y) == '.'){
MAP(x,y) = '#';
break;
}
}
}
// //プレイヤの位置決定 プレイヤー>"@"
while(1){
x=(rand()%(mapWidth-4)) + 2;
y=(rand()%(mapHeight-4)) + 2;
if(MAP(x,y)=='.'){
MAP(x,y)=getImage(getStates(0));
X_Player=x;
Y_Player=y;
break;
}
}
count = 0;
// //敵の位置決定 敵>"E"
while (count < ENEMYS){
x=(rand()%(mapWidth-4)) + 2;
y=(rand()%(mapHeight-4)) + 2;
if(MAP(x,y)=='.'){
z = 1+(rand() % 4);//1~4
MAP(x,y) = getImage(getStates(z));
count++;
}
}
// //ゴール(階段) ゴール>"K"
while(1){
x=(rand()%(mapWidth-4)) + 2;
y=(rand()%(mapHeight-4)) + 2;
if(MAP(x,y)=='.'){
MAP(x,y)='K';
break;
}
}
return MAP;
}
ポインタ変数MAPを別で定義した変数MAX_MAP_HEIGHTなどでマップのサイズを可変長化を実現しています。そして、
- #(壁)と.(道)で初期化
- 乱数の座標が.(道)を指すまで繰り返す
- 目的のオブジェクトを配置(敵やプレイヤー、階段等)
でランダムマップを表現しました。しかし、これはマップが一つの部屋であるからできます。
複数の部屋とそれをつなぐ通路を実現させる場合には、別のアルゴリズムを考える必要があります。
つまるところ、このアルゴリズムはもっと効率化できますが、その修正作業は未来の自分にまかせるとします。
##2-3.ステータス管理
プレイヤーや敵のステータスは以下の通りになっています。
typedef struct STATE{
char image;//プレイヤーなら@
char name[8];
int LV;
int HP;
int MaxHP;
int ATK;
int DEF;
int nextLV;
int ExpPoint;
int Motivation;
}state;
void setLV(state* const a,int x){
a->LV = x;
}
int getLV(const state* const a){
return a->LV;
}
ステータスのアクセスはオブジェクト思考的に実装しました。
Qiita - C言語でオブジェクト指向を表現する (クラス、継承)を参考にしました。
#3.最後に
中略ではありますが、バトルシステムはただのジャンケンです。
とりあえず動けば良いかぐらいのモチベで作りました。
エラーを解消しようと試行錯誤した結果、冗長なコードになってしまいました。
次からはオブジェクトのつながりを意識して実装しようと思いました。
##参考サイト
Ncurses 入門
curses による端末制御
Qiita - C言語でオブジェクト指向を表現する (クラス、継承)