게임 개발 공부 기록

24일차 - PenguinRun 팀 프로젝트 2

00lwt 2025. 2. 25. 22:15

▶▷ PenguinRun

오늘은 플레이어가 부스터 아이템을 먹었을 때의 속도 증가 및 무적 효과와 장애물에 부딫였을 때 일시적인 무적 효과를 구현하였다.

PlayerController

public class PlayerController : MonoBehaviour
{
    // 플레이어 상태 변수들
    private bool isDead; // 플레이어가 죽었는지 여부
    [SerializeField] private bool isJumping; // 점프 상태 여부
    [SerializeField] private bool isSliding; // 슬라이딩 상태 여부

    // 점프 관련 변수
    [SerializeField] private int jumpForce; // 점프 힘
    [SerializeField] private int jumpCount = 2; // 남은 점프 가능 횟수

    [SerializeField] private float deathY = -10f; // 사망 Y축 좌표
    public float DeathY => deathY;

    // 컴포넌트 및 매니저 참조 변수
    private GameManager gameManager; // 게임 매니저 참조
    private StatHandler statHandler; // 상태 관리 핸들러
    public StatHandler Stat => statHandler;
    public AnimationHandler animationHandler; // 애니메이션 핸들러
    private Rigidbody2D rb; // Rigidbody2D 컴포넌트 참조

    // 이벤트 선언: 체력 변화, 속도 변화, 점수 추가 시 호출
    public event Action<PlayerController, int> OnAddScore;

    private void Awake()
    {
        isDead = false;
        rb = GetComponent<Rigidbody2D>();
        statHandler = GetComponent<StatHandler>();
        animationHandler = GetComponent<AnimationHandler>();
    }

    private void Start()
    {
        if (rb == null)
        {
            Debug.Log("Not Founded Rigidbody");
        }

        isDead = false;
        isJumping = false;
        gameManager = GameManager.Instance;
    }

    /// <summary>
    /// 체력이 0 이하이면 게임 오버 처리
    /// 스페이스바 입력 시 점프 활성화
    /// 왼쪽 Shift 입력 시 슬라이딩 활성화
    /// 일정 높이 이하로 떨어지면 게임 오버 처리
    /// </summary>
    private void Update()
    {
        animationHandler.Move();

        if (!isDead)
        {
            // 점프 입력 감지
            if (Input.GetKeyDown(KeyCode.Space))
            {
                animationHandler.Jump();
                isJumping = true;
            }

            // 슬라이딩 입력 감지
            if (Input.GetKey(KeyCode.LeftShift))
            {
                animationHandler.Slide();
                isSliding = true;
            }
            else if (Input.GetKeyUp(KeyCode.LeftShift))
            {
                animationHandler.Move();
                isSliding = false;
            }
        }

        // 플레이어가 사망 영역(높이 아래로 떨어짐)에 도달하면 게임 오버 처리
        if (transform.position.y < deathY)
        {
            gameManager.GameOver();
        }
    }

    /// <summary>
    /// 전진 이동, 점프, 슬라이딩, 바닥 감지 등의 물리 처리
    /// </summary>
    private void FixedUpdate()
    {
        if (isDead)
            return;

        Move();
        Jump();
        Sliding();
    }

    /// <summary>
    /// 플레이어 이동 처리
    /// 현재 속도를 statHandler.Speed 값으로 설정
    /// </summary>
    public void Move()
    {
        Vector2 velocity = rb.velocity;
        velocity.x = statHandler.Speed;
        rb.velocity = velocity;
    }

    /// <summary>
    /// 플레이어 점프 처리
    /// 남은 점프 횟수가 있을 경우 점프를 수행하고 점프 횟수를 감소
    /// </summary>
    public void Jump()
    {
        if (isJumping)
        {
            if (jumpCount > 0)
            {
            	Vector2 velocity = rb.velocity * 0;
				rb.velocity = velocity;
                Vector2 vel = rb.velocity + Vector2.up * jumpForce;
                rb.velocity = vel;
                --jumpCount;
                isJumping = false;
            }
        }
    }

