poi’s tech blog

3D多人数同時接続型球体アクション成人向けゲーム開発のためのアイデア、ナレッジ

Unity3Dでマリオのジャンプ

Unity 2020.2.7

参考元

qiita.com

Unityの場合

f:id:poipoipoip:20210331215702g:plain

多分動くコード

適当な3Dモデルのゲームオブジェクトにアタッチ

Use GravityはOFFで

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

public class MyMarioJump : MonoBehaviour
{
    [SerializeField] float jumpPower = 3f;        // ジャンプ力
    [SerializeField] float fallPower = -5f;        // 落下力

    private GroundCheck gr;  // CharacterControllerのisGroundedでも可

    bool jumping = false;  // ジャンプ中かどうかのフラグ
    float yPrev;  // 前フレームのtransform.potision.yの値
    float f;  // 最初のフレームはジャンプ力、それ以降は落下力が入力される
    
    void Jump()
    {
        Vector3 tmp = transform.position;
        Vector3 t = transform.position;
        bool isFalling = transform.position.y < yPrev;

        t.y = (transform.position.y - yPrev) + f;
        t.x = 0f;
        t.z = 0f;

        transform.position += t;
        yPrev = tmp.y;

        f = GetFallPower();

        if (gr.isGrounded && isFalling)
        {
            Debug.Log("ground");
            jumping = false;
        }
    }

    // SerializeFieldで使いやすいようにここで値を小さくする
    private float GetJumpPower()
    {
        return jumpPower / 100;
    }

    private float GetFallPower()
    {
        return fallPower / 10000;
    }

    private void Start()
    {
        gr = GetComponent<GroundCheck>();
        f = GetFallPower();
    }

    void Update()
    {
        if (jumping)
        {
            Jump();
            return;
        }

        if (Input.GetKeyDown(KeyCode.X))
        {
            if (!jumping)
            {
                Debug.Log("jump");
                jumping = true;
                f = GetJumpPower();
                yPrev = transform.position.y;
            }
        }
    }
}

Unity3DでH◯L研ジャンプが作りたかったお話

Unity 2020.2.7

参考

qiita.com

www.hallab.co.jp

モチベーション

一言でジャンプといっても色んな手法がある。

思いつくもので3通り

  1. 重力が働く環境下でジャンプボタン押下時にy軸に初速を与える方法
  2. 放物運動の公式を使う方法
  3. 物理挙動を無視した方法

この3通りのジャンプにおいて、ロジックはこのように分けられると思う

番号 ロジック
1と2 常に同じ計算処理が走っていて、ボタンを押したときに初期値が与えられる
3 地面にいる時とジャンプ中で移動ロジックが切り替わる

それぞれメリットデメリットがあると思うが、個人的に単純に放物線を描くジャンプはつまらないので3の物理挙動を無視したジャンプを作りたかった。

その中でも参考記事のH◯L研ジャンプが「高さ」「持続時間」「sin波の軌道のゆがみ」の調整が柔軟にでき、理想のジャンプが作りやすそうだったためUnity3Dで動くものを作った。

前提

  • キーボードのXキーを押してジャンプする
  • 接地中かどうかの判定方法がある(CharacterControllerのisGroundedでもいいはず)
  • 重力加速度はOFF

とりあえず動くコード

適当に3Dモデルのゲームオブジェクトにアタッチすれば動くはず。

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

public class MyJumpController : MonoBehaviour
{
    bool isJumping = false;
    float t = 0f; //経過時間 ( 0.0 -> 1.0 )
    Vector3 nextPos; //次に設定される座標
    Vector3 firstPos; //動作開始した座標
    Vector3 targetPos; //目標位置
    float height = 1f; // ジャンプの高さ
    float distortion = 1f; // 曲線のゆがみ

    private void Start()
    {
        gr = GetComponent<MyGroundCheck>();  // CharacterControllerのisGroundedでもいい
    }

    void Update()
    {
        if(isJumping)
        {
            Jump();
            return;
        }
        
        if (Input.GetKeyDown(KeyCode.X))
        {
            isJumping = true;
            firstPos = transform.position;
        }
    }

    void Jump()
    {
        nextPos = transform.position;
        t += Time.deltaTime;

        float pow = (float)Math.Pow(1.0 - Math.Sin(Math.PI * t), distortion);
        nextPos.y = firstPos.y + (1.0f - pow) * height;
        transform.position = nextPos;

        if (gr.isGrounded)
        {
            t = 0f;
            isJumping = false;
        }
    }
}

