Falling Foods 개발일지5

SMN·2025년 4월 29일

Falling Foods

목록 보기
6/8
post-thumbnail

1. PlayerRotation (플레이어 방향)

2. TimeManager (레벨 디자인)

3. SceneryManager (씬 관리)

이번에 개발할 것은 캐릭터가 움직이는 방향에 따라 캐릭터의 시선방향을 변경할 것이다.

캐릭터의 회전을 담당하는 스크립트를 만들어서 플레이어의 입력에따라 캐릭터의 회전값을 변화시키는 코드를 작성할 것이다.

PlayerRotation 스크립트 일부분

일단은 실행되는 것을 목표로 하기에 이 스크립트에서 if - else if문으로 입력을 받았다.
playingCoroutine과 Coroutine함수에 관해서는 나중에 알아보고 A,D,W,S에 따라 Quaternion.Euler로 바라볼 rotation값을 정해준뒤 Rotate라는 코루틴 함수의 매개변수로 전달한다.

PlayerRotation 스크립트 Rotate함수부분

Rotate코루틴 함수는 Quaternion형으로 최종적으로 바라볼 방향을 받아오고
while문을 이용하여 자연스럽게 해당 방향을 바라보도록 작성하였다.

while문 안쪽의 코드를 살펴보자면,

transform.rotation값을 Quaternion.Lerp를 사용하여 정해주는데
Lerp의 시작점으로 Quaternion.Euler를 사용하여 현재 회전값을 eulerAngles.y를 통해 사용하고
Lerp의 도착점으로 end (아까 받은 최종방향)을 잡고,
속도 (비율)은 대충 0.3f로 잡아주었다.

매 프레임마다 현재 방향에서 도착 방향까지의 특정 비율만큼 움직인다.

Quaternion.Lerp와 Quaternion.Euler이 조금 어려웠다..

이번엔 while 조건문에 대해 살펴보면,

도착방향 y값의 절댓값 - 현재방향 y값의 절댓값이 0.1보다 클때와,
도착방향 y값의 절댓값 - 현재방향 y값의 절댓값이 -0.1보다 작을때,
while문을 반복하게 되는데, 이 조건문의 의미는
현재 회전값.y의 크기가 도착회전값.y보다 클 때와 작을 때를 판단하여,
클 때는 0.1차이가 날 때까지 회전값을 줄이고,
작을 때는 0.1차이가 날 때까지 회전값을 늘리게끔 되어있다.

시각화 하면 이런식이다.

도착방향 - 현재방향이 양수일때와 음수일때로 구분되어 처리된다.

다시 입력 관련 코드를 살펴보면

위 코드에선 A를 눌렸을 때, if문을 지나서, playingCoroutine이라는 변수에 Coroutine을 넣으며, 함수를 호출한다.
만약 함수가 실행중일떄 입력을 받게되면 if문에 걸리며, 실행중인 Coroutine을 정지하고 다시 코루틴을 실행되게끔 한다.

playingCoroutine에 실행시킬 Coroutine을 넣으면서 호출시키면, 현재 실행중인 Coroutine을 확인할 수 있기에 이러한 방식을 사용했다.

실제로 이런식으로 작동하게된다.

하지만 이렇게 대충 작성만 하게되면, 전에 만들었던 시스템인, '게임오버'를 했을 때, 캐릭터의 회전값이 돌아갈수 있는 것을 볼수 있다

게임오버가 되어도 캐릭터가 회전하는 모습.

때문에, PlayerInput에서 입력을 받아 게임오버가 되면 입력을 받지 못해 회전하지 못하도록 하겠다.

PlayerInput 스크립트.

Quaternion형의 프로퍼티를 만들고,
OnRotate라는 함수를 만들어 플레이어의 입력에 따라 rotate의 값이 바뀐다.

PlayerRotation 스크립트.

PlayerInput에서 Quatertion을 가져와 함수를 호출하게끔 되어있다.
(코드가 매우 간결해졌다.)

PlayerInput에서 입력을 받기에 게임오버가 되면 더이상 회전할수 없게끔 되어있다.

(살짝 아쉬운 점은 두개의 키를 동시에 눌렀을때, 대각선을 바라보지 않는다는점..?)

마지막으로 체력 UI또한 캐릭터 회전값에 영향을 받는 것을 막기 위해 따로 UI에 스크립트를 부착할 것이다.

HpbarRotation 스크립트.

