상세 컨텐츠

본문 제목

[개발일지] 2D 방치형 게임 개발: 플레이어 상태 관리 시스템 구현

카테고리 없음

by 강자이 2024. 6. 21. 16:10

본문

 

이번 프로젝트에서는 플레이어 상태 관리 시스템을 유니티에서 구현하였습니다. 이 시스템은 플레이어의 이동 및 상호작용을 관리하고, 상태 머신을 통해 다양한 상태를 전환하며 플레이어의 행동을 제어합니다. 이 일지에서는 구현된 주요 기능과 코드의 세부적인 설명을 다룹니다.

개요 및 구현된 기능

1. 주요 기능

  • 플레이어 상태 관리: 플레이어의 Idle(정지) 및 Walk(이동) 상태를 관리하고 전환하는 상태 머신 구현
  • 입력 처리: 플레이어의 입력을 처리하여 이동 및 상호작용을 제어
  • 애니메이션 관리: 상태에 따른 애니메이션 시작 및 종료
  • 상호작용 처리: 민간인과의 충돌 및 상호작용 처리

2. 주요 클래스 및 구조

  • PlayerBaseState: 모든 플레이어 상태의 기본 클래스
  • PlayerIdleState: 플레이어의 Idle 상태를 관리하는 클래스
  • PlayerWalkState: 플레이어의 Walk 상태를 관리하는 클래스
  • PlayerStateMachine: 상태 전환 및 상태 관리를 담당하는 클래스
  • PlayerController: 플레이어의 입력을 처리하는 클래스
  • Player: 플레이어 게임 오브젝트를 관리하는 클래스

코드 세부 설명

1. PlayerBaseState 클래스

public class PlayerBaseState : IState
{
    protected PlayerStateMachine stateMachine;
    protected readonly PlayerSO playerSO;

    private Transform civilianTransform;
    private GameObject detectedCivilian; // 플레이어와 충돌한 민간인

    private Vector2 targetDirection; // 목표 방향
    private bool isMoving = false; // 이동 여부 확인

    private float mouseButtonDownTime = 0f; // 마우스 버튼이 눌린 시간 추적
    private const float requiredHoldTime = 0.2f; // 마우스 버튼을 눌러야 하는 최소 시간

    public PlayerBaseState(PlayerStateMachine stateMachine)
    {
        this.stateMachine = stateMachine;
        playerSO = stateMachine.Player.Data;
    }

    public virtual void Enter()
    {
        AddInputActionsCallbacks(); // 입력 액션 콜백 추가
        PromptManager.Instance.OnPromptClosed += HandlePromptClosed;
    }

    public virtual void Exit()
    {
        RemoveInputActionsCallbacks(); // 입력 액션 콜백 제거
        PromptManager.Instance.OnPromptClosed -= HandlePromptClosed;
    }

    protected void StartAnimation(int animatorHash)
    {
        stateMachine.Player.Animator.SetBool(animatorHash, true);
    }

    protected void SetAnimation(int animatorHashX, int animatorHashY)
    {
        stateMachine.Player.Animator.SetFloat(animatorHashX, targetDirection.x);
        stateMachine.Player.Animator.SetFloat(animatorHashY, targetDirection.y);
    }

    protected void StopAnimation(int animatorHash)
    {
        stateMachine.Player.Animator.SetBool(animatorHash, false);
    }

    public virtual void HandleInput()
    {
        ReadMovementInput();

        if (stateMachine.Player.Input.IsLeftMouseButtonPressed()) 
        {
            mouseButtonDownTime += Time.deltaTime;

            if (!isMoving && mouseButtonDownTime >= requiredHoldTime) 
            {
                Vector2 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
                targetDirection = (mousePosition - (Vector2)stateMachine.Player.transform.position).normalized;
                isMoving = true;
            }
        }
        else
        {
            mouseButtonDownTime = 0f;
            isMoving = false;
        }
    }

    public virtual void Update()
    {
        if (!PromptManager.Instance.promptPanel.activeSelf && isMoving)
        {
            UpdateTargetDirection();
            Move();
        }

        if (!PromptManager.Instance.isCivilianDetected)
        {
            CheckForCollisionWithCivilian();
        }
        else
        {
            CheckCivilianDistance();
        }
    }

    protected virtual void AddInputActionsCallbacks()
    {
        PlayerController input = stateMachine.Player.Input;
        input.playerActions.MouseDelta.canceled += OnMovementCanceled;
    }

    protected virtual void RemoveInputActionsCallbacks()
    {
        PlayerController input = stateMachine.Player.Input;
        input.playerActions.MouseDelta.canceled -= OnMovementCanceled;
    }

    protected virtual void OnMovementCanceled(InputAction.CallbackContext context)
    {
        if (stateMachine.MovementInput == Vector2.zero) return;
        stateMachine.ChangeState(stateMachine.IdleState);
    }

    private void ReadMovementInput()
    {
        stateMachine.MovementInput = stateMachine.Player.Input.GetMouseDelta();
    }

