유니티/게임 프로젝트

2D RPG - (8) 적을 위한 세팅

monstro 2024. 12. 18. 12:06
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
반응형