Unityの忘備録です('◇')ゞ

自分用の忘備録です!

【Unity】NavMesh.SamplePositionで有効な座標を取得する際、到達不可能な離れ小島(分断されたNavMesh領域)の座標を除外する方法【忘備録】

内容

SamplePositionでNavMeshの有効座標を取得する際、
上記画像のような離れ小島の座標を除外したい

手順

スクリプトにて、NavMesh.SamplePosition()の直下に下のコードを入力しさらにフィルターをかける

スクリプト

下の離れ小島対策用コードをSamplePosition直下に記入

NavMeshPath path = new NavMeshPath();
if (agent.CalculatePath(destination, path) && path.status == NavMeshPathStatus.PathComplete)
{
    // 到達可能な経路が存在する場合の処理
}

出典:「Perplexity AIより」

使用例

youtu.be
© UTJ/UCL

プレイヤーがNavMeshの外側にいる場合、プレイヤーから最も近いNavMeshの有効座標(離れ小島は除外)へ移動する

        void Set_Destination()
        {
                NavMeshPath path = new();
                NavMesh.CalculatePath(transform.position, player.position, NavMesh.AllAreas, path);// プレイヤーまでの道を取得

                if (path.status == NavMeshPathStatus.PathPartial || path.status == NavMeshPathStatus.PathInvalid)// プレイヤーがNavmeshの有効範囲外などによりプレイヤーまで到達不可能なら
                {
                    Debug.Log("プレイヤーは NavMesh の外です");
                    if (agent.remainingDistance < 0.1)// 目的地に近づいたら
                    {
                        if (NavMesh.SamplePosition(player.position, out NavMeshHit hit, 3.0f, agent.areaMask))
                        {
                            if (agent.CalculatePath(hit.position, path) && path.status == NavMeshPathStatus.PathComplete)// 離れ小島でなければ
                            {
                                if (Vector3.Distance(hit.position, destination) > 0.1)
                                {
                                    Debug.Log("目的地を更新しました");
                                    destination = hit.position;
                                }
                            }
                        }
                    }

                    if (Vector3.Distance(transform.position, destination) < 0.1)// 目的地に着いたら止まる
                    {
                        agent.isStopped = true;
                    }
                    else
                    {
                        agent.isStopped = false;
                        agent.speed = speed;
                        agent.destination = destination;
                        Debug.Log(Vector3.Distance(transform.position, destination));
                    }
                }
                else
                {
                    agent.isStopped = false;
                    agent.speed = speed;
                    agent.destination = player.position;
                }               
            }
        }


  

【Unity】特定のオブジェクトを避けて移動する敵の実装【日記】


敵の条件

敵(以下:エネミー)は床に置かれたアイテム(十字架)の半径5以内に入れない

実装の手順

【アイテムの準備】
十字架のタグを”Cross”に設定
十字架にNavMeshObstacleコンポーネントをアタッチ、 インスペクターで半径を5に設定
インスペクターのCarve(切り抜き)を有効にし、NavMesh上に通行不可エリアを作る
十字架にカプセルコライダーをアタッチし、IsTrggerにチェックをいれ、インスペクターで半径を5に設定
※カプセルコライダーやNavMeshObstacleの半径は、スケールに影響されるようです。
 オブジェクトのスケールが0.5の場合、半径は5でも実際は2.5になっているみたいです。



【エネミーの移動】
NavMeshAgent(以下:エージェント)は自動的に障害物(この場合は円)を避けてプレイヤーに近づくようになる
円がプレイヤーとエネミーの間の前経路を塞いでいる場合、スクリプトで一時停止させる



【エネミーの今いる座標に十字架の円が展開された場合の挙動】
NavMeshObstacle(Carve有効)がエージェントの現在位置で有効化されると、NavMeshの穴あけ処理によって、その座標のNavMeshが消滅する
このとき、エージェントは「NavMesh上にいられない」と判断され、**NavMesh上の有効な近傍座標(通常は穴の縁)にワープ(瞬時移動)する
これはUnityのNavMeshAgentの仕様であり、自動的に発生する

