[Unity] Player Script(2D) Refactoring

Lingtea_luv·2025년 5월 21일
0

Unity

목록 보기
15/30
post-thumbnail

Player


Script

아래 작성된 player 스크립트에는 크게 4가지 기능이 구현되어있다. 하나의 클래스에 4개의 기능이 모두 들어가있어 유지보수가 어렵고, 단일책임원칙에 위배된다고 볼 수 있다. 따라서 아래의 스크립트를 기능 별로 나누어 다시 작성해볼 것이다.

public class PlayerController : MonoBehaviour
{
    [Header("Drag&Drop")]
    [SerializeField] private Rigidbody2D _rigid;
    [SerializeField] private Animator _animator;
    [SerializeField] private Transform _attackPoint;
    
    [Header("Number")]
    [SerializeField] private float _moveSpeed;
    [SerializeField] private float _jumpPower;
    [SerializeField] private int _atk;
    [SerializeField] private Vector2 _attackBoxSize = new Vector2(0.1f, 0.1f);
    [SerializeField] private LayerMask _monsterLayer;

    private Vector2 _moveDir;
    private bool _isJump;
    private bool _isGround;
    private bool _isAttack;
    private bool _isAttacking;

    private float _atkLength;
    private float _atkCoolDown;
    
    private void Awake()
    {
        Init();
    }

    private void Update()
    {
        PlayerInput();
        Attack();
    }
    
    private void FixedUpdate()
    {
        Move();
        Jump();
    }
 
    private void OnCollisionEnter2D(Collision2D other)
    {
        if (other.gameObject.CompareTag("Ground"))
        {
            _isGround = true;
            _isJump = false;
            _animator.SetBool("isJumped", false);
        }
    }

    private void Move()
    {
        if (_moveDir != Vector2.zero)
        {
            _animator.SetBool("isWalked", true);
            _rigid.velocity = new Vector2(_moveDir.x * _moveSpeed, _rigid.velocity.y);
        }
        else
        {
            _animator.SetBool("isWalked", false);
        }
    }
    
    private void Init()
    {
        _rigid = GetComponent<Rigidbody2D>();
        _animator = GetComponent<Animator>();       
        _atkLength = GetAnimationLength("Attack");
    }

    private void Jump()
    {
        if (_isJump && _isGround)
        {
            _rigid.AddForce(Vector2.up * _jumpPower, ForceMode2D.Impulse);
            _animator.SetBool("isJumped", true);
            _isGround = false;
        }
    }

    private void Attack()
    {
        if (_isAttack)
        {
            _atkCoolDown -= Time.deltaTime;
            _moveDir.x = 0;

            if (_atkCoolDown <= _atkLength * 0.7f && !_isAttacking)
            {
                _isAttacking = true;

                Collider2D[] hits = Physics2D.OverlapBoxAll(_attackPoint.position, _attackBoxSize, 0f, _monsterLayer);

                foreach (var hit in hits)
                {
                    IDamageable damageable = hit.GetComponent<IDamageable>();
                    if(damageable != null)
                    {
                        damageable.Damaged(_atk);
                    }
                }
            }
            
            if (_atkCoolDown <= 0)
            {
                _isAttacking = false;
                _isAttack = false;
                _animator.SetBool("isAttacked", false);
            }
        }
        
    }
    
    private void PlayerInput()
    {
        float x = Input.GetAxis("Horizontal");
        _moveDir = new Vector2(x, 0);

        if (_moveDir != Vector2.zero)
        {
            if (_moveDir.x < 0)
            {
                gameObject.transform.localScale = new Vector3(-1, 1, 1);
            }
            else
            {
                gameObject.transform.localScale = new Vector3(1, 1, 1);
            }
        }
        
        if (Input.GetKeyDown(KeyCode.Space))
        {
            _isJump = true;
        }

        if (Input.GetKeyDown(KeyCode.Mouse0) && !_isAttack)
        {
            _isAttack = true;
            _animator.SetBool("isAttacked", true);
            _atkCoolDown = _atkLength;
        }
    }

