Post

유니티3D - 8

유니티3D - 8

이번 목표

플레이어와 AI에 체력을 만들어 보자

체력

체력을 구현하기위해 AI와 Player가 사용할 Status 클래스를 만들어 관리해주자.

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
using UnityEngine;

public class Status : MonoBehaviour
{
    [SerializeField] private float maxHP = 100f;
    [SerializeField] private float currentHP;

    void Start()
    {
        currentHP = maxHP;
    }

    public void IncreaseHP(float amount)
    {
        currentHP = Mathf.Clamp(currentHP + amount, 0, maxHP);
    }

    public void DecreaseHP(float amount)
    {
        currentHP = Mathf.Clamp(currentHP - amount, 0, maxHP);
    }

    public float GetCurrentHP() => currentHP;
    public float GetMaxHP() => maxHP;
    public float GetHPPercent() => currentHP / maxHP;
}

HP 구현을 마친모습이고 이제 TakeDamage를 만들어보자.

데미지

데미지를 구현하려면 일단 몸통과 헤드샷으로 분리되는 콜리더를 캐릭터에 배치해주도록 해야겠다.
여기서 Tag를 만들어 Head인 경우만 처리하고 나머지는 통일하게 하도록 하겠다.
헤드샷은 데미지를 3배 더 높여서 주고 Status에도 Dead 판정을 만들어놨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
        if (Physics.Raycast(origin, direction, out RaycastHit hit, MaxDistance))
        {
            Status status = hit.collider.GetComponentInParent<Status>();
            if (status != null)
            {
                if (hit.collider.CompareTag("Head"))
                {
                    status.DecreaseHP(data.damage * 3);
                }
                else
                {
                    status.DecreaseHP(data.damage);
                }
            }

State의 OnDead를 호출하여 애니메이션에도 영향이 가게하자.

1
2
3
4
5
6
7
8
9
    public void DecreaseHP(float amount)
    {
        currentHP = Mathf.Clamp(currentHP - amount, 0, maxHP);

        if(currentHP <= 0)
        {
           State.OnDead();
        }
    }

State에서 델리게이트로 애니메이션에 보내주어 새로운 상황을 추가해도 역시 편하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    public void SetCharacterState(int InState)
    {
        if (characterState == (ECharacterState)InState)
        {
            characterState = ECharacterState.Idle;
        }
        else
        {
            characterState = (ECharacterState)InState;
        }

        characterStateChange.Invoke(characterState);
    }

    public void OnDead()
    {
        SetCharacterState((int)ECharacterState.Dead);
    }

애니메이션 컨트롤러 부분에서 어떻게 적용되고 있는지 확인 후 죽는 애니메이션을 만들어보자.


    void SetAnimState(ECharacterState state)
    {
        animator.SetInteger("State", (int)state);
    }

그에 맞게 애님컨트롤러에서 Any State에서 조건에 맞게 실행시켜보았다.

DEAD상태BT우선순위로해결

Dead

실행 결과 애니메이션이 반복 재생됐다. 이 구간은 상태 유지형 파라미터 대신 트리거로 바꾸는 게 적절하다. State 클래스에 트리거용 델리게이트 변수를 만들어 애님컨트롤러에 바인딩해 사용하자.

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
public class State : MonoBehaviour
{
    public event Action OnCharacterDead;

    public void OnDead()
    {
        OnCharacterDead.Invoke();
    }
}

public class AnimController : MonoBehaviour
{
      void OnEnable()
    {
        state.OnCharacterDead += OnDead;
    }
        void OnDisable()
    {
        state.OnCharacterDead -= OnDead;
    }

    void OnDead()
    {
        animator.SetTrigger("Dead");
    }
}

DeadFix

잘 한번만 실행된다. 하지만 문제점이 보인다 이제 죽었을 때 BT에서 처리와 HP가 0 이하일 때 공격을 받으면 트리거가 한 번 더 실행되는 상황인거같기때문에 예외처리해주자.
일단 Status에서 Dead처리를 하기 떄문에 Player와 AI 둘 다 다르게 구현되기 떄문에 각자 컴포터는트에서 실행되어야한다.
컨트롤러의 부모클래스를 만들까 인터페이스로 만들까 생각하다 인터페이스로 그냥 추가했다.

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
public class Status : MonoBehaviour
{
    private IController controller;

    void Start()
    {
        controller = GetComponent<IController>();
    }

    public void DecreaseHP(float amount)
    {
        if (currentHP == 0) return;
        currentHP = Mathf.Clamp(currentHP - amount, 0, maxHP);

        if(currentHP <= 0)
        {
            OnDead();
        }
    }

    private void OnDead()
    {
        State.OnDead();
        controller.OnDead();
    }
}

이제 AI컨트롤러에서 OnDead를 구현해보자 BTState를 Dead로 바꿔주고 StopChase() 함수로 이동을 멈춰주자

1
2
3
4
5
    public void OnDead()
    {
        StopChase();
        SetBehaviorState(BehaviorState.Dead);
    }

이렇게 하고보니 이러면 DeadAction에서는 어떤걸 정의해야할까 고민좀 해봐야겠다. 필요가 없는거 같기도 한데 일단 추가할 부분이 생길수도 있으니 냅둬보자.

DeadFix2

문제: AI가 사격 중 Dead 상태가 되면 여전히 총을 쏜다.

죽어도쏘는문제

해결: State가 Idle이 아닐 때는 발사 불가로 가드하고, IsCanFire/StartFire 모두에 동일 가드를 둔다.

1
2
3
4
5
6
7
8
    public bool IsCanFire()
    {
        if(State.GetState() != ECharacterState.Idle) return false;
    }
        public void StartFire()
    {
        if (State.GetState() != ECharacterState.Idle) return;
    }

고친후문제영상

예외처리 후에도 이동이 발생해, BT 우선순위를 Dead가 최상위가 되도록 재배치하여 탈출을 차단했다.

Chase문제

근데 이 상황을 관찰하면서 문제가 하나 더 발생했다. 총을 쏠 때 움직이면 Chase 부분 액션이 실행 될 때가 있다.
이것도 고쳐야 자연스럽게 될 것이다 원인을 찾아보자.

DEAD상태BT우선순위로해결

Dead 상태 추격을 막기 위해 BT Action 순서를 Dead를 최상위로 옮겼다.

일단 첫번째로 의심되는건 BT의 Try In Order다 내가 제대로 이해하지 못하고 사용하고 있을 가능성이 높다.
두번째로는 내 코드의 문제점을 찾아보는 것. FireAction 코드를 살펴보자.

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
using System;
using Unity.Behavior;
using UnityEngine;
using Action = Unity.Behavior.Action;
using Unity.Properties;

[Serializable, GeneratePropertyBag]
[NodeDescription(name: "Fire", story: "[Self] Firing", category: "Action", id: "e5228f79ee4bb5328d1ffceb33134cdf")]
public partial class FireAction : Action
{
    [SerializeReference] public BlackboardVariable<GameObject> Self;
    private AIController Controller;

    protected override Status OnStart()
    {
        GameObject AI = Self.Value;

        if (AI == null) return Status.Failure;

        Controller = AI.GetComponent<AIController>();
        if (Controller == null) return Status.Failure;

        Controller.StartFire();
        return Status.Running;
    }

    protected override Status OnUpdate()
    {
        Controller.RotateToTarget();

        if(Controller.IsDead())
            return Status.Failure;

        if (Controller.IsCanFire())
            return Status.Running;

        Controller.SetBehaviorState(BehaviorState.Chase);
        return Status.Success;
    }

    protected override void OnEnd()
    {
        Controller.OffFire();
    }
}

코드를 봤을 때 IsCanFire()가 false가 될때를 생각해서 디버그를 생각했다.
IsCanFire() 함수내에 false가 return 되는 경우에서 Raycast가 Player로 인지 안됐을 때 디버그를 걸었다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
    public bool IsCanFire()
    {
        if(State.GetState() != ECharacterState.Idle) return false;

        float TargetDistance = Vector3.Distance(transform.position, Target.transform.position);
        if (AttackRange < TargetDistance) return false;

        Vector3 dirToTarget = (Target.transform.position - transform.position).normalized;
        float distToTarget = Vector3.Distance(transform.position, Target.transform.position);
        if (Physics.Raycast(transform.position + Vector3.up, dirToTarget, out RaycastHit hit, distToTarget))
        {
            if (hit.transform.CompareTag("Player"))
            {
                return true;
            }
        }
        return false; // 여 줄에 디버그를 걸고 걸렸을 때 hit 정보를 보자
    }

디버그

원인은 총알이 레이 트레이스에 걸린 것이었다. Bullet 레이어를 별도 분리하고 Raycast 마스크에서 제외해 충돌 간섭을 제거했다.

해결전

해결후

해결이 잘된모습이다. 다음에는 Dead처리를 제대로 만들고 템이 필드에 드랍되고 총알도 생기게하겠다.

This post is licensed under CC BY 4.0 by the author.