上記の理由により、Navmeshの範囲外になる前に、エネミーのエージェントをスクリプトで無効化する必要性がある




十字架はインスタンス化した1秒後にNavmeshObstacleをスクリプトで有効化する
エネミーのスクリプトで十字架の位置を取得し、範囲5以内であるかを確認し、5以内ならスクリプトでエージェントを無効化する
エネミーの座標のNavmeshが無効化された後、エネミーから一番近いNavmeshの有効座標をスクリプトで取得(以下:ターゲット)
エネミーのスクリプトでTransformの位置を変更しターゲットまで移動させ、この間アニメーションの変更も行う
エネミーがNavmeshの範囲内になった場合、スクリプトでエージェントを有効にする

使用したアセット

【十字架】
assetstore.unity.com
【エネミー】
assetstore.unity.com



スクリプト

十字架のNavmeshObstacleを有効化する

    using UnityEngine.AI;
   
    public class Cross : MonoBehaviour
    {
        NavMeshObstacle obstacle;
        float timer;

        void Start()
        {
            obstacle = GetComponent<NavMeshObstacle>();
            obstacle.enabled = false;
        }

        void Update()
        {
            timer += Time.deltaTime;
            if (timer > 1)
            {
                obstacle.enabled = true;
            }
        }
  }

エネミーのスクリプトにて十字架の位置を取得

        Transform crossTransform;

        void OnTriggerEnter(Collider other)
        {
            if (other.CompareTag("Cross"))
            {
                crossTransform = other.transform;
            }
        }

エネミーのスクリプトのUpdate関数内で十字架から5の範囲内を検知
5の範囲内ならエージェントを無効化する

        vod Update()
        {
            if (crossTransform)
            {
                if (Vector3.Distance(transform.position, crossTransform.position) < 5)
                {
                    agent.enabled = false;
                }
            }
         }

