アクションゲームのジャンプを滑らかにしよう with Siv3D その1

はじめに

Siv3Dというそれはそれは楽しくて素晴らしくてサンプルが豊富なC++のライブラリ。

github.com

その数あるサンプルの1つに横スクロールアクションゲームがあります。

横スクロールゲームを作ろう · Siv3D/Reference-JP Wiki · GitHub

プレイヤーキャラがあり足場となるブロックがありジャンプと左右移動ができる素晴らしいサンプルなのですがジャンプの挙動がどうも気に食わない。

それもそのばず、このサンプルのジャンプの挙動は30フレーム(0.5秒)間毎フレーム10pxずつ上昇しその後31フレーム目から着地するまでは毎フレーム10px落下する仕様になってます。

文書で書いても分かりづらいのですがこのジャンプでは、一番高いところに来たら即カクっと落下します。プレイヤーのY座標(高さ)と経過時間をグラフにするとΛみたいなカクっとしたグラフになります。*1

ゲームなら滑らかな軌道を描く自然なジャンプがいいですよね。ボタン長押しで高さが変わるジャンプも欲しいです。 分かりやすい様にイメージイラストを用意しました。

f:id:movementi:20171126122537j:plain
百聞は一見にしかず

マウスで書いた絵なんか貼るな!*2という方のためにサンプル状態でのGIFを用意したのでそれを見てもらえるとわかりやすいです。

f:id:movementi:20171126093826g:plain
サンプルそのままのジャンプ

今回の記事ではこのジャンプを自然な感じに改造したいと思います。

コードを弄る前に

ジャンプの仕様

こんな仕様で作ってみます。今回は横移動は扱わないのでジャンプの速度を移動速度として扱います。

  1. ジャンプボタンを押すと移動速度に初速度(画面上方向へのYを減らす負の値)がかかり上へ移動する
  2. 空中にいる間は重力(画面下方向へのYを増やす正の値)がかかり移動速度が変化する
  3. 重力により移動速度が負→0→正となり、上がる→止まる(一瞬)→落ちるという風に動く
  4. ジャンプボタンを長押しするかどうかでジャンプの高さが変わる

処理的には上のとおりですがなにより滑らかな軌道を描く自然なジャンプ ボタン長押しで高さが変わるジャンプを目標にします。 最後にコピペしてすぐ使える今回改造したコードの全てを載せますね。

Siv3Dのバージョン

August 2016 v2版を使います。

サンプルコードの場所

スクロールの実装 · Siv3D/Reference-JP Wiki · GitHub

このページの一番下にある見本のコードをこれからいじって行きます。Siv3Dのクラスや関数の解説は省きますのでご了承ください。

コード改造

Playerクラス

    //Playerクラス
    // 残りのジャンプ時間
    //int m_jumpFrame; 使わない

    //ジャンプの初速度
    float m_jump_v0 = -12.0f;

    //ジャンプの重力
    float m_gravity = 0.3f;

    //キャラの速度
    Vec2 m_speed = { 0,0 };

まずはPlayerクラスにメンバを追加&削除します。

ジャンプ時間は使わないのでm_jumpFrameは削除。
初速度と重力を示すm_jump_v0m_gravityを追加しました。今回は固定値なのでメンバ変数にしなくても出来ますが、今後の拡張やメンテナンス性を考慮しました。
キャラの速度を示すVec2型*3m_speedについて、今回X軸の横移動はノータッチなのでベクトルクラスにする恩恵は薄いのですがこちらも今後の拡張やメンテナンス性を考慮しVec2クラスとしました。

