LoginSignup
1
1

More than 5 years have passed since last update.

キャラクターの移動 - DXライブラリ

Last updated at Posted at 2018-11-21

準備

 下準備に関してはここでは触れる予定はないのでDXライブラリ置き場を参考にDXライブラリの導入とプロジェクトの設定までは行っておいてください。

開発内容

 今回はウィンドウ上でキャラクターを動かすプログラムを紹介したいと思います。大まかな流れとしては

   ①ウィンドウの生成
     ↓
   ②キャラクターの描画
     ↓
   ③キー入力判定の作成
     ↓
   ④キー入力に従ったキャラクターの移動
     ↓
   ⑤画面端の処理(今回は壁として判定)

   ⑥ちょっとした小技の紹介

 といった流れになります。

①ウィンドウの生成

 ウィンドウの生成はDXライブラリがほぼ自動で行ってくれます。自力でするとウィンドウプロシージャとか面倒なんですけどね...。

#include "DxLib.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    if (DxLib_Init() == -1)     // DxLibの初期化
        return 0;

    DxLib_End();                // DxLibの終了
    return 0;
}

 普段の開発ではint main(void)ですがウィンドウを作るとき(GUI)はint WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)となります。たったこれだけでウィンドウが表示されます。
 ここでまだ説明していないif (DxLib_Init() == -1)DxLib_End()というのが出てきました。説明はしませんがDXライブラリを使うときのお約束と覚えておきましょう。

ちょっとオリジナリティを出してみよう

 このままでも使えるのですが画面のサイズとか背景色くらいは自分で設定したいですよね。ということで少しコードを追加してみます。

#include "DxLib.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    ChangeWindowMode(TRUE);                 // ウィンドウモードで起動
    SetWindowSize(640, 480);                // ウィンドウサイズ設定
    SetBackgroundColor(255, 255, 255);      // 背景色設定

    if (DxLib_Init() == -1)     // DxLibの初期化
        return 0;

    DxLib_End();
    return 0;
}

 これらの処理をDXライブラリの初期化前に行うことで設定の反映されたウィンドウが生成されます。初期化後に行った場合、デフォルト設定のウィンドウが生成されてから設定が変更されるため、画面にちらつきが発生してしまいます。
※関数によっては初期化前だと実行できないものや正しく動作しないものがあるので、確認した上で設計しましょう。

②キャラクターの描画

 今回は頑張って?作ったこのイラストを使っていきたいと思います。サイズは32×32です。
charcter.png
背景はすでに透過してあるのでたぶん大丈夫だと思います。丸も満足にかけなくて情けない...。

    int charcterImage = LoadGraph("charcter.png");  // 画像の読み込み
    DrawGraph(0, 0, charcterImage, TRUE);           // 画像の描画

    WaitKey();      // キーを押すまで処理待ち

 これをif (DxLib_Init() == -1)からDxLib_End()の間に記述してください。実行して下の画像のようになっていれば成功です。
image.png

③キー入力判定の作成

 キー入力も本来ならイベントハンドラやあれやこれやと面倒なのですがそこはDXライブラリがしっかりとサポートしてくれています。
 int GetHitKeyStateAll(char* KeyStateArray)という関数が用意されています。

キー キーコード
ESC      KEY_INPUT_ESCAPE
十字キー右 KEY_INPUT_RIGHT
十字キー左 KEY_INPUT_LEFT
十字キー上 KEY_INPUT_UP
十字キー下 KEY_INPUT_DOWN

 キーコードは上表のようにDXライブラリ内で定義されています。表には今回使うコードしか載せていませんので興味がある方はDXライブラリのリファレンスページで検索してみてください。

 では実際にコードを書く前にまたしても注意事項があります。DXライブラリで無限ループを作る際はint ProcessMessage()という関数を定期的に呼び出す必要があります。エラーが起きなければ戻り値は0なので一般的に

while (!ProcessMessage()) {
    // ここに処理を記述
}

