ゲームの演出のテクニック ~時間差編~

  • 7
    Like
  • 0
    Comment
More than 1 year has passed since last update.

Siv3D Advent Calendar 2015 12 日目の記事です。
本日はミツゴロウが担当します。

ゲーム演出について

ゲーム制作において、「見た目」は相当重要な要素です。
どんなにシステム面を凝ったとしても、その見た目が悪ければそれだけでユーザーに評価を下げられてしまいます。逆に、ちょっとしたミニゲームでも見た目の演出がよければ良作となり得ます。どうせつくるなら見た目をよくしていきましょう。

ここで取り上げる記事は「見た目をよくする演出のテクニック」で、イラストレーションのスキルは基本的に必要としません。絵がかけないよ!というプログラマーでも実装できます。
また、このページのソースコードは自由に使って構いません。メソッドだけを参考にし、Siv3D以外の環境で使用しても可。ミツゴロウが用意したテクスチャ画像については、なんか適当なものに差し替えてお使いください。

変数情報と描画は分けて考える

たとえば、上下左右にプレイヤーがマス単位で動くRPGみたいなマップを作ることを考える。単純なものであればこんなコードで動きます。

RPGMap.cpp
# include <Siv3D.hpp>

// プレイヤーのクラス。実用的なサンプルにするために、クラス化できるものはクラス化していく。
class Player {
private:
    // Point型は、座標をint型で表す型で、中身はint型変数が2個入っているものだ。

    Texture tex;    // プレイヤーの画像。↑→↓←の4つの向きがある
    Point position; // プレイヤーの位置。(マス座標)
    int direction;  // プレイヤーの向き。0=↑, 1=→, 2=↓, 3=←
public:
    // コンストラクター
    Player() {
        tex = Texture(L"Player.png");
        position = { 3, 2 };
        direction = 0;
    }
    // デストラクタ―
    ~Player(){}

    // ボタン入力
    void input() {
        // 上方向が押されたら(他の向きも同様の処理)
        if (Input::KeyUp.clicked) {
            direction = 0;          // 向きを変える
            if (position.y > 0) {   // 枠外に行かないように
                --position.y;       // 1マス移動
            }
        }
        else if (Input::KeyRight.clicked) {
            direction = 1;          // 向きを変える
            if (position.x < 10) {  // 枠外に行かないように
                ++position.x;       // 1マス移動
            }
        }
        else if (Input::KeyDown.clicked) {
            direction = 2;          // 向きを変える
            if (position.y < 7) {   // 枠外に行かないように
                ++position.y;       // 1マス移動
            }
        }
        else if (Input::KeyLeft.clicked) {
            direction = 3;          // 向きを変える
            if (position.x > 0) {   // 枠外に行かないように
                --position.x;       // 1マス移動
            }
        }
    }

    // 描画
    void draw() {
        tex(direction * 60, 0, 60, 60).draw(position * 60);
    }
};

// メイン関数
void Main() {
    const Texture map(L"Map.png");  // マップ。マップチップじゃなくて一枚の画像だからクラス化しなくていいや

    Player player01;
    while (System::Update()) {
        player01.input();

        map.draw();
        player01.draw();
    }
}

https://gyazo.com/a48e1217d9e9bf61aa7dd095d1c46085

これだとプレイヤーの移動がカクカクじゃね?(この画像だとカクカク移動でも違和感がないが、プレイヤー画像を人間とかにすると違和感がでる)
でも、マス単位に移動する変数構成はこのままにしたい・・・

そんなときは、プレイヤーに「移動の時間」をあらわす変数を1個追加して、その移動時間に応じて描画座標をずらせばいいのです。
移動していないなら常に0、移動ボタンが押された瞬間に数値を30にし、1フレームごとに1ずつ減っていくようにする。0になったら移動終了という感じだ。

RPGMap.cpp
# include <Siv3D.hpp>

// プレイヤーのクラス。実用的なサンプルにするために、クラス化できるものはクラス化していく。
class Player {
private:
    // Point型は、座標をint型で表す型で、中身はint型変数が2個入っているものだ。

    Texture tex;    // プレイヤーの画像。↑→↓←の4つの向きがある
    Point position; // プレイヤーの位置。(マス座標)
    int direction;  // プレイヤーの向き。0=↑, 1=→, 2=↓, 3=←

