Frog on Lotus 개발 기록 (2)


4일차

다음날 맑은 정신으로 다시 생각해보니 원인은 우스울 정도로 간단했다.
혀에 OnTriggerEnter2D()이 들어가 있는게 아니라,
개구리에 OnTriggerEnter2D()가 들어가 있었다.

원인을 알게되니 금방 해결했다.

혀가 파리에 닿으면 없어지는 것까지 구현 완료했다.

기획 고민

방향성을 잠깐 고민해보았다.
처음엔 스테이지 형식으로 하려고 했는데,
협력사 이력서 제출 마감 기한이 얼마 남지 않아서 기획에 쏟을 시간이 없었다.
그래서 제한 시간 안에 최대한 점수를 많이 내는 식으로 게임의 구조를 변경했다.



5일차


게임 매니저 만들기

게임 매니저를 만들어야 할 때가 왔다.
나는 지금껏 게임 매니저는 싱글톤 패턴으로 만들어야 한다고만 알고 있었는데,
문득 의문이 들었다. 왜 써야되지?

싱글톤 패턴에 대해 찾아보니 대략 이런 느낌이었다.
싱글톤 패턴이란? : 유일성 보장, 전역 접근
쓰는 이유? : 씬 전환 시 데이터 보존, 유일성 보장, 객체지향 설계 제공

내 게임은 딱히 ‘싱글톤 패턴’을 쓸 이유가 없어 보였다.
씬 하나에서만 모든 절차가 이루어졌고,
프로젝트의 크기도 작아서 유일성을 보장할만큼 복잡하지도 않았고,
협업자도 없어서 사고를 미연에 방지할 필요도 없었다.
객체 지향 설계도 굳이?

나중에 경험이 많아지면 이 판단을 어떻게 생각하게 될진 모르겠지만,
일단 내가 갖고 있는 근거대로면 굳이 유일성을 보장할 이유가 없었다.
그래서 static으로 전역 접근만 구현하고, 문제가 되면 그때 유일성을 보장해주기로 했다.

public class GameManager : MonoBehaviour
{
    public static GameManager Instance; // 전역 접근용 정적 변수
    public int Score;
    [SerializeField] private GameObject InGameManager;
    private InGameLogic inGameLogic;
    [SerializeField] private GameObject EndGameManager;
    private EndGameLogic endGameLogic;

    private void Awake()
    {
        Instance = this; // 현재 인스턴스를 정적 변수에 할당
        Score = 0;
        GameStart();
    }

    public void GameStart()
    {
        Debug.Log("GameManager :: Game Start!");
        Score = 0;
        Instantiate(InGameManager);
    }

    public void AddScore()
    {
        Score += 1;
    }

    public void GameEnd()
    {
        Debug.Log("GameManager :: Game End!");
        Instantiate(EndGameManager);
    }
}

그래서 초라하지만 게임 매니저를 만들었다.
게임 상태를 변경하기 위해 GameManager의 메서드들을 호출하면
할당돼있는 InGameManager나 EndGameManager들을 사용하게 되는 구조로 만들었다.

GameStart()가 호출되면 InGameManager가 생성되어 Update문에서 게임 로직을 돌리고,
GameEnd()가 호출되면 EndGameManager가 생성되어 게임 종료 화면을 보여준다.

기본적인 게임 사이클을 구현 완료했다.

공격하지 않아도 파리가 와서 죽는 버그

분명 공격이 끝나서 콜라이더의 크기가 0이 됐는데도
파리가 개구리 위치로 오면 죽는 버그가 발생했다.

https://discussions.unity.com/t/why-does-a-collider-with-0-scale-still-take-collisions/912856/2
검색했더니 바로 답이 나오긴 했다.
뭔가 해결책이 있는 건 아니고 원래 그렇다는 것.
(더 파고들자면 유니티 물리 엔진의 근원인 Nvidia의 PhysX가 그렇게 설계돼있으니까)

공격이 끝나면 크기를 0으로 되돌리는게 아니라 비활성화 시켜서 간단하게 해결했다.

파리 생성하기

private Vector2 GetRandomSpawnPosition()
{
    // 뚫린 영역을 제외한 네 구역 정의
    Vector2[] corners = new Vector2[4]
    {
    new Vector2(Random.Range(outerBounds.min.x, holeBounds.min.x), Random.Range(outerBounds.min.y, outerBounds.max.y)), // 좌측
    new Vector2(Random.Range(holeBounds.max.x, outerBounds.max.x), Random.Range(outerBounds.min.y, outerBounds.max.y)), // 우측
    new Vector2(Random.Range(holeBounds.min.x, holeBounds.max.x), Random.Range(outerBounds.min.y, holeBounds.min.y)), // 아래
    new Vector2(Random.Range(holeBounds.min.x, holeBounds.max.x), Random.Range(holeBounds.max.y, outerBounds.max.y)) // 위
    };

    // 랜덤하게 하나의 구역에서 위치 선택
    return corners[Random.Range(0, corners.Length)];
}

