C
C++
console
cmd

コンソールゲーム制作 第1章 描画(4)

前回

コンソールゲーム制作 第1章 描画(3)
前回からだいぶ時間が経ちましたが、暇ができたので続きを書きます。
今回は予定通り簡単に描画できるような仕組みを作っていきます。
本当はちゃんとクラスとC++を使っていきたいところですが、今回は構造体と関数のみで作ります。

必要部品の構造体化

writeconsoleoutput
BOOL WriteConsoleOutput(
  HANDLE hConsoleOutput,      // スクリーンバッファのハンドル
  CONST CHAR_INFO *lpBuffer,  // データバッファへのポインタ
  COORD dwBufferSize,         // コピー元バッファのサイズ
  COORD dwBufferCoord,        // コピー元長方形の左上隅のセル
  PSMALL_RECT lpWriteRegion   // 書き込み先長方形
);

コンソール画面の描画に必要な情報を構造体のメンバーにします。
・スクリーンバッファのハンドル
・データバッファ
・バッファサイズ
・長方形の左上隅のセル
・書き込み先長方形

console.h
#include <windows.h>

#define SCREEN_WIDTH  (100)
#define SCREEN_HEIGHT (20)

typedef struct
{
    HANDLE handle_;
    COORD buffer_size_;
    COORD start_coord_;
    SMALL_RECT sr_;
    CHAR_INFO cell_[SCREEN_HEIGHT][SCREEN_WIDTH];
} Console;

今回はコンソール画面一つで十分なのでグローバルにでも定義しておきましょう。

console.cpp
Console console;

次に初期化と描画関数です。

console.cpp
void InitConsole(void)
{
    //これで標準のウィンドウハンドルが取得できます。
    console.handle_ = GetStdHandle(STD_OUTPUT_HANDLE);
    console.buffer_size_ = { SCREEN_WIDTH, SCREEN_HEIGHT };
    console.start_coord_ = { 0, 0 };
    console.sr_ = { 0, 0, SCREEN_WIDTH - 1, SCREEN_HEIGHT - 1 };
    //構造体の中身を全部0で埋めます。
    ZeroMemory(console.cell_, sizeof(console.cell_));
}

void Draw(void)
{
    //描画処理
    WriteConsoleOutput(console.handle_, (CHAR_INFO*)console.cell_, console.buffer_size_, console.start_coord_, &console.sr_);
    //描画後再び全て0に。
    ZeroMemory(console.cell_, sizeof(console.cell_));
}

今度は描画オブジェクトの構造体も作っておきます。(プレイヤーなどです)

console.h
//最大サイズ
#define OBJ_WIDTH     (40)
#define OBJ_HEIGHT    (10)

typedef struct
{
    float x, y;
} Position;

typedef struct
{
    Position position_;
    CHAR_INFO cell_[OBJ_HEIGHT][OBJ_WIDTH];
} DrawObject;

あとはこの描画用オブジェクトの初期化と画面に出力する関数です。

console.cpp
void InitObject(DrawObject * object)
{
    object->position_.x = 0;
    object->position_.y = 0;
    //こちらも0で埋めます。
    ZeroMemory(object->cell_, sizeof(object->cell_));
}

void Add(DrawObject * object)
{
    for (int w = 0; w < OBJ_WIDTH; w++)
    {
        for (int h = 0; h < OBJ_HEIGHT; h++)
        {
            //初期値が0なので、0以外の場合のみ描画するようにしてます。
            //理由はこのif分をなくしてみるとわかります。
            if(object->cell_[h][w].Char.AsciiChar != 0)
                console.cell_[(int)object->position_.y + h][(int)object->position_.x + w] = object->cell_[h][w];
        }
    }
}

必要なものはこれだけです。

実際に使ってみる

main.cpp
#include "console.h"