    int movetime;   // 追加:移動の制御時間
    Point old_pos;  // 移動前にいた座標
public:
    // コンストラクター
    Player() {
        tex = Texture(L"Player.png");
        position = { 3, 2 };
        direction = 0;
    }
    // デストラクタ―
    ~Player(){}

    // ボタン入力
    void input() {
        // 移動時間が0なら(停止しているなら)
        if (movetime == 0) {
            // 上方向が押されたら(他の向きも同様の処理)
            if (Input::KeyUp.pressed) { // 変更:「おされていたら」
                direction = 0;
                if (position.y > 0) {
                    old_pos = position; // 移動前の座標を記録。(描画時に使う)
                    --position.y;       // 移動
                    movetime = 30;      // 移動時間を30フレームに設定
                }
            }
            else if (Input::KeyRight.pressed) {
                direction = 1;
                if (position.x < 10) {
                    old_pos = position; // 移動前の座標を記録。(描画時に使う)
                    ++position.x;       // 移動
                    movetime = 30;      // 移動時間を30フレームに設定
                }
            }
            else if (Input::KeyDown.pressed) {
                direction = 2;
                if (position.y < 7) {
                    old_pos = position; // 移動前の座標を記録。(描画時に使う)
                    ++position.y;       // 移動
                    movetime = 30;      // 移動時間を30フレームに設定
                }
            }
            else if (Input::KeyLeft.pressed) {
                direction = 3;
                if (position.x > 0) {
                    old_pos = position; // 移動前の座標を記録。(描画時に使う)
                    --position.x;       // 移動
                    movetime = 30;      // 移動時間を30フレームに設定
                }
            }
        }

        // そうでなければ
        else {
            --movetime; // 時間を1フレーム減らす
        }
    }

    // 描画
    void draw() {
        // 移動前の座標と移動後の座標をなめらかに動かす計算式を追加
        tex(direction * 60, 0, 60, 60).draw(position * 60 - (position - old_pos) * movetime * 60 / 30);

        // 移動後の位置を枠で書き足す
        Rect(position * 60, 60, 60).drawFrame(1.0, 1.0, Color(0, 0, 255));
    }
};

// メイン関数
void Main() {
    const Texture map(L"Map.png");  // マップ。マップチップじゃなくて一枚の画像だからクラス化しなくていいや

    Player player01;
    while (System::Update()) {
        player01.input();

        map.draw();
        player01.draw();
    }

}

https://gyazo.com/6d7760e49dc8c5cbec34d58158196950

移動中は前までいた座標と動いた位置の中間の位置を少しずつ動いていく。前までいた座標は1つの変数で管理する。
プレイヤーが移動中のときはボタン操作を受け付けないようにすれば、マスに沿って動く歩行プログラムの完成。

この手法の特徴は、移動を始めた瞬間にはすでにposition変数上では移動が終わっていること。歩いた後に敵が出現して戦闘になるようにするなら、移動が終わってからにする。そこだけは注意すべきポイントです。逆にカウントが終わったら1マス動くというのもアリです。

時間で動かす考え方

上記のマップ歩行ような時間をかけて描画をずらす考え方は、ゲームのグラフィック演出においてとても重要なものである。かなり多くの場面で使える。市販のゲームにはこのような時間制御の演出(エフェクトなど)がいたるところに存在し、そういう細かい部分がゲームプレイの快適度にも影響するのでできるだけ盛り込んでいきましょう。

時間計測の2つの方法

ゲーム中演出で使う時間には2種類ある。ひとつは現実世界と同じ時間経過と、もうひとつはフレームレートを利用した時間計測だ。

フレームレートを用いた場合、60FPSの場合、経過フレーム数/60で理論上の秒数が計算できる。ゲームの動作と同期しやすいのが長所で、処理落ちでノロノロになるのが短所。
正確な時間を用いる場合、timerMillisecクラスで時間を取得し、基準フレームから何ミリ秒経過したかで時間計測をする。時間が正しいのが長所だが、モノによっては使い勝手が悪いことも。ゲームの用途に合わせて使い分けていこう。

Siv3DのEffectを活用しよう

