[Unity]魚群のスクリプト。任意の位置にランダム生成される魚群のプログラム。

2022年8月25日

どうも、だらはです。
現在鋭意制作中のモルディブ旅行ゲームに、魚群プログラムを実装しました!
基本コピペで実装可能なので、良かったらシェアしていって下さい(*’▽’)

スポンサーリンク

スクリプト概要

本稿にて紹介するスクリプトでできることは、以下のことです。

  • 指定した領域に任意数の魚(魚群)を生成する。
  • プレイヤーから離れたら非アクティブとなる。

また、スクリプトは『凹みTips』様の記事を参考にさせて頂きました。(マジ神!!)

◆参考とさせて頂いた記事

こちらで紹介されているスクリプトに対して、以下の変更を施しました。

◆変更内容

  • 壁の位置を、XYZ座標入力化。
  • 魚を、親オブジェクのトBox Collider内に分散生成。
  • 魚群を、プレイヤーから離れると非アクティブ化。

導入手順

①魚オブジェクトにCollider、Rigidbody、スクリプトをアタッチ

魚オブジェクトに以下の要素をアタッチします。

  • Collider(今回は魚オブジェクトが簡易なのでBox Colliderを採用)
  • BoidFish.cs
  • Rigidbody

②ScriptableObjectを使って、パラメタテーブルを作る。

ParamFish.csを右クリック -> Create -> BoidFish -> ParamFish。
ParamFish.csからパラメタテーブルを作成する。

③空のGameObjectを作成し、BoxCollider、スクリプトをアタッチ

Create Emptyにて空のGameObjectを作成し、以下の要素をアタッチします。

  • Box Collider(IsTrigger ON。魚の移動領域のため)
  • SimBoidFish.cs(魚オブジェクトのプレハブ/Paramを指定)
  • EnableNearChild.cs

以上です。

◆BoidFish.cs

using UnityEngine;
using System.Collections.Generic;

public class BoidFish : MonoBehaviour
{
    public SimBoidFish simboidFish { get; set; }
    public ParamFish paramFish { get; set; }
    public Vector3 pos { get; private set; }
    public Vector3 velocity { get; private set; }
    Vector3 accel = Vector3.zero;
    List neighbors = new List();

    void Start()
    {
        pos = transform.position;
        velocity = transform.forward * paramFish.initSpeed;
    }

    void Update()
    {
        // 近隣の個体を探して neighbors リストを更新
        UpdateNeighbors();

        // 壁に当たりそうになったら向きを変える
        UpdateWalls();

        // 近隣の個体から離れる
        UpdateSeparation();

        // 近隣の個体と速度を合わせる
        UpdateAlignment();

        // 近隣の個体の中心に移動する
        UpdateCohesion();

        // 上記 4 つの結果更新された accel を velocity に反映して位置を動かす
        UpdateMove();

    }
    void UpdateMove()
    {
        var dt = Time.deltaTime;
        velocity += accel * dt;
        var dir = velocity.normalized;
        var speed = velocity.magnitude;
        velocity = Mathf.Clamp(speed, paramFish.minSpeed, paramFish.maxSpeed) * dir;
        pos += velocity * dt;
        var rot = Quaternion.LookRotation(velocity);
        transform.SetPositionAndRotation(pos, rot);
        accel = Vector3.zero;
    }
    void UpdateWalls()
    {
        if (!simboidFish) return;
        var Collider = GetComponentsInParent<BoxCollider>();
        var scaleXP = Collider[1].size.x * 0.5f + transform.parent.position.x;
        var scaleXM = Collider[1].size.x * 0.5f - transform.parent.position.x;
        var scaleYP = Collider[1].size.y * 0.5f + transform.parent.position.y;
        var scaleYM = Collider[1].size.y * 0.5f - transform.parent.position.y;
        var scaleZP = Collider[1].size.z * 0.5f + transform.parent.position.z;
        var scaleZM = Collider[1].size.z * 0.5f - transform.parent.position.z;

        accel +=
            CalcAccelAgainstWall(-scaleXM - pos.x, Vector3.right) +
            CalcAccelAgainstWall(-scaleYM - pos.y, Vector3.up) +
            CalcAccelAgainstWall(-scaleZM - pos.z, Vector3.forward) +
            CalcAccelAgainstWall(+scaleXP - pos.x, Vector3.left) +
            CalcAccelAgainstWall(+scaleYP - pos.y, Vector3.down) +
            CalcAccelAgainstWall(+scaleZP - pos.z, Vector3.back);
    }
    Vector3 CalcAccelAgainstWall(float distance, Vector3 dir)
    {
        if (distance < paramFish.wallDistance)
        {
            return dir * (paramFish.wallWeight / Mathf.Abs(distance / paramFish.wallDistance));
        }
        return Vector3.zero;
    }
    void UpdateNeighbors()
    {
        neighbors.Clear();
        if (!simboidFish) return;
        var prodThresh = Mathf.Cos(paramFish.neighborFov * Mathf.Deg2Rad);
        var distThresh = paramFish.neighborDistance;

        foreach (var other in simboidFish.boidFishes)
        {
            if (other == this) continue;
            var to = other.pos - pos;
            var dist = to.magnitude;
            if (dist < distThresh) { var dir = to.normalized; var fwd = velocity.normalized; var prod = Vector3.Dot(fwd, dir); if (prod > prodThresh)
                {
                    neighbors.Add(other);
                }
            }
        }
    }
    void UpdateSeparation()
    {
        if (neighbors.Count == 0) return;

        Vector3 force = Vector3.zero;
        foreach (var neighbor in neighbors)
        {
            force += (pos - neighbor.pos).normalized;
        }
        force /= neighbors.Count;

        accel += force * paramFish.separationWeight;
    }
    void UpdateAlignment()
    {
        if (neighbors.Count == 0) return;

        var averageVelocity = Vector3.zero;
        foreach (var neighbor in neighbors)
        {
            averageVelocity += neighbor.velocity;
        }
        averageVelocity /= neighbors.Count;

        accel += (averageVelocity - velocity) * paramFish.alignmentWeight;
    }
    void UpdateCohesion()
    {
        if (neighbors.Count == 0) return;

        var averagePos = Vector3.zero;
        foreach (var neighbor in neighbors)
        {
            averagePos += neighbor.pos;
        }
        averagePos /= neighbors.Count;

        accel += (averagePos - pos) * paramFish.cohesionWeight;
    }
}

