Edited at

C言語でオブジェクト指向:「昔懐かしRobotsゲーム」をリファクタリング

@fygar256 さんが「昔懐かし、CBM-3032のようなキャラクタゲームをncurseswで再現しやう。」を投稿されました。

昔のゲームということで、単純変数と配列と関数で実装されています。バラバラに散らばって定義されたデータと処理、昔ながらの実装でした。

オブジェクト指向では、登場人物や物事を抽出し、関連するデータと処理をまとめてクラス定義(ユーザ型定義)します。

このゲームでは、「プレイヤー」「ロボット」「岩」「画面」「ゲーム」といったクラスを抽出できました。

オブジェクト指向言語であればクラス定義構文があり、データと処理の集まりを一つのクラスブロック内に定義できます。

C言語にはクラス定義がないので、クラス毎のデータを構造体として型定義(typedef)し、データを処理する関数は型名と同じ名前で始まるようにします。

データ毎に関数を呼び出すに渡すときには、データを第一引数にして、変数名をthisにします。オブジェクト指向では、クラスに属する個々のデータを「インスタンス」といいます。インスタンスは、実体・実物・実データという意味で、thisは「このクラスのインスタンス」という意味です。

構造体のtypedefは、静的データ定義用は小文字で始まる型名、参照用ポインタ型は大文字で始まる型名にします。

構造体ではない単純型も、大文字で始まる型にtypedefして、専用の型にしてしまいましょう。

そのようにして書いたソースを以下に示します。

オブジェクト指向的な考え方が伝われば幸いです。

もちろん、これが最終形ではありません。他にも良いアイデアなどありましたらコメントいただけたると有難いです。

C言語でもオブジェクト指向プログラミングを楽しみましょう。

//

// 昔懐かし、CBM-3032のようなキャラクタゲームを
// ncurseswで実現。
// Raging robots.
//

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <time.h>
#include <locale.h>
#include <ncursesw/ncurses.h>

#define NUM_ROCK (120)
#define NUM_ROBOT (12)

#define rnd(x) (rand() % (x))
#define center(x) ((x) / 2)

typedef const char * const String;
typedef int X, Y, Width, Height;

// class Symbol extends String {

typedef String Symbol;

static Symbol ROCK = "#";
static Symbol ROBOT = "@";
static Symbol PLAYER = "O";
static Symbol CRASH = "X";
static Symbol SPACE = " ";

// }

// class Screen {

struct {
Width width;
Height height;
} screen;

void screen_open() {
initscr(); // 画面初期化
noecho(); // キーボードエコーなし
curs_set(0); // カーソル非表示
getmaxyx(stdscr, screen.height, screen.width); // 画面サイズ取得
screen.width /= 2; // 全角文字表示で2倍幅になるので画面幅を半分に見立てる
}

void screen_close() {
endwin();
}

void screen_text(X x, Y y, String message) {
move(y, x * 2);
addstr(message);
}

bool screen_contains(X x, Y y) {
return 0 <= x && x < screen.width
&& 0 <= y && y < screen.height;
}

// }

// class Character {

typedef struct {
bool is_vaild;
X x;
Y y;
} character_t, *Character;

void Character_put(Character this, X x, Y y, Symbol symbol) {
this->is_vaild = true;
this->x = x;
this->y = y;
screen_text(this->x, this->y, symbol);
}

void Character_put_random(Character this, Symbol symbol) {
Character_put(this, rnd(screen.width), rnd(screen.height), symbol);
}

void Character_put_center(Character this, Symbol symbol) {
Character_put(this, center(screen.width), center(screen.height), symbol);
}

bool Character_hit(Character this, const Character target) {
return this != target
&& target->is_vaild
&& this->x == target->x
&& this->y == target->y;
}

// }

// class Rock extends Character {

typedef character_t rock_t;
typedef Character Rock;

static rock_t rocks[NUM_ROCK];
static const Rock rocks_end = &rocks[NUM_ROCK];

void rocks_init() {
for (Rock rock = rocks; rock != rocks_end; rock++)
Character_put_random(rock, ROCK);
}

bool rocks_hit(const Character target) {
for (Rock rock = rocks; rock != rocks_end; rock++)
if (Character_hit(rock, target)) return true;
return false;
}

// }

// class Robot extends Character {

typedef character_t robot_t;
typedef Character Robot;

static robot_t robots[NUM_ROBOT];
static const Robot robots_end = &robots[NUM_ROBOT];

