[Unity3D] 객체 미리 생성 후 재활용 - Memory pool [Part 5] 추가사항

※ 주의 

이 글은 아마추어가 개인적으로 생각하여 작성하는 것으로, 이곳에 나오는 내용을 맹신하지 않는것을 당부드립니다.



Menu

0. 미리보기


1. 오브젝트를 미리 생성.

- 프리팹 제작.

- 싱글톤 제작.

- 오브젝트를 생성시킬 함수 제작.

- 오브젝트를 찾을 함수를 제작.


2. 만들어진 오브젝트 활용하기.


추가.

3. 적 생성

- 적 생성 후 프리팹으로 만들기.

- 적과 총알이 충돌하면 총알 비활성화.

- 적이 총알에 맞아 죽었을 때 단순한 객체로 이펙트 효과 표현하기.


4. 메모리 풀을 이용해서 적 생성하기.

- 적 리스폰

 * Enemy를 생산하는 Area만들기. (랜덤한 위치로 Area생산 및 Area간 간격조절.)

 * Araa범위 안에 Enemy 랜덤한 위치로 리스폰 시키기.

 * Enemy가 죽었을 경우 리스폰 되는 간격 조절.


5. FSM을 이용해 적 AI만들기.


6. 추가.




미리보기




6. 추가. (EnemyAI Script)


1. 적이 플레이어에게 피격 시 Material을 빨간색으로 반짝이게 함.


플레이어에게 피격되면 HP를 감소시켜 주기 위해

InfoUpdate() 함수가 호출되는데, 이때 에너미가 가지고있는 마테리얼을 가져와서

색깔을 반복적으로 바꿔주면 된다. 하지만 InfoUpdate()함수는 피격 될때만 호출되는 함수로

색을 반복적으로 바꿔주는 것이 불가능 하다.

그래서 InfoUpdate()함수 안에서 코루틴을 호출시켜 for문 + 시간지연을 이용하여 색깔을 깜빡거려 준다.


에너미가 가지고있는 마테리얼의 색을 바꿔줄것이기 때문에 마테리얼을 가지고 오기로 하자.

Awake에서 초기화 시켜주면 된다.

1
2
3
4
5
6
7
private Material Mate;        // 자신이 가지고있는 마테리얼.    
 
void Awake()
{
    player = GameObject.FindGameObjectWithTag("Player").transform;
    Mate   = GetComponent<Renderer>().material;
}
cs



 InfoUpdate() 함수.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 정보 갱신.
public void InfoUpdate(float _Damage)
{
    // HP감소.
    HP -= _Damage;
 
    // 반짝거림.
    StartCoroutine("MaterialColorShine");
 
    // HP가 0이하 이면.
    if (HP <= 0)
    {
        // 폭발한다.
        explosion();
 
        HP   = 0;       // 체력의 수치가 음의 값으로 갔을 경우를 대비한 초기화.
        Life = false;   // 죽었음을 알림.
 
        // 내 죽음을 부모에리어에게 알려라!
        // 부모 에리어가 가진 스크립트를 가져와 DeadEnemy()함수를 호출.
        transform.parent.GetComponent<CreateEnemy>().DeadEnemy();
        gameObject.SetActive(false);
    }
}
cs


이런식으로 코루틴을 호출시켜 주고

코루틴의 내용으로는


1
2
3
4
5
6
7
8
9
10
11
12
13
// 색깔이 반짝반짝.
IEnumerator MaterialColorShine()
{
    for (float i = 0; i < 0.5f; i += 0.1f)
    {
        Mate.color = Color.red;
        yield return new WaitForSeconds(0.05f);
        Mate.color = Color.yellow;
        yield return new WaitForSeconds(0.05f);
    }
 
    Mate.color = Color.yellow;
}
cs


이렇게 해주면 된다.


for문이 한 번 돌 때 i값이 0.1씩 증가하는데 총 5번 for문을 돌게 된다.

이때 색을 빨간색으로 바꿔주고

시간을 0.05초간 지연시켜준다.


그리고 다시 색을 노란색으로 바꿔주고 시간을 다시 0.05초간 지연시켜 준다.