    /// <summary>
    /// 플레이어 슬라이딩 처리
    /// 슬라이딩 중이면 90도로 회전, 그렇지 않으면 원래 상태 유지
    /// </summary>
    public void Sliding()
    {
        if (isSliding)
        {
            transform.rotation = Quaternion.Euler(0, 0, 90);
        }
        else
        {
            transform.rotation = Quaternion.Euler(0, 0, 0);
        }
    }

    /// <summary>
    /// 트리거 충돌 발생 시 상호작용 가능한 오브젝트와의 상호작용 처리
    /// </summary>
    /// <param name="collision">충돌한 콜라이더</param>
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.tag.Equals("Interactable"))
        {
            InteractObject inter = collision.GetComponent<InteractObject>();
            if (inter == null)
                return;
            inter.OnInteraction(statHandler);
        }
    }

    /// <summary>
    /// 지면과의 충돌 감지하여 점프 횟수 초기화
    /// </summary>
    /// <param name="collision">충돌한 오브젝트 정보</param>
    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("Ground"))
        {
            jumpCount = 2;
        }
    }

    /// <summary>
    /// 점수 추가 이벤트 호출
    /// </summary>
    /// <param name="amount">추가할 점수</param>
    public void AddScore(int amount = 1)
    {
        OnAddScore?.Invoke(this, amount);
    }
}

 

StatHandler

public class StatHandler : MonoBehaviour
{
    // 플레이어 컨트롤러 및 애니메이션 핸들러 참조
    private PlayerController player;
    public AnimationHandler animationHandler;

    // 체력 및 속도 관련 변수
    [SerializeField, Range(0f, 100f)] private float hp;      // 현재 체력
    public float Hp => hp;

    [SerializeField, Range(0f, 100f)] private float maxHp;   // 최대 체력
    public float MaxHp => maxHp;

    [SerializeField, Range(0f, 100f)] private float speed;   // 현재 이동 속도
    public float Speed => speed;

    private float decreaseHPRatio; // 체력 자연 감소 비율

    // 무적 상태 관련 변수
    private float invincibilityTime;          // 무적 지속 시간
    private float invincibilityDurationTime;  // 무적 경과 시간
    [SerializeField] private bool isInvincibility; // 무적 여부

    private void Awake()
    {
        // 초기값 설정
        decreaseHPRatio = 1f;      // 초당 체력 감소량
        invincibilityTime = 3f;    // 무적 지속 시간
        invincibilityDurationTime = 0f;
        isInvincibility = false;

        // 컴포넌트 가져오기
        player = GetComponent<PlayerController>();
        animationHandler = GetComponent<AnimationHandler>();
    }

    private void Update()
    {
        // 체력 자연 감소
        hp -= decreaseHPRatio * Time.deltaTime;

        // 체력이 0 이하가 되면 게임 오버 처리
        if (hp <= 0)
        {
            GameManager.Instance.GameOver();
            return;
        }

        // 무적 상태 시간 확인
        if (isInvincibility)
        {
            invincibilityDurationTime += Time.deltaTime;
            if (invincibilityDurationTime >= invincibilityTime)
            {
                isInvincibility = false;
                invincibilityDurationTime = 0f;
            }
        }
    }

    /// <summary>
    /// 체력을 변경하는 함수
    /// 양수 값이면 회복, 음수 값이면 데미지 처리
    /// </summary>
    /// <param name="figure">변경할 체력 값</param>
    public void ChangeHP(float figure)
    {
        if (figure > 0f)
        {
            Heal(figure);
        }
        else
        {
            Damage(figure);
        }
    }

    /// <summary>
    /// 체력을 회복하는 함수
    /// </summary>
    /// <param name="figure">회복할 체력량</param>
    private void Heal(float figure)
    {
        hp += figure;
        hp = hp >= maxHp ? maxHp : hp; // 최대 체력을 초과하지 않도록 설정
    }

    /// <summary>
    /// 체력 감소(데미지 처리) 함수
    /// 무적 상태가 아닐 경우만 적용됨
    /// </summary>
    /// <param name="figure">감소할 체력량 (음수 값)</param>
    private void Damage(float figure)
    {
        if (!isInvincibility)
        {
            animationHandler.Damage(); // 피격 애니메이션 재생
            isInvincibility = true;
            hp += figure; // figure가 음수이므로 실제로는 체력이 감소함
        }
    }