해당 스크립트는 체력 UI에 넣어줄 스크립트로 시작할때의 방향값을 계속해서 유지하게끔 작성하였다.

캐릭터 방향이 변할 때마다 찔끔씩 움직였다가 돌아오는데,

이건 왜 이런지 잘 모르겠어서 이정도로 만족한다.

이번에는 플레이타임이 길어질수록 천천히 어려워지게끔 하는 레벨디자인에 대해 만들어 볼 것이다.

우선 난이도를 점점 올릴 요소들은 ( - )

'음식이 떨어질 속도', '감소하는 Hp의 양', 'hp감소 빈도' 정도가 존재하고,

그리고 난이도를 적절히 맞춰주며, 게임의 재미를 더해줄 요소로는 ( + )

'음식이 떨어질 빈도', '캐릭터의 이동속도' 등이 존재한다.

TimeManager 스크립트.

먼저 TimeManager에 프로퍼티를 선언하여,
TimeManger안에서 값을 수정하고 밖에서 읽을 수 있게끔 하였다.
초깃값은 현재 난이도 정보를 넣어주었다.

TimeManger 스크립트.

먼저 이 플레이되는 씬이 시작될 때 부터 플레이시간을 판단해야 하기 때문에,
씬이 시작될 때 실행될 함수를 만들고, OnEnable과 OnDisable에
씬이 실행될때 브로드캐스팅을 하는 sceneLoaded 이벤트 함수에 넣어주었다.

이런식으로 OnSceneLoaded를 통해 씬이 실행될 때, 코루틴 함수를 호출시키고 코루틴 함수는 일정 시간이 지났을 때, 변수의 값을 증감시키는 방식으로 설계되어있다.

기본적으로 증감계수를 이렇게 잡아주고, 테스트를 하면서 적절하게 값을 잡는다.

그리고 실시간으로 변화가 필요한 변수의 스크립트에서 TimeManager에서 변수를 가져와 사용한다.

빈게임 오브젝트에 TimeManager를 넣어준 이후 실행을 해보면

실행을 해보았을때,

음식의 떨어지는 속도가 순식간에 너무 빨라져서 먹기가 힘들다던가, 음식이 떨어지는 빈도가 낮아서 재미가 없는 것 같다. 그렇기에 계속해서 테스트를 해가며 플레이어의 흥미를 유발할 수 있는 레벨디자인을 만든다.

FoodSpawner 스크립트

먼저 플레이어를 기준으로 나오는 음식의 범위를 좁혀서 플레이어가 더 많이 먹을 수 있도록 하였다.

이런식으로 초깃값과 증감변수를 잡아주었다.

이렇게 수정한 결과 대략적으로 최대 2분 정도의 플레이 타임을 가졌다.

그리고 값이 비이상적으로 변화하면 플레이의 지장을 줄수있는 변수의 한계값을 프로퍼티를 통해 잡아주었다.

음식이 떨어지는 빈도를 너무 낮추면 게임이 음식에 가득차게되고,
HP감소시간을 너무 줄이면 게임이 너무 어려워 지기에 둘다 한계값을 지니게 해주었다.
다른 변수들도 값이 크게 변화하면 정상적인 플레이의 지장을 주지만, 정상적인 게임플레이 시간에는 나타나지 않기때문에 따로 한계값을 지정해주진 않았다.

근데 이상하게도 한계값으로 값이 되지 않는다. if문안에 Debug.Log를 통해 출력을 확인해보아도, 출력이 되지않는다.

그래서 열심히 찾아보니, Coroutine안에 변수의 값을 지정해줄 때, 프로퍼티를 통해 값을 할당하지 않고, 변수에 직접적으로 할당을 하니 set함수를 거쳐 저장되지 않았던 것이였다.

(foodFallTime와 decreaseHpTime을 프로퍼티를 통해 할당해주었다.)

이렇게 작성하였다. 이제 프로퍼티의 set을 작성하게 되면 변수로 할당하지 않고 프로퍼티를 통해 값을 할당해야 한다는 것을 알게된 것 같다.

foodFallTime이 0.05, decreaseHpTime이 0.2가 이하가 되면 한계값으로 값이 된다는 것을 볼 수 있다.

그리고 이렇게 한계값을 정해주니, 생각보다 플레이 타임이 길어지기에 음식의 떨어지는 속도와 캐릭터의 속도의 한계값도 정해주었다.

foodFallSpeed의 한계값으로 10을, playerSpeed의 한계값으로는 15의 값을 가지게 하였다.