이러한 일을 반복하게되면 우리들의 눈에는 색깔이 깜박거리는 걸로 느껴진다.


시간지연을 0.05초간 2번씩 시켜줬기 때문에

0.05 + 0.05 = 0.1초가 되고


for문을 한 번 돌때마다 0.1초가 지연된다.

for문은 총 5번 돌기 때문에 결과적으로는 0.5초간 색깔이 깜박거린다고 생각하면 된다.


※ 색깔이 깜박거리는 도중 적이 죽을경우.

다시 리스폰 되면 색깔이 빨간색으로 바뀌어서 나올수도 있다.

그렇기 때문에 Init()함수에서 마테리얼의 색을 원래의 색깔로 초기화 하는 것이 좋다.


2. 플레이어에게 피격 시 추격거리 3배 증가.


적이 기존에 가지고있는 추적거리는 매우 짧다.

그렇기 때문에 플레이어에게 피격당해도 쫓아오지 않는 경우가 있다.

이러한 경우를 막기 위해 플레이어에게 피격 시 추적 거리를 대폭 늘려

쫓아오게 만들도록 하자.


추적상태가 끝나면 추적거리의 값을 원상태로 복구해줄 필요가 있다.

그렇기 때문에 본래 기본이 되는 값을 변수로 가지고 있어야 하는데, 이 값들을 먼저 정리해 보도록하자.


이 기본값들을 한눈에 보이도록 관리하기 위해 기본값 구조체를 하나 만들것이다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 기본 값 구조체
public struct DEFAULT
{
    public Vector3 AreaPos;   // 부모 에리어의 위치.
    public float MAX_HP;      // 체력 
    public float TRACE_DIS;   // 추적거리
    public float SPEED;       // 스피드
}
 
// 공개
public STATE state;           // 에너미의 상태.
public float HP;              // 현재 체력
public float Speed;           // 움직임 스피드.
public float TraceDis;        // 추적 거리.
 
// 비공개
private DEFAULT DefaultVaule; // 기본 값 구조체 변수.
private Material Mate;        // 자신이 가지고있는 마테리얼.         
private Transform player;     // 플레이어.
private Vector3 RandomPoint;  // 랜덤한 위치. 
 
private bool Life;            // 살아있는지에 대한 여부.
private bool trace;           // 추적 플래그.
 
// 각 상태의 코루틴을 시작하기 위한 플래그.
private bool idle;      
private bool walk;
private bool attack;
private bool area_return;
 
// 정보 초기화 함수.
public void Init(Vector3 _AreaPos)
{
    DefaultVaule.AreaPos   = _AreaPos;
    DefaultVaule.TRACE_DIS = TraceDis;
    DefaultVaule.MAX_HP    = 100;
    DefaultVaule.SPEED     = Speed;
    HP = DefaultVaule.MAX_HP;
 
    Life   = true;
    trace  = false;
    state  = STATE.IDLE;
 
    idle   = false;
    walk   = false;
    attack = false;
    area_return = false;
 
    Mate.color = Color.yellow;
 
    ReandomPos();
    StartCoroutine("DisCheck");
    StartCoroutine("StateCheck");
}
cs


기본 값 구조체에는 4개의 변수가 있는데, 각 내용은 이렇다.

AreaPos는 부모 에리어의 처음 위치로 적이 부모 에리어로부터 너무 멀어졌을 경우 복귀하는 용도로 사용한다.

MAX_HP는 적이 리스폰 되거나 부모에리어로 복귀할 때 HP를 만땅으로 채워주기 위한 값이다.

TRACE_DIS는 추적거리로 부모 에리어로의 복귀나 플레이어가 일정거리 이상 멀어지면 초기화 시켜주기 위한 값이다.

SPEED는 부모 에리어로 복귀할때 빠르게 복귀시켜주기 위해 스피드를 올리는데, 부모 에리어로 복귀 했을 때 고유 스피드 값을

초기화 시켜주기 위한 값이다.


그래서 각 변수들을 정리해 위와같이 완성시켜주면 된다.


추적거리를 3배 증가시켜 주는 부분은 플레이어에게 피격당했을 때로 설정해주면 된다.

