うら干物書き

ゲームを作っています。

Unityでベルトコンベアを作ってみる

 最近、プログラム質問サイトのUnityの質問に回答することを趣味にしています。

teratail.com

 「Unityのことならそこそこ分かっているよ!」

と自負していたのですが、結構答えられない・分からない質問が多く、まだまだ修練不足だなと感じております。

 多くの人の質問に答えることはなかなかいい訓練になっています。

 今回は「Unityでベルトコンベアの処理を作る」という質問に答えるまでの思考錯誤についてお話したいと思います。

f:id:urahimono:20160708073913g:plain


概要

 アクションゲームでよく見かけるギミックの一つベルトコンベア、動く歩道とも呼ばれていますね。

 その上にオブジェクトが載ると、ベルトコンベアの進行に向かって勝手に進んでいくというギミックです。

 移動するオブジェクトの場合は、ベルトコンベアの進行方向に進むと更に速く移動でき、逆方向に進むと移動が遅くなったり、時には少しずつ押し戻されたりしてしまいます。

 定番のギミックではあるのですが、私自身にアクションゲームを作った経験が少ないため、実際に処理を作成したことはなかったのでこの機会に勉強してみましょう。    

Lesson1 見た目の作成

 後回しでも良さそうですけど、一番簡単そうなのでちょちょいと作ってしまいましょう。

 ネット上から縞模様のフリー素材のテクスチャを拾ってきて、マテリアルを作成します。

 あとはそのマテリアルのオフセット値をスクロールさせてやればOKです。  

// UVスクロール速度
[SerializeField]
private float       m_uvSpeed   = 1.0f;

/// <summary>
/// テクスチャのUV値をスクロールさせて、ベルトコンベアの見た目を表現する
/// </summary>
void ScrollUV()
{
    var material                = GetComponent< Renderer >().material;
    Vector2 offset              = material.mainTextureOffset;
    offset                     += Vector2.up * m_uvSpeed * Time.deltaTime;
    material.mainTextureOffset  = offset;
}

f:id:urahimono:20160708073813g:plain

 いい感じにスクロールしています。

Lesson2 Rigidbody.MovePosition()を使用してみる

 ベルトコンベアのGameObjectにコンポーネントとコライダーをアタッチし、そのコンポーネントのOnCollisionStay()が呼ばれた際に、当たったGameObjectにアタッチされているRigidbodyに対してMovePosition()を呼んで移動させて忌みます。

 これが質問された際の状態です。

[SerializeField]
private float   m_moveSpeed = 1.0f;

void OnCollisionStay( Collision other )
{
    var body    = other.gameObject.GetComponent<Rigidbody>();
    if( body != null )
    {
        Vector3 add = transform.forward * m_moveSpeed * Time.deltaTime;
        body.MovePosition( other.transform.position + add );
    }
}

f:id:urahimono:20160708073824g:plain

 この処理の場合の利点は、Unity既存のRigidbodyを利用して移動制御をしているので、当たったGameObject側に新規でコンポーネントを作成する必要がないという点です。

 ただ、移動させる際に移動先の位置を直接指定しているため、ベルトコンベア以外の移動制御をGameObjectにかける場合にはうまくいかないです。

 例えば、方向キーで移動制御しているGameObjectがこのベルトコンベアに乗っても正しく制御できないです。

 そのため、ベルトコンベアに当たったGameObjectに対する処理を違う方法で試す必要がありそうですね。

Lesson3 Rigidbody.AddForce()を使用してみる

 MovePosition()の位置指定ではなく、AddForce()の制御に切り替えましょう。

 ベルトコンベアに乗った状態で、同じ方向に移動制御する場合は、更に力がかかり早く移動でき、逆方向の場合は力が相殺され遅く移動できるはずです。

 先ほどのMovePosition()AddForce()に書き換えて見ました。

 結果は、

f:id:urahimono:20160708073851g:plain

 ( ゚д゚)ポカーン

 予想以上の速さで飛んでいってしまいました。

 この結果の原因は、OnCollisionStay()に力が加算され続けるので、等加速度運動になってしまっているようです。

 この動きは望んでいるものと違うので改良する必要がありますね。