◆ParamFish.cs

using UnityEngine;

[CreateAssetMenu(menuName = "BoidFish/ParamFish")]
public class ParamFish : ScriptableObject
{
    public float initSpeed = 2f;
    public float minSpeed = 2f;
    public float maxSpeed = 5f;
    public float neighborDistance = 1f;
    public float neighborFov = 90f;
    public float separationWeight = 5f;
    public float wallDistance = 3f;
    public float wallWeight = 1f;
    public float alignmentWeight = 2f;
    public float cohesionWeight = 3f;
}

◆SimBoidFish.cs

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

public class SimBoidFish : MonoBehaviour
{
    [SerializeField]
    int boidCount = 100;

    [SerializeField]
    GameObject boidPrefab;

    [SerializeField]
    ParamFish paramFish;

    List boidFish_ = new List();
    public ReadOnlyCollection boidFishes
    {
        get { return boidFish_.AsReadOnly(); }
    }

    void AddBoid()
    {
        var Collider = GetComponent<BoxCollider>();
        var ColX = transform.position.x + Collider.size.x * (0.5f - Random.value)*  0.5f;
        var ColY = transform.position.y + Collider.size.y * (0.5f - Random.value) * 0.5f;
        var ColZ = transform.position.z + Collider.size.z * (0.5f - Random.value) * 0.5f;
        var go = Instantiate(boidPrefab, new Vector3(ColX, ColY, ColZ), Random.rotation);
        go.transform.SetParent(transform);
        var boidFish = go.GetComponent<BoidFish>();
        boidFish.simboidFish = this;
        boidFish.paramFish = paramFish;
        boidFish_.Add(boidFish);

        for (int i = 0; i < boidFish_.Count; i++)
        {
            boidFish_[i].gameObject.SetActive(false);
        }
    }

    void RemoveBoid()
    {
        if (boidFish_.Count == 0) return;

        var lastIndex = boidFish_.Count - 1;
        var boid = boidFish_[lastIndex];
        Destroy(boid.gameObject);
        boidFish_.RemoveAt(lastIndex);
    }

    void FixedUpdate()
    {
        while (boidFish_.Count < boidCount) { AddBoid(); } while (boidFish_.Count > boidCount)
        {
            RemoveBoid();
        }
    }
}

◆EnableNearChild.cs

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

public class EnableNear : MonoBehaviour
{
    private bool near;
    // Start is called before the first frame update
    void Start()
    {
    }

    // Update is called once per frame
    void Update()
    {
        if (near)
        {
            foreach (Transform item in this.transform)
            {
                item.transform.gameObject.SetActive(true);
            }
        }
        else
        {
            foreach (Transform item in this.transform)
            {
                item.transform.gameObject.SetActive(false);
            }
        }
    }

    void OnTriggerEnter(Collider collider)
    {
        if (collider.gameObject.name == "Player") {
            near = true;
        }
    }

    void OnTriggerExit(Collider collider)
    {
        if (collider.gameObject.name == "Player")
        {
            near = false;
        }
    }
}

スポンサーリンク

応用

Posted by daraha_gm