즉, InfoUpdate()함수가 호출될때 거리를 변환시켜주면 된다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 정보 갱신.
public void InfoUpdate(float _Damage)
{
    // HP감소.
    HP -= _Damage;
 
    // 플레이어에게 선제공격 받을 시 추적거리 3배 증가.
    TraceDis = DefaultVaule.TRACE_DIS * 3;
 
    // 반짝거림.
    StartCoroutine("MaterialColorShine");
 
    // HP가 0이하 이면.
    if (HP <= 0)
    {
        // 폭발한다.
        explosion();
 
        HP   = 0;       // 체력의 수치가 음의 값으로 갔을 경우를 대비한 초기화.
        Life = false;   // 죽었음을 알림.
 
        // 내 죽음을 부모에리어에게 알려라!
        // 부모 에리어가 가진 스크립트를 가져와 DeadEnemy()함수를 호출.
        transform.parent.GetComponent<CreateEnemy>().DeadEnemy();
        gameObject.SetActive(false);
    }
}
cs



적이 Walk상태일 때 배회인지 추적인지 판단할수 있는 재료는 bool값인 trace변수이다.

trace값이 true면 적은 플레이어를 추적하는 상태이고, false이면 배회상태이다.


※ trace값은 DisCheck() 코루틴으로 0.2초에 한 번씩 플레이어와의 거리를 체크해 변환 시켜주고 있다.


플레이어 추적중에 둘 사이의 거리가 너무 멀어지면 trace값이 false로 바뀌면서

상태값을 IDLE로 바꿔버린다. 그러면서 적은 추적을 포기하게 된다.

이 때 추적거리를 원상태로 복구 시키면 된다.




1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 걷는상태.
IEnumerator WalkState()
{
    while (Life)
    {
        if(state != STATE.WALK)
            break;
 
        
        
 
        // 플레이어와의 거리가 가까우면 trace = true
        if (trace)
        {
            Rotations(player.position);
            DisCheckToState(player.position, 2f, STATE.ATTACK, true);
        }
        else // 추적 포기.
        {
            state = STATE.IDLE;
            TraceDis = DefaultVaule.TRACE_DIS; // 추적 길이 원상태 복구.
        }
 
        Run();
 
        yield return null;
    }
 
    walk = false;
}
cs



※ Run()함수는 이동 부분을 함수로 빼준 함수이다. (여러곳에서 쓰기 때문에 함수로 뺐다.)

1
2
3
4
5
// 앞을 바라보는 방향으로 전진.
void Run()
{
    transform.Translate(Vector3.forward * Speed * Time.deltaTime);
}
cs



3. 자신의 부모 에리에어서 멀어지면 에리어로 복귀

- 추적거리 기본 값으로 복구.

- 이동 스피드 3배 상승.

- 에리어 복귀까지 무적.

- 체력 MAX



부모 에리어로 복귀하는것이 한 가지에 상태이므로 상태 값을 추가했다.


1
public enum STATE { IDLE, WALK, ATTACK, AREA_RETURN }
cs


AREA_RETURN이다.



상태 체크 코루틴에 한 가지 상태(복귀)를 추가해준다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 상태체크.
IEnumerator StateCheck()
{
    // 적 AI
    // 살아있는 동안에만 반복문을 돌림.
    while (Life)
    {
        switch (state)
        {
            case STATE.IDLE: // 평화.
            {
                if (!idle)
                {
                    idle = true;
                    StartCoroutine("IdleState");
                }
                break;
            }
            case STATE.WALK: // 걷기
            {
                if (!walk)
                {
                    walk = true;
                    StartCoroutine("WalkState");
                }
                break;
            }
            case STATE.ATTACK: // 공격.
            {
                if (!attack)
                {
                    attack = true;
                    StartCoroutine("AttackState");
                }
                break;
            }
            case STATE.AREA_RETURN: // 복귀
            {
                if(!area_return)
                {
                    area_return = true;
                    StartCoroutine("Area_ReturnState");
                }
                break;
            }
        }
        yield return new WaitForSeconds(0.2f);
    }
}
cs