Lesson4 OnCollisionEnter()とOnCollisionExit()を使用してみる

 OnCollisionStay()で乗っているGameObjectに対して常に処理を行い続けるのではなく、OnCollisionEnter()が呼ばれコリジョンに触れたときに力を与えOnCollisionExit()が呼ばれコリジョンから離れたときに逆方向の力をかけて先ほど与えた力を相殺するという方法を取ってみましょう。

[SerializeField]
private float       m_movePower = 100.0f;

void OnCollisionEnter( Collision other )
{
    var body = other.gameObject.GetComponent<Rigidbody>();
    if( body != null )
    {
        Vector3 addPower = transform.forward * m_movePower;
        body.AddForce( addPower, ForceMode.Acceleration );
    }
}

void OnCollisionExit( Collision other )
{
    var body    = other.gameObject.GetComponent<Rigidbody>();
    if( body != null )
    {
        Vector3 addPower = -transform.forward * m_movePower;
        body.AddForce( addPower, ForceMode.Acceleration );
    }
}

f:id:urahimono:20160708073856g:plain

 ( ゚д゚)ポカーン

 すぐ止まりおる。

 うーん、私の脳内コンパイルはいけるような気がしたのですが。

 デバッグしてみたところ、コリジョンにはまだ接触している模様。

 そのため、OnCollisionExit()はまだ呼ばれてはいない。

 すなわち、他に外部から力がかかっている可能性が考えられます。

 ええーと、何だろう……。

 そうか、摩擦だ!

 摩擦が掛かっているからか!

Lesson5 摩擦力を無くす

 摩擦力0のPhysicMaterialを作成してベルトコンベアにアタッチしましょう。

f:id:urahimono:20160708073858p:plain

 その結果がこちら。

f:id:urahimono:20160708073901g:plain

 Lesson2の状態に戻りました。    では次に方向キーで移動制御するコンポーネントを追加しましょう。

Lesson6 方向キーで移動制御するコンポーネントの作成

 では以下のスクリプトをオブジェクトにアタッチして移動させてみましょう。

 Update()毎に力を加算させ続けるとLesson3の二の舞になるので、前回与えた力を記憶しておき、Update()の最初に前回の与えた力と逆方向の力を与えて、プラスマイナス0にするという荒業で解決してみます。

using UnityEngine;

public class BeltPlayer : MonoBehaviour
{
    // 移動に与える力
    [SerializeField]
    private float   m_movePower = 500.0f;

    // 前回与えた移動の力
    private Vector3 m_prevVelocity  = Vector3.zero;

    void Update()
    {
        var body        = GetComponent<Rigidbody>();

        // 前回与えた力の逆方向の力を与えて相殺
        body.AddForce( -m_prevVelocity );

        var velocity    = Vector3.zero;

        if( Input.GetKey( KeyCode.UpArrow ) )
        {
            velocity   += Vector3.forward;
        }

        if( Input.GetKey( KeyCode.DownArrow ) )
        {
            velocity   += Vector3.back;
        }

        velocity   *= m_movePower;
        body.AddForce( velocity );

        // 与えた力を保存
        m_prevVelocity  = velocity;
    }


} // class BeltPlayer

f:id:urahimono:20160708073905g:plain

 おお、これは当初望んでいたベルトコンベアの挙動ですよ!

 これでこそベルトコンベアです!

Lesson7 ベルトコンベア完成

 最終的にCubeを空からポンポンとランダムで振りそそがせたり、時間経過と共にベルトコンベアの速度を上昇させたりの処理を追加して以下のようになりましたよ。

【Unity】 ベルトコンベアを作ってみる

f:id:urahimono:20160708073909g:plain

 いい感じになりました。

 ただ、力を逆方向に掛けて相殺するなど、少々トリッキーなスクリプトになってしまいました。

 できる限り自作コンポーネントの数を少なくしようという裏ルールにのっとって作成してみたのですが、ゲームのルールやオブジェクトの管理方法によっては、このやり方ではうまくいかないかもしれませんね。

 ベルトコンベアに当たるGameObjectには、オブジェクト用のコンポーネントを必ずアタッチして、外部から与えられた力などは変数で保存して管理した方がきっといいんでしょうね。

 今回の場合は、落ちてくるCubeなどにもコンポーネントをアタッチして管理せねばならないのは面倒ですが……。

 Unityのゲーム作りの何かしらの参考になればと思います。