void update()
    {
        if (m_isGrounded)
        {
            if (Input::KeySpace.clicked)
            {
                //初速度の設定
                m_speed.y = m_jump_v0;
                //Y座標の更新
                m_position.y += m_speed.y;
            }
        }
        else
        {
            //Y方向の速度に加速度を加える
            m_speed.y += m_gravity;
            //Y座標の更新
            m_position.y += m_speed.y;

            //上昇中にボタンを離したときの処理
            if (m_speed.y < 0 && !Input::KeySpace.pressed)
            {
                m_speed.y *= 0.9f;
            }
        }  

Playerクラスのupdate関数の中です。スペースキー(以下ジャンプボタン)を押したときの判定ですがm_jumpFrameは使わないので消します。他の箇所も同様です。 ポイントは以下

m_speed.yに初速度であるm_jump_v0を代入する

見たまんまです。移動速度に初速度を代入しています。

ジャンプボタンを押してすぐプレイヤーを移動させる

m_speed.y = m_jump_v0のすぐ下にあるm_position.y += m_speed.yの部分です。ここは結構重要ですね。elseの部分で移動処理(Y座標の更新)をするから不要に見えますが必要です。コレを入れないとボタンを押してもジャンプしません。このelse節が呼ばれるのはジャンプボタンが押された次のフレームからですが、ジャンプボタンが押されたタイミングで移動させないとelse節に入る前に別の箇所にあるプレイヤーとブロックの接地判定が行われてしまい着地したと判定されます。地面を蹴る時には地面に足をついているからですね。

重力をかける

m_speed.y m_gravityを足しています。宙に浮いてるときは毎フレーム呼ばれるのでだんだん画面下へと速度がかかっていきます。今回はしていませんが必要なら速度にリミッターをかけましょう

上昇中にボタンを離したときの処理

ここは超重要であり今回のキモですね。
ジャンプボタンを長押しするか離すかでジャンプの高さを変える処理です。m_speed.y < 0は速度が0以下、つまり上昇中ということです。上昇中かつジャンプボタンが押されていない時毎フレーム移動速度が0.9倍されます。平たく言うと「ボタンを離すと強い重力がかかる」ですが0.9倍と掛け算にすることで速度が0に近いときは穏やかに、0から離れた大きい数値のときは激しく速度が0に近づき結果としてなめらかな軌道が生まれます。

完成?

これでジャンプの改造は終わりです。遊んでみましょう。

f:id:movementi:20171126122538g:plain
いいジャンプ
なめらかな軌道もボタン長押しでの高さの調整もうまく出来ていますね。

しばらく遊んでいると…

f:id:movementi:20171126122536j:plain
地面に突き刺さる図
残念なことにこのままでは高いところから着地したときに地面に突き刺さってしまう事があります。非常によろしくないですね。

今回の記事はここまで。次回は高いところから着地しても地面に突き刺さらないように改造する予定です。

今回のコード

Siv3Dがインストールされた環境ならコピペして使えます。大半はサンプルそのまんまです。サンプル作者様そしてSiv3Dの開発者の皆様に感謝。

# include <Siv3D.hpp>
//サンプル改造
class Block
{
public:

    Block() {}

    Block(const RectF& region) :
        m_region(region),
        m_texture(L"Example/Brick.jpg") {}

    // プレイヤーの現在位置を更新する関数
    void setPlayerPos(const Vec2& pos)
    {
        m_playerPosition = pos;
    }

    // 描画以外の操作をする関数
    void update() {}

    // 点との当たり判定を取る関数
    bool intersects(const Vec2 &shape) const
    {
        return m_region.intersects(shape);
    }

    // 描画をする関数(描画操作以外行わないこと.)
    void draw()
    {
        m_region.movedBy(-m_playerPosition + Window::Center())(m_texture).draw();
    }
          
private:

    // ブロックの領域
    RectF m_region;
    
    // ブロックのテキスチャ(画像)
    Texture m_texture;

    // プレイヤーの現在の位置
    Vec2 m_playerPosition;
};


class Player
{
public:

    Player() :
        m_position(100, 200),
        m_texture(L"Example/Siv3D-kun.png"),
        m_isGrounded(false) {}

    // 位置を取得する関数
    Vec2 getPos()
    {
        return m_position;
    }

    // 地面に接しているかを更新する関数
    void checkGround(const Array<Block>& blocks)
    {
        m_isGrounded = false;

        for (size_t i = 0; i < blocks.size(); i++)
        {
            if (blocks[i].intersects(m_position))
            {
                m_isGrounded = true;
            }
        }
    }

    // 描画以外の操作をする関数
    void update()
    {
        if (m_isGrounded)
        {
            if (Input::KeySpace.clicked)
            {
                 //初速度の設定
                m_speed.y = m_jump_v0;
                //Y座標の更新
                m_position.y += m_speed.y;
            }
        }
        else
        {
            //Y方向の速度に加速度を加える
            m_speed.y += m_gravity;
            //Y座標の更新
            m_position.y += m_speed.y;

            //上昇中にボタンを離したときの処理
            if (m_speed.y < 0 && !Input::KeySpace.pressed)
            {
                m_speed.y *= 0.9f;
            }
        }

        if (Input::KeyRight.pressed)
        {
            m_position.x += 5.0;
        }
        if (Input::KeyLeft.pressed)
        {
            m_position.x -= 5.0;
        }
    }

    // 描画をする関数(描画操作以外行わないこと.)
    void draw()
    {
        RectF(Vec2(-72.5, -200) + Window::Center(), 145, 200)(m_texture).draw();
    }

private:

    // プレイヤーの座標
    Vec2 m_position;

    // プレイヤーのテクスチャ(画像)
    Texture m_texture;

    // 地面に接しているか否か
    bool m_isGrounded;

    // 残りのジャンプ時間
    //int m_jumpFrame; 使わない

    //ジャンプの初速度
    float m_jump_v0 = -12.0f;

    //ジャンプの重力
    float m_gravity = 0.3f;

    //キャラの速度
    Vec2 m_speed = { 0,0 };
};


void Main()
{
    Window::Resize(1280, 720);

    Texture background(L"Example/Windmill.png");
    Player player;
    Array<Block> blocks;

    blocks.push_back(Block({ -400, 400, 200, 200 }));
    blocks.push_back(Block({ -200, 400, 200, 200 }));
    blocks.push_back(Block({ 0, 400, 200, 200 }));
    blocks.push_back(Block({ 200, 400, 200, 200 }));
    blocks.push_back(Block({ 200, 200, 200, 200 }));
    blocks.push_back(Block({ 400, 400, 200, 200 }));
    blocks.push_back(Block({ 800, 400, 200, 200 }));
    blocks.push_back(Block({ 1000, 400, 200, 200 }));
    blocks.push_back(Block({ 1300, 200, 400, 30 }));

    while (System::Update())
    {
        for (size_t i = 0; i < blocks.size(); i++)
        {
            blocks[i].setPlayerPos(player.getPos());
            blocks[i].update();
        }

        ///player.checkGround(blocks);
        player.update();
        player.checkGround(blocks);

        // 実際には縦横比を合わせるように.
        Rect(Window::Size())(background).draw();

        for (size_t i = 0; i < blocks.size(); i++)
        {
            blocks[i].draw();
        }

        player.draw();
    }
}

次の記事

movement.hatenablog.jp

参考書籍

アクションゲームアルゴリズムマニアックス
著 松浦健一郎/司ゆき ISBN 978-4-7973-3895-9

アクションゲームアルゴリズムマニアックス

アクションゲームアルゴリズムマニアックス

*1:分かりづらいね

*2:使ったソフトはGIMPです

*3:型の詳細は上の公式ページから各自確認してください