이렇게 TimeManager의 코드를 작성하였다.

이렇게 레벨디자인을 하고 난 뒤,

플레이타임 1분30초 이후부터는 값의 변경점은 없었고, 실력만 좋다면 계속해서 살아남을 수 있었다.

이번엔 씬 관리를 해보겠다.

SceneryManager 스크립트는 게임시작 버튼을 통해서 게임메뉴화면에서 게임 실행 화면으로 전환하게끔 되야한다. 또한, 페이드 인-아웃 효과와 각각의 음식이 어떤 효과가 있는지도 알려주는 창에 들어갈 수 있는 코드도 작성 할 것이다.

(결론적으로 여기서는 씬 전환만 작성하였다.)

먼저 MainScene이라는 씬을 만들어 주고 이 씬을 꾸며준다.

기본적으로 게임을 시작하는 Start버튼과, 떨어지는 음식의 종류를 보여주는 Foods 버튼 그리고 게임을 나갈수 있는 Quit 버튼을 만들어 줄 것이다.

먼저 이렇게 버튼을 구성하고, SceneryManager라는 스크립트와 그 안에 씬을 호출할 함수를 미리 만들어 버튼을 클릭 했을때, 해당 씬으로 넘어간다. (Start만 씬이 존재하다..)

이후 빌드 세팅에서 사용할 씬을 모두 넣어준 다음 Start 버튼을 눌렀을 때, 씬이 이동되도록 코드를 작성한다.

제목과 이미지를 부착한 모습이다.

button같은경우 Panel에 Vertical Layout Group과 Content size Fitter를 통해 버튼의 규칙적인 간격을 잡아주었으며, 이미지의 경우 ChatGPT를 이용하여 가져왔다.

SceneryManager에서 Start버튼을 눌렀을 때 실행시킬 함수이다.

이렇게 작성하고 Start 버튼을 누르게 되면 맵의 Direction Light가 제대로 작동하지 않은채로 실행된다.

화면이 뭔가 어두운 것을 볼 수 있다.

아마 이러한 이유는 맵의 로딩이 전부 되지 않고 씬을 불러오기에 연산 시간이 부족한 것이다.

때문에 이번에는 로딩 시스템을 만들어 볼 것이다.

SceneryManager의 AsyncLoad함수이다.

정확히 비동기 씬 이동 시스템인데 간단하게 검은 화면이나, 로딩 바같은 건 생략하고 씬이 준비되었을때 이동하는 함수만 작성해 주었다.

AsyncOperation asyncOperation = SceneManager.LoadSceneAsync(index);

이 코드는 이동할 씬을 백그라운드에서 로딩시작하고, 변수에 담는다.

asyncOperation.allowSceneActivation = false;

이 코드는 장면이 준비된 즉시 장면을 활성화하는 것을 허용하는 변수는 꺼둔건데,
내가 작성한 코드처럼 로딩화면이나, 로딩 바를 사용하지 않는다면 작성하지 않아도 될 것이다.

while(asyncOperation.isDone == false)

이 코드는 해당 동작이 준비 되었는지를 반환하는데,
완료가 되면 true를 반환하는 변수로 while 조건문을 사용했다.

if(asyncOperation.progress >= 0.9f)

마지막으로 이 코드는 작업의 진행정도를 0 ~ 1(float)로 나타내는 코드는 0.9이상일때로 조건문을 사용했는데, 왜 0.9이상일때 씬을 불러오냐면,
0.9가 되었을 때, 씬의 거의 모든 것을 불러오고 이제 씬을 불러오면 되는 진행정도를 가지게 된다.
그리고 씬이 넘어갔을때, 1이라는 값을 가지게 됨으로 씬을 불러올때는 0.9이상으로 잡아주었다.

이후 Start버튼을 눌렀을 때, 게임 씬의 인덱스값인 1을 AsyncLoad 코루틴 함수의 매개변수로 호출한다.

이 문제가 아니였다.

맵이 바래지는 현상은 계속 되었다.

그렇게 찾아보다가 이러한 현상이 빛(조명)관련 문제라는 것을 알았고,

창 - 렌더링 - 조명 - 조명 생성 을 통해 맵의 밝기를 유지할 수 있었다.

(모든 맵에다가 조명 생성을 해주었다.)

이제 이렇게 씬을 이동해도 맵이 어둡게 보이는 현상이 사라졌다.

이렇게 메인 화면에서 게임 화면으로 이동할 수 있게 만들었다.