    private float GetAnimationLength(string clipName)
    {
        AnimationClip[] clips = _animator.runtimeAnimatorController.animationClips;
        foreach (AnimationClip clip in clips)
        {
            if (clip.name == clipName)
            {
                return clip.length;
            }
        }
        return 0f;
    }
}

Refactoring

InputHandler

public class PlayerInputHandler : MonoBehaviour
{
	public Vector2 MoveDir { get; private set; }
    public bool IsJump { get; private set; }
    public bool IsAttack { get; private set; }

    public void PlayerInput()
    {
        float x = Input.GetAxis("Horizontal");
        MoveDir = new Vector2(x, 0);

        IsJump = Input.GetKeyDown(KeyCode.Space);
		IsAttack = Input.GetKeyDown(KeyCode.Mouse0);
    }
}

Movement

public class PlayerMovement : MonoBehaviour
{
	[SerializeField] private float _moveSpeed;
    [SerializeField] private float _jumpPower;
    
    private Rigidbody2D _rigid;
    private bool _isGround;
    public bool IsGround => _isGround;

	public void Move(Vector2 moveDir)
    {        
        _rigid.velocity = new Vector2(moveDir.x * _moveSpeed, _rigid.velocity.y);

        if (moveDir.x < 0)
        {
            gameObject.transform.localScale = new Vector3(-1, 1, 1);
        }
        else if(moveDir.x > 0)
        {
            gameObject.transform.localScale = new Vector3(1, 1, 1);
        }
    }
    
	public void Jump(bool isJump)
    {
        if (isJump && _isGround)
        {
            _rigid.AddForce(Vector2.up * _jumpPower, ForceMode2D.Impulse);            
            _isGround = false;
        }
    }
    
    private void OnCollisionEnter2D(Collision2D other)
    {
        if (other.gameObject.CompareTag("Ground"))
        {
            _isGround = true;
        }
    }
}

Attack

public class PlayerAttack : MonoBehaviour
{
	[SerializeField] private Transform _attackPoint;    
    [SerializeField] private Vector2 _attackBoxSize = new Vector2(0.1f, 0.1f);
    [SerializeField] private int _atk;
    [SerializeField] private LayerMask _monsterLayer;
    [SerializeField] private Animator _animator;
    
    private bool _isAttacking;
    private float _atkLength;
    private float _atkCoolDown;
    
    private void Awake()
    {
    	Init();
    }
    
	public void Attack(bool IsAttack)
    {
        if (IsAttack && !_isAttacking)
        {       
			_isAttacking = true;             
            _atkCoolDown = _atkLength;
            _animator.SetBool("isAttacked", true);
        }           

        if (_isAttacking)
        {
            _atkCoolDown -= Time.deltaTime;
                
            if(_atkCoolDown <= _atkLength * 0.5f)
            {
                Collider2D[] hits = Physics2D.OverlapBoxAll(
                _attackPoint.position, _attackBoxSize, 0f, _monsterLayer);

                foreach (var hit in hits)
                {
                    IDamageable damageable = hit.GetComponent<IDamageable>();
                    damageable?.Damaged(_atk);
                }
            }
            
            if (_atkCoolDown <= 0)
            {
                _isAttacking = false;
                _animator.SetBool("isAttacked", false);
            }
        }  
    }
    
    private float GetAnimationLength(string clipName)
    {
        AnimationClip[] clips = _animator.runtimeAnimatorController.animationClips;
        foreach (AnimationClip clip in clips)
        {
            if (clip.name == clipName)
            {
                return clip.length;
            }
        }
        return 0f;
    }
    
    private void Init()
    {
    	_atkLength = GetAnimationLength("Attack");
    }
}

Animator

public class PlayerAnimator : MonoBehaviour
{
    [SerializeField] private Animator _animator;
    