복귀 상태도 Area_ReturnState라는 코루틴을 만들어 돌리는데,

그렇게 하는 이유는 상태체크 함수의 시간 체크 간격때문에 그렇다.

복귀 상태가 되면 적은 부모 에리어까지 뛰어가야 한다.

만약 코루틴을 사용하지 않고 case STATE.AREA_RETURN: { } 안에 복귀 구문을 수행해 버리면

0.2초당 한 번씩 움직이게 된다. (상태체크 코루틴은 0.2초에 한 번씩 시간을 지연시키기 때문이다.)

이러한 일을 막기 위하여 아에 다른 코루틴을 하나 만들어 구문을 수행시킨다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 복귀 상태.
IEnumerator Area_ReturnState()
{
    Vector3 Pos = DefaultVaule.AreaPos;
 
    while(true)
    {
        // 복귀 상태가 아니면 반복문을 빠져나감.
        if(state != STATE.AREA_RETURN)
            break;
 
        Run();                                  // 열심히 뛴다.
        Rotations(Pos);                         // 부모 에리어 방향으로.
        DisCheckToState(Pos, 1.5f, STATE.IDLE); // 부모 에리어에 도착하면 상태 값을 IDLE로 바꾼다.
 
        yield return null;
    }
 
    area_return = false;
}
cs



이제 적의 상태가 복귀상태(AREA_RETURN)로 바뀌면 알아서 부모 에리어로 복귀할 것이다.

그럼 복귀 상태로 바꿔주는 타이밍은 어디서 잡아야 할까?

바로 WalkState코루틴에서 잡아주면 된다.


걷고있는 동안 자신과 부모 에리어의 거리를 지속적으로 체크한다. 

그러다가 일정거리이상 멀어지면 상태값을 복귀상태로 바꿔주고

스피드를 증가시키고, 체력을 만땅으로 채우고, 추적 거리를 원상태로 만들고, 무적상태로 만든다.



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 걷는상태.
IEnumerator WalkState()
{
    while (Life)
    {
        if(state != STATE.WALK)
            break;
 
        // 부모에리어와 거리가 너무 멀어지면 에리어로 복귀.
        AreaDisCheck();
 
        // 플레이어와의 거리가 가까우면 trace = true
        if (trace)
        {
            Rotations(player.position);
            DisCheckToState(player.position, 2f, STATE.ATTACK, true);
        }
        else // 추적 포기.
        {
            state = STATE.IDLE;
            // 추적 길이 원상태 복구.
            TraceDis = DefaultVaule.TRACE_DIS;
        }
 
        Run();
        yield return null;
    }
 
    walk = false;
}
cs



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 부모 에리어와의 거리 체크.
void AreaDisCheck()
{
    Vector3 Pos = transform.parent.position;
    float dis = Vector3.Distance(Pos, transform.position);
 
    // 거리가 15이상 넘어가면 부모 에리어로 복귀.
    if(dis >= 15f)
    {
        state = STATE.AREA_RETURN;
        Speed = DefaultVaule.SPEED * 3;                  // 스피드 3배
        TraceDis = DefaultVaule.TRACE_DIS;               // 추적거리 기본 값
        HP = DefaultVaule.MAX_HP;                        // HP 기본 값
        GetComponent<CapsuleCollider>().enabled = false// 콜라이더를 꺼 무적으로.
    }
}
cs



부모 에리어로 복귀하는 순간.

Area_ReturnState()코루틴에서는 DisCheckToState()함수의 호출을 통해 각 값들을 기본 값으로 돌려준다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 거리체크 및 상태변화.
void DisCheckToState(Vector3 _Pos, float _Dis, STATE _state, bool _UpDown = true)
{
    float dis = Vector3.Distance(transform.position, _Pos);
 
    if (_UpDown)
    {
        if (dis <= _Dis)
        {
            // 기본 값으로 복구.
            if(state == STATE.AREA_RETURN)
            {
                Speed = DefaultVaule.SPEED;
                GetComponent<CapsuleCollider>().enabled = true;
            }
 
            state = _state;
        }
    }
    else
    {
        if (dis > _Dis)
            state = _state;
    }
}
cs



Full Source