【Unity C#】動くだけじゃない!!回転や拡大にも対応して一緒に移動する「動く床」を作りたい!!!!

Unityで「動く床」を実装したい!!!!

しかし動く床だけではプレイヤーは滑り落ちてしまいます。
床と一緒に移動するためにはプレイヤーを床の動きに合わせて動かすスクリプトが必要です。

実装方法はいろいろありますが、アクションゲームでたまによくあるような「回転する床」「拡大・縮小する床」に対応する実装方法は調べても見つからなかったので、考えてみました。

これを使えば、

  • あらゆる形状の床に対応し、
  • 移動・回転・拡大に合わせて一緒に移動

することができます。
(後述しますが、対応できない動きもあります。)

目次

サンプルシーン

hakumairise.github.io
↑本記事のスクリプトを使用したアクションゲームのサンプルシーンを公開しています。
ブラウザ上で動かせるので、プレイしてみてください! ※PC限定
移動:WASDキー ジャンプ:スペースキー

サンプルプロジェクト

github.com
実行環境

使い方

プレイヤーの設定

  • プレイヤーオブジェクトの用意

まずは、自分がプレイヤーとして使用したいキャラクターやオブジェクトを用意してください。
(例えば、hierarchyウィンドウで右クリック→3D Object→Capsuleでカプセルを生成できます。)

プレイヤーを動かすスクリプトがない場合は、例えば以下のスクリプトをプレイヤーオブジェクトに加えてください。
(projectウィンドウで右クリック→Create→C# Script→ファイル名を「MoveTest.cs」にしてコードをコピペ→プレイヤーオブジェクトにドラッグ&ドロップ)

MoveTest.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MoveTest : MonoBehaviour
{
    Rigidbody m_rigidbody;
    Platforming m_platforming;

    void Start()
    {
        m_rigidbody = transform.GetComponent<Rigidbody>();
        m_platforming = transform.GetComponent<Platforming>();
    }
    void Update()
    {
        if (Input.GetKey(KeyCode.W))
        {
            transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.Euler(new Vector3(0, 0, 0)), Time.deltaTime * 1200);
            m_rigidbody.velocity = new Vector3(0, m_rigidbody.velocity.y, 3);
        }
        else if (Input.GetKey(KeyCode.A))
        {
            transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.Euler(new Vector3(0, 270, 0)), Time.deltaTime * 1200);
            m_rigidbody.velocity = new Vector3(-3, m_rigidbody.velocity.y, 0);
        }
        else if (Input.GetKey(KeyCode.S))
        {
            transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.Euler(new Vector3(0, 180, 0)), Time.deltaTime * 1200);
            m_rigidbody.velocity = new Vector3(0, m_rigidbody.velocity.y, -3);
        }
        else if (Input.GetKey(KeyCode.D))
        {
            transform.rotation = Quaternion.RotateTowards(transform.rotation, Quaternion.Euler(new Vector3(0, 90, 0)), Time.deltaTime * 1200);
            m_rigidbody.velocity = new Vector3(3, m_rigidbody.velocity.y, 0);
        }
        else
        {
            m_rigidbody.velocity = new Vector3(0, m_rigidbody.velocity.y, 0);
        }

        if (Input.GetKey(KeyCode.Space) && m_platforming.m_isGround)
        {
            m_rigidbody.velocity = new Vector3(m_rigidbody.velocity.x, 5, m_rigidbody.velocity.z);
            m_platforming.plat = null;
            m_platforming.center = null;
        }

        if(transform.position.y < -5)
        {
            transform.position = new Vector3(0, 0.5f, 0);
        }
    }
}


  • プレイヤーのLayerを変更

なんでもいいですが、動く床や地面のオブジェクトと違うものにしてください。
私は新しくPlayerというレイヤーを追加しました。
(inspectorの上方にあるLayerを選択→Add Layer...から新しいLayerを作成)

  • Rigidbodyの追加

(inspectorの一番下にあるAdd Component→Physics→Rigidbodyを選択)

Freeze Rotationにチェックを入れると、オブジェクトに押された時に回転しなくなります。
  • Platforming.csを作成し、プレイヤーオブジェクトに追加

こちらが今回の要となる、床と一緒に移動するスクリプトです。プレイヤーに追加してください。