Siv3DにはEffectクラス(本当は構造体)いうとても便利な機能が実装されている。これは、ある瞬間に誕生し、時間経過で動き、一定時間経過後に消滅するオブジェクトを生成するのに使える関数だ。
敵を倒して爆発するエフェクトだったり、剣で攻撃した時の斬撃だったり、お星さまがキラキラ飛び交うパーティクルエフェクトなどをいろいろ作れる。
このEffectはリアルタイムで管理することができる。クラスにフレーム数を表す変数を1個用意すれば、フレームレートで制御することもできる。

使い方は、まずエフェクト構造体を用意し、そこに初期化処理と更新処理を書く。
そして、メイン関数内でエフェクトを生成するタイミングで「effct.add(...);」と呼び出す。
その後1フレームに1回update関数を呼び出す。update関数の戻り値がfalseになればエフェクトは消滅する。
内部処理のコードを書きかえることで様々な処理を行うことができるので自由度が高い。これを使ってド派手なエフェクトをつくってみよう。

Effect.cpp
# include <Siv3D.hpp>

// エフェクトクラス(構造体)
struct Ef01 : IEffect {
    // 使用する変数をかく
    Point mousePos;

    // コンストラクター
    Ef01(const Point& pos) {
        mousePos = pos;
    }
    // 更新関数 引数tは時間(秒数)
    bool update(double t) override {
        Circle(mousePos, 32).draw(ColorF(1.0, 1.0 - t, 0, 1.0 - t));    // 円を描画
        return t < 1.0; // ここがfalseになるか、10秒たつと消滅
    }
};

// エフェクトは2種類以上用意できる
struct Ef02 : IEffect {
    // 使用する変数をかく
    Array<Point> effectPos; // 配列(個数変動することを想定して可変長配列)

    // コンストラクター
    Ef02(const Point& pos) {
        effectPos.resize(4);    // 配列サイズを4個にする

        for (auto& i : effectPos) { // 範囲ループで代入する
            i = pos;
        }
    }
    // 更新関数 引数tは時間(秒数)
    bool update(double t) override {
        effectPos[0].x += 2;        // 座標を移動させる
        effectPos[1].y += 2;
        effectPos[2].x -= 2;
        effectPos[3].y -= 2;

        for (auto& i : effectPos) {
            Rect(i.x - 4, i.y - 4, 8, 8).draw(AlphaF(1.0 - t * 2)); // 円を描画
        }
        return t < 0.5; // ここがfalseになるか、10秒たつと消滅
    }
};

// メイン関数
void Main() {
    Effect effect;  // エフェクトのオブジェクト(インスタンス)。複数エフェクトがある場合でも1つでじゅうぶん

    while (System::Update()) {
        if (Input::MouseL.clicked) {
            effect.add<Ef01>(Mouse::Pos()); // 左クリックでエフェクト01を出現させる
        }
        if (Input::MouseR.clicked) {
            effect.add<Ef02>(Mouse::Pos()); // 右クリックでエフェクト02を出現させる
        }
        effect.update();
    }
}

https://gyazo.com/76b77d80749b75ba554688a1387764e3

コードのエフェクトは、
・左クリックするとその位置に黄色い円を表示して色が赤に変わりながら徐々に消えていく。
・右クリックするとその位置に小さい四角形を4個表示して、四方に発散しながら徐々に消えていく。
の2つを実装したものだ。

また、エフェクトにテクスチャを使うこともできる。その場合はテクスチャアセットを使用するのがおすすめだ。テクスチャアセットを使えばソースコードのどこからでもテクスチャが使えるからだ。

イージング関数を使いこなそう

0≦t≦1の範囲で、tが(主に時間経過で)直線的に動くとXが曲線的に変化するイージング関数というものがある。このイージング関数は時間で動くエフェクトの制御に適している。これを使えるようになると様々な表現ができるようになる。
移動速度を次第にゆっくりにしたり速くしたり、イージング関数はいくつか種類が用意されています。
※戻り値はdouble型の「数値」が返ってきます。

Easing.cpp
EaseIn(Easing::Quart, t);

20151219-035418-773.png

まとめ

・時間で動く処理を入れよう
・Siv3DのEffectクラスを使おう
・イージング関数で速度変更

この記事が少しでも制作の役に立ちますように・・・

明日12月20日は@farma_11さんの記事です。よろしくお願いします。