void main(void)
{


    DrawObject a, b;

    InitConsole();

    InitObject(&a);
    InitObject(&b);

    //オブジェクトAの生成
    a.cell_[0][0] = { 'A', 200 };
    a.cell_[1][1] = { 'A', 200 };
    a.cell_[2][2] = { 'A', 200 };

    //オブジェクトBの生成
    b.cell_[0][0] = { 'B', 100 };
    b.cell_[1][2] = { 'B', 100 };
    b.cell_[2][1] = { 'B', 100 };
    b.cell_[3][3] = { 'B', 100 };

    //ゲームループ
    while (true)
    {
        //ちょっと移動
        a.position_.x += 0.08f;

        //オブジェクトを画面に追加
        Add(&a);
        Add(&b);

        //画面を描画
        Draw();

        //約60FPSになるように1000/60ミリ秒待つ(てきとう)
        Sleep(1000 / 60);
    }
}

これで終わりです、どうでしょうか。
結構いい簡単に描画できたと思いますが、オブジェクトの生成だけちょっと気になりますね、

少しだけ使いやすくなるようにこういう関数作っておくといいかもしれないです。

console.cpp
void SetObjectFGColor(DrawObject * obj, char * c)
{
    int x = 0;
    int y = 0;
    int fg = 0;
    for (int n = 0; c[n] != '\0'; n++)
    {
        if (c[n] == '\n')
        {
            ++y;
            x = 0;
        }
        else
        {
            if (c[n] >= 'a' && c[n] <= 'f')
            {
                //10 ~ 16に
                fg = c[n] - 87;
            }
            else if (c[n] >= 'A' && c[n] <= 'F')
            {
                //10 ~ 16に
                fg = c[n] - 55;
            }
            else if (c[n] >= '0' && c[n] <= '9')
            {
                //0 ~ 9に
                fg = c[n] - 48;
            }
            else
            {
                //それ以外の場合は色を変更しない
                ++x;
                continue;
            }
            obj->cell_[(int)obj->position_.y + y][(int)obj->position_.x + x].Attributes -= obj->cell_[(int)obj->position_.y + y][(int)obj->position_.x + x].Attributes % 16;
            obj->cell_[(int)obj->position_.y + y][(int)obj->position_.x + x].Attributes += fg;
            ++x;
        }
    }
}

void SetObjectBGColor(DrawObject * obj, char * c)
{
    int x = 0;
    int y = 0;
    int bg = 0;
    for (int n = 0; c[n] != '\0'; n++)
    {
        if (c[n] == '\n')
        {
            ++y;
            x = 0;
        }
        else
        {
            if (c[n] >= 'a' && c[n] <= 'f')
            {
                //10 ~ 16に
                bg = c[n] - 87;
            }
            else if (c[n] >= 'A' && c[n] <= 'F')
            {
                //10 ~ 16に
                bg = c[n] - 55;
            }
            else if (c[n] >= '0' && c[n] <= '9')
            {
                //0 ~ 9に
                bg = c[n] - 48;
            }
            else
            {
                //それ以外の場合は色を変更しない
                ++x;
                continue;
            }
            obj->cell_[(int)obj->position_.y + y][(int)obj->position_.x + x].Attributes = obj->cell_[(int)obj->position_.y + y][(int)obj->position_.x + x].Attributes % 16;
            obj->cell_[(int)obj->position_.y + y][(int)obj->position_.x + x].Attributes += bg * 16;
            ++x;
        }
    }
}

16色なので文字から16進数に変換して使うといいかもしれないです。 0~f(1~16番目の色)
こういう使い方できて少しすっきりしますがまだまだですね。

console.cpp
SetObjectChar(&a, "abc\ndef\ngh\nijkl");
    SetObjectFGColor(&a, "111\n222\n33\n4444");
    SetObjectBGColor(&a, "555\n555\n55\n5555");

ゲームを作るのが目的なので最終的には手打ちでこんな面倒くさいことはしないです。
基本的にゲームのキャラクターは画像を使いますが、コンソールなので画像の表示はできないですが、描画は可能です(?)。

次回

コンソールでBMP描画。

ありがとうございました。