このように無限ループを作ります。正常時は0が戻ってきますのでwhile (!0)と同じとなり、while (1)と同義になります。
 これに関しても詳しい説明は省くのでとりあえず無限ループのお約束と覚えておきましょう。

④キー入力に従ったキャラクターの移動

 処理の流れは単純で「貼る→隠す→貼る」をひたすら繰り返すだけです。
 では実際にコードを記述していきます(今回は同時押しはできないことにします)。

    char keyState[256];                             // キー情報格納用変数
    GetHitKeyStateAll(keyState);                    // キー入力情報取得

    while (!ProcessMessage() && !keyState[KEY_INPUT_ESCAPE]) {
        GetHitKeyStateAll(keyState);
        drawX = x, drawY = y;
        if (keyState[KEY_INPUT_RIGHT])
            x += 2; // 塗りつぶしX座標, 塗りつぶしY座標, 移動量
        else if (keyState[KEY_INPUT_LEFT])
            x -= 2;
        else if (keyState[KEY_INPUT_DOWN])
            y += 2;
        else if (keyState[KEY_INPUT_UP])
            y -= 2;

        if (CheckHitKeyAll()) {
            DrawBox(drawX, drawY, drawX + 32, drawY + 32, GetColor(0, 0, 0), TRUE); // 周辺の塗りつぶし
            DrawGraph(x, y, charcterImage, TRUE);
            Sleep(20);                              // 移動のスピード調整
        }
    }

 GetHitKeyStateAll(keyState);ではchar型配列keyStateに全キーの入力状態を格納しています。keyStateは256個の要素を持つ配列でなくてはなりません。

状態
未入力
入力の瞬間
入力している

 例えばESCキーの状態にアクセスするならkeyState[KEY_INPUT_ESCAPE]にアクセスすれば上表のいずれかの値が格納されています。あとは入力されているかどうかはif (keyState[KEY_INPUT_ESCAPE])で、入力の瞬間はif (keyState[KEY_INPUT_ESCAPE] == 1)で、未入力ならif (!keyState[KEY_INPUT_ESCAPE])で確認できます。

 DrawBox(drawX, drawY, drawX + 32, drawY + 32, GetColor(0, 0, 0), TRUE);は四角を描画する関数です。画像を移動して再描画する際に前の画像を消さないと再描画した画像と重なってしまいます。
(例)
1. 画用紙にシールを貼る
2. 貼ったシールをはがすか上から新しい紙を貼って隠す
3. 新しいシールを貼る

この3ステップでキャラクターを動かしています。DrawBox関数は2のステップの「新しい紙を貼って隠す」処理を行っています。はがす処理ならClearDrawScreen();の鶴の一声で終わるのですがアニメーションとして見せるには時間がかかりすぎてしまいます。ほんの1割に満たない範囲を処理するのにすべてを対象にするのは無駄が多いので今回はDrawBox関数を使用しています。ちなみにマップチップを使ってマップを描画した際はこれの応用でマップチップのIDを割り出してその部分のチップを再描画することで対処します。

 Sleep(20);はただの処理待ちです。これを付けずに実行すると一瞬キーを押しただけで画面から消えてしまうのでそれを避けるため少し待っても押されているようならユーザーが連続の処理を望んでいるとして再度処理を行うといった流れになってます。

⑤画面端の処理

 画面端を壁と同義であるとした場合の処理について学んでいきます。その処理の仕方には大きく2種類あります。(実際にはもう少しありますが)

  1. 座標を操作してエラーチェックを行い、エラー値なら元に戻す
  2. 仮座標に現座標をコピーして仮座標で計算して正常値なら現座標にコピーする

 まぁどっちもどっちなんですが現座標がエラー値になることがない2番の方法で実装していきます。それに伴って処理の順番等を入れ替えているのでコード全体を載せておきたいと思います。

