3주 차 프로젝트에서는 <고양이 밥 주기> 게임을 제작했다.
[ 기본 씬 구성 ]
카메라 화면 크기 조절
메인 씬 하이어아키에서 메인 카메라를 클릭하고 사이즈를 25로 맞춘다. 카메라에 보이는 화면이 넓어졌고, 더 많은 오브젝트를 생성할 수 있다.
오브젝트에 이미지 적용
오브젝트에 이미지를 적용하는 법은 간단하다. 오브젝트를 클릭하고 스프라이트에 이미지를 드래그 앤 드롭하면 자동으로 적용된다. 위치만 설정해 주면 되고, 크기는 이미지 크기에 맞춰서 자동으로 조절된다.
새로운 씬 생성
씬 폴더 우클릭으로 Create > Scene을 클릭하면 새로운 씬이 생성된다. 게임 시작 전 인트로 씬을 만들기 위해 StartScene으로 이름을 변경한다. MainScene과 동일하게 메인 카메라의 사이즈를 25로 맞춘다.
using UnityEngine.SceneManagement;
public class StartButton : MonoBehaviour
{
public void StartGame()
{
SceneManager.LoadScene("MainScene");
}
}
StartButton 오브젝트를 생성하고, StartButton 스크립트에서 버튼을 클릭하면 메인 페이지로 넘어가는 코드를 작성한다.
그리고 On Click 변수에 StartButton 오브젝트를 할당하고 StartGame 메서드를 선택한다. 스크립트도 드래그 앤 드롭한다.
cf. 게임을 실행하고 버튼을 클릭했는데 위와 같은 에러가 발생했다. File > Build Settings 창에서 추가하고 싶은 씬 화면에서 Add Open Scenes를 클릭하면 씬이 생성된다. 저장을 하고 다시 프로젝트를 실행하면 정상적으로 작동된다.
[ 밥 주기 ]
밥 생성
밥을 표현하기 위해 원 스프라이트를 생성한다. 크기와 색상을 바꿔주고, 스프라이트를 Knob으로 변경한다.
*Knob과 Circle의 차이
밥 발사 및 파괴
void Update()
{
transform.position += Vector3.up * 0.5f;
if (transform.position.y > 27)
{
Destroy(gameObject);
}
}
밥이 위로 날아가게 하기 위해서 Food 스크립트를 생성하고 Food 오브젝트에 드래그 앤 드롭한다. Vector3.up은 (0, 1, 0)의 값을 가지고 있어서 Y축의 값만 변경시킬 수 있다. 이 값을 계속해서 Food 오브젝트의 위치에 더해주면 위로 올라가는 것처럼 보이게 된다. 1이 너무 빠르게 느껴지면 *0.5f를 해서 속도를 늦춘다.
그리고 밥을 반복 생성하기 전에 클론들이 화면 밖으로 벗어나면 파괴될 수 있도록 미리 코드를 작성한다.
밥 반복 생성
public GameObject food;
void Start()
{
InvokeRepeating("MakeFood", 0f, 0.5f);
}
void MakeFood()
{
float x = transform.position.x;
float y = transform.position.y + 2.0f;
Instantiate(food, new Vector2(x, y), Quaternion.identity);
}
반복 생성을 위해 Food 오브젝트를 프리팹화한다. Dog 스크립트를 생성해서 Dog 오브젝트에 드래그 앤 드롭한다.
MakeFood 메서드에 밥이 생성될 위치를 표현한다. 밥은 강아지한테서 만들어져서 위로 발사되어야 하므로, Dog 스크립트에서 Food 오브젝트의 위치를 참조한다. x와 y를 그대로 가져다 사용하면 강아지의 몸통에서 만들어지기 때문에, y값에 +2.0f을 해서 머리 위에서 만들어지게 한다.
그리고 food를 생성하는 Instantiate 함수를 사용한다. new Vector2(x, y)를 통해 생성 위치를 지정해 주고, Quaternion.identity를 통해 회전율은 주지 않는다. Start 메서드에서 InvokeRepeating 함수로 0.5초마다 MakeFood 메서드가 반복 실행되게 한다.
이후 유니티 화면으로 돌아와 food 변수에 프리팹화한 Food 오브젝트를 할당한다.
강아지 이동
void Update()
{
Vector2 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
float x = mousePos.x;
if (x > 8.7f)
{
x = 8.7f;
}
if (x < -8.7f)
{
x = -8.7f;
}
transform.position = new Vector2(x, transform.position.y);
}
Update 메서드에서 강아지가 마우스 포인터를 따라 X축 방향으로 움직이는 로직을 구현한다. 마우스 포인터의 화면 좌표를 게임 월드의 좌표로 변환하여 mousePos 변수에 할당한다.
x 변수에 mousePos의 x값을 할당하고, 생선 가게 끝 부분 위치인 -8.7과 8.7을 벗어나지 못하도록 하는 조건문을 작성한다. 그리고 강아지의 위치 변수에 y는 고정시키고 x만 변화할 수 있도록 위치 값을 할당한다.
[ 밥 먹이기 ]
고양이 생성
하이어아키에서 Create Empty로 NormalCat 부모 오브젝트를 만든다. 자식 오브젝트로는 Square 스프라이트로 Hungry와 Full을 생성한다. 스프라이트에 이미지를 드래그 앤 드롭하고, Full 오브젝트는 보이지 않게 체크 박스를 해제한다.
애니메이션을 생성해서 Hungry 오브젝트에 드래그 앤 드롭한다. Loof Time을 체크하고 녹화 버튼을 누른 뒤 프레임 0과 프레임 20에는 hungryCat_1 이미지, 프레임 10에는 hungryCat_2 이미지를 추가한다.
체력바 생성
Hungry 오브젝트에 Image를 생성한다. Canvas에 가서 위치와 크기를 사진과 같이 맞춰주고, Render Mode를 World Space로 변경한다. 기본으로 설정되어 있는 Screen Space - Overlay는 게임 화면 위에 보이는 것이고, World Space는 게임 속에서 보이는 것이다.
Image이름을 Back으로 변경하고, 네모 상자 클릭 후 Shift+Alt를 누른 채로 오른쪽 하단을 클릭하여 이미지를 캔버스 크기에 맞춘다.
Back을 복제하여 Front로 이름을 변경하고, 스포이드를 이용하여 가게 지붕에서 색을 추출한다. 피벗 X값을 0으로 설정해서 맨 왼쪽부터 이미지가 움직이도록 하면, 스케일 X값에 따라 게이지 바가 움직이는 것을 볼 수 있다.
완성된 NormalCat 오브젝트를 복제해서 FatCat도 동일하게 만들어준다. 스프라이트 이미지를 변경하고, Hungry 오브젝트에 있던 NormalCat의 애니메이션을 삭제하고 FatCat 이미지로 애니메이션을 새로 생성한다. 체력바는 고양이의 가운데에 위치하도록 -0.5만큼 이동한다.
그리고 고양이가 반복적으로 등장해야 하므로 NormalCat과 FatCat 모두 프리팹화한다.
고양이 이동
void Start()
{
Application.targetFrameRate = 60;
}
void Update()
{
transform.position += Vector3.down*0.05f;
}
Cat 스크립트를 생성한 뒤, 고양이가 생선 가게를 향해서 아래로 움직이는 로직을 구현한다. 밥을 발사할 때는 Y값을 1씩 더해주기 위해서 Vector3.up을 사용했다면, 고양이는 -1을 해줘야 하므로 Vector3.down을 사용한다. 고양이의 이동 속도가 빨라서 *0.05f를 작성해 조금씩 이동하게 하고, 프레임 수를 60으로 통일해서 고양이가 천천히 내려오게 한다.
밥, 고양이 물리 적용
밥과 고양이가 충돌했을 때 이벤트를 발생시키기 위해, 두 오브젝트에 충돌을 감지할 수 있도록 Collider 2D 컴포넌트를 추가한다. 적어도 둘 중 하나는 Rigidbody를 가지고 있어야 하므로, Food 오브젝트에 Rigidbody 2D 컴포넌트를 추가한다.
하지만 밥이 고양이와 충돌했을 때 중력으로 인해 아래로 떨어지면 안 되기 때문에, Body Type을 Kinematic으로 변경하여 중력이 발생하지 않도록 해준다. Kinematic으로 설정하면 자동으로 물리 법칙이 적용되지 않아 중력을 무시하고, 스크립트나 애니메이션을 통해서만 적용할 수 있다. 또한 물리적인 힘은 적용되지 않지만, 다른 오브젝트와 부딪혔을 때 충돌을 감지할 수 있다.
Kinematic Rigidbody로 변경한 뒤에 Collider에서 is Trigger까지 활성화해야 충돌을 감지할 수 있다. is Trigger을 비활성화하면, 다른 오브젝트를 밀어내거나 튕겨나가는 등의 물리적인 힘이 적용된다. 하지만 활성화를 하면 충돌 감지용으로만 작동하고 물리적인 힘은 적용되지 않기 때문에, 오브젝트 간 상호작용이 일어났을 때 이벤트를 발생시킬 수 있다. 예를 들어 아이템과 캐릭터 사이에 충돌이 감지되었을 때, 아이템이 튕겨나가지 않고 아이템 정보창을 띄울 수 있다.
NormalCat 오브젝트에도 Collider 2D 컴포넌트를 생성하고 is Trigger를 활성화한다. 충돌 감지 영역을 조절하기 위해 NormalCat 프리팹을 하이어아키에 옮기고, Edit Collider 버튼을 통해 영역을 원하는 크기로 조절한다. Cat 스크립트도 컴포넌트에 추가한다.
하이어아키에서 작업한 행동은 프리팹에는 저장되지 않았다. 변경 사항을 적용하기 위해 Prefabs 컴포넌트에서 Apply All 버튼을 클릭해 프리팹에도 적용시켜 준다.
체력바 게이지 증가
public RectTransform front;
float full = 5.0f;
float energy = 0.0f;
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.CompareTag("Food"))
{
energy += 1.0f;
front.localScale = new Vector3(energy / full, 1.0f, 1.0f);
Destroy(collision.gameObject);
}
}
밥과 고양이가 충돌하면 체력바 게이지를 증가시켜야 한다. Front 오브젝트에서 크기를 조절하는 Rect Transform 컴포넌트를 클래스명으로 해서 front 변수를 생성한다. 그리고 체력바가 다 찬 상태인 full 변수에 5를, 게이지를 표현하는 energy 변수에 0을 할당한다.
Collider에 is Trigger를 활성화했기 때문에 OnTriggerEnter2D 함수를 사용한다. Cat 오브젝트와 충돌했을 때 그 오브젝트에 Food 태그가 있다면, 에너지 변수에 1을 더한다. 그리고 전체 체력바 게이지에서 현재 게이지를 나눈 값을 Scale X값에 할당하고, 충돌한 오브젝트는 파괴한다.
public GameObject hungryCat;
public GameObject fullCat;
private void OnTriggerEnter2D(Collider2D collision)
{
if (collision.gameObject.CompareTag("Food"))
{
if (energy < full)
{
energy += 1.0f;
front.localScale = new Vector3(energy / full, 1.0f, 1.0f);
Destroy(collision.gameObject);
}
if (energy == full)
{
hungryCat.SetActive(false);
fullCat.SetActive(true);
}
}
}
하지만 게이지가 계속해서 증가하는 모습을 보이기 때문에, 체력바가 꽉 차면 더 이상 증가하지 않도록 해줘야 한다. 조건문을 이용해서 현재 게이지가 총게이지보다 작다면 기존의 코드를 실행시킨다. 같다면 hungry 이미지를 비활성화하고 full 이미지를 활성화한다. 게이지 바가 다 차면 바로 fullCat이 활성화되어야 하는데, else문에 작성하면 앞에서 한 번 조건 확인을 하고 넘어오기 때문에 바로 바뀌지 않으므로 새로운 if문에 작성한다.
고양이 제거
void Update()
{
if (energy < full)
{
transform.position += Vector3.down * 0.05f;
}
else
{
if (transform.position.x < 0)
{
transform.position += Vector3.left * 0.05f;
}
else
{
transform.position += Vector3.right * 0.05f;
}
}
}
그리고 full 상태의 고양이가 옆으로 움직여서 화면 밖으로 사라지게 하는 코드를 작성한다. 현재 게이지가 총게이지보다 작으면 아래로 내려오고, 크다면 왼쪽의 고양이는 왼쪽으로, 오른쪽의 고양이는 오른쪽으로 사라지게 한다.
void Update()
{
if (transform.position.x < -18.0f)
{
Destroy(gameObject);
}
if (transform.position.x > 18.0f)
{
Destroy(gameObject);
}
}
배부른 고양이가 양옆으로 움직이고 나면 화면상에서는 보이지 않지만, 씬에서는 끝없이 옆으로 가는 것을 볼 수 있다. 오브젝트도 계속해서 생성되므로 화면 밖의 X 좌표 값을 구해서 일정 좌표 이상을 벗어나면 오브젝트가 파괴되도록 코드를 작성한다.
if (energy == full)
{
hungryCat.SetActive(false);
fullCat.SetActive(true);
Destroy(gameObject, 5.0f);
}
또는 OnTriggerEnter2D 메서드에서 배부른 고양이가 된 후 5초 뒤에 파괴를 할 수도 있다.
고양이 랜덤, 반복 생성
void Start()
{
float x = Random.Range(-9.0f, 9.0f);
float y = 30.0f;
transform.position = new Vector2(x, y);
}
Y값은 동일하고 X값만 변화시켜서 고양이가 랜덤하게 등장하도록 한다.
public GameObject normalCat;
void Start()
{
InvokeRepeating("MakeCat", 0f, 1.0f);
}
void MakeCat()
{
Instantiate(normalCat);
}
GameManager 오브젝트와 스크립트를 생성하고, 고양이가 반복해서 생성되게 하는 로직을 구현한다.
하지만 실행을 했을 때 고양이가 밥을 먹어도 게이지가 차는 모습이 보이지 않고, 배부른 모습으로 바뀌지도 않았다. 에러를 확인해 보니 스크립트에서 참조하려는 변수나 오브젝트에 값이 할당되지 않았다.
각각 맞는 오브젝트를 할당하니 정상적으로 게임이 실행되는 것을 볼 수 있었다. 프리팹화한 오브젝트를 하이어아키에서 작업했다면, 반드시 Apply All 버튼을 클릭해야 프리팹도 적용이 되기 때문에 잊지 말고 클릭해줘야 한다.
[ 게임 종료 ]
Retry 버튼
게임이 종료됐을 때 화면에 보일 Retry 버튼을 생성한다. 버튼이 클릭되면 MainScene을 로드해야 한다. StartScene에서 사용됐던 Start Button 스크립트에서 사용된 코드랑 동일하기 때문에 해당 스크립트를 다시 가져다 사용한다. On Click에서 함수도 지정해 준다.
게임 오버
public static GameManager Instance;
private void Awake()
{
if (Instance == null)
{
Instance = this;
}
Application.targetFrameRate = 60;
}
먼저 외부에서 GameManager 스크립트에 접근할 수 있도록 싱글톤을 적용한다. Awake 메서드에 Cat 스크립트에 있던 targetFrameRate 함수도 옮겨와 준다.
public void GameOver()
{
Time.timeScale = 0.0f;
retryBtn.SetActive(true);
}
GameOver 메서드에 게임 시간이 멈추고 Retry 버튼이 활성화되는 코드를 작성한다.
void Update()
{
if (energy < full)
{
transform.position += Vector3.down * 0.05f;
if (transform.position.y < -16.0f)
{
GameManager.Instance.GameOver();
}
}
}
Cat 스크립트에서 Update 메서드에 작성했던 if문을 수정한다. 체력바 게이지가 다 차지 않은 고양이가 생선 가게에 도착했다면 GameOver 메서드를 불러오게 한다. 생선 가게와 고양이가 맞닿는 지점은 Y축 기준 -16이다.
다시 시작
private void Awake()
{
Time.timeScale = 1.0f;
}
게임이 다시 실행됐을 때 메인 씬이 로드되면서 게임 내 시간도 원래대로 돌아오도록 실시간으로 맞춰준다.
❤️🔥 Try | 게임 종료 시 고양이 정지
게임이 종료됐을 때 게임 내 시간은 0이 돼서 더 이상 새로운 고양이는 생성되지 않았지만, 이미 생성된 고양이들이 멈추지 않고 계속해서 아래로 내려가는 모습을 볼 수 있었다. 그래서 게임이 멈추는 순간 고양이도 같이 멈추는 로직 구현에 도전해 봤다. (0 레벨 기준)
Try 1.
//GameManager 스크립트
public void GameOver()
{
Time.timeScale = 0.0f;
retryBtn.SetActive(true);
float nowPosX = normalCat.transform.position.x;
float nowPosY = normalCat.transform.position.y;
PlayerPrefs.SetFloat("posX", nowPosX);
PlayerPrefs.SetFloat("posY", nowPosY);
float stopPx = PlayerPrefs.GetFloat("posX");
float stopPy = PlayerPrefs.GetFloat("posY");
transform.position = new Vector2(stopPx, stopPy);
}
기존의 코드는 고양이가 생선 가게에 닿았을 때, GameOver 메서드를 참조하여 게임 내 시간이 0이 되고 Retry 버튼을 띄우는 것이 전부였다.
거기에 고양이의 x좌표와 y좌표를 변수에 할당해서 데이터를 저장하고, 그 데이터를 다시 불러와서 위치 값을 할당시키는 로직을 구현했다. 하지만 여전히 생성된 고양이들이 움직였다.
Try 2.
//GameManager 스크립트
public void GameOver()
{
normalCat.transform.position += Vector3.down * 0.0f;
Time.timeScale = 0.0f;
retryBtn.SetActive(true);
}
GameOver 메서드를 불러왔을 때, 고양이가 아래로 1씩 움직이던 것을 *0을 통해 (0, 0, 0)으로 만들어봤다. 하지만 마찬가지로 여전히 고양이들은 움직였다.
아직은 게임이 종료되었을 때 움직이고 있던 생성된 고양이들의 움직임까지 멈추는 것까지는 구현하지 못했다. 추후 관련 공부를 하게 되면 한 번 적용을 해 볼 예정이다.
[ 레벨 업 ]
레벨 UI
현재 레벨과 다음 레벨로 넘어가기까지의 스코어를 나타내는 이미지를 생성한다.
스코어, 레벨 업 적용
//GameManager 스크립트
using UnityEngine.UI;
public class GameManager : MonoBehaviour
{
public Text LevelTxt;
public RectTransform FrontBar;
int score = 0;
int level = 0;
public void AddScore()
{
score++;
level = score / 5;
LevelTxt.text = level.ToString();
FrontBar.localScale = new Vector3((score - level * 5) / 5.0f, 1.0f, 1.0f);
}
}
AddSocre 메서드에서 배부른 고양이 1마리당 1점이 올라가게 하는 로직을 구현한다. 레벨은 스코어가 5점이 될 때마다 1 레벨씩 올라간다. 레벨은 string으로 변환해서 텍스트에 적용시킨다. 스코어 게이지는 전체 점수에서 레벨로 계산된 나머지를 표현하기 위해 score - level * 5 식을 사용한다.
잊지 않고 Unity에서 LevelTxt 함수에는 level num 오브젝트를, FrontBar 함수에는 Front 오브젝트를 할당해 준다.
//Cat 스크립트
bool isFull = false;
private void OnTriggerEnter2D(Collider2D collision)
{
~ 중략 ~
if (energy == full)
{
if (!isFull)
{
isFull = true;
hungryCat.SetActive(false);
fullCat.SetActive(true);
GameManager.Instance.AddScore();
}
}
}
고양이의 상태를 표현해 주는 isFull 변수를 생성하고 기본값을 false로 지정한다. 배고픈 고양이는 false, 배부른 고양이는 true이다.
만약 배고픈 고양이(false)라면, ! 연산자로 인해 true가 되고 조건을 만족해서 조건문이 실행된다. 그리고 배부른 고양이일 때 조건문이 다시 실행되지 않도록 true 값을 할당한다. 그러면 조건에서 false로 변환되어 조건을 만족하지 못해 조건문이 실행되지 않는다.
조건문이 실행되면 배고픈 고양이가 배부른 상태로 바뀌면서, AddScore 함수를 참조하여 스코어를 1점 증가시킨다.
뚱뚱한 고양이 적용
//Cat 스크립트
public int type;
float full = 5.0f;
float speed = 0.05f;
void Start()
{
if (type == 1)
{
speed = 0.05f;
full = 5.0f;
}
else if (type == 2)
{
speed = 0.02f;
full = 10.0f;
}
}
노멀 캣과 팻 캣 두 종류가 존재하므로, 구분을 위해 int 자료형인 type 변수를 생성한다. full 값은 원래 생성되어 있던 변수를 사용하고, 새롭게 speed 변수도 생성한다.
type 1은 노멀 캣으로, 스피드는 빠르지만 포만감은 빨리 찬다. type 2는 팻 캣으로, 스피드는 느리지만 포만감은 늦게 찬다.
타입을 지정해주고 난 뒤에는 각 프리팻의 인스펙터에서 스크립트 컴포넌트의 Type 변수에 알맞은 숫자를 입력한다.
레벨 난이도 반영
//GameManager 스크립트
public GameObject fatCat;
void MakeCat()
{
Instantiate(normalCat);
int p = Random.Range(0, 10);
//20%의 확률로 노멀 캣 생성
if (level == 1)
if (p < 2)
Instantiate(normalCat);
//50%의 확률로 노멀 캣 생성
else if (level == 2)
if (p < 5)
Instantiate(normalCat);
//팻 캣 생성
else if (level >= 3)
Instantiate(fatCat);
}
레벨이 올라갈수록 난이도를 올리기 위한 조건문 로직을 구현한다.
레벨 0일 때는 노멀 캣만 생성된다.
레벨 1일 때는 20%의 확률로 노멀 캣이 추가로 생성된다. 0~9 사이의 숫자가 무작위로 하나 생성 됐을 때, 그 숫자가 0 또는 1이라면 생성되도록 조건을 작성한다.
레벨 2일 때는 50%의 확률로 노멀 캣이 추가로 생성된다. 마찬가지로 무작위 숫자가 0~4의 숫자라면 생성되도록 조건을 작성한다.
레벨 3일 때는 추가 확률성 조건 없이 팻 캣이 무조건 추가로 생성되게 한다.
[ 회고 ]
본격적으로 구체화된 게임의 형태를 갖추고 있는 프로젝트를 진행했다. 단계를 나누고 그에 따른 난이도를 추가로 설정하면서, 레벨이 많아질수록 구현해야 하는 것들의 가짓수도 매우 많고 복잡해질 수 있다는 것을 알게 됐다. 현재는 관리가 어려운 정도의 개수는 아니지만, 더 큰 규모의 게임을 만들려면 체계화된 정리가 필요하고 이를 위해 깃을 사용하는 법을 제대로 숙지해야 할 것 같다는 생각이 들었다.
기능적인 부분에서는 게임이 종료됐을 때 고양이의 움직임을 멈추는 것을 구현하고 싶었지만, 현재의 수준에서는 아직 할 수 없는 것 같다고 생각되었다. 그리고 is Kinematic과 is Trigger를 이용해서 물리 작용 없이 충돌을 감지할 수 있다는 것을 배웠고, 이 기능이 되게 많이 사용될 것 같아서 잊지 않도록 복습을 꾸준하게 해야겠다.
'Coding > Unity' 카테고리의 다른 글
[내일배움캠프 18일차 TIL] 쿼터니언, 아크탄젠트 (0) | 2024.05.09 |
---|---|
[내일배움캠프 17일차 TIL] 게임 엔진, PPU, 계층 구조, Input.GetAxis, 직렬화 (0) | 2024.05.08 |
[내일배움캠프 4일차 TIL] 복습, 마우스 포인터 추적, 점수 저장 (1) | 2024.04.18 |
[내일배움캠프 3일차 TIL] UI, 싱글톤, 점수 적용, 게임 오버, 다시 시작 (0) | 2024.04.17 |
[내일배움캠프 2일차 TIL] 오브젝트 중력, 충돌, 파괴, 랜덤, 반복, 클래스 (0) | 2024.04.16 |