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

あらすじ

f:id:movementi:20171126122536j:plain
めりこむ
ジャンプは良くなったけど地面にめり込むのをなんとかしたい。なんとかしよう!
前回のコードを更に改造します。

前回の記事

movement.hatenablog.jp 完全に続きものだから先にこっち読んでね。

Siv3D公式

github.com

Siv3Dのバージョン

August 2016 v2版を使います。

大本のサンプルコードの場所

スクロールの実装 · Siv3D/Reference-JP Wiki · GitHub 一番下に有るやつです。

地面にめり込む問題

※以下足場ブロックのことを地面と呼称

最初になぜ地面に足が突き刺さってしまうのかについて考えます。

ゲームのループにおける落下時の処理

  1. 移動速度分だけ落下(Y座標を大きくする)
  2. 地面と接触判定を行い接地してたらもう落下しないようにする
  3. 描画する

以上のループです。シンプルですね。

しかし落下した結果プレイヤーの足元と地表の座標がピッタリ一致することはそうそうありません。

大抵の場合、落下して地中までめり込む→接地判定されたのでもう落ちない→なんかずっとめり込んでる!! となってしまいます。 コレを防ぐために…

  1. 移動速度分だけ落下(Y座標を大きくする)
  2. 地面と接触判定を行い接地してたらもう落下しないようにする
  3. キャラクターが地面にめり込んでいたら位置を補正する
  4. 描画する

他にもやり方は色々あるはずですが今回はこのやり方で行きたいと思います。

Blockクラス

       //ブロックの上部のY座標を返す関数
       float topY() const
       {
           return (m_region.top.begin.y + m_region.top.end.y) / 2.0f;
       }

Blockクラスに関数を1つ追加します。m_regionは元からあるRectF型のメンバで四角いブロックの領域を示します。後ろのtop.begin.yは画面左上の頂点のY座標です。top.end.yは右上の頂点のY座標ですね。このあたりの型の詳しい解説は飛ばしますので是非Siv3Dを自分の手で触って確かめてみてください。

この関数が返したいのは四角いブロックの画面上側の辺のY座標です。このサンプルに出てくるブロックは画面に対して水平垂直なお行儀の良いブロックなので足して2で割らなくてもreturn m_region.top.begin.y;で動くのですがここは今後の拡張を考えて一手間。

Playerクラス

 // 地面に接しているかを更新する関数
       void checkGround(const Array<Block>& blocks)
       {
           m_isGrounded = false;
           for (size_t i = 0; i < blocks.size(); i++)
           {
               if (blocks[i].intersects(m_position))
               {
                   //プレイヤーの位置補正
                   float blockY = blocks[i].topY();
                   if (m_position.y > blockY)
                   {
                       m_position.y = blockY;
                   }
                   m_speed.y = 0.0;
                   m_isGrounded = true;
               }
           }
       }

PlayerクラスのcheckGround関数です。元のサンプルではプレイヤーと地面が接触したらm_isGrounded = true;で接地フラグを立てて終わりですが、ココのすぐ上に書き足します。

まずは接触したブロックから先程作ったTopY関数を呼び出し適当な変数にいれます。
if (m_position.y > blockY)ココが重要ポイントm_position.yはプレイヤーの足元の座標を示しています。左上じゃないのが素晴らしいですね。*1ここで地面にめり込んでいるかどうかの判定をしてめり込んでいたら補正してあげます。

m_speed.y = 0.0の部分は前回書く部分ですね、すみません。これがないと着地した後も重力による落下速度が残ったままになりジャンプせずに足場を降りると挙動がおかしくなります。着地時に速度はリセットしてあげましょう。

Main関数

ここまでやればもうキャラクターが地面にめり込んでも補正してくれます。しかし実際に遊んでみるとどうもおかしい。一瞬だけ地面にめり込んだシーンがちらっと写ってしまうのです。

どうしてなのか。原因はMain関数にあります。

//Main関数内(サンプルママ)
//ココにBlockクラスの関数
player.checkGround(blocks);
player.update();
//ココにdraw関数

checkGround関数は接地判定と位置補正を行う関数。
update関数はボタンの入力とプレイヤーの移動を行う関数です。
この順番だと落下時の処理は…


checkGround関数

まだ接地してないので何もしない

update関数

空中判定なので落下→地面にめり込む!!!

draw関数

地面にめり込んだキャラクターが描画される

  • ココでループ
checkGround関数

接地したので位置補正

update関数

地上判定なので落下しない。ジャンプ入力待ち。

draw関数

地面に立ったキャラクターが描画される



一瞬だけ地面にめり込んだシーンがちらっと写ってしまう原因がわかりました。関数を呼ぶ順番を変えれば良さそうです。

//Main関数内(改造)
player.update();
player.checkGround(blocks);
//ココにBlockクラスの関数
//ココにdraw関数

ゲームプログラミングでは入力→更新→描画で行うべしというお約束があります。

お約束にのっとりupdate関数を一番にその次にcheckGround関数を、その後でBlockクラスの関数を呼ぶようにしました。 この順番での落下時の処理は…


update関数

空中判定なので落下→地面にめり込む

checkGround関数

接地したので位置補正

draw関数

地面に立ったキャラクターが描画される



これでもうめり込みません!美しい着地の完成です!

f:id:movementi:20171127192658g:plain
高いところからでも大丈夫

さいごに

Siv3Dのサンプルに無事綺麗なジャンプを実装することが出来ました。ボタン長押しで高さ調整できるし着地もきれいだし言うことなしです。ここまで読んでいただきありがとうございます、お疲れ様でした。

これにてアクションゲームのジャンプを滑らかにしよう with 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();
    }

    //ブロックの上部のY座標を返す関数
    float topY() const
    {
        return (m_region.top.begin.y + m_region.top.end.y) / 2.0f;
    }

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))
            {
                //プレイヤーの位置補正
                float blockY = blocks[i].topY();
                if (m_position.y > blockY)
                {
                    m_position.y = blockY;
                }
                m_speed.y = 0.0;
                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())
    {
        player.update();
        player.checkGround(blocks);
        for (size_t i = 0; i < blocks.size(); i++)
        {
            blocks[i].setPlayerPos(player.getPos());
            blocks[i].update();
        }  

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

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

        player.draw();
    }
}

参考書籍

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

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

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

*1:左上にしたほうが最初は楽だったり後で楽じゃなかったりします。これからは自分も足元で座標管理したいですね