Platforming.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Platforming : MonoBehaviour
{
    public GameObject plat = null;
    public GameObject center = null;
    public bool m_isGround = false;

    [SerializeField] Vector3 origin;
    [SerializeField] float radius;
    [SerializeField] float footPos;
    [SerializeField] float groundCheckDistance_onPlat;
    [SerializeField] float groundCheckDistance_up;
    [SerializeField] float groundCheckDistance_down;

    Quaternion pastRot;
    Vector3 pastPos;
    Vector3 pastSca;
    Quaternion nowRot;
    Vector3 nowPos;
    Vector3 nowSca;

    public LayerMask m_layerMask;
    Rigidbody m_rigidbody;

    void Start()
    {
        m_rigidbody = transform.GetComponent<Rigidbody>();
    }

    void FixedUpdate()
    {
        if (plat != null)
        {
            if (pastSca.x <= 0 || pastSca.y <= 0 || pastSca.z <= 0 || nowSca.x <= 0 || nowSca.y <= 0 || nowSca.z <= 0)
            {   //platのScaleが0以下の場合はMoveWithしない
                plat = null;
                center = null;
                return;
            }

            if (center != null)
            {   //Centerあり
                nowRot = center.transform.rotation;
                nowPos = center.transform.position;
                nowSca = center.transform.lossyScale;

                MoveWith();

                pastRot = center.transform.rotation;
                pastPos = center.transform.position;
                pastSca = center.transform.lossyScale;

            }
            else
            {   //Centerなし
                nowRot = plat.transform.rotation;
                nowPos = plat.transform.position;
                nowSca = plat.transform.lossyScale;

                MoveWith();

                pastRot = plat.transform.rotation;
                pastPos = plat.transform.position;
                pastSca = plat.transform.lossyScale;
            }
        }

        GroundCheck();

        //プレイヤーを垂直に戻す
        transform.eulerAngles = new Vector3(0, transform.rotation.eulerAngles.y, 0);
    }
    public void MoveWith()
    {
        Vector3 player_pastPos = transform.position;

        //Rotate
        Quaternion deltaRot = nowRot * Quaternion.Inverse(pastRot);
        transform.RotateAround(nowPos, new Vector3(1, 0, 0), deltaRot.eulerAngles.x);
        transform.RotateAround(nowPos, new Vector3(0, 1, 0), deltaRot.eulerAngles.y);
        transform.RotateAround(nowPos, new Vector3(0, 0, 1), deltaRot.eulerAngles.z);

        //Move
        Vector3 deltaMove = (nowPos - pastPos);

        //Scale
        Vector3 deltaSca = new Vector3((nowSca.x - pastSca.x) / pastSca.x, (nowSca.y - pastSca.y) / pastSca.y, (nowSca.z - pastSca.z) / pastSca.z);
        Vector3 playerDis = Quaternion.Inverse(nowRot) * (transform.position - nowPos);
        deltaSca = nowRot * new Vector3(playerDis.x * deltaSca.x, playerDis.y * deltaSca.y, playerDis.z * deltaSca.z);

        //プレイヤーの位置にMoveとScaleの計算結果(移動量)を加えて、移動
        transform.position += deltaMove + deltaSca;

        //Y軸上方向へは床に押される力で上がるので、ここでは移動させない
        if (transform.position.y - player_pastPos.y > 0)
        {
            transform.position = new Vector3(transform.position.x, player_pastPos.y, transform.position.z);
        }
    }

    public void GroundCheck()
    {
        //プレイヤーの状態に合わせてRayの長さを設定
        float rayDistance = origin.y - footPos - radius;
        if (plat != null)
        {
            rayDistance += groundCheckDistance_onPlat;
        }
        else if (m_rigidbody.velocity.y > 0)
        {
            rayDistance += groundCheckDistance_up;
        }
        else
        {
            rayDistance += groundCheckDistance_down;
        }

        //足元にRayを発射して床を確認
        RaycastHit hit;
        if (Physics.SphereCast(transform.position + origin, radius, Vector3.down, out hit, rayDistance, m_layerMask))
        {
            m_isGround = true;
            if ((plat != null && hit.collider.name == plat.name))
            {
                //Debug.Log("同じ床: " + hit.collider.name);
                return;
            }
            else if (hit.collider.transform.CompareTag("Move") || hit.collider.transform.CompareTag("Center"))
            {
                //Debug.Log("新しい床: " + hit.collider.name);
                plat = hit.collider.gameObject;
                pastRot = plat.transform.rotation;
                pastPos = plat.transform.position;
                pastSca = plat.transform.lossyScale;
                nowSca = plat.transform.lossyScale;

                //親にcenterがいるか調べる
                GameObject p = plat;
                while (p.CompareTag("Move") && p.transform.parent != null)
                {
                    if (p.transform.parent.CompareTag("Center"))
                    {
                        center = p.transform.parent.gameObject;
                        //Debug.Log("center: " + center.name);
                        pastRot = center.transform.rotation;
                        pastPos = center.transform.position;
                        pastSca = center.transform.lossyScale;
                        nowSca = center.transform.lossyScale;
                        return;
                    }
                    else
                    {
                        p = p.transform.parent.gameObject;
                    }
                }
            }
            else
            {
                //Debug.Log("動く床じゃない: " + hit.collider.name);
                plat = null;
                center = null;
            }
        }
        else
        {
            //Debug.Log("空中");
            plat = null;
            center = null;
            m_isGround = false;
        }
    }
}