    /// <summary>
    /// 속도를 변경하는 함수
    /// 양수 값이면 부스터 효과 적용
    /// </summary>
    /// <param name="amount">속도 변경 값</param>
    /// <param name="duration">지속 시간 (초)</param>
    public void ChangeSpeed(int amount, int duration)
    {
        if (amount > 0)
        {
            Booster(amount, duration);
        }
    }

    /// <summary>
    /// 부스터 효과 적용 (일정 시간 동안 속도 증가)
    /// </summary>
    /// <param name="amount">추가할 속도 값</param>
    /// <param name="duration">지속 시간 (초)</param>
    public void Booster(int amount, int duration)
    {
        if (amount > 0)
        {
            isInvincibility = true; // 부스터 중에는 무적 상태
            speed += amount; // 속도 증가
            Invoke("ResetSpeed", duration); // 지정된 시간이 지나면 속도 초기화
        }
    }

    /// <summary>
    /// 속도를 기본값(8)으로 초기화하는 함수
    /// </summary>
    public void ResetSpeed()
    {
        speed = 8f;
    }
}

 

기존의 플레이어 스크립트에 포함된 내용이 너무 많아서 가시성이 좋지 않기도 하고 작성하면서 헷갈리는 부분이 자꾸 생겨 StatHandler 스크립트를 추가해서 체력, 속도에 관한 기능을 나누어 작성하였다.

 

AnimationHandler

public class PlayerController : MonoBehaviour
{
    // 플레이어 상태 변수들
    private bool isDead; // 플레이어가 죽었는지 여부
    [SerializeField] private bool isJumping; // 점프 상태 여부
    [SerializeField] private bool isSliding; // 슬라이딩 상태 여부

    // 점프 관련 변수
    [SerializeField] private int jumpForce; // 점프 힘
    [SerializeField] private int jumpCount = 2; // 남은 점프 가능 횟수

    [SerializeField] private float deathY = -10f; // 사망 Y축 좌표
    public float DeathY => deathY;

    // 컴포넌트 및 매니저 참조 변수
    private GameManager gameManager; // 게임 매니저 참조
    private StatHandler statHandler; // 상태 관리 핸들러
    public StatHandler Stat => statHandler;
    public AnimationHandler animationHandler; // 애니메이션 핸들러
    private Rigidbody2D rb; // Rigidbody2D 컴포넌트 참조

    // 이벤트 선언: 체력 변화, 속도 변화, 점수 추가 시 호출
    public event Action<PlayerController, int> OnAddScore;

    private void Awake()
    {
        isDead = false;
        rb = GetComponent<Rigidbody2D>();
        statHandler = GetComponent<StatHandler>();
        animationHandler = GetComponent<AnimationHandler>();
    }

    private void Start()
    {
        if (rb == null)
        {
            Debug.Log("Not Founded Rigidbody");
        }

        isDead = false;
        isJumping = false;
        gameManager = GameManager.Instance;
    }

    /// <summary>
    /// 체력이 0 이하이면 게임 오버 처리
    /// 스페이스바 입력 시 점프 활성화
    /// 왼쪽 Shift 입력 시 슬라이딩 활성화
    /// 일정 높이 이하로 떨어지면 게임 오버 처리
    /// </summary>
    private void Update()
    {
        animationHandler.Move();

        if (!isDead)
        {
            // 점프 입력 감지
            if (Input.GetKeyDown(KeyCode.Space))
            {
                animationHandler.Jump();
                isJumping = true;
            }

            // 슬라이딩 입력 감지
            if (Input.GetKey(KeyCode.LeftShift))
            {
                animationHandler.Slide();
                isSliding = true;
            }
            else if (Input.GetKeyUp(KeyCode.LeftShift))
            {
                animationHandler.Move();
                isSliding = false;
            }
        }

        // 플레이어가 사망 영역(높이 아래로 떨어짐)에 도달하면 게임 오버 처리
        if (transform.position.y < deathY)
        {
            gameManager.GameOver();
        }
    }