プレイヤーがNavmesh有効範囲外(十字架の内側)または到達不可能だったら停止する
エージェントが無効の場合は有効範囲までtransformを移動する
Navmesh有効範囲に入った時はエージェントを有効にする

        void Set_Destination()
        {
            if (agent.enabled == false)// エージェントが無効なら
            {
                if (NavMesh.SamplePosition(transform.position, out NavMeshHit hit, 0.1f, NavMesh.AllAreas))// Navmesh有効範囲内に入ったら
                {
                    agent.enabled = true;
                }
                else// Navmesh有効範囲外だったら
                {
                    for (int i = 1; i < 30; i++)
                    {
                        if (NavMesh.SamplePosition(transform.position, out hit, i, NavMesh.AllAreas))// iは半径。1度で取得できない場合、for分で半径を1づつ広げる
                        {
                            destination = hit.position;// ターゲットの座標を取得
                            break;
                        }
                        else
                        {
                            destination = Vector3.zero;// 30回ループしてターゲットが見つからない場合はVector3(0, 0, 0)を取得
                        }
                    }

                    if (destination != Vector3.zero)// Vector3(0, 0, 0)以外なら
                    {
                         transform.position = Vector3.MoveTowards(transform.position, destination, Time.deltaTime);
                         EA.UpdateAnimatorValues(1);// 歩くアニメーションに変更
                    }
                }                
            }
            else// エージェントが有効なら
            {
      NavMeshPath path = new();
                NavMesh.CalculatePath(transform.position, player.position, NavMesh.AllAreas, path);// プレイヤーまでの道を取得

                if (path.status == NavMeshPathStatus.PathPartial || path.status == NavMeshPathStatus.PathInvalid)// プレイヤーがNavmeshの有効範囲外などによりプレイヤーまで到達不可能なら
                {
                    Debug.Log("ターゲットが NavMesh 外にあります");
                    agent.isStopped = true;// エージェントを停止
                 }
                 else
                 {
                    agent.isStopped = false;
                    agent.destination = player.position;
                 }
            }

進行方向にオブジェクトの向きを合わせる

        void Rotation()
        {
            if (agent.enabled == false && (destination - transform.position).sqrMagnitude > 0.0001f)
            {
                transform.forward = (destination - transform.position).normalized;
            }                
        }


© UTJ/UCL

今回苦戦したところは...

十字架の円がエネミーに重なった瞬間にエネミーが瞬間移動してしまうところですね。
NavMeshObstacleの発動を遅らせることで、エージェントの無効化ができ瞬間移動を阻止できました(^-^)

【Unity】近づくと逃げる敵の実装【日記】

敵(以下エネミー)の条件

  • プレイヤーとの距離が2以下になるとプレイヤーの反対方向へ5以上離れる
  • 5以上離れるとその場で止まる
  • 離れている最中にプレイヤーが近づいた場合でも、プレイヤーから5以上離れきるまで目的地を更新し続ける
  • 5.5以上離れると5になるまで近づく

実装の手順

  • エネミーに3つのステイタス「逃げている」「止まっている」「追いかけている」を設ける
  • 「プレイヤーとの距離が2以下になった」または「逃げている時プレイヤーとの距離が5以下になった」を検知 
  • プレイヤーと反対かつ5離れている座標(以下 ターゲット)を取得
  • ターゲットを中心とした半径1の円内にある有効で最も近いNavmeshの座標を次の目的地とする
  • for分と組み合わせることにより、ターゲットがNavmeshの範囲外の場合でも有効座標を取得するまで検索範囲を拡大できるようにする

※  NavMesh.SamplePositionを使用
   この関数は、sourcePosition から maxDistance の半径内で、NavMesh上に存在する最も近い地点を探します
   検索範囲は「半径 maxDistance の球体(3D)」になります

NavMesh.SamplePosition(Vector3 sourcePosition, out NavMeshHit hit, float maxDistance, int areaMask)

コードの実装

        [SerializeField] string status;

        void Set_Destination()
        {
                if (distanceToPlayer < 2  || status == "逃げてる" && distanceToPlayer < 5)
                {
                    if (agent.remainingDistance > 0.2) return;// 目的地から離れている場合は目的地を更新しない

                    Vector3 dir = (transform.position - player.position).normalized; // プレイヤーの反対方向に逃げる座標を計算
                    Vector3 fleeTarget = transform.position + dir * 5;

                    for (int i = 0; i < 30; i++) // Navemesh範囲外対策    目的地が決まるまでループ
                    {
                        // NavMesh上の有効なポイントに補正
                        if (NavMesh.SamplePosition(fleeTarget, out NavMeshHit hit, i, NavMesh.AllAreas))
                        {
                                agent.destination = hit.position;
                                status = "逃げてる";
                         }

                            break;
                        }
                    }
                }
                else if (distanceToPlayer > 5.5)
                {
                    status = "追いかけてる";                    
                }
                else if (distanceToPlayer > 5)
                {
                    status = "止まってる";
                }


                // 移動の処理
                if (status == "逃げてる")
                {
                    agent.speed = speed * 4;
                }
                else if (status == "追いかけてる")
                {
                    agent.speed = speed;
                    agent.destination = player.position;
                }
                else if (status == "止まってる")
                {
                    agent.speed = 0;
                    agent.ResetPath();// 目的地を削除する
                }
            }
        }

問題発生

【現象】エネミーが壁に向かって歩き続ける
【タイミング】 プレイヤーとの距離が5以下 かつ エネミーが壁の前にいる時

エネミーがNavMeshの端や角にいる場合、プレイヤーから遠ざかる方向にNavMeshが続いていないため、SamplePositionで得られる最遠点が「今いる場所」になる。
for分で検索範囲を拡大しても、取得できる有効座標は「今いる場所」となってしまうようだ。
※5離れた壁の向こう側から円を描くと、円の半径が5になった時点で今いる座標が取得され目的地となる為
そのため、目的地にしても移動が発生せず、以降も同じ座標を目的地にし続けるだけになる。
プレイヤーとの距離が5以下の場合、同じ場所に向かって歩き続けるという状態になってしまった。

対策案

  • 目的地が現在地とほぼ同じ場合(距離が小さい場合)は「これ以上逃げられない」と判定する
  • 「これ以上逃げられない」時の処理を新たに実装

コード追加

