▶▷ PenguinRun
쿠x런과 비슷한 게임을 만들 계획을 세웠고 위 이미지들은 대략적인 배치 구도와 클래스 설계 와이어 프레임이다.
클래스 설계에는 각 클래스에 어떤 변수와 메소드가 들어갈지 미리 작성해서 이것을 토대로 스켈레톤 코드를 작성한 다음 각자의 구현을 시작하였다. 스켈레톤 코드를 미리 작성하고 시작하니 확실히 나의 할 일이 눈에 잘 들어왔고 머리에 그릴 수 있었다.
내가 맡은 부분은 PlayerController 클래스이다. 여기서는 플레이어의 이동에 관한 부분들을 다루고 각종 이벤트와 메서드를 포함한다.
using System;
using System.Collections;
using UnityEngine;
public class PlayerController : MonoBehaviour
{
// 플레이어 상태 변수들
private bool isDead;
[SerializeField] private bool isJumping;
[SerializeField] private bool isSliding;
[SerializeField] private bool isInvincibility;
// 점프 관련 변수
[SerializeField] private int jumpForce;
[SerializeField] private int jumpCount = 2;
[SerializeField] private int score;
// 체력 관련 변수 및 프로퍼티
[SerializeField] private int hp = 10;
public int Hp => hp;
private int maxHp = 40;
public int MaxHp => maxHp;
// 이동 속도 및 사망 Y 좌표 관련 변수와 프로퍼티
[SerializeField] private float speed = 8f;
public float Speed => speed;
[SerializeField] private float deathY = -10f;
public float DeathY => deathY;
// 속도 재설정 코루틴 참조 변수
private Coroutine resetSpeed;
// 컴포넌트 및 매니저 참조 변수
private AnimationHandler animationHandler;
private GameManager gameManager;
private BoxCollider2D col;
private Rigidbody2D rb;
// 이벤트 선언: 체력 변화, 속도 변화, 점수 추가시 호출
public event Action<PlayerController, int> OnChangeHp;
public event Action<PlayerController, float> OnChangeSpeed;
public event Action<PlayerController, int> OnAddScore;
private void Awake()
{
rb = GetComponent<Rigidbody2D>();
col = GetComponent<BoxCollider2D>();
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()
{
if (hp <= 0)
{
isDead = true;
gameManager.GameOver();
return;
}
if (!isDead)
{
// 점프 입력 감지
if (Input.GetKeyDown(KeyCode.Space))
{
isJumping = true;
}
// 슬라이딩 입력 감지
if (Input.GetKey(KeyCode.LeftShift))
{
isSliding = true;
}
else if (Input.GetKeyUp(KeyCode.LeftShift))
{
isSliding = false;
}
}
// 플레이어가 사망 영역(높이 아래로 떨어짐)에 도달하면 게임 오버 처리
if (transform.position.y < deathY)
{
gameManager.GameOver();
}
}
/// <summary>
/// 전진 이동, 점프, 슬라이딩, 바닥 감지 등의 물리 처리
/// </summary>
private void FixedUpdate()
{
if (isDead) return;
// 전진: x축 속도를 현재 speed 값으로 설정
Vector3 velocity = rb.velocity;
velocity.x = speed;
rb.velocity = velocity;
// 점프 처리: 점프 중이면 jumpForce 만큼 위로 속도 적용, 남은 점프 횟수 감소
if (isJumping)
{
if (jumpCount >= 0)
{
rb.velocity = Vector3.up * jumpForce;
jumpCount--;
}
}
// 슬라이딩 처리: 슬라이딩 중이면 회전 (90도), 그렇지 않으면 초기 회전값 (0도)
if (isSliding)
{
transform.rotation = Quaternion.Euler(0, 0, 90);
}
else
{
transform.rotation = Quaternion.Euler(0, 0, 0);
}
// 바닥 감지: 아래 방향으로 레이캐스트를 쏴서 바닥("Ground" 레이어)과의 충돌 확인
Debug.DrawRay(rb.position, Vector3.down * 2.5f, Color.green);
RaycastHit2D rayHit = Physics2D.Raycast(rb.position, Vector3.down, 2.5f, LayerMask.GetMask("Ground"));
if (rayHit.collider != null)
{
isJumping = false;
jumpCount = 2;
}
}
/// <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(this);
}
}
/// <summary>
/// 체력 변경 (amount 양에 따라 회복 또는 데미지 적용)
/// amount가 양수이면 Heal, 음수이면 Damage 처리 후 이벤트 발생
/// </summary>
/// <param name="amount">체력 변화량</param>
public void ChangeHP(int amount)
{
if (amount >= 0)
{
Heal(amount);
}
else
{
Damage(-amount);
}
// 체력 변화 이벤트 호출 (양수, 음수에 관계없이 절대값 전달)
if (amount >= 0)
{
OnChangeHp?.Invoke(this, amount);
}
else
{
OnChangeHp?.Invoke(this, -amount);
}
}
/// <summary>
/// 체력 회복
/// </summary>
/// <param name="amount">회복량</param>
private void Heal(float amount)
{
// 체력 회복 로직
}
/// <summary>
/// 데미지 수치가 0보다 작으면 무적 상태 활성화 및 속도 변경
/// </summary>
/// <param name="amount">데미지 수치</param>
public void Damage(int amount)
{
if (amount < 0)
{
isInvincibility = true;
ChangeSpeed(amount);
}
}
/// <summary>
/// 속도 변경
/// 무적 상태일 경우 속도를 일시적으로 낮추고, InvincibilityEnd를 호출하여 무적 해제
/// 속도 변경 이벤트 발생 후, 일정 시간 후 속도를 초기화하는 코루틴 시작
/// </summary>
/// <param name="amount">속도 변화량</param>
public void ChangeSpeed(int amount)
{
if (isInvincibility)
{
speed = 2f;
Invoke("InvincibilityEnd", 0.5f);
}
speed += amount;
OnChangeSpeed?.Invoke(this, speed);
// 기존에 실행 중인 속도 재설정 코루틴이 있다면 중지
if (resetSpeed != null)
{
StopCoroutine(resetSpeed);
}
// 일정 시간 후 속도를 초기화하는 코루틴 시작
StartCoroutine(ResetSpeed(3f));
}
/// <summary>
/// 무적 상태 종료 및 속도 초기화
/// </summary>
public void InvincibilityEnd()
{
isInvincibility = false;
speed = 8f;
}
/// <summary>
/// 일정 시간 후 속도를 기본값(8f)으로 재설정하는 코루틴
/// </summary>
/// <param name="duration">지속 시간</param>
/// <returns></returns>
private IEnumerator ResetSpeed(float duration)
{
yield return new WaitForSeconds(duration);
speed = 8f;
OnChangeSpeed.Invoke(this, speed);
}
/// <summary>
/// 점수 추가 이벤트 호출
/// </summary>
/// <param name="amount">추가할 점수</param>
public void AddScore(int amount)
{
OnAddScore?.Invoke(this, amount);
}
}
플레이어는 기본적으로 전진하는 상태이고 더블 점프와 슬라이딩을 사용해서 장애물들을 피하는 방식이다.
이동 로직은 velocity와 같은 물리적인 연산은 FixedUpdate에서 처리하였고 키보드 입력 부분은 Update문에 작성하였다. 이렇게 나눈 이유는 FixedUpdate에서 움직임을 관리하는 것이 더 자연스럽게 보이고 키보드 입력은 Update문에서 해야 인식이 잘 되기 때문이다.
레이캐스트로 바닥을 감지하여 점프를 했다면 착지를 해야만 점프 카운트가 2로 채워지고 이 점프 카운트가 남아있을 때는 점프를 할 수 있다. (2단 점프) 하지만 현재 두번째 점프를 할 때 잠깐 전진이 멈추는 현상이 있어 수정할 예정이다.
슬라이딩은 현재는 오브젝트를 90도 회전시키는 방법으로 구현했는데 회전하는 순간 아래쪽에 공간이 생겨 바닥에 닿을 때까지 약간의 공백이 생겨 아마 내일 방법을 바꿀 것 같다. 생각중인 방법 중 하나는 상, 하체 콜라이더를 나누어 슬라이딩을 할 때는 상체의 콜라이더만 비활성화 하고 슬라이딩 하는 모션을 넣는 것이다.
OnTriggerEnter2D()로 아이템과 충돌 시 상호작용을 하도록 했고 ChangeHP(), ChangeSpeed() 등으로 체력과 속도를 조절하였다.
자세한 코드의 내용은 주석으로 모두 정리하였다.
▶▷ 트러블 슈팅
문제점
1. 2단 점프가 되지 않는 현상
2. rb.velocity = velocity; 이 부분의 유무에 따라 플레이어 이동과 점프가 둘 중 하나만 작동하는 현상
시도
1-1. isGround를 추가하여 true일 시 점프 초기화 시도
1-2. 콜라이더간 충돌 시 착지 상태 감지 및 점프 초기화 시도
2-1. 문제되는 부분 삭제 시도
2-2. 이동과 점프 메서드를 각각 만들어 관리 시도
해결
1. 타일맵에 "Ground" 레이어 설정 후 레이캐스트로 탐지하여 탐지 시 점프 카운트 초기화
2. 점프 연산 아래에 작성했던 rb.velocity = velocity; 의 위치를 전진 연산 아래로 수정 (점프 연산 아래에 작성 시 y축 점프 값이 덮어 쓰여져서 생긴 오류)
▶▷ 알게된 점
타일맵의 윗부분만 사용하는 방법
Platform Effector2D 컴포넌트 추가 => Composite Collider2D에서 Used By Effector 체크 => Platforn Effecter 컴포넌트 추가 => Surface Arc를 180보다 낮게 설정해서 측면 부분은 충돌하지 않도록 설정
(타일맵 아랫줄이 있다면 Use One Way Grouping 추가 체크)
'게임 개발 공부 기록' 카테고리의 다른 글
25일차 - PenguinRun 팀 프로젝트 3 (0) | 2025.02.26 |
---|---|
24일차 - PenguinRun 팀 프로젝트 2 (0) | 2025.02.25 |
22일차 - 디버깅, 단축키, 객체 지향 (0) | 2025.02.21 |
21일차 - SimpleMetaverse 트러블 슈팅 (0) | 2025.02.20 |
20일차 - SimpleMetaverse 2 (0) | 2025.02.19 |