    public void UpdateAnimator(Vector2 moveDir, bool isGround)
    {            
        _animator.SetBool("isWalked", moveDir != Vector2.zero);
        _animator.SetBool("isJumped", !isGround);
    }
}

Controller

public class PlayerController : MonoBehaviour
{
    private PlayerInputHandler _input;
    private PlayerMovement _movement;
    private PlayerAttack _attack;
    private PlayerAnimator _animator;
    
    private void Awake()
    {
        Init();
    }

    private void Update()
    {
        _input.PlayerInput();
        _attack.Attack(_input.IsAttack);
        _animator.UpdateAnimator(_input.MoveDir, _movement.IsGround);
    }

    private void FixedUpdate()
    {
        _movement.Move(_input.MoveDir);
        _movement.Jump(_input.IsJump);
    }
    
    private void Init()
    {
    	_input = GetComponent<PlayerInputHandler>();
        _movement = GetComponent<PlayerMovement>();
        _attack = GetComponent<PlayerAttack>();
        _animator = GetComponent<PlayerAnimator>();
    }  
}

Bug Fix

  1. Jump()가 간헐적으로 씹히는 현상

원인 : InputHandler에서 IsJump = Input.GetKeyDown(KeyCode.Space); → Update( )에서 딱 1프레임만 true가 되어 FixedUpdate( ) 에서는 이미 false가 되어 점프가 안 될 가능성이 있음.

해결 : InputHandler에 아래 메서드를 추가하여 Controller FixedUpdate( )에서 IsJump를 호출하는 방식이 아닌 ConsumeJump()를 호출하여 IsJump를 소비하는 방식으로 리팩토링

public void PlayerInput()
{
	if (Input.GetKeyDown(KeyCode.Space))
    {
       	IsJump = true;
    }
}
    
public bool ConsumeJump()
{
    if (IsJump)
    {
        IsJump = false;
        return true;
    }
    return false;
}
private void FixedUpdate()
{
	_movement.Jump(_input.ConsumeJump());
}
  1. player가 monster를 한 번 공격할 때 Damaged가 중복으로 호출되는 현상

원인 : PlayerAttack의 Attack 메서드에서 공격키를 눌렀을 때 기존 플래그를 담당했던_isAttacking이 true가 되어 Damaged를 중복으로 호출하게 됨.

해결 : PlayerAttack에 다른 플래그 _isAttacked를 추가하고, 이를 Damaged를 호출하는 조건문에 넣어 중복으로 호출하는 것을 방지

public void Attack(bool IsAttack)
{
    if (IsAttack && !_isAttacking)
    {
        _isAttacking = true;
        _atkCoolDown = _atkLength;
        _animator.SetBool("isAttacked", true);
    }

    if (_isAttacking)
    {
        _atkCoolDown -= Time.deltaTime;

        if (_atkCoolDown <= _atkLength * 0.7f && !_isAttacked)
        {
            Collider2D[] hits = Physics2D.OverlapBoxAll(
            _attackPoint.position, _attackBoxSize, 0f, _monsterLayer);
            foreach (var hit in hits)
            {
                IDamageable target = hit.GetComponent<IDamageable>();
                target?.Damaged(_atk);
                _isAttacked = true;
            }
        }

        if (_atkCoolDown <= 0)
        {
            _isAttacking = false;
            _isAttacked = false;
            _animator.SetBool("isAttacked", false);
        }
    }
}
  1. player가 monster를 공격할 때 이동 방향으로 미끄러지는 현상

원인 : 기존에는 Attack() 메서드에서 _moveDir = Vector2.zero 로 해결했으나, 리팩토링 하는 과정에서 제거하여 문제 발생

해결 : Controller에서 PlayerAttack에 접근하여 IsAttacking이 false일 때만 Move()를 호출하도록 변경

private void FixedUpdate()
{
    if (!_attack.IsAttacking)
    {
        _movement.Move(_input.MoveDir);
    }
    _movement.Jump(_input.ConsumeJump());
}
profile
뚠뚠뚠뚠

0개의 댓글