// 目的地が今いる場所とほぼ同じなら、逃げられないと判定
if (Vector3.Distance(agent.transform.position, hit.position) < 0.1)
{
 status = "止まっている";
}
else
{
     agent.destination = hit.position;
     status = "逃げてる";
}

問題発生

youtu.be
【現象】エネミーが壁の前で「逃げている」と「止まっている」を繰り返す
【タイミング】プレイヤーとの距離が5以下 かつ エネミーが壁の前にいる時

目的地が「今いる場所」でも目的地までの距離が0.1以上になったり、
0.1以下になったりして「逃げている」と「止まっている」を交互に繰り返してしまう。
おそらく、for文で探索範囲を1づつ増やしている事が原因の可能性あり。
どうやら端の手前で止まった場合 かつ 次の目的地を端にした時に発生しているようだ。
remainingDistance が 0.2 以下で目的地を更新できるため、端まで0.2以下の距離であれば目的地をマイフレーム更新しようとしている。
壁との距離が縮まっていくのに目的地との距離が0.1を行き来するのかは不明。

対策案

  • エネミーのステイタスに「待機している」を追加
  • 一度壁に行くと「待機している」になり、プレイヤーから5以上離れないと目的地を更新しないようにする

コードの改善

       void Set_Destination()
        {
                if (distanceToPlayer < 2 && status != "待機している" || status == "逃げてる" && distanceToPlayer < 5)
                {
                    if (agent.remainingDistance > 0.2) return;

                    Vector3 dir = (transform.position - player.position).normalized; //プレイヤーの反対方向に逃げる座標を計算
                    Vector3 fleeTarget = transform.position + dir * 5;

                    for (int i = 0; i < 30; i++) //目的地が決まるまでループ
                    {
                        // NavMesh上の有効なポイントに補正
                        if (NavMesh.SamplePosition(fleeTarget, out NavMeshHit hit, i, NavMesh.AllAreas))
                        {
                            // 目的地が今いる場所とほぼ同じなら、逃げられないと判定
                            if (Vector3.Distance(agent.transform.position, hit.position) < 0.1)
                            {
                                status = "待機している";                          
                            }
                            else
                            {
                                agent.destination = hit.position;
                                status = "逃げてる";
                            }

                            break;
                        }
                    }
                }
                else if (distanceToPlayer > 5.5)
                {
                    status = "追いかけてる";                    
                }
                else if (distanceToPlayer > 5)
                {
                    status = "止まってる";
                }


                // 移動の処理
                if (status == "逃げてる")
                {
                    agent.speed = speed * 4;
                }
                else if (status == "追いかけてる")
                {
                    agent.speed = speed;
                    agent.destination = player.position;
                }
                else if (status == "止まってる" || status == "待機している")
                {
                    agent.speed = 0;
                    agent.ResetPath();
                }
            }
        }

おわり

youtu.be
壁の前に来ても回転しなくなった!!
次回はプレイヤーに攻撃するコードを実装する('◇')ゞ

【Unity】OnBecameVisible関数が機能しない...【日記】




カメラにオブジェクトが映ってるか判断する方法の1つに
OnBecameVisible関数とOnBecameInvisible関数があります。

オブジェクト ( 以下ターゲット )がカメラに映っていない時だけ移動するようにしたいと思い使用しました。

しかし関数が実行されたりされなかったりで
不安定でした(゜-゜)

下のコードでテストした結果、
変数がTrueになる時もあればならない時もありました。

おまけに、ターゲットとカメラの間に壁があったとしても
カメラの射程内に入っていれば検知されてしまうようです。

public class OnCamera : MonoBehaviour // ターゲットにアタッチする(レンダラーが付いてなければならない)
{
    public bool onCamera;
    
    void OnBecameVisible()
    {     
        if (Camera.current.name == "SecurityCamera")
        onCamera = true;
    }

    void OnBecameInvisible()
    {
        onCamera = false;
    }
}


そこで改善案を考えました('◇')ゞ
カメラからターゲットへRayを飛ばして壁のある無しを判断するというものです。
Rayがターゲットへ当たっていれば True。
当たらなければ Falseを返します。

