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関数は次回以降使っていきます。
では、お疲れ様でした。