이번에는 게임 화면에서 게임 오버가 되었을 때, 게임을 다시 시작할 수 있게끔 만들 것이다.

이건 쉽다. 그냥 게임 오버가 되었을 때, 다시시작 버튼과, 메인 메뉴 버튼을 둔 이후,
다시시작 버튼을 눌렀을 땐, 게임씬 다시시작, 메인메뉴 버튼을 눌렀을 땐, 메인 씬 시작을 하면 된다.

먼저 메인 씬과 게임 씬에 존재할 수 있도록 싱글톤을 만들어 준다. (SceneryManager)

그전에 ButtonManager라는 스크립트를 만들어 모든 버튼에 부착시키고 이 스크립트를 활용해 볼 것이다.

ButtonManager 스크립트이다

buttonText로 Text형의 자식 오브젝트를 가져오고 textFontSize로 기존의 폰트크기를 가져온다.
여기서의 이 코드의 목적은 버튼에 마우스를 올릴때, 내릴때, 클릭할때 폰트의 크기를 변경시키는 코드이다.

EventTrigger 컴포넌트를 활용하여 각 상황에 맞게 함수가 호출되도록 작성하였다.

살짝 번거로웠던 점은 프리팹을 통해 ButtonManager, EventTrigger,
코드를 통해 자식 오브젝트 가져오기, 폰트크기 가져오기는 한번에 가능했지만,
Event Trigger에 자신의 오브젝트를 모두 하나하나씩 드래그하고 각 실행 조건을 맞춰야 한다는점?

마우스를 올렸을 때, START처럼 글자가 잘리는 현상이 있기 때문에 프리팹에서 Text의 수평 오버플로를 Overflow로 맞춰주었다.

그러면 이렇게 버튼과의 상호작용을 통해 버튼이 움직이는 것을 볼 수 있다.

미리 만들어둔 게임오버 창의 버튼들도 똑같은 방법으로 작동되게 되었다.

GameManager 스크립트에서 위와 같이 변수를 선언해 준다.

Action이벤트 두개를 통해, 게임오버창에서 버튼을 눌렀을 때, 브로드캐스팅 방식으로 함수를 호출한다.
gameOverImage는 버튼 두개를 포함한 게임오버 창을 뜻한다.

Game Over시 게임오버창 활성화, 각각의 버튼을 누를때, 각각에 맞는 함수를 호출한다.

이후 GameScene으로 돌아와 버튼의 클릭 호출 함수에 알맞는 함수를 넣어준후 Canvas를 비활성화 해준다.

아 살짝 큰일이다.

SceneryManager는 MainScene에 있고, GameManager는 GameScene에 있다.
SceneryManager에서 GameManager에 있는 이벤트에 구독하려면 GameManager 또한 MainScene에 있어야 하는데, GameManager 같은 경우 게임 오버창에 있는 버튼 두개의 구독을 받고 있기 때문에,
MainScene으로 이동하게 된다면, 게임 오버창의 버튼 두개를 어떻게 관리 해야하는지 문제이다.

(현재 상황)

근데 GameManager의 경우 SceneryManager처럼 Main화면 부터 있으면 좋기에 버튼을 어떻게 연결할 것인가에 대해 생각해 보았다.

그렇게 생각해 본 결과, ButtonManager를 상속받는 스크립트를 버튼마다 따로 만들어서 사용하는 것이 나에게는 최선이라고 생각하고, 실행에 옮겼다.

ButtonManager를 상속받는 ReStartButton스크립트를 만들고

ReStart버튼에 부착시킨후 버튼 클릭시 GameManager의 함수가 실행되도록 하게끔 만들어 주었다.

이렇게 GameManager에 연결되어있는 곳을 자신의 오브젝트로 대체하여 연결을 끊어주었다.

곰곰히 생각해 봤는데, 이 방법도 뭔가 비효율적인 느낌이다. 그래서 다시 생각해본 방법은,

GameScene에서의 UI를 담당하는 GameSceneUIManager를 만들어서 모든것을 통제하면 어떨까 생각 했다.

다시 돌아가서 ButtonManager를 상속받는 스크립트를 삭제하고, GameSceneUIManager를 만들어주었다.

이렇게 선택한 이유중 하나는 너무 세부적으로 들어감과 동시에 Canvas또한 제어해줄 스크립트가 필요하다는 점에서 이러한 방법을 최종적으로 선택하게 되었다.

다시 두개의 버튼에 ButtonManager를 넣어주고, 특정상황에 맞는 함수를 호출할 수 있게끔 한다.