public class WallDetection : MonoBehaviour // カメラにアタッチする
    {
        [SerializeField] Transform target; // ターゲットオブジェクト
        
        void Update()
        {
            Vector3 direction = (target.position - transform.position).normalized;

            if (Physics.Raycast(transform.position, direction, out RaycastHit hit, Vector3.Distance(transform.position, target.position)))
            {
                Debug.Log(hit.collider.name);
                if (hit.collider.CompareTag("Enemy"))
                    target.GetComponent<OnCamera>().onCamera = true;
                else
                    target.GetComponent<OnCamera>().onCamera = false;

            }
        }

        void OnDrawGizmos() // Rayを描写するだけ
        {
            if (target == null) return;

            // カメラの位置(このスクリプトがアタッチされているオブジェクトの位置)
            Vector3 start = transform.position;
            
            // ターゲットへの方向
            Vector3 direction = (target.position - start).normalized;
            
            // Rayの終点(カメラからターゲットまでの距離)
            Vector3 end = start + direction * Vector3.Distance(start, target.position);

            // Rayの描画
            Gizmos.color = Color.yellow;
            Gizmos.DrawLine(start, end);
        }        
    }
}


下の動画のように、ターゲットをRayが検知しています。
Rayがターゲットを検知すると、ターゲットの動きが止まるようにしました。




© UTJ/UCL



問題点
カメラの裏にもRayが飛んでしまうので、
カメラの裏側は壁にするなど工夫が必要ですね。
とりあえず思惑通りの結果になったのでこのまま進めます(^-^)
おわり

【ScreenPointToRay】銃口(Rayの発射位置)とカメラの間に壁がある時の対処方【Unity】

© UTJ/UCL

上の画像のようになってしまい解決に凄い時間がかかったので忘備録を残します!



ScreenPointToRayでは、カメラ(スクリーン)中央の座標を取得できます。

Ray ray = Camera.main.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2));




3人称視点ではキャラクター(プレイヤー)が銃を構え画面の真ん中に銃弾を撃つという処理があったりします。
この時画面の真ん中の座標をScreenPointToRayで取得する場合、
カメラはキャラクター(プレイヤー)から見て銃口より手前にあるので、銃口とカメラの間にオブジェクトがあればそこにRayがヒットしてしまう事になります。
下記スクリプトはScreenPointToRayで得た座標にカメラからレイを飛ばすコードです。

if (Physics.Raycast(ray, out RaycastHit hit, 10))
{
     //ヒットした時の処理
}




上記の事情からRayの発射位置をカメラではなく銃口にしたいと考えへ下記のスクリプトを試しました。
カメラ中央へ1つ目のRayを飛ばし、取得した座標の方向へ2つ目のRayを銃口から飛ばすという方法です。