    private void UpdateTargetDirection()
    {
        Vector2 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        targetDirection = (mousePosition - (Vector2)stateMachine.Player.transform.position).normalized;
    }

    private void Move()
    {
        if (!stateMachine.Player.Input.IsLeftMouseButtonPressed() || mouseButtonDownTime < requiredHoldTime)
        {
            stateMachine.ChangeState(stateMachine.IdleState);
            return;
        }

        Transform playerTransform = stateMachine.Player.transform;
        playerTransform.position += (Vector3)targetDirection * GetMovementSpeed() * Time.deltaTime;
    }

    private float GetMovementSpeed()
    {
        return stateMachine.MovementSpeed * stateMachine.MovementSpeedModifier;
    }

    private void CheckForCollisionWithCivilian()
    {
        CapsuleCollider2D capsuleCollider = stateMachine.Player.GetComponent<CapsuleCollider2D>();
        if (capsuleCollider == null)
        {
            Debug.LogError("CapsuleCollider2D 컴포넌트를 찾을 수 없습니다!");
            return;
        }

        Vector2 playerPosition = stateMachine.Player.transform.position;
        float radius = capsuleCollider.size.y / 2f;

        RaycastHit2D[] hits = Physics2D.CircleCastAll(playerPosition, radius, Vector2.zero);

        foreach (var hit in hits)
        {
            if (hit.collider.CompareTag("Civilian"))
            {
                PromptManager.Instance.OpenPromptPanel();
                stateMachine.ChangeState(stateMachine.IdleState);
                PromptManager.Instance.isCivilianDetected = true;

                civilianTransform = hit.collider.transform;
                detectedCivilian = hit.collider.gameObject;
                PromptManager.Instance.SetDetectedCivilian(detectedCivilian);

                DialogManager.Instance.sc = hit.collider.gameObject.GetComponent<SpecialCharacter>();
                if (!DialogManager.Instance.sc.isCorrectAns)
                    PromptManager.Instance.dominateBtnColor.color = Color.gray;
                else
                    PromptManager.Instance.dominateBtnColor.color = Color.white;

                InteractionEvent intercationEvent = detectedCivilian.transform.GetComponent<InteractionEvent>();
                Dialogue[] dialogues = intercationEvent.GetDialogue();
                DialogManager.Instance.GetDialogues(dialogues);

                break;
            }
        }
    }

    private void CheckCivilianDistance()
    {
        CapsuleCollider2D capsuleCollider = stateMachine.Player.GetComponent<CapsuleCollider2D>();
        if (capsuleCollider == null)
        {
            Debug.LogError("CapsuleCollider2D 컴포넌트를 찾을 수 없습니다!");
            return;
        }

        float radius = capsuleCollider.size.x * 2;

        if (civilianTransform == null) return;

        float distance = Vector3.Distance(stateMachine.Player.transform.position, civilianTransform.position);

        if (distance > radius)
        {
            PromptManager.Instance.isCivilianDetected = false;
        }
    }

    private void HandlePromptClosed()
    {
        if (!PromptManager.Instance.isCivilianDetected)
        {
            stateMachine.ChangeState(stateMachine.WalkState);
        }
    }
}

PlayerBaseState 클래스는 모든 플레이어 상태의 기본 클래스로, 상태 전환, 애니메이션 관리, 입력 처리, 이동 로직 및 민간인과의 상호작용을 처리합니다.

  • Enter / Exit: 상태 진입 및 종료 시 입력 액션 콜백을 추가/제거하고 프롬프트 매니저 이벤트를 처리합니다.
  • HandleInput: 플레이어의 입력을 처리하여 이동 및 목표 방향을 설정합니다.
  • Update: 매 프레임마다 호출되어 이동 및 민간인과의 충돌을 처리합니다.
  • AddInputActionsCallbacks / RemoveInputActionsCallbacks: 입력 액션 콜백을 추가/제거합니다.
  • OnMovementCanceled: 이동이 취소될 때 상태를 Idle로 전환합니다.
  • ReadMovementInput: 이동 입력을 읽습니다.
  • UpdateTargetDirection: 목표 방향을 업데이트합니다.
  • Move: 플레이어를 목표 방향으로 이동시킵니다.
  • GetMovementSpeed: 이동 속도를 반환합니다.
  • CheckForCollisionWithCivilian: 민간인과의 충돌을 체크합니다.
  • CheckCivilianDistance: 민간인과의 거리를 체크합니다.
  • HandlePromptClosed: 프롬프트 종료 시 상태를 Walk로 전환합니다.

 

 

2. PlayerIdleState 클래스

public class PlayerIdleState : PlayerBaseState
{
    public PlayerIdleState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
    {
    }

    public override void Enter()
    {
        stateMachine.MovementSpeedModifier = 0f;
        base.Enter();
        StartAnimation(stateMachine.Player.AnimationData.IdleParameterHash);
    }

    public override void Exit()
    {
        base.Exit();
        StopAnimation(stateMachine.Player.AnimationData.IdleParameterHash);
    }