이후에 GameSceneUIManager 스크립트를 만들고, 빈 오브젝트에 넣어준다.

GameSceneUIManager의 코드는 이렇게 작성하고

각각의 버튼을 Inspector에서 넣어준후 gameOverImage에도 Canvas를 넣어준다.

이후 각각의 버튼의 클릭시 호출되는 함수에 GameSceneUIManager를 넣고 알맞는 함수를 넣어준다.

이후 게임씬에 있는 GameManager를 삭제 시키고 메인씬에 GameManager를 넣어준다.
이후

이후에 GameManager에 있는 Action과 gameOverImage관련 코드를 모두 지워준다.

GameSceneUIManager에서 이제는 GameManager를 거쳐갈 필요없이 SceneryManager로 함수를 호출하면 되기 때문에 GameManager에서의 관련 코드는 모두 지워주는 것이다.

처음과 바뀐게 없는 GameManager 스크립트

GameSceneUIManager를 마저 작성한다.

뭔가 코드간의 진행, 게임의 흐름이 이상하지만 되기만 하면 일단은..? 그만이다...

PlayerHealth 스크립트

Hp가 0 이하로 내려갔을때, GameManager의 게임오버와 GameSceneUIManager의 게임오버 함수를 둘다 실행시킨다....(음...)

이제 드디어 실행을 해보자

게임 오버 창까지는 잘 뜨지만, 버튼을 선택하고 나면 오류가 뜨는 것을 볼 수있다.


이 오류는 캐릭터에 있는 PlayerInput 스크립트가 싱글톤으로 작성되어있기 때문이다.
(결국 한번 발목을 잡는구나)

코드를 짤때, 단순히 쉽게 접근 하려고 싱글톤으로 작성한 것이기 때문에 싱글톤을 풀어주고, 이에 참조하는 스크립트들에도 GetComponent로 접근 하면 해결될 듯 하다.


PlayerInput의 싱글톤을 빼준후,

이를 참조하고 있던 PlayerMove, PlayerRotation의 코드를 수정해준다.

다시 실행해 보면

게임오버 창까지 뜨고 버튼을 눌렀을 때 오류가 뜨지 않으면서, Main씬으로 돌아와지는 것을 볼 수 있지만,

또 문제가 발생하는데 다시 Start버튼을 눌러도 실행이 안된다는 점이다 다른 버튼도 그렇다.
그래서 왜 이런지 찾아보니,

다시 돌아왔을 때의 버튼의 Inspector이다

클릭 시 호출될 오브젝트가 존재하지 않는다. 내 예상으로는 아마 SceneryManager가 여기 있다가 다른씬으로 가고 이때 연결이 끊어진 것으로 보인다.

이건 이미 겪어본 문제이다. 때문에 아까와 똑같은 방법으로 이 문제를 해결 할 것이다.

MainSceneUIManager라는 스크립트를 만들어주고 아까와 같이 버튼 클릭 함수에 이 스크립트가 있는 오브젝트를 넣어주고 알맞는 함수를 실행시켜주면 된다.

(GameSceneUIManager와 같은 방법)

이제 다시 메인씬에서 버튼을 눌렀을 때 실행까지 된다.

화면과 같이 다시 실행했을 때,
게임이 멈춘 이유는 GameManager의 state를 변화시켜주지 않았기 때문이다.

SceneryManager스크립트에서

Game씬으로 갈때 GameManager의 시작 함수를 호출시켜 state를 제어한다.

GameManager의 OnEnable을 제거해, GameManager에서 게임시작을 실행하지 못하도록 한다.

PlayerHealth 스크립트

이때, ReStart버튼을 눌렀을 때,GameManager의 State가 false인채로 실행된 이후, 다시 true로 변환되어 게임이 멈추기 때문에 PlayerHealth 에서 Update로 GameState를 제어하던 코드를 Hp가 닳았을 때만 호출하게끔 Die함수를 만들고 HealthUpdate함수에 넣어 State를 제어해준다.

재시작 후 실행이 안 된 이유

이거 때문에 꽤나 골치아팠다... (앞으로 게임 state을 제어하는 코드는 Update같은 코드에서 하지 말자..)

최종적으로 ReStart를 누르면 다시 게임이 진행되는 것을 볼 수있다.

이렇게 씬 간의 이동을 만들었다.

개발기간 2504/ 20,22,23,26,27,28,29
profile
모든 생각까지

0개의 댓글