Platformingの設定

Platformingを追加したら、次は値を設定します。キャラクターの足元から地面を検出するためのSphereCast(球状の太いRaycast)を飛ばすために、サイズや位置を調整します。

  • transform.positionを確認

まず、プレイヤーのtransform.positionがどこを基準点にしているか確認してください。
例えばCapsuleの場合、transform.positionの基準点は中心です。
インポートしたオブジェクトによっては、足元が基準点に設定されている場合もあります。

  • Originを設定

Rayを発射する原点の位置を決めます。transform.positionとの相対距離です。
ここで注意!!SphereCastでは球状のRayを発射しますが、原点の球部分は判定がありません。
なので、transform.positionの基準点が足元にある場合は、Originを(0, 0.5f, 0)にするなどしてRayの発射原点を少し上にずらしてください。

球の半径です。プレイヤーの半径よりやや小さくします。

  • FootPosを設定

オブジェクトの足元の位置(Y座標)を設定します。これもtransform.positionとの相対距離です。
transform.positionの基準点が足元の場合は0となります。

  • GroundCheckDistanceを設定

ここまで設定すると、地面ピッタリの位置までSpereCastが出るようになります。
足元から少しはみ出た方が床を検知しやすいので、各状態に合わせてはみ出す量を設定します。

  • GroundCheckDistance_onPlat......動く床に乗ってる時の長さ。長めに設定しておくと抜けにくいと思います。
  • GroundCheckDistance_up..............キャラが飛び上がっている時の長さ。少なめでOK。
  • GroundCheckDistance_down........キャラが落ちている時の長さ。少し長めに設定するといいと思います。
  • LayerMaskを設定

最後に、自身が放ったSphereCastに自分自身がヒットしないように、レイヤーマスクを設定します。
選択するとチェック欄が出るので、プレイヤーに設定したレイヤーをチェックから外してください。

Ignore Raycastなど、床オブジェクトに関係ないレイヤーは全て外していいです。

以上の設定を、例えばScaleが(0.5, 0.5, 0.5)のCapsuleで行うとこのような値になります。

床の設定

床のオブジェクトを用意します。
どんな形でもOK。適切なColliderを設定してください。

次に、床のオブジェクトのタグを「Move」に設定してください。
(inspectorの上方にあるTagを選択→Add Tag...から新しいTag「Move」を作成)

これで完了です。
床のtransformを変化させると、移動、回転、拡大に合わせて移動します。
(ちなみに、床の移動のスクリプトFixedUpdate()を使うと物理計算と同じ時間軸で一定間隔で呼び出されるので、プレイヤーがガタガタしにくくなります。)

Platformingを見ると、現在乗っている床の名前が表示されます。

床の回転・拡大が中心以外の場合

このスクリプトは床オブジェクトが自身のtransform.positionを中心に回転・拡大していることが前提です。
もし、床オブジェクトが違う点を中心として回転・拡大している場合、ひと手間必要です。

  • Centerの設定

回転・拡大の中心となる位置にオブジェクトを配置します。これはEmptyでも、実体があるオブジェクトでもOK。
(hierarchyウィンドウで右クリック→Create Emptyでカラのオブジェクトを生成できます)

回転・拡大の中心となるオブジェクト

中心となるオブジェクトのタグを「Center」に設定してください。
(inspectorの上方にあるTagを選択→Add Tag...から新しいTag「Center」を作成)

