프로그래밍/기타

3D공간 flock(Boids) 알고리즘(군체 AI, 새 무리 이동) C# 코드 리뷰

하루에 한번 방문하기 2020. 10. 11. 20:47

youtu.be/_d8M3Y-hiUs

Flock(Boids) 알고리즘

 :: 생물의 집단 행동 (새의 무리 이동 등)에서 아이디어를 얻어 만들어졌다.

모든 새는 3가지 규칙을 따른다.

1. cohesion : 모든 boid의 평균 위치에 더 근접하게 한다.

2. separation : 이웃한 boid와 충돌하지 않도록 한다

3. alignment : 각 보이드는 주변의 보이드와 같은 방향을 향하려는 특성을 가진다.

장점 : 구현과 사용이 쉽다.

단점 : 성능 문제, 완전한 컨트롤의 어려움, 대부분의 행동이 예기치 않을것


전체 코드(World of Zero님)

더보기
더보기
//속력 방향 (velocity)을 관리한다.
public class Boid : MonoBehaviour
{
    public Vector3 velocity;
    public float maxVelocity;
    // Start is called before the first frame update
    void Start()
    {
        velocity = transform.forward * maxVelocity;
    }

    // Update is called once per frame
    void Update()
    {
        if (velocity.magnitude > maxVelocity) {
            velocity = velocity.normalized * maxVelocity;
        }
        this.transform.position += velocity * Time.deltaTime;
        transform.rotation = Quaternion.LookRotation(velocity);

    }
}
//radius 거리 안에 있는 Boid에게로 방향을 선형보간해 이동한다
[RequireComponent(typeof(Boid))]
public class Boid_Alignment : MonoBehaviour
{
    private Boid boid;

    [InspectorName("another boid Search")]
    public float radius;

    // Start is called before the first frame update
    void Start()
    {
        boid = GetComponent<Boid>();
    }

    // Update is called once per frame
    void Update()
    {
        //모든 보이드들의 배열 가져오기
        Boid[] boids = FindObjectsOfType<Boid>();
        Vector3 average = Vector3.zero;
        float found = 0;

        //linq where :: System.Linq 사용
        //boids.Where(b => b != boid)
        //where : 자기 자신을 제외한 boids배열 내에서 범위기반 반목문(foreach) 수행
        foreach (Boid boid in boids.Where(b => b != boid))
        {
            Vector3 diff = boid.transform.position - transform.position;

            if (diff.magnitude < radius)
            {
                average += boid.velocity;
                found += 1;
            }
        }
        if (found > 0)
        {
            average /= found;
            boid.velocity += Vector3.Lerp(boid.velocity, average, Time.deltaTime);
        }
    }
}
//타 Boid와 너무 가까히 있으면 선형보간해 역방향으로 이동한다
[RequireComponent(typeof(Boid))]
public class Boid_Inverse : MonoBehaviour
{
    private Boid boid;

    [InspectorName("another boid Search")]
    public float radius;
    public float repulsionForce;

    // Start is called before the first frame update
    void Start()
    {
        boid = GetComponent<Boid>();
    }

    // Update is called once per frame
    void Update()
    {
        //모든 보이드들의 배열 가져오기
        Boid[] boids = FindObjectsOfType<Boid>();
        Vector3 average = Vector3.zero;
        float found = 0;

        //linq where :: 
        foreach (Boid boid in boids.Where(b => b != boid)) {
            Vector3 diff = boid.transform.position - transform.position;

            if (diff.magnitude < radius)
            {
                average += diff;
                found += 1;
            }
        }
        if (found > 0) {
            average /= found;
            boid.velocity -= Vector3.Lerp(Vector3.zero, 
                average, average.magnitude /radius) * repulsionForce;
        }
    }
}
//boid를 선형보간으로 뭉치게 한다
[RequireComponent(typeof(Boid))]
public class Boid_Cohesion : MonoBehaviour
{
    private Boid boid;

    [InspectorName("another boid Search")]
    public float radius;

    // Start is called before the first frame update
    void Start()
    {
        boid = GetComponent<Boid>();
    }

    // Update is called once per frame
    void Update()
    {
        //모든 보이드들의 배열 가져오기
        Boid[] boids = FindObjectsOfType<Boid>();
        Vector3 average = Vector3.zero;
        float found = 0;

        //linq where :: 
        foreach (Boid boid in boids.Where(b => b != boid)) {
            Vector3 diff = boid.transform.position - transform.position;

            if (diff.magnitude < radius)
            {
                average += diff;
                found += 1;
            }
        }
        if (found > 0) {
            average /= found;
            boid.velocity += Vector3.Lerp(Vector3.zero, 
                average, average.magnitude /radius);
        }
    }
}
//Boid를 Radius 반경의 구 내의 랜덤한 pos에 스폰해주는 역할
public class BoidSpawner : MonoBehaviour
{
    public GameObject prefab;

    public float radius;
    public int number;

    void Start()
    {
        for (int i = 0; i < number; i++) {
            Instantiate(prefab, transform.position +
                Random.insideUnitSphere * radius, Random.rotation);
        }
    }
}
//boid가 Radius 반경을 가진 구 내에서 활동하도록 제한한다.
[RequireComponent(typeof(Boid))]
public class Boid_Container : MonoBehaviour
{
    private Boid boid;

    public float radius;
    public float boundaryForce;

    // Start is called before the first frame update
    void Start()
    {
        boid = GetComponent<Boid>();
    }

    // Update is called once per frame
    void Update()
    {
        if (boid.transform.position.magnitude > radius)
        {
            boid.velocity += transform.position.normalized * 
            	(radius - boid.transform.position.magnitude) * 
            	boundaryForce * Time.deltaTime;
        }
    }
}

 코드 리뷰 : 성능 이슈

1. 매 프레임마다 FindObjects사용

void Update(){
	//모든 보이드들의 배열 가져오기
        Boid[] boids = FindObjectsOfType<Boid>();
        //...
}

  게임매니저나 BoidSpawner에서 모든 boid를 참조해 관리해주면

  매 프레임마다 boid를 새로 찾을 일이 없게 된다.

2. 매 프레임마다 Linq의 사용

  자기 자신을 제외한 모든 boid 참조를 받기 위해 Linq.Where() 등 사용하는데, 

  1번과 마찬가지로 게임매니저나 BoidSpawner에서 모든 boid를 List로 관리해주고

  List함수로 자기 자신 제외를 한번 하게 하면 성능 향상 가능

3. boid간 거리 비교에서의 Vector.magnitude (제곱근)의 사용

  원 충돌은 제곱으로도 가능함


3개의 간단한 룰이 복잡한 새 무리 이동 시뮬레이션을 만든다는게 신기했네요.

생명게임이 생각나기도 했어요.

ko.wikipedia.org/wiki/라이프_게임

 

라이프 게임 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 둘러보기로 가기 검색하러 가기 Hasbro의 보드 게임인 인생 게임도 가끔 ‘라이프 게임’으로 불린다. ‘글라이더’ 패턴의 진행. 라이프 게임(Game of Life) 또는 ��

ko.wikipedia.org