    public override void HandleInput()
    {
        base.HandleInput();

        if (stateMachine.Player.Input.IsLeftMouseButtonPressed())
        {
            stateMachine.ChangeState(stateMachine.WalkState);
        }
    }

    public override void Update()
    {
        base.Update();
    }
}

PlayerStateMachine 클래스는 상태 머신을 관리하며 상태 전환을 담당합니다.

  • Player: 플레이어 객체를 참조합니다.
  • IdleState / WalkState: Idle 및 Walk 상태를 관리합니다.
  • MovementInput: 이동 입력을 저장합니다.
  • MovementSpeed / RotationDamping / MovementSpeedModifier: 이동 속도 및 회전 감속을 저장합니다.
  • Start: 초기 상태를 Idle로 설정합니다.

 

4. PlayerWalkState 클래스

public class PlayerWalkState : PlayerBaseState
{
    public PlayerWalkState(PlayerStateMachine playerStateMachine) : base(playerStateMachine)
    {
    }

    public override void Enter()
    {
        stateMachine.MovementSpeedModifier = playerSO.WalkSpeedModifier;
        base.Enter();
        StartAnimation(stateMachine.Player.AnimationData.WalkParameterHash);
        SetAnimation(stateMachine.Player.AnimationData.DirXParameterHash, stateMachine.Player.AnimationData.DirYParameterHash);
    }

    public override void Exit()
    {
        base.Exit();
        StopAnimation(stateMachine.Player.AnimationData.WalkParameterHash);
    }

    public override void HandleInput()
    {
        base.HandleInput();
        if (!stateMachine.Player.Input.IsLeftMouseButtonPressed())
        {
            stateMachine.ChangeState(stateMachine.IdleState);
        }
    }
}

PlayerWalkState 클래스는 플레이어의 Walk 상태를 관리합니다.

  • Enter: 상태 진입 시 이동 속도 수정자를 Walk 속도로 설정하고 Walk 애니메이션을 시작합니다.
  • Exit: 상태 종료 시 Walk 애니메이션을 중지합니다.
  • HandleInput: 입력을 처리하여 왼쪽 마우스 버튼이 눌리지 않으면 Idle 상태로 전환합니다.

 

 

5. PlayerController 클래스

public class PlayerController : MonoBehaviour
{
    public PlayerInputs playerInputs { get; private set; }
    public PlayerInputs.PlayerActions playerActions { get; private set; }

    public event Action OnLeftClick;

    private void Awake()
    {
        playerInputs = new PlayerInputs();
        playerActions = playerInputs.Player;

        playerActions.LeftClick.performed += context => OnLeftClick?.Invoke();
    }

    private void OnEnable()
    {
        playerInputs.Enable();
    }

    private void OnDisable()
    {
        playerInputs.Disable();
    }

    public Vector2 GetMouseDelta()
    {
        return playerActions.MouseDelta.ReadValue<Vector2>();
    }

    public bool IsLeftMouseButtonPressed()
    {
        return playerActions.LeftClick.IsPressed();
    }
}

PlayerController 클래스는 플레이어의 입력을 처리합니다.

  • Awake: PlayerInputs 인스턴스를 생성하고 LeftClick 액션에 대한 콜백을 추가합니다.
  • OnEnable / OnDisable: 입력 시스템을 활성화/비활성화합니다.
  • GetMouseDelta: 마우스 이동 변화를 반환합니다.
  • IsLeftMouseButtonPressed: 마우스 왼쪽 버튼의 상태를 확인합니다.

 

6. Player 클래스

public class Player : MonoBehaviour
{
    [field: Header("References")]
    [field: SerializeField] public PlayerSO Data { get; private set; }

    public PlayerController Input { get; private set; }

    private PlayerStateMachine stateMachine;

    [field: Header("Animations")]
    [field: SerializeField] public AnimationData AnimationData { get; private set; }

    public Animator Animator { get; private set; }

    private void Awake()
    {
        AnimationData.Initialize();
        Animator = GetComponentInChildren<Animator>();
        Input = GetComponent<PlayerController>();
        stateMachine = new PlayerStateMachine(this);
    }

    void Start()
    {
        stateMachine.ChangeState(stateMachine.IdleState);
    }

    private void Update()
    {
        stateMachine.HandleInput();
        stateMachine.Update();
    }
}

 

Player 클래스는 플레이어 게임 오브젝트를 관리합니다.

  • Awake: 애니메이션 데이터를 초기화하고 상태 머신을 생성합니다.
  • Start: 초기 상태를 Idle로 설정합니다.
  • Update: 매 프레임마다 상태 머신의 입력 처리 및 업데이트를 호출합니다.

 

결론

이번 프로젝트를 통해 유니티에서의 플레이어 상태 관리 시스템을 구현하면서 상태 머신의 개념을 깊이 이해하게 되었습니다. 이 시스템을 통해 플레이어의 이동 및 상호작용을 효율적으로 관리할 수 있으며, 향후 더 복잡한 상태 및 기능을 추가할 수 있는 기반을 마련하였습니다.

댓글 영역