
아래 작성된 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;
}
}
InputHandlerpublic 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);
}
}
Movementpublic 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;
}
}
}
Attackpublic 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");
}
}
Animatorpublic class PlayerAnimator : MonoBehaviour
{
[SerializeField] private Animator _animator;
public void UpdateAnimator(Vector2 moveDir, bool isGround)
{
_animator.SetBool("isWalked", moveDir != Vector2.zero);
_animator.SetBool("isJumped", !isGround);
}
}
Controllerpublic 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
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());
}
- 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);
}
}
}
- 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());
}