#include "DxLib.h"

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    ChangeWindowMode(TRUE);                 // ウィンドウモードで起動
    SetWindowSize(640, 480);                // ウィンドウサイズ設定
    SetBackgroundColor(0, 0, 0);            // 背景色設定(DxLibはデフォルトで黒だがあえて)

    if (DxLib_Init() == -1)                 // DxLibの初期化
        return 0;

    int charcterImage = LoadGraph("charcter.png");  // 画像の読み込み

    int x = 0, y = 0;                               // 画像の現座標
    int virtualX = x, virtualY = y;                 // 画像の仮座標
    int drawX = x, drawY = y;                       // 塗りつぶし用座標

    DrawGraph(x, y, charcterImage, TRUE);           // 画像の描画

    char keyState[256];                             // キー情報格納用変数
    GetHitKeyStateAll(keyState);                    // キー入力情報取得

    while (!ProcessMessage() && !keyState[KEY_INPUT_ESCAPE]) {
        GetHitKeyStateAll(keyState);
        if (keyState[KEY_INPUT_RIGHT])
            virtualX += 2;  // 移動量
        else if (keyState[KEY_INPUT_LEFT])
            virtualX -= 2;
        else if (keyState[KEY_INPUT_DOWN])
            virtualY += 2;
        else if (keyState[KEY_INPUT_UP])
            virtualY -= 2;

        if (!((virtualX <= (640 - 32)) && (virtualX >= 0) && (virtualY <= (480 - 32)) && (virtualY >= 0))) {
            virtualX = x;
            virtualY = y;
            continue;
        }

        drawX = x, drawY = y;
        x = virtualX, y = virtualY;

        if (CheckHitKeyAll()) {
            DrawBox(drawX, drawY, drawX + 32, drawY + 32, GetColor(0, 0, 0), TRUE); // 周辺の塗りつぶし
            DrawGraph(x, y, charcterImage, TRUE);
            Sleep(20);                              // 移動のスピード調整
        }
    }

    DxLib_End();
    return 0;
}

 if (!((virtualX <= (640 - 32)) && (virtualX >= 0) &&.....))は4つの条件のうち1つでも満たされていなければ仮計算に使用した変数をリセットしてループの最初に戻っているだけです。範囲指定はシューティングの当たり判定と同じ方法で行っているので気になる人は調べてみてくださいね。

ちょっとした小技の紹介

 先ほどのプログラムでも動作としては問題ないのですが描画に時間がかかりいわゆる「ちらつき」が発生してしまいます。このままでは見た目が悪いので修正したいと思います。
 今は1枚の画用紙で「貼る→隠す→貼る」という処理をしているのでユーザーには間の隠す処理がちらつきとして違和感を与えています。そこで先に「隠す→貼る」の処理を行った透明のフィルムを作り、それを画用紙に重ねることで一度の処理で「隠して貼る」となり画面が更新されたように見せることができます。

    if (DxLib_Init() == -1)
        return 0;

    SetDrawScreen(DX_SCREEN_BACK);

 DXライブラリを初期化したあとSetDrawScreen()を呼び出すことで描画先を裏画面とよばれる先の透明なフィルムに変更することができます。しかし、フィルムに書いてもユーザーに見えなくては意味がありません。そこでScreenFlip()を呼び出します。これがフィルムを画用紙に重ねる処理にあたります。使い方としてはScreenFlip()からScreenFlip()の間の描画内容を表示します。

    if (DxLib_Init() == -1)
        return 0;

    int x = 0, y = 0;

    SetDrawScreen(DX_SCREEN_BACK);
    while (!ProcessMessage && !CheckHitKey(KEY_INPUT_ESCAPE)) {
        DrawBox(x, y, x + 32, y + 32, GetColor(0,0,0), TRUE);
        ScreenFlip();
        x += 64, y += 64;
        //Sleep(20);
    }

 32×32の黒い箱を斜めに描き続けるプログラムですね。もし変化がわかりにくければ一番下のコメントアウトを外すとわかりやすくなるので試してみてください。

最後に

 今回はキー判定とアニメーションを組み合わせてキャラクターが移動するプログラムを作成しました。もしゲーム等で使用する際はSleep関数を無作為に使うのではなくFPSを計算したうえで誤差修正で使うにとどめるようにしましょう。
 最後までお読みいただきありがとうございました。

1
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1