【Unity】大量のオブジェクト同士が押し出し合うものを作る

# 概要
大量の敵が出てくるゲームを作りたい

安直にプレイヤーを追いかけるだけだと、敵同士が重なってしまい表示上の密度感が出ない

また、ダメージ判定が同時に発生してしまうなど, いろいろな不都合が発生することがある

なので敵同士を適当な間隔で広げたい

https://www.docswell.com/s/DeNA_Tech/KEN3QR-2024-02-29-090246#p1

そこで、上記のスライドで紹介されているようなミチミチの敵を実装してみた



# 実装
![画面収録 2024-12-19 2.02.21.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100503/2564c8f6-1bd2-a077-a58d-89728b80c57d.gif)

いい感じにミチミチにできた

---
敵の表示は以下の記事で紹介したRenderMeshInstancedを使った
```csharp
for(var offs = 0; offs < matrices.Length; offs += 1023)
{
    var count = Mathf.Min(1023, matrices.Length - offs);
    Graphics.RenderMeshInstanced(rparams, _myMesh, 0, matrices, count, offs);
}
```
---

中央に集まるだけだと面白みにかけたので、プレイヤーを追いかけるような形にした
動くプレイヤーを作り....
```csharp
void Update()
{
    var horizontalInputAxisRaw = Input.GetAxisRaw("Horizontal");
    var verticalInputAxisRaw = Input.GetAxisRaw("Vertical");

    var position = transform.position;
    position.x += horizontalInputAxisRaw * _speed * Time.deltaTime;
    position.y += verticalInputAxisRaw * _speed * Time.deltaTime;
    transform.position = position;
}
```
それを追いかける
```csharp
// ターゲットへ近づく
var p = _arrays.p[offs];
var d = target - p;
if(math.length(d) < moveSpeedInTime) continue;
var v = math.normalize(d) * moveSpeedInTime;
var np = p + v;
```
`math.normalize`の引数に`float3(0,0,0)`を入れてしまうと、NaNが返ってしまうので注意する
自分は手を抜いてcontinueしたけど、適切なfloat3を入れてあげた方がいいかも(それかここだけスキップするか)

---

当たり判定の形は気にする必要がなかったので、`math.length(対象 - 自分) > 当たり判定の大きさ`で行った

```csharp
// オブジェクト同士の当たり判定
for(var k = 0; k < _dims.x; k++)
{
    for(var l = 0; l < _dims.y; l++)
    {
        // 同じオブジェクトならスキップ
        if(k == i && l == j) continue;
        var op = _arrays.p[k * _dims.y + l];

        // 距離が近すぎないかチェック
        if(math.length(np - op) < 1.0f)
        {
            // 近すぎるなら単純に離す
            var od = op - np;
            var ov = math.normalize(od) * moveSpeedInTime;
            np -= ov;
        }
    }
}
```

---

オブジェクトごとの速度を計算して、めり込みすぎてたら外側への速度を増やすなど拡張できそう
```csharp
private (
  NativeArray<float2> p,
  NativeArray<float4x4> m, 
  NativeArray<float2> vector
) _arrays;
```
こんな感じにして、Update内で速度を元に次のフレームの位置を決める

---

全てのオブジェクトと当たり判定チェックをしているので、計算量がO(N^2)になってしまっている
なので、250オブジェクトぐらいに増やすと7FPSしか出なかった
これでは実用に耐えれるレベルではないので、座標で区切ったグリットを作り、その中でしか当たり判定を行わないようにするなど工夫が必要
(これもスライドに記載があった)

# コード