「Center」オブジェクトを作成したら、「Move」オブジェクトを子オブジェクトにしてください。
(床オブジェクトを中心のオブジェクトにドラッグ&ドロップ)

子オブジェクトの位置を中心(親)からずらす
Moveを子オブジェクトに配置

そして、中心のオブジェクトで回転・拡大を行えば、子オブジェクトは親オブジェクトを中心として動きます。

親子にすることで、回転・拡大の中心を変えることができる

※このとき、子オブジェクトを個別に動かすことはできません。回転・拡大していいのは親のCenterオブジェクト1つのみで、他の子オブジェクトは静止です。
※移動しかしない親オブジェクトなら「Move」タグでOKです。

プログラムの説明

Platformingは、毎フレームごとに

  • GroundCheck()
  • MoveWith()

を行っています。

GroundCheck()は足元にSphereCastを飛ばして床があるかチェックしています。
(OnCollisionだと床の乗り移りで正確に検出できませんでした。)

Rayがヒットしたオブジェクトのうち、タグが「Move」「Center」であればそのオブジェクトの情報(位置・回転・拡大)を覚えておきます。
(オブジェクトが「Move」だった場合、親にCenterがいるか辿って調べます。)

そして、MoveWith()で1フレーム前の床の情報と現在の床の情報から床の移動量・回転量・拡大量を計算してプレイヤーを移動させています。
(プレイヤーを床の子オブジェクトにする方法だと、回転と拡大で変形してしまいました。)

床の移動・回転・拡大量の計算ですが、
移動量は単純に

Vector3 deltaMove = nowPos - pastPos;

で求まります。

回転はQuaternion型です。Quaternion型の引き算はInverseを使います。ここから求まった回転量を、X・Y・Z軸でRotateAroundします。

Quaternion deltaRot = nowRot * Quaternion.Inverse(pastRot);
transform.RotateAround(nowPos, new Vector3(1, 0, 0), deltaRot.eulerAngles.x);
transform.RotateAround(nowPos, new Vector3(0, 1, 0), deltaRot.eulerAngles.y);
transform.RotateAround(nowPos, new Vector3(0, 0, 1), deltaRot.eulerAngles.z);

拡大の変化量は以下の1行目の式で求まります。
拡大による移動量は床の拡大の中心から遠いほど大きく、近いほど小さくなります。よって、
拡大による移動量=床とプレイヤーの距離×拡大の変化量
となります。
(プレイヤーの距離を計算する際に回転を0に直すことで拡大の向きと合わせています。)

Vector3 deltaSca = new Vector3((nowSca.x - pastSca.x) / pastSca.x, (nowSca.y - pastSca.y) / pastSca.y, (nowSca.z - pastSca.z) / pastSca.z);
Vector3 playerDis = Quaternion.Inverse(nowRot) * (transform.position - plat.transform.position);
deltaSca = nowRot * new Vector3(playerDis.x * deltaSca.x, playerDis.y * deltaSca.y, playerDis.z * deltaSca.z);

回転移動はもう行ったので、最後に移動と拡大の移動量を適応してプレイヤーを移動させます。

transform.position += deltaMove + deltaSca;

これで移動完了です。

Centerがある場合は、Centerの中心位置・回転・拡大からMoveWithを行います。

できないこと

速すぎる動きには対応できない

このスクリプトは床の位置から移動量を計算してプレイヤーを移動させていますが、上方向の移動は床がプレイヤーを押し上げて移動させているので、その押し上げに任せてスクリプト側では移動させません。

そのため、床がものすごい高速で上昇する際にはプレイヤーを横に押しのけたり、すり抜けてしまいます。

複数の回転・拡大には対応できない

親オブジェクトであるCenterを動かしている間は、子オブジェクトを個別に動かさないでください。このスクリプトは移動・回転・拡大を行うオブジェクトを1つしか扱えないので、例えば遊園地のコーヒーカップのような、下の土台(親)の回転が上の土台(子)に影響していて、上の土台も回転するようなギミックなどには対応できません。

1つのオブジェクトが移動と回転と拡大を同時に行うことは可能です。


最後まで見たいただきありがとうございます。
質問・改善案などあればコメントいただけるとありがたいです。


am1tanaka.hatenablog.com
コードの表示方法について参考にさせていただきました。



unityroom.com
↑unityroomでゲーム公開中です!遊んでみてね!!