게임을 할 때 플레이어와 보스 그리고 적이 있을 것이다. 그중에 적을 구현할 것인데, 적도 종류가 다양할 것이다.
적의 종류가 땅에서 움직이는 놈도 있고 나는놈도 있고 공격하는 놈도 있고 여러가지이다.
적들 중에 검, 궁수, 물리 적인 것들을 구현할 것이다.
우선 검사적부터 구현해보자
인스펙터를 살펴보면
가장 중요한 핵심은 SwordEnemy이다.
SwordEnemy의 상태도 당연히 존재할텐데 idle, move, attack, skill, stun, dead가 있을 것이다.
우선 컴포넌트를 연결할 스크립트부터 작성해보자
using UnityEngine;
//인터페이스는 적 검사마다 공격 동작이 변동될 수 있다.
public interface ISwordType
{
public void UseSwordAttack();
public void UseSwordSkill();
}
public class SwordEnemy : NormalEnemy, ISwordType
{
[Header("상태 머신")]
public SwordEnemyStateMachine stateMachine;
public NormalEnemyAnimation animationData;
[Header("공격 판정")]
public BoxCollider2D attackTrigger;
public AttackTrigger swordEnemyAttack;
protected override void Awake()
{
base.Awake();
stateMachine = new SwordEnemyStateMachine(this);
animationData = new NormalEnemyAnimation();
animationData.Initialize();
InitializeStat();
//트리거 버그 방지 코드
swordEnemyAttack = attackTrigger.GetComponent<AttackTrigger>();
attackTrigger.enabled = false;
}
private void Update()
{ stateMachine.Update(); }
private void FixedUpdate()
{ stateMachine.PhysicsUpdate(); }
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.tag == Define.TAG_WALL)
{ stateMachine.ChangeState(stateMachine.moveState); }
}
protected override void InitializeStat()
{
stat.maxHealth = 20f;
stat.maxMana = 10f;
stat.attackDamage = 5f;
stat.attackSpeed = 3f;
stat.moveSpeed = 3f;
}
public override void EnemyDeath()
{
base.EnemyDeath();
stateMachine.ChangeState(stateMachine.deadState);
}
//FSM
public override void EnterStateMachine()
{
SetBattleMode(true);
stateMachine.ChangeState(stateMachine.idleState);
}
public override void ExitStateMachine()
{
SetBattleMode(false);
stateMachine.ChangeState(null);
}
//인터페이스 메서드
//같은 적 검사이여도 공격하는 동작이 다를수 있으니 일단 비우자
public void UseSwordAttack() { }
public void UseSwordSkill() { }
}
그 다음으로 상태를 전달해줄 StateMachine을 구현하자
구현은 간단하다.
public class SwordEnemyStateMachine : CharacterState
{
public SwordEnemy enemy;
public SwordIdleState idleState;
public SwordMoveState moveState;
public SwordAttackState attackState;
public SwordSkillState skillState;
public SwordStunState stunState;
public SwordDeadState deadState;
public SwordEnemyStateMachine(SwordEnemy _enemy)
{
enemy = _enemy;
idleState = new SwordIdleState(this);
moveState = new SwordMoveState(this);
attackState = new SwordAttackState(this);
skillState = new SwordSkillState(this);
stunState = new SwordStunState(this);
deadState = new SwordDeadState(this);
}
}
그 다음으로 상태머신을 연결하고 전달할 BaseState를 구성하자.
using UnityEngine;
public class SwordEnemyBaseState : IState
{
protected SwordEnemyStateMachine stateMachine;
protected float aimDistance = 1.2f; //플레이어 근접 거리
public SwordEnemyBaseState(SwordEnemyStateMachine _stateMachine)
{ stateMachine = _stateMachine; }
//인터페이스의 메서드들
//하위 클래스에서 구현을 위하여 가상 메서드로 만들었다.
public virtual void Enter();
public virtual void Exit();
public virtual void PhysicsUpdate();
public virtual void Update();
protected void StartAnimation(int animationHash)
{ stateMachine.enemy.animator.SetBool(animationHash, true); }
protected void StopAnimation(int animationHash)
{ stateMachine.enemy.animator.SetBool(animationHash, false); }
//상태머신에서만 사용할 수 있는 메서드
//이런게 여기서 구현해도 되는지 모르겠다.....
protected float GetDistance(Transform targetPos)
{ return Vector2.Distance(stateMachine.enemy.transform.position, targetPos.position); }
protected void ChoiceAttackOrSkill()
{
float choiceAttack = Random.Range(0f, 1f);
if (choiceAttack > 0.3f) //30%확률로 스킬
stateMachine.ChangeState(stateMachine.attackState);
else
stateMachine.ChangeState(stateMachine.skillState);
}
}
각 상태머신 Idle, Move, Attack, Skill, Stun, Dead를 구현하자!!!
using UnityEngine;
public class SwordIdleState : SwordEnemyBaseState
{
private float flagTime = 2f;//대기시간
private float readyTime; //지연시간
public SwordIdleState(SwordEnemyStateMachine _stateMachine) : base(_stateMachine) { }
public override void Enter()
{
base.Enter();
StartAnimation(stateMachine.enemy.animationData.IdleParameterHash);
flagTime = Random.Range(1.8f, 2.5f); //랜덤한 시간으로 상태 변경
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.enemy.animationData.IdleParameterHash);
}
public override void Update()
{
base.Update();
readyTime += Time.deltaTime;
if(readyTime > flagTime) //일정시간 이상을 초과하면
{
readyTime = 0f;
CheckingPlayer();
}
}
//플레이어랑 붙어있다면 공격 또는 스킬
private void CheckingPlayer()
{
stateMachine.enemy.TurnAround();
//일정 거리보다 떨어져 있으면
if (GetDistance(CharacterManager.instance.player.transform) > aimDistance)
stateMachine.ChangeState(stateMachine.moveState);
else
ChoiceAttackOrSkill();
}
}
public class SwordMoveState : SwordEnemyBaseState
{
private Vector2 moving = Vector2.zero;
public SwordMoveState(SwordEnemyStateMachine _stateMachine) : base(_stateMachine) { }
public override void Enter()
{
base.Enter();
StartAnimation(stateMachine.enemy.animationData.MoveParameterHash);
//플레이어 방향대로 전진
if (stateMachine.enemy.transform.localScale.x > 0) //왼쪽
moving = Vector2.left;
else if (stateMachine.enemy.transform.localScale.x < 0) //오른쪽
moving = Vector2.right;
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.enemy.animationData.MoveParameterHash);
stateMachine.enemy.rb.velocity = Vector2.zero;
}
public override void Update()
{
base.Update();
//플레이어와 근접해있으면
if(GetDistance(CharacterManager.instance.player.transform) < aimDistance)
ChoiceAttackOrSkill();
}
public override void PhysicsUpdate()
{
base.PhysicsUpdate();
//플레이어 방향대로 전진
stateMachine.enemy.rb.velocity = moving * stateMachine.enemy.stat.moveSpeed;
}
}
public class SwordAttackState : SwordEnemyBaseState
{
public SwordAttackState(SwordEnemyStateMachine _stateMachine) : base(_stateMachine) { }
public override void Enter()
{
base.Enter();
StartAnimation(stateMachine.enemy.animationData.AttackParameterHash);
stateMachine.enemy.TurnAround();
stateMachine.enemy.UseSwordAttack();
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.enemy.animationData.AttackParameterHash);
}
public override void Update()
{
base.Update();
float normalizeTime = stateMachine.enemy.NormalizeTime(stateMachine.enemy.animator, "Attack");
if (normalizeTime < 1f) //한 루프가 끝나면
stateMachine.ChangeState(stateMachine.idleState);
}
}
public class SwordSkillState : SwordEnemyBaseState
{
public SwordSkillState(SwordEnemyStateMachine _stateMachine) : base(_stateMachine) { }
public override void Enter()
{
base.Enter();
//동작 이후에 idle로
StartAnimation(stateMachine.enemy.animationData.SkillParameterHash);
stateMachine.enemy.UseSwordSkill();
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.enemy.animationData.SkillParameterHash);
}
public override void Update()
{
base.Update();
float normalizeTime = stateMachine.enemy.NormalizeTime(stateMachine.enemy.animator, "Skill");
if (normalizeTime < 1f) //한 루프가 끝나면stateMachine.ChangeState(stateMachine.idleState);
}
}
public class SwordStunState : SwordEnemyBaseState
{
public SwordStunState(SwordEnemyStateMachine _stateMachine) : base(_stateMachine)
{
//이 상태는 크리티컬이나 쿨타임이 아직 없어서 구현하지 않아 보류에 있다.
}
}
public class SwordDeadState : SwordEnemyBaseState
{
public SwordDeadState(SwordEnemyStateMachine _stateMachine) : base(_stateMachine) { }
public override void Enter()
{
base.Enter();
StartAnimation(stateMachine.enemy.animationData.DeadParameterHash);
}
public override void Exit()
{
base.Exit();
StopAnimation(stateMachine.enemy.animationData.DeadParameterHash);
}
}
다 되었으면 애니메이션을 전환할 스크립트를 작성하자
using UnityEngine;
public class NormalEnemyAnimation
{
[SerializeField] private string idleParameterName = "Idle";
[SerializeField] private string moveParameterName = "Move";
[SerializeField] private string attackParameterName = "Attack";
[SerializeField] private string skillParameterName = "Skill";
[SerializeField] private string stunParameterName = "Stun";
[SerializeField] private string deadParameterName = "Dead";
public int IdleParameterHash { get; private set; }
public int MoveParameterHash { get; private set; }
public int AttackParameterHash { get; private set; }
public int SkillParameterHash { get; private set; }
public int StunParameterHash { get; private set; }
public int DeadParameterHash { get; private set; }
public void Initialize()
{
IdleParameterHash = Animator.StringToHash(idleParameterName);
MoveParameterHash = Animator.StringToHash(moveParameterName);
AttackParameterHash = Animator.StringToHash(attackParameterName);
SkillParameterHash = Animator.StringToHash(skillParameterName);
StunParameterHash = Animator.StringToHash(stunParameterName);
DeadParameterHash = Animator.StringToHash(deadParameterName);
}
}
AttackTrigger는 플레이어를 피격해주기 위한 역할로 적의 공격력을 연결해서 데미지를 준다.
using UnityEngine;
public class AttackTrigger : MonoBehaviour
{
public float Damage { get; set; }
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.tag.Equals(Define.TAG_PLAYER))
collision.gameObject.GetComponent<Player>().hpSystem.TakeDamage(Damage);
}
}
참고로 이건 자식 오브젝트인 AttackCollider에 있다.
스크립트가 완성되면 Animator에 그림처럼 연결한다.
Attack과 Skill에서 Idle로만 가는 로직인데 유일하게 Has Exit Time을 체크해야 동작을 다 취한 다음에 다음 애니메이션으로 넘어간다. 나머지들은 체크 해제
애니메이션 노드이름이 다른 애니메이션의 노드로 갈때 Condition에서 데이터들을 설정한다.
예를 들어 Idle에서 Move로 가는 화살표로 갈 때 Idle을 false, Move를 true로 설정한다. (많이 얘기하면 복잡하니 간단히 설명한다....)
스크립트와 애니메이션, 콜라이더의 속성들을 설정하고 적용했더니 영상처럼 구현이 되었다.
이것을 프리팹으로 저장해 다른 맵에서 나올수 있도록 설계해보자
by 스파르타 코딩클럽
'개발 TIL' 카테고리의 다른 글
7/11 부트캠프 개발 TIL (적 구현하기 3) (1) | 2024.07.18 |
---|---|
7/10 부트캠프 개발 TIL (적 구현하기 2) (0) | 2024.07.18 |
7/8 부트캠프 개발 TIL (프로젝트 최적화) (0) | 2024.07.10 |
7/5 부트캠프 개발 TIL (보스몹 구현) (0) | 2024.07.10 |
7/4 부트캠프 개발 TIL (Debug의 최적화) (0) | 2024.07.09 |