結果

f:id:poipoipoip:20210326002808g:plain

着地をさせたかったのでジャンプの終了判定をt>1.0からisGroundedに無理やり変えた。

そのため同じ高さ~高いところへの着地はこれでいいが、崖からジャンプした時などはsin波を描いてしまう。

ユーザーからの操作の介入がない、敵キャラクターなどの動きではこのようなカーブの計算式で十分だったりします。

とのことなのでユースケースに合わせたジャンプ手法を採用しよう。

Unityメモ

Unity 2020.2.7

Sceneビューの表示モードをPersp↔Isoに切り替えたときに「Expanding invalid MinMaxAABB」というエラーが出る

立体透視か並行透視かのアレ。

しかも、PerspモードのときにSceneビューに表示されたモデルだけが視点を変えるたびに真っ暗になったり元に戻ったりとバグっぽい見た目になる。

とりあえず、Hierarchyに配置されたDirectional Lightを作成し直したら直った。

Unity 2019からアップデートしてきたSceneだったからなのかの関係性は不明。

Unity 高頻度で呼ばれるメソッド内ではなるべくnew()しないほうがいいという話を身をもって実感したお話

Unity 2020.2.7

Ryzen9 3900X

こないだ作った接地判定スクリプトにおいて、Update()内でnew()してる部分がある。

poipoipoip.hatenablog.com

よく「インスタンス生成コストが高いので高頻度で呼ばれるメソッド内でなるべくnew()しないほうがいい」という話は耳にしていたが、実際のところどの程度パフォーマンスに影響が出るか気になった。

今回は、newする場合としない場合のメソッドをループ実行して掛かった合計時間を計測してみた。

(newする事以外の処理内容はほぼ同じ)

計測用コード

以下の記事を参考にしました。

qiita.com

    private void Start()
    {
        System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
        sw.Start();
        for (int i = 0; i < 10000; i++)
        {
            for (int j = 0; j < 10000; j++)
            {
                GetRayStartPosition();
            }
        }
        sw.Stop();
        Debug.Log(sw.ElapsedMilliseconds + "ms");
    }

newするメソッド

    Vector3 GetRayStartPosition(float offset = 0)
    {
        return new Vector3(transform.position.x, transform.position.y + offset, transform.position.z);
    }

newしないメソッド

    Vector3 GetRayStartPosition(float offset = 0)
    {
        Vector3 t = transform.position;
        t.y += offset;
        return t;
    }

測定結果

メソッド 合計時間
newする 18214ms
newしない 6424ms

なんと倍以上の差が出た。

接地判定するために常に動き続ける処理なので軽いのに越したことはない。

塵も積もればなんとやら。

メモ

newしたときのメモリ管理についてわかりやすく説明されている。

chocochoco.hatenablog.com

Unity3DでCharacterControllerを使わずに自分なりに納得いく接地判定を作る

Unity 2020.2.7

キャラクターがジャンプや落下中などに2段ジャンプしないように接地判定を実現しようと思った時、最初に上がる候補としてCharacterControllerのIsGroundedが挙がる。

しかし色んな記事を参考にするとCharacterControllerの接地判定はイマイチらしく、ShereCastを使うといいよという記事が出てくる。

が、

実装について触れている記事があまり無かったため、自分なりに納得行く接地判定を作ったというメモ。

やりかた

以下のスライムモデルを参考に説明。

f:id:poipoipoip:20210312223352p:plain
かわいい。

f:id:poipoipoip:20210312223527p:plain
モデル(transform)の原点は高さ0

以下のスクリプトをモデルのゲームオブジェクトにアタッチ

using UnityEngine;

public class GroundCheck : MonoBehaviour
{
    public bool isGrounded;
    float radius = 0.1f;

    RaycastHit hit;

    void Update()
    {
        isGrounded = Physics.SphereCast(
            GetRayStartPosition(radius),
            radius,
            Vector3.down * GetMaxDistance(),
            out hit, 
            GetMaxDistance()
        );
        Debug.Log(isGrounded);
    }

    void OnDrawGizmos()
    {
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(GetRayStartPosition(), radius);
        Gizmos.color = Color.blue;
        Gizmos.DrawRay(GetRayStartPosition(radius), Vector3.down * GetMaxDistance());
    }

    Vector3 GetRayStartPosition(float offset = 0)
    {
        Vector3 t = transform.position;
        t.y += offset;
        return t;
    }

    float GetMaxDistance()
    {
        return radius * 2;
    }
}

