728x90
반응형
이번에는 플레이어를 공격할 적을 만들어보겠습니다.
그전에 플레이어와 적은 어느정도 공통적으로 사용하는 부분이 존재합니다.
이를 해결하기 위해 플레이어와 적 모두 공통적으로 상속받는 클래스를 하나 만들겠습니다.
1) 플레이어와 적의 공통 부모인 BaseCharacterController
public class BaseCharacterController : MonoBehaviour
{
[Header("Collision Info")]
[SerializeField]
protected Transform _groundCheck;
[SerializeField]
protected float _groundCheckDistance;
[SerializeField]
protected Transform _wallCheck;
[SerializeField]
protected float _wallCheckDistance;
public Animator _animator { get; set; }
public Rigidbody2D _rigidbody2D { get; set; }
public int _facingDir { get; set; } = 1;
protected bool _facingRight = true;
protected virtual void Awake()
{
}
// Animator와 RigidBody2D를 설정
protected virtual void Start()
{
_animator = GetComponentInChildren<Animator>();
_rigidbody2D = GetComponent<Rigidbody2D>();
}
protected virtual void Update()
{
}
// 속력을 설정하고 설정한 속력에 따라 방향 회전 수행
public virtual void SetVelocity(float xVelocity, float yVelcoity)
{
_rigidbody2D.velocity = new Vector2(xVelocity, yVelcoity);
DoFlip(xVelocity);
}
// 속력을 0으로 설정
public virtual void SetZeroVelocity() => _rigidbody2D.velocity = Vector2.zero;
// 방향 회전
public virtual void Flip()
{
_facingDir *= -1;
_facingRight = !_facingRight;
gameObject.transform.Rotate(0, 180, 0);
}
// 방향 회전을 수행
public virtual void DoFlip(float xParam)
{
if (xParam > 0 && !_facingRight)
Flip();
else if (xParam < 0 && _facingRight)
Flip();
}
// 땅을 딛고 있는지 확인
public virtual bool DoDetectIsGrounded() => Physics2D.Raycast(_groundCheck.position, Vector2.down, _groundCheckDistance, LayerMask.GetMask("Ground"));
// 벽을 마주하고 있는지 확인
public virtual bool DoDetectIsFacingWall() => Physics2D.Raycast(_wallCheck.position, Vector2.right * _facingDir, _wallCheckDistance, LayerMask.GetMask("Ground"));
// 땅을 딛고 있는지와 벽을 마주하고 있는지를 Debug Line을 그려 확인
protected virtual void OnDrawGizmos()
{
Gizmos.DrawLine(
_groundCheck.position,
new Vector3(_groundCheck.position.x, _groundCheck.position.y - _groundCheckDistance)
);
Gizmos.DrawLine(
_wallCheck.position,
new Vector3(_wallCheck.position.x + _wallCheckDistance, _wallCheck.position.y)
);
}
}
따라서 기존의 PlayerController는 위의 BaseCharacterController를 상속받게끔 수정하였습니다.
2) PlayerController
public class PlayerController : BaseCharacterController
{
// Animtion StateMachine
public PlayerStateMachine _stateMachine { get; private set; }
#region States
public PlayerStateIdle _idleState { get; private set; }
public PlayerStateMove _moveState { get; private set; }
public PlayerStateJump _jumpState { get; private set; }
public PlayerStateInAir _inAirState { get; private set; }
public PlayerStateDash _dashState { get; private set; }
public PlayerStateWallSlide _wallSlideState { get; private set; }
public PlayerStateWallJump _wallJumpState { get; private set; }
public PlayerStatePrimaryAttack _priamaryAttackState { get; private set; }
#endregion
// Player Input
[SerializeField]
InputAction _moveAction;
[SerializeField]
InputAction _jumpAction;
[SerializeField]
InputAction _dashAction;
[SerializeField]
InputAction _attackAction;
// Player Move Info
public float _moveSpeed = 12f;
public float _jumpForce = 12f;
public bool _isJumpPressed;
public bool _isAttackClicked;
public float _horizontalValue { get; private set; }
public float _verticalValue { get; private set; }
// Dash Info
[SerializeField]
private float _dashCooldown;
private float _dashUsageTimer;
public float _dashSpeed;
public float _dashDuration;
public float _dashDir { get; private set; }
public bool _doingSomething { get; private set; }
[Header("Attack Details")]
public Vector2[] _attackMovement;
// InputSystem 활성화
private void OnEnable()
{
_moveAction.performed += DoMove;
_moveAction.canceled += DoStopMove;
_moveAction.Enable();
_jumpAction.performed += DoJump;
_jumpAction.canceled += DoStopJump;
_jumpAction.Enable();
_dashAction.performed += DoDash;
_dashAction.Enable();
_attackAction.performed += DoAttack;
_attackAction.canceled += DoStopAttack;
_attackAction.Enable();
}
// InputSystem 비활성화
private void OnDisable()
{
_moveAction.performed -= DoMove;
_moveAction.canceled -= DoStopMove;
_moveAction.Disable();
_jumpAction.performed -= DoJump;
_jumpAction.canceled -= DoStopJump;
_jumpAction.Disable();
_dashAction.performed -= DoDash;
_dashAction.Disable();
_attackAction.performed -= DoAttack;
_attackAction.canceled -= DoStopAttack;
_attackAction.Disable();
}
// Controller의 Awake에서는 StateMachine과 StateMachine에서 사용할 State를 설정
protected override void Awake()
{
base.Awake();
_stateMachine = new PlayerStateMachine();
_idleState = new PlayerStateIdle(this, _stateMachine, "Idle");
_moveState = new PlayerStateMove(this, _stateMachine, "Move");
_jumpState = new PlayerStateJump(this, _stateMachine, "Jump");
_inAirState = new PlayerStateInAir(this, _stateMachine, "Jump");
_dashState = new PlayerStateDash(this, _stateMachine, "Dash");
_wallSlideState = new PlayerStateWallSlide(this, _stateMachine, "WallSlide");
_wallJumpState = new PlayerStateWallJump(this, _stateMachine, "WallJump");
_priamaryAttackState = new PlayerStatePrimaryAttack(this, _stateMachine, "Attack");
}
// Controller의 Start에서는 처음의 State를 IdleState로 설정
protected override void Start()
{
base.Start();
_stateMachine.Init(_idleState);
}
// Controller의 Update에서는 현재 State를 Update를 수행하고, PlayerController는 Dash를 위한 시간을 설정
protected override void Update()
{
base.Update();
_stateMachine._currentState.Update();
_dashUsageTimer -= Time.deltaTime;
}
// 무언가 하고 있는 경우를 설정하기 위한 DoSomething 함수, 코루틴을 통해 하고 있는 경우를 treue/false로 전환
public IEnumerator DoSomething(float _seconds)
{
_doingSomething = true;
yield return new WaitForSeconds(_seconds);
_doingSomething = false;
}
void DoMove(InputAction.CallbackContext value)
{
_horizontalValue = value.ReadValue<Vector2>().x;
_verticalValue = value.ReadValue<Vector2>().y;
}
void DoStopMove(InputAction.CallbackContext value)
{
_horizontalValue = value.ReadValue<Vector2>().x;
_verticalValue = value.ReadValue<Vector2>().y;
}
void DoJump(InputAction.CallbackContext value)
{
_isJumpPressed = value.ReadValueAsButton();
}
void DoStopJump(InputAction.CallbackContext value)
{
_isJumpPressed = value.ReadValueAsButton();
}
void DoDash(InputAction.CallbackContext value)
{
if (DoDetectIsFacingWall())
return;
if (value.ReadValueAsButton() && _dashUsageTimer < 0)
{
_dashUsageTimer = _dashCooldown;
_dashDir = _moveAction.ReadValue<Vector2>().x;
if (_dashDir == 0)
_dashDir = _facingDir;
_stateMachine.ChangeState(_dashState);
}
}
void DoAttack(InputAction.CallbackContext value)
{
_isAttackClicked = value.ReadValueAsButton();
}
void DoStopAttack(InputAction.CallbackContext value)
{
_isAttackClicked = value.ReadValueAsButton();
}
// 현재 State의 AnimationFinishTrigger 함수를 호출
public void AnimationTrigger() => _stateMachine._currentState.AnimationFinishTrigger();
}
그리고 추가적으로 기존의 Update 문에서 매번 체크하던 입력여부를
Event가 발생했을 시에만 입력을 수행하도록 수정하였습니다.
3) EnemyController
public class EnemyController : BaseCharacterController
{
// 적 이동 정보
[Header("Move Info")]
public float _moveSpeed;
public float _idleTime;
public float _engageTime;
// 적 공격 정보
[Header("Attack Info")]
public float _attackDistance;
public float _attackCooldown;
[HideInInspector]
public float _lastTimeAttacked;
public EnemyStateMachine _stateMachine { get; private set; }
// 제일 초기에 적을 위한 EnemyStateMachine을 설정
protected override void Awake()
{
base.Awake();
_stateMachine = new EnemyStateMachine();
}
// EnemyController의 Update에서는 현재 State의 Update를 수행
protected override void Update()
{
base.Update();
_stateMachine._currentState.Update();
}
// 추가적으로 땅과 벽을 위한 Debug Line 말고도 공격 사거리를 그리는 Debug Line을 그림
protected override void OnDrawGizmos()
{
base.OnDrawGizmos();
Gizmos.color = Color.yellow;
Gizmos.DrawLine(transform.position, new Vector3(transform.position.x + _attackDistance * _facingDir, transform.position.y));
}
// Player를 탐지하는 함수
public virtual RaycastHit2D DoDetectPlayer() => Physics2D.Raycast(_wallCheck.position, Vector2.right * _facingDir, 50, LayerMask.GetMask("Player"));
// 현재 State의 AnimationFinishTrigger 함수를 호출
public void AnimationTrigger() => _stateMachine._currentState.AnimationFinishTrigger();
}
적 이동 정보의 _engageTime의 경우, 경계에 들어가는 시간을 측정하는 용도로 사용합니다.
4) EnemyStateMachine
public class EnemyStateMachine
{
public EnemyState _currentState { get; private set; }
// EnemyStateMachine을 통해 현재 State를 설정하고 State에 Enter
public void Init(EnemyState StartState)
{
_currentState = StartState;
_currentState.Enter();
}
// EnemtStateMachine을 통해 현재 State를 탈출하고 현재 State를 새로 설정, 설정 후 Enter
public void ChangeState(EnemyState newState)
{
_currentState.Exit();
_currentState = newState;
_currentState.Enter();
}
}
5) EnemyState
public class EnemyState
{
// EnemyState의 경우 기본적인 적의 Controller인 EnemyController말고도
// 적의 타입 별 Controller를 가질 예정이므로 다음과 같이 구분지었음
protected EnemyStateMachine _enemyStateMachine;
protected EnemyController _enemyBaseController;
protected Rigidbody2D _rigidbody2D;
protected bool _triggerCalled;
protected string _animatorBoolParamName;
protected float _stateTimer;
// 생성자에서는 EnemyController, EnemyStateMachine, AnimatorBoolParamName을 설정함
public EnemyState(EnemyController enemyBaseController, EnemyStateMachine enemyStateMachine, string animatorBoolParamName)
{
this._enemyBaseController = enemyBaseController;
this._enemyStateMachine = enemyStateMachine;
this._animatorBoolParamName = animatorBoolParamName;
}
// EnemyState의 Update에서는 State별로 사용할 시간인 _stateTimer를 조정함
public virtual void Update()
{
_stateTimer -= Time.deltaTime;
}
// Enter에서는 해당 State에 들어가므로,
// State를 탈출하는 용도인 _triggerCalled를 false로 설정
// EnemyContorller의 RigidBody2D를 가져옴
// State를 Animator에서 활성화하기 위해 bool 변수를 true로 설정
public virtual void Enter()
{
_triggerCalled = false;
_rigidbody2D = _enemyBaseController._rigidbody2D;
_enemyBaseController._animator.SetBool(_animatorBoolParamName, true);
}
// Exit의 경우 단순하게 State를 Animator에서 비활성화하기 위해 bool 변수를 false로 설정
public virtual void Exit()
{
_enemyBaseController._animator.SetBool(_animatorBoolParamName, false);
}
// AnimationEvent로 발동될 함수, State를 탈출하기 위해 _triggerCalled를 true로 바꿈
public virtual void AnimationFinishTrigger()
{
_triggerCalled = true;
}
}
728x90
반응형
'유니티 > 게임 프로젝트' 카테고리의 다른 글
2D RPG - (10) 스켈레톤 만들기 (마무리) (0) | 2024.12.18 |
---|---|
2D RPG - (9) 스켈레톤 만들기 (1) (0) | 2024.12.18 |
2D RPG - (7) 무한반복되는 배경 만들기 (0) | 2024.12.11 |
2D RPG - (6) Tilemap을 통한 배경 그리기 + Cinemachine (0) | 2024.12.11 |
2D RPG - (5) StateMachine의 구성과 완성 (마지막) (0) | 2024.12.09 |