스폰 영역은 안쪽 영역과 바깥쪽 영역의 경계를 이용하여
상하좌우 외곽 영역 중 랜덤한 위치를 지정했다.

alt text

이때 스폰 영역(빨간색 영역)에서 파리가 생성되게 하려면,
파리를 생성하는 InGameManager는 스폰 영역을 계산하기 위해
안쪽과 바깥쪽 영역을 변수에 갖고 있어야 했다.

씬에 스폰 영역이 있긴 했지만 InGameManager 오브젝트는 프리팹이었기에
[SerializeField]를 이용해도 할당할 수 없었다.

  • 좌표들을 하드코딩
  • Find 사용
  • 영역들에 ‘Area’라는 Tag를 지정하고 Tag로 찾는다.

이런 선택지들을 떠올려봤지만,

alt text

생각해보니 프리팹 안에 안쪽과 바깥쪽 영역을 자식으로 넣어버리고,
프리팹 안에서 [SerializeField] 변수에 할당하면 초기화를 할 수 있었다.

난이도에 따른 파리 숫자와 속도 조절

AddScore()이 호출될 때 Balancing()이 함께 호출되고,
시그모이드 함수를 기반으로 한 난이도 함수에 따라 파리를 생성하도록 만들었다. 그랬더니

파리가 중앙으로 오질 않았다.

짰던 난이도 관련 코드를 git에서 전부 discard하고
이동 로직이 잘못됐던 건지 테스트해봤는데, 정상적으로 중앙에 돌아왔다.

이쯤되니 굳이 시그모이드를 사용해야 할지 의문이 들어서
난이도 별 속도는 로그 함수,
난이도 별 파리 수는 upper limit과 lower limit이 있는 선형 함수로 결정했다.

그리고 다시 구현을 해보다가 왜 중앙으로 가지 않았던 것인지 이유를 찾아냈다.

// 중심에 대해 원운동 시키기
fall_v = (Center - transform.position).normalized;
moving_v = new Vector3(-fall_v.y, fall_v.x) * speed;
fall_v *= grav;
transform.position += (moving_v + fall_v) * Time.deltaTime;

fall_v는 중심을 가리키는 단위 벡터이다.
moving_v는 fall_v의 직교 벡터에 speed를 곱한 값이다.

moving_v에 계수로 speed를 곱해주듯이,
fall_v에도 grav를 곱해주었다.

speed와 grav의 비율이 1:2가 되면 파리가 적절하게 원 운동을 해줬다.

alt text

여기서 내 실수가 발생했다.

grav는 speed의 2배“돼야” 하는거라
원래는 grav = speed * 2를 했어야 했는데,
나는 grav *= 2를 해놓고 왜 안되지? 이러고 있었던 것.

해당 문제를 해결하여 난이도별 속도와 파리 수 조절 구현을 완료했다.

폴리싱 작업

그 밖에

  • 첫 파이가 스폰 영역 할당 못 받아서 (0,0)에서 스폰되는 버그
  • PlayerPrefs를 이용한 하이 스코어 기능
  • 게임 시작 press any key 애니메이션 효과
  • 신기록 달성시 글자 애니메이션 효과
  • 개구리와 연잎이 위아래로 둥실거리는 효과
  • UI canvas를 이용하여 배경 어둡게
  • 배경음과 공격 효과음
  • 마우스 커서 변경

등 여러 폴리싱 작업을 진행했다.

최종 결과물



6일차

alt text

webgl 빌드를 뽑아 itch.io에 올려보니 UI 요소들이 전부 작아져 있었다.
UI들을 전부 ‘Scale With Screen Size’로 바꾸고
Reference Resolution은 x: 1920, y: 1080으로 바꾸니까 해결됐다.

alt text

https://lazyartisan.itch.io/frog-on-lotus

그리고 itch.io에 출시까지 완료했다.



느낀 점

출시 후 인디 게임 개발 갤러리에도 홍보하고,
지인들에게도 게임을 보여줬다.
다들 좋게 평가해줬다.

내가 만든 게임을 다른 사람이 몇 점 나왔다고 스샷을 올려주는 걸 보니 너무나도 재밌었다.
게임 개발자로 살고 싶다는 동기 부여가 더욱 강력해졌다.