유니티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
실행 결과 애니메이션이 반복 재생됐다. 이 구간은 상태 유지형 파라미터 대신 트리거로 바꾸는 게 적절하다. 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 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처리를 제대로 만들고 템이 필드에 드랍되고 총알도 생기게하겠다.