するとモデルの真下に小さなGizmosが表示される。

f:id:poipoipoip:20210312223812p:plain
ギズモ

簡単に説明すると、赤丸の範囲内にオブジェクトが重なっていればisGroundedがtrueになる。
(ちょうど赤丸の直径が青い線の部分だけどみづらい…)

f:id:poipoipoip:20210312224523g:plain
ぴょんぴょん

着地の挙動が若干不自然なのは大目に見て(DOLocalJumpでジャンプ)

所感

いちおうレイヤー分ける必要はないのはメリットかもしれないけど、この判定方法はSphere Collider使っても同じことできそう…

3/14追記

new()しないほうがパフォーマンス的に良かったのでGetRayStartPosition()メソッドを書き換えた。
古いコードは以下の通り。

    Vector3 GetRayStartPosition(float offset = 0)
    {
        return new Vector3(transform.position.x, transform.position.y + offset, transform.position.z);
    }

Timelineで指定したMarkerの位置にジャンプするMarkerを作った。

Unity2019.3.14f1

ググってもいい感じのMarkerが見つからなかったので自作した。

f:id:poipoipoip:20210307185257g:plain

MarkerReceiver

Timelineの"Jump Track"という名前のトラックを探すようにしているので適宜変更して下さい。

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

// Jumpするよーという情報を受信するマーカー
public class JumpFromMarkerReceiver : MonoBehaviour, INotificationReceiver
{
    private PlayableDirector director;
    private IEnumerable<IMarker> markers;

    private void Awake()
    {
        director = GetComponent<PlayableDirector>();

        // Timelineのトラック一覧を取得
        IEnumerable<TrackAsset> tracks = (director.playableAsset as TimelineAsset).GetOutputTracks();

        // 指定した名前のトラックを取得
        TrackAsset track = tracks.FirstOrDefault(x => x.name == "Jump Track");

        // トラック内のマーカー一覧を取得
        markers = track.GetMarkers().ToArray();
    }

    public void OnNotify(Playable origin, INotification notification, object context)
    {
        var element = notification as JumpFromMarker;
        if (element == null)
            return;

        // タグが一致したマーカーの位置にジャンプ
        foreach (var m in markers)
        {
            JumpToMarker j = m as JumpToMarker;
            if(j == null) continue;

            if (j.jumpTag == element.jumpTo)
            {
                Debug.Log(element.jumpTo);

                director.time = j.time;
                break;
            }
        }
    }
}

f:id:poipoipoip:20210307182942p:plain
Playable Directorと同じオブジェクトに設定してください

Jump元となるMarker

using System.ComponentModel;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

// Jumpするよーという情報を送信するマーカー
[System.Serializable, DisplayName("Jump From Marker")]

public class JumpFromMarker : Marker, INotification
{
    public string jumpTo;

    public PropertyName id
    {
        get
        {
            return new PropertyName("method");
        }
    }
}

f:id:poipoipoip:20210307183031p:plain
Jump先MarkerのJump Tagと同じ文字列を入力してください。

Jump先となるMarker

using System.ComponentModel;
using UnityEngine;
using UnityEngine.Playables;
using UnityEngine.Timeline;

// Jump先情報を記録するためのマーカー
[System.Serializable, DisplayName("Jump To Marker")]

public class JumpToMarker : Marker, INotification
{
    public string jumpTag;

    public PropertyName id
    {
        get
        {
            return new PropertyName("method");
        }
    }
}

f:id:poipoipoip:20210307183135p:plain
Jump先Marker

メモ

  • 同じタグのJump先が複数ある場合は最初に作成したMarkerのほうに飛ぶはず

  • 適当に条件式追加してあげれば「特定の状態を満たしていればジャンプ」みたいな使い方ができるはず

ジャンプした時にまたいだ別のSignalが全部動いてしまう

こまった…

directorをPause→ジャンプ→Resumeはダメ、

Signal Trackの動的Muteしようとdirector.RebuildGraph()を叩くとジャンプ地点がバグるからダメ
https://forum.unity.com/threads/mute-unmute-a-track-via-scripting.509604/

いい方法が思い浮かばないので以下のような最悪な実装方法をしている。

  • 標準のSignalではなく独自Markerを作成してUnityEvent発生させる
  • 独自Marker内の処理で、特定のフラグがONの場合はUnityEvent発生させない
  • ジャンプの手前でフラグをON、ジャンプ終わって1秒後とかにフラグをOFF