if (Physics.Raycast(ray, out RaycastHit hit, 10))
{
 if (Physics.Raycast(muzzle.position, hit.point , out RaycastHit hit2, 10)
   {
        //ヒットした時の処理
   }
}



しかし結果は同じでした。
①Rayの発射位置を変えたところで、銃口から手前の壁にRayが飛んでいくだけ
②ScreenPointToRayは発射位置(Z軸)を指定できない!!※たぶん





そもそも上記の現象は当たり前で、1つ目のRayが当たった壁に2つ目のRayが飛ぶのは当然の事。
僕は途中までScreenPointToRayは画面の座標を取得してその方向へRayを飛ばすもので、発射位置をカメラから見て奥にすれば手前のオブジェクトには当たらない
という謎の勘違いしていました(-_-)






諦めようかと思ったのですが、裏を返せば銃口より手前にRayがヒットした事を取得できれば何とかなりそうだなと考えました('◇')ゞ
こういう時すごい便利なのがTransform.InverseTransformPointです。
Transformの部分は基準にしたい座標のTransform型を指定します。
この場合、銃口(muzzle)を指定しています。
上記の関数は引数に入れた座標を、基準にした座標のローカル空間座標に変換してくれます。
下記スクリプトでは銃口から見たhit.pointのZ軸の値をFlaot型に格納しています。

if (Physics.Raycast(ray, out RaycastHit hit, 10))
{
     float dirZ = muzzlePoint.InverseTransformPoint(hit.point).z;
}






下記動画では、取得できたZ軸の値が0以下なら発砲しないという処理にしています。

youtu.be

Ray ray = Camera.main.ScreenPointToRay(new Vector3(Screen.width / 2, Screen.height / 2));

float dirZ = 0.1f;// Rayがヒットしなくても発砲したいから0以上にする

if (Physics.Raycast(ray, out RaycastHit hit, 10))
{
      dirZ = muzzlePoint.InverseTransformPoint(hit.point).z; //ここで0以下になった場合発砲できない

     if (dirZ > 0)
       {
          if (Physics.Raycast(muzzle.position, hit.point , out RaycastHit hit2, 10)
            {                            
                 //何かにヒットした場合の処理 ※弾痕やダメージなど
            }
       } 
}

if (dirZ > 0)
{
    //発砲音や弾数を減らす処理
}






色々考えた結果上記の方法しか思いつきませんでした(´;ω;`)
正直、カメラの目の前に壁があろうとなかろうと目の前の壁を貫通して銃口より奥にRayが飛んで行ってくれれば一番いいのですが
調べても方法が分からなかったのでこのやり方でひとまず?良しとします。
もし参考になりましたら嬉しいです(^-^)
では('◇')ゞ

【Unity】Rayが目的地点とずれてしまう時の対処法?

Rayは第1引数に開始地点、第2引数に方向と長さ(*長さ)を代入することで第2引数の方向へ光線を飛ばすことができる・・・と今まで思って生きてきたのですが、
下の画像のようになぜか目的地の方向に対してずれが生じました。
ray.directionは方向とありますが、なぜ目的地とずれてしまうのでしょうか。
たぶん僕の解釈が間違っている・・・


© UTJ/UCL

 Ray ray = new (muzzleTransform.position, aimTargetTransform.position);
 Debug.DrawRay(ray.origin, ray.direction * Vector3.Distance(ray.origin, ray.direction), Color.red);


下のコードでは上手く目的地と光線の終点が一致しました。

Debug.DrawRay(muzzleTransform.position, aimTargetTransform.position - muzzleTransform.position, Color.red);

おわり('◇')ゞ

【Unity】BlendTreeを使うと足音がミックスされちゃう時の対処法

アニメーションのイベントの項目で足音などのSEを設定できますが、
BlendTreeでアニメーションの切り替えを行う際に、2つのアニメーションの音が
同時に再生されてしまう問題が発生
しました('◇')ゞ



そもそもBlendTreeはアニメーションをいい感じにミックスしてくれる機能なので、
音もミックスされるのは自然な事なのかもしれませんが、足音だと困ってしまいます!


改善した結果の動画
youtu.be
キャラクター素材 © Unity Technologies Japan/UCL
音素材 魔王魂様: https://maou.audio/



僕がある程度触って分かったことなのですが、
BlendTreeはパラメーターの数値が、アニメーションのしきい値とピッタリの場合はそのアニメーションのみ再生される。
ピッタリでない場合は、パラメーターの数値に近いしきい値を持つ2つのアニメーションがミックスされる
ようです。



なので全てのアニメーションにSEを設定するのではなく、1つ飛ばしで設定します。
でもこの時、SEが設定されていないアニメ―ションのしきい値にパラメーターの数値がピッタリ合ってしまうと当然SEは再生されないので、ピッタリにならないように調整する必要がありますね!
スクリプトでパラメーターの数値をしきい値と被らない数値にすればOKです。



下の図ですと、しきい値は赤丸の項目です。変更も可能です。
その右の数値はアニメーションの再生速度を変更できる項目です。
4番目の走るアニメーションは3番と一緒ですが、再生速度を1.7倍速にしています。
上記のように同じアニメーションを使う場合、同じアニメーションですとイベントが被ってしまうので、アニメーションをコピーして2つにし、SEのあるなしで分けるといいと思います。


下の図ですと、赤字の2と4にのみSEを設定しています。

あとはパラメーター(上記画像ではSpeed)の値をしきい値と被らないようにスクリプトで調整すれば上手く行きそうですね!