見出し画像

【Unity】ボーンのないモデルをキックボードに乗せてみる

今回はアセットの検証実験のため、3Dモデルをキックボードに乗せて走らせてみました。

今回検証で使ったアセットは

1.VeryAnimation

2.RealSize

3.Electric Scooter Pack

の3つです。3番は無料プレゼントでもらったものです。

今回試したことは以下の通りです(目次)。


1.ボーンのない3Dモデルにmixamoでボーンを入れる

今回は以下のピエロの3Dモデルをキックボードに乗せます。

嬉しくなるとつい

なぜピエロとキックボードかというと、キックボードで迷惑運転をするピエロっていう絵がおぼろげながら浮かんできたんです。

さて、このピエロ、Unityにインポートしてもボーンがありません。なのでアニメーションをさせることもできません。ただのカカシです。

インポート設定→Rigでエラーが出ます

ボーンを入れる方法として、一番簡単なのはMixamoに3Dモデルをアップロードする方法だと思われます。
Mixamoとは何ぞやという方は、ひろはすさんの動画を見てください。

詳しいことは参考記事を読んでいただきたいのですが、Mixamoの「UPLOAD A CHARACTER」にFBXモデルをドラッグして、あご、ひじ、手首、ひざ、股の位置を設定すれば、勝手にボーンを入れてくれます。

下部では、「左右対称」のオンオフ、指のボーンの本数を設定できます。
明らかに挙動不審なピエロ

あとはMixamoからWith SkinのFBXファイルを出力し、Unityにインポートすれば、アニメーションをつけられるようになります。

SkinをWith Skinにします。

↓過去記事。Mixamoについて、もう少し詳しく書いています。

↓UnityとMixamoの参考記事

2.VeryAnimationの導入

Mixamoには沢山のモーション素材があり、他にもBoothやUnityAssetstoreにもあるのですが、電動キックボードに乗ってるモーションは見つけられなかったので、自作する必要があります。

今回はVeryAnimationというアセットを使いました。
参考記事が充実しており、さらに日本製のアセットで日本語のマニュアルもあるため、詳細は省きますが、かなり直感的にアニメーションを作ることができました。
指や関節の角度をくるくる回して、微調整すると以下のように電動キックボードに乗る姿勢が出来ました。

↓参考記事

3.RealSizeを使用して、オブジェクトの正確な大きさを把握する

RealSizeというアセットをインポートすると、インスペクタ上にオブジェクトの正確な大きさを表示してくれます。
単位も色々あります。ヤードとか誰が使うんでしょうか?

ピエロのインスペクタ。
電動キックボードのインスペクタ

大きさを漠然と決めるのではなく、ピエロを約180㎝にしたから、電動キックボードはこれくらいで…と基準ができるのがとても良いです。

4.テクスチャ用画像の繰り返し(リピート)設定(小ネタ)

作成した3D図形(Plate)に道路のpng画像をドラッグすると、以下のようになりますが、それには設定が必要です。

果てしなく続く
道路の画像

まず道路の画像のラップモードを繰り返し(英語ではrepeat)にすることです。

そしてPlateのインスペクタの道路の画像のタイリングを以下のようにします。道路は縦に長く伸ばしているので、YがXの100倍になります。これは調節してください。

設定が適切に行われていないと、以下のように画像が引き延ばされます。

↓参考記事

5.ピエロを電動キックボードで疾走させてみる

最後に無限ランゲーム風に電動キックボードを走らせて、カメラをそれに追従させたので、コードを書いておきます。
なお、自分の環境では電動キックボードのRigidbodyの補間を「なし」から「補間」に変更しないと、ジッダー(オブジェクトがガタガタ震える現象)が発生しました。

↓参考記事

英語ではInterpolate
緑の中を走り抜けてく真っ黄なピエロ

(1)ScooterController

using UnityEngine;

public class ScooterController : MonoBehaviour
{
    [Header("Movement Settings")]
    public float forwardSpeed = 10f;      // 前進速度
    public float horizontalSpeed = 5f;    // 左右移動速度
    public float horizontalLimit = 3f;    // 左右移動範囲
    
    [Header("Physics")]
    public Rigidbody playerRigidbody;
    public bool isGameOver = false;

    void Start()
    {
        if (playerRigidbody == null)
            playerRigidbody = GetComponent<Rigidbody>();

        // Root Motion無効化(必要なら)
        Animator anim = GetComponent<Animator>();
        if (anim != null) anim.applyRootMotion = false;

        // 物理設定
        playerRigidbody.useGravity = false;   // 必要に応じて
        playerRigidbody.drag = 0f;
        playerRigidbody.angularDrag = 0f;
        playerRigidbody.interpolation = RigidbodyInterpolation.Interpolate;
    }

    void FixedUpdate()
    {
        if (isGameOver)
        {
            // ゲームオーバー時は停止
            playerRigidbody.velocity = Vector3.zero;
            return;
        }

        // 入力取得(デッドゾーン適用)
        float horizontalInput = Input.GetAxis("Horizontal");
        if (Mathf.Abs(horizontalInput) < 0.01f)
            horizontalInput = 0f;

        Vector3 pos = playerRigidbody.position;

        // 現在の位置を確認し、限界に達している場合は入力を抑制
        if (pos.x >= horizontalLimit && horizontalInput > 0f)
        {
            // 右限界に達していてさらに右入力を受けている場合、横入力を0に
            horizontalInput = 0f;
            // 念のため位置を限界値に固定
            if (pos.x > horizontalLimit)
            {
                pos.x = horizontalLimit;
                playerRigidbody.position = pos;
            }
        }
        else if (pos.x <= -horizontalLimit && horizontalInput < 0f)
        {
            // 左限界に達していてさらに左入力を受けている場合、横入力を0に
            horizontalInput = 0f;
            // 念のため位置を限界値に固定
            if (pos.x < -horizontalLimit)
            {
                pos.x = -horizontalLimit;
                playerRigidbody.position = pos;
            }
        }

        // 目標速度をvelocityで設定
        Vector3 targetVel = new Vector3(
            horizontalInput * horizontalSpeed,
            0f,
            forwardSpeed
        );

        playerRigidbody.velocity = targetVel;
    }

    void OnCollisionEnter(Collision collision)
    {
        if (collision.gameObject.CompareTag("Enemy"))
        {
            // 敵衝突時の吹っ飛び表現
            playerRigidbody.AddForce((Vector3.up + -transform.forward) * 500f);
            isGameOver = true;
        }
    }
}

(2)CameraFollow

using UnityEngine;

public class CameraFollow : MonoBehaviour
{
    public Transform player;
    public Vector3 offset = new Vector3(0, 5, -10);
    public float followSpeed = 5f;

    // カメラのX座標を固定
    public float fixedX = 0f;

    // カメラの回転を固定したい場合、初期回転を覚えておく
    private Quaternion initialRotation;

    void Start()
    {
        // 現在の回転を初期回転として記憶
        initialRotation = transform.rotation;
        // 必要に応じてfixedXの値を現在位置から取得も可能
        // fixedX = transform.position.x;
    }

    void LateUpdate()
    {
        if (player == null) return;

        // プレイヤー位置+オフセットに基づく目標位置を計算
        Vector3 targetPos = player.position + offset;

        // X座標を固定
        targetPos.x = fixedX;

        // カメラを滑らかに目標位置へ移動
        transform.position = Vector3.Lerp(transform.position, targetPos, followSpeed * Time.deltaTime);

        // 回転を初期回転に固定(Y回転しない)
        transform.rotation = initialRotation;
    }
}

いいなと思ったら応援しよう!