    /// <summary>
    /// 전진 이동, 점프, 슬라이딩, 바닥 감지 등의 물리 처리
    /// </summary>
    private void FixedUpdate()
    {
        if (isDead)
            return;

        Move();
        Jump();
        Sliding();
    }

    /// <summary>
    /// 플레이어 이동 처리
    /// 현재 속도를 statHandler.Speed 값으로 설정
    /// </summary>
    public void Move()
    {
        Vector2 velocity = rb.velocity;
        velocity.x = statHandler.Speed;
        rb.velocity = velocity;
    }

    /// <summary>
    /// 플레이어 점프 처리
    /// 남은 점프 횟수가 있을 경우 점프를 수행하고 점프 횟수를 감소
    /// </summary>
    public void Jump()
    {
        if (isJumping)
        {
            if (jumpCount > 0)
            {
                Vector2 vel = rb.velocity + Vector2.up * jumpForce;
                rb.velocity = vel;
                --jumpCount;
                isJumping = false;
            }
        }
    }

    /// <summary>
    /// 플레이어 슬라이딩 처리
    /// 슬라이딩 중이면 90도로 회전, 그렇지 않으면 원래 상태 유지
    /// </summary>
    public void Sliding()
    {
        if (isSliding)
        {
            transform.rotation = Quaternion.Euler(0, 0, 90);
        }
        else
        {
            transform.rotation = Quaternion.Euler(0, 0, 0);
        }
    }

    /// <summary>
    /// 트리거 충돌 발생 시 상호작용 가능한 오브젝트와의 상호작용 처리
    /// </summary>
    /// <param name="collision">충돌한 콜라이더</param>
    private void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.tag.Equals("Interactable"))
        {
            InteractObject inter = collision.GetComponent<InteractObject>();
            if (inter == null)
                return;
            inter.OnInteraction(statHandler);
        }
    }

    /// <summary>
    /// 지면과의 충돌 감지하여 점프 횟수 초기화
    /// </summary>
    /// <param name="collision">충돌한 오브젝트 정보</param>
    private void OnCollisionEnter2D(Collision2D collision)
    {
        if (collision.gameObject.CompareTag("Ground"))
        {
            jumpCount = 2;
        }
    }

    /// <summary>
    /// 점수 추가 이벤트 호출
    /// </summary>
    /// <param name="amount">추가할 점수</param>
    public void AddScore(int amount = 1)
    {
        OnAddScore?.Invoke(this, amount);
    }
}

사전에 제작해둔 애니메이션을 애니메이터에서 파라미터를 통해 연결하기 위해 작성한 AnimationHandler 스크립트이다. 애니메이션에는 움직임, 점프, 슬라이딩, 피격, 무적 등이 있고 Bool형 파라미터로 관리하였다.

이렇게 해서 현재까지 2단 점프

 

 

▶▷ 알게된 점

OnValidate() 메소드는 컴파일 시점에 작동

 

▶▷ 트러블 슈팅

막혔던 부분

1. StatHandler 스크립트를 추가하면서 여기저기에 퍼져있는 참조 부분에 오류들이 생기는 현상

2. 두번째 점프를 할 때 전진을 잠시 멈추는 현상

3. 첫번째 점프 후 바로 두번째 점프를 하면 정상적으로 점프하는 것처럼 보이지만 사이에 딜레이를 주고 누르면 두번째 점프가 힘을 받지 못하는 현상

 

시도한 점

1-1. 유니티 에디터 하단에 나오는 오류 메시지를 통해 문제가 되는 스크립트 확인

1-2. 우클릭 메뉴에서 모든 참조 찾기를 눌러 참조중인 모든 부분에 이동하여 확인

2-1. 점프 로직 수정

3-1.  캐릭터가 받는 힘을 점프 직전에 0으로 변경

 

해결

1. 오류가 생긴 모든 부분으로 이동해서 각 요소들이 Playercontroller를 참조하는지 StatHandler를 참조하는지 구분하여 수정

2. 점프 시 전진할 때 받는 힘 추가

3. Vector2 velocity = rb.velocity * 0; 와  rb.velocity = velocity;를 점프 힘을 받기 직전에 추가