void robots_init() {
for (Robot robot = robots; robot != robots_end; robot++)
Character_put_random(robot, ROBOT);
}

bool robots_hit(const Character target) {
for (Robot robot = robots; robot != robots_end; robot++)
if (Character_hit(robot, target)) return true;
return false;
}

void robots_move(const Character target) {
for (Robot robot = robots; robot != robots_end; robot++) {
if (!robot->is_vaild) continue;

// 現表示消去
screen_text(robot->x, robot->y, SPACE);

// ターゲット(プレイヤー)を追いかけるように移動
if (robot->x < target->x) robot->x += 1;
if (robot->x > target->x) robot->x -= 1;
if (robot->y < target->y) robot->y += 1;
if (robot->y > target->y) robot->y -= 1;

// 他のロボットや岩に衝突してなければ表示
if (robots_hit(robot) || rocks_hit(robot))
robot->is_vaild = false;
if (robot->is_vaild)
screen_text(robot->x, robot->y, ROBOT);
}
}

bool robots_are_wiped() {
for (Robot robot = robots; robot != robots_end; robot++)
if (robot->is_vaild) return false;
return true;
}

// }

// class Player extends Character {

typedef character_t player_t;
typedef Character Player;

static player_t player;

void player_init() {
Character_put_center(&player, PLAYER);
}

void player_move() {
player_t move = player;
do {
switch(getch()) {
case '7': move.x -= 1, move.y -= 1; break;
case '8': move.x += 0, move.y -= 1; break;
case '9': move.x += 1, move.y -= 1; break;

case '4': case 'u': move.x -= 1, move.y += 0; break;
case '5': case 'i': move.x += 0, move.y += 0; break;
case '6': case 'o': move.x += 1, move.y += 0; break;

case '1': case 'j': move.x -= 1, move.y += 1; break;
case '2': case 'k': move.x += 0, move.y += 1; break;
case '3': case 'l': move.x += 1, move.y += 1; break;
default: continue;
}
} while (false);

if (screen_contains(move.x, move.y)) {
screen_text(player.x, player.y, SPACE);
player = move;
screen_text(player.x, player.y, PLAYER);
}
}

bool player_is_crashed() {
return robots_hit(&player) || rocks_hit(&player);
}

// }

// class Game {

void game_show_instructions() {
clear();

printw("Raging robots Ver 0.8\n"); // printwで、stdscrに出力します。

printw("Mission : survive from raging robots.\n");
printw("%s -- 岩 : 自分やロボットが当たると死ぬ\n", ROCK);
printw("%s -- ロボット : 段階的に追い詰めてくるロボット\n", ROBOT);
printw("%s -- プレイヤー : キーで操作し、岩にロボットを誘って"
"消滅させよう\n", PLAYER);
printw("\n");
printw("キーコントロール テンキー \n");
printw(" 7 8 9 7 8 9 \n");
printw(" ↖ ↑ ↗ ↖ ↑ ↗ \n");
printw(" u← i→ o 4← 5→ 6 \n");
printw(" ↙ ↓ ↘ ↙ ↓ ↘ \n");
printw(" j k l 1 2 3 \n");
printw("\n");
printw(" 'i' と '5' は、自分が動かず、ロボットが動くだけです。\n");
printw(" Ctrl-Cで、止まります。\n");
printw("\n");
printw(" Good Luck\n");
printw("\n");
printw("Hit a key to start game\n");

getch();
}

void game_init() {
clear();
rocks_init();
robots_init();
player_init();
// 処理簡略化のため、特に岩とロボットと、プレイヤーの
// 初期位置のバッティングは考えないこととします。
}

typedef enum { WIN, LOSE } GameResult;

GameResult game_battle() {
while (true) {
player_move();
if (player_is_crashed()) return LOSE;

robots_move(&player);
if (player_is_crashed()) return LOSE;
if (robots_are_wiped()) return WIN;
}
}

void game_play() {
game_show_instructions();
game_init();
if (game_battle() == WIN) {
screen_text(0, 0, "You Win!!\n");
} else{
screen_text(player.x, player.y, CRASH);
screen_text(0, 0, "You Lose.\n");
}
}

// }

bool try_again() {
printw("Try Again? [y/n] ");
while (true) {
int c = getch();
if (c == 'y') return true;
if (c == 'n') return false;
}
}

int main() {
setlocale(LC_ALL, "");
srand((unsigned)time(NULL));

screen_open();

do {
game_play();
} while (try_again());

screen_close();

return EXIT_SUCCESS;
}