```csharp:RenderMeshTest.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;

public class RenderMeshTest : MonoBehaviour
{
    private PositionBuffer _buffer;

    [SerializeField] Material _material;
    [SerializeField] Vector2Int _counts = new Vector2Int(5, 5);
    [SerializeField] List<Texture2D> _textures = new List<Texture2D>();
    [SerializeField] float _speed = 1.0f;
    [SerializeField] Transform _moveTarget;

    private Mesh _myMesh;

    void Start()
    {
        _buffer = new PositionBuffer(_counts.x, _counts.y, _speed);
        CreateMyMesh();
    }

    void CreateMyMesh()
    {
        _myMesh = new Mesh
        {
            // 頂点は4つ
            // 2---0
            // |   |
            // 3---1
            vertices = new Vector3[]
            {
                new(1, 1, 0),
                new(1, -1, 0),
                new(-1, 1, 0),
                new(-1, -1, 0)
            },
            subMeshCount = 1,
            uv = new Vector2[]
            {
                new(1, 1),
                new(1, 0),
                new(0, 1),
                new(0, 0),
            }
        };

        _myMesh.SetTriangles(new[] { 0, 1, 3, 0, 3, 2 }, 0);
    }

    void OnDestroy()
    {
        _buffer.Dispose();
    }

    void Update()
    {
        _material.SetTexture("_BaseMap", _textures[0]);
        _buffer.Update(Time.deltaTime, _moveTarget.position);

        var matrices = _buffer.Matrices;
        var rparams = new RenderParams(_material);

        for(var offs = 0; offs < matrices.Length; offs += 1023)
        {
            var count = Mathf.Min(1023, matrices.Length - offs);
            Graphics.RenderMeshInstanced(rparams, _myMesh, 0, matrices, count, offs);
        }

        // Graphics.RenderMeshを使う場合
        // for(var offs = 0; offs < matrices.Length; offs++)
        // {
        //     Graphics.RenderMesh(rparams, _myMesh, 0, matrices[offs]);
        // }
    }
}
```

```csharp:PositionBuffer.cs
using System;
using Unity.Collections;
using Unity.Mathematics;
using UnityEngine;
using Unity.Burst;
using UnityEngine.Profiling;

sealed class PositionBuffer : IDisposable
{
    public NativeArray<Vector3> Positions => _arrays.p.Reinterpret<Vector3>();
    public NativeArray<Matrix4x4> Matrices => _arrays.m.Reinterpret<Matrix4x4>();

    private (NativeArray<float3> p, NativeArray<float4x4> m) _arrays;
    private readonly (int x, int y) _dims;
    private readonly float _moveSpeed;

    public PositionBuffer(int xCount, int yCount, float moveSpeed)
    {
        _dims = (xCount, yCount);
        _arrays = (new NativeArray<float3>(_dims.x * _dims.y, Allocator.Persistent),
                   new NativeArray<float4x4>(_dims.x * _dims.y, Allocator.Persistent));
        _moveSpeed = moveSpeed;

        SetInitialPosition();
    }

    void SetInitialPosition()
    {
        var offs = 0;

        for (var i = 0; i < _dims.x; i++)
        {
            var x = (i - _dims.x / 2) * 2 + 3;
            for (var j = 0; j < _dims.y; j++)
            {
                var y = (j - _dims.y / 2) * 2+ 3;
                var p = math.float3(x, y, 0);

                _arrays.p[offs] = p;
                _arrays.m[offs] = float4x4.Translate(p);
                offs++;
            }
        }
    }

    public void Dispose()
    {
        if(_arrays.p.IsCreated) _arrays.p.Dispose();
        if(_arrays.m.IsCreated) _arrays.m.Dispose();
    }

    [BurstCompile]
    public void Update(float time, Vector3 targetPosition)
    {
        var target = math.float3(targetPosition.x, targetPosition.y, 0);
        var offs = -1;
        var moveSpeedInTime = _moveSpeed * time;

        for(var i = 0; i < _dims.x; i++)
        {
            for(var j = 0; j < _dims.y; j++)
            {
                offs++;

                // ターゲットへ近づく
                var p = _arrays.p[offs];
                var d = target - p;
                if(math.length(d) < moveSpeedInTime) continue;
                var v = math.normalize(d) * moveSpeedInTime;
                var np = p + v;

                // オブジェクト同士の当たり判定
                for(var k = 0; k < _dims.x; k++)
                {
                    for(var l = 0; l < _dims.y; l++)
                    {
                        if(k == i && l == j) continue;
                        var op = _arrays.p[k * _dims.y + l];
                        if(math.length(np - op) < 1.0f)
                        {
                            var od = op - np;
                            var ov = math.normalize(od) * moveSpeedInTime;
                            np -= ov;
                        }
                    }
                }

                _arrays.p[offs] = np;
                _arrays.m[offs] = float4x4.Translate(np);
            }
        }
    }
}

```

```csharp:MoveTarget.cs
using UnityEngine;

public class MoveTarget : MonoBehaviour
{
    [SerializeField] private float _speed = 1.0f;

    void Update()
    {
        var horizontalInputAxisRaw = Input.GetAxisRaw("Horizontal");
        var verticalInputAxisRaw = Input.GetAxisRaw("Vertical");

        var position = transform.position;
        position.x += horizontalInputAxisRaw * _speed * Time.deltaTime;
        position.y += verticalInputAxisRaw * _speed * Time.deltaTime;
        transform.position = position;
    }
}

```