시작화면에서 PLAY!를 클릭하면 시작한다.
위에서 아래로 고양이들이 내려오고, 개는 아래에서 투사체(음식)를 계속 쏘아올려 고양이의 게이지를 채워야 하며, 게이지가 다 찬 고양이는 옆으로 빠지다가 사라진다. 개가 고양이를 쫓아낸다고 표현하겠다. 게이지가 다 찬 고양이가 많아질수록 우측 상단의 난이도가 높아진다.고양이들 중 하나라도 개의 생선가게에 닿으면 게임오버.
게임 씬은 다음과 같다.

아래는 각각의 스크립트다.
시작버튼(시작화면의 PLAY!버튼)
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class startBtn : MonoBehaviour
{
void Start()
{
}
void Update()
{
}
public void GameStart()
{
SceneManager.LoadScene("MainScene");
}
}
개
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class dog : MonoBehaviour
{
void Start()
{
}
void Update()
{
Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
float x = mousePos.x;
if (x > 8.5f)
{
x = 8.5f;
}
if (x < -8.5f)
{
x = -8.5f;
}
transform.position = new Vector3(x, transform.position.y, 0);
}
}
음식
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class food : MonoBehaviour
{
void Start()
{
}
void Update()
{
transform.position += new Vector3(0, 0.5f, 0);
if (transform.position.y > 26.0f)
{
Destroy(gameObject);
}
}
}
고양이
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class cat : MonoBehaviour
{
float full = 5.0f;
float energy = 0.0f;
bool isFull;
public int type;
void Start()
{
float x = Random.Range(-8.5f, 8.5f);
float y = 30.0f;
if (type == 1)
{
full = 10.0f;
}
transform.position = new Vector3(x, y, 0);
}
void Update()
{
if (energy < full)
{
if (type == 0)
{
transform.position += new Vector3(0, -0.04f, 0);
}
else if (type == 1)
{
transform.position += new Vector3(0, -0.02f, 0);
}
else if (type == 2)
{
transform.position += new Vector3(0, -0.08f, 0); //-0.1f하면 눈깜짝 새 내려옵니다..
}
if (transform.position.y < -16.0f)
{
GameManager.I.gameOver();
}
}
else
{
if (transform.position.x < 0)
{
transform.position += new Vector3(-0.01f, 0.0f, 0.0f);
}
else
{
transform.position += new Vector3(0.01f, 0.0f, 0.0f);
}
Destroy(gameObject, 3.0f);
}
}
private void OnTriggerEnter2D(Collider2D coll)
{
if (coll.gameObject.tag == "food")
{
if (energy < full)
{
energy += 1.0f;
Destroy(coll.gameObject);
gameObject.transform.Find("hungry/Canvas/front").transform.localScale = new Vector3(energy / full, 1.0f, 1.0f);
}
else
{
if (isFull == false)
{
GameManager.I.addCat();
gameObject.transform.Find("hungry").gameObject.SetActive(false);
gameObject.transform.Find("full").gameObject.SetActive(true);
isFull = true;
}
}
}
}
}
GM
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class GameManager : MonoBehaviour
{
public static GameManager I;
public GameObject dog;
public GameObject food;
public GameObject normalCat;
public GameObject fatCat;
public GameObject pirateCat;
public GameObject retryBtn;
public Text levelText;
public GameObject levelFront;
int level = 0;
int cat = 0;
private void Awake()
{
I = this;
}
void Start()
{
Time.timeScale = 1.0f;
InvokeRepeating("makeFood", 1.0f, 0.1f);
InvokeRepeating("makeCat", 1.0f, 1.0f);
}
void Update()
{
}
void makeFood()
{
float x = dog.transform.position.x;
float y = dog.transform.position.y + 2.0f;
Instantiate(food, new Vector3(x, y, 0), Quaternion.identity);
}
void makeCat()
{
if (level <= 1)
{
float p = Random.Range(0, 10);
if (p < 3)
{
Instantiate(normalCat);
}
}
else
{
float p = Random.Range(0, 10);
if (p < 5)
{
Instantiate(normalCat);
}
}
if (level >= 3)
{
float p = Random.Range(0, 10);
if (p < 5)
{
Instantiate(fatCat);
}
}
if (level >= 4)
{
float p = Random.Range(0, 10);
if (p < 5)
{
Instantiate(pirateCat);
}
}
Instantiate(normalCat);
}
public void gameOver()
{
retryBtn.SetActive(true);
Time.timeScale = 0.0f;
}
public void addCat()
{
cat += 1;
level = cat / 5;
levelText.text = level.ToString();
levelFront.transform.localScale = new Vector3((cat - level * 5) / 5.0f, 1.0f, 1.0f);
}
}
재시작버튼
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;
public class retryBtn : MonoBehaviour
{
void Start()
{
}
void Update()
{
}
public void reGame()
{
SceneManager.LoadScene("MainScene");
}
}
씬 넘어가기
버튼을 클릭하면 메인화면으로 전환되는 기능은 이전에 구현했다.
새로운 시작화면을 만들고, 시작화면의 UI에 이 기능을 작성한다.
+ 다른 씬으로 뷰를 전환할 때, 기존의 씬을 저장해야 한다.
Quaternion 클래스
Quaternion 클래스는 게임 오브젝트의 3차원 방향을 정의한다.
이 게임에는 오브젝트의 회전이나, 각도가 바뀌는 경우는 없다. 하지만, 개에게서 발사되는 음식은 생성 위치를 개에 고정해야 하고, 지정된 위치를 가진 프리팹(음식)에는 방향도 정의해야 한다.
Instantiate(음식)으로는 당연히 안되고, Istantiate(음식, 생성 위치, 생성 방향)이 필요하다.
아래는 GM에 작성한 그 코드다.
void makeFood()
{
float x = dog.transform.position.x;
float y = dog.transform.position.y + 2.0f;
Instantiate(food, new Vector3(x, y, 0), Quaternion.identity);
}
Quaternion.identity : 오브젝트의 회전이 없다는 것을 의미한다.
mouseposition에 따른 오브젝트 이동(x축 고정)
또 쓸까 말까 하다가 쓴다. 이전에도 Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition)에 대한 내용은 작성했지만, 상기할 겸 다시 적는다.
위 코드로 mousePos는 x, y, z의 값을 저장한다. 개는 뷰 내의 제한된 범위를 x축 방향으로만 움직인다.
그러므로 개위 이동 범위는 (mousePos.x, 개의 위치 y값, 0)이다.
다음은 개에 작성된 코드다.
void Update()
{
Vector3 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
float x = mousePos.x;
if (x > 8.5f)
{
x = 8.5f;
}
if (x < -8.5f)
{
x = -8.5f;
}
transform.position = new Vector3(x, transform.position.y, 0);
}
오브젝트가 가지는 UI
내려오는 고양이를 보면 바로 하단에 게이지를 달고 내려온다. 게이지는 UI로 작성했고, UI를 object차원으로 바꾸기 위해 게이지가 속한 Canvas의 Canvas컴포넌트-RenderMode를 ScreenSpace-Overlay=>WorldSpace로 변경한다.
다음은 고양이(프리팹)의 계층 뷰 상세다.

normalCat : Empty Object / hungry&full : sprite에 이미지 추가
Canvas : Canvas UI / back&front : Image UI
로 작성했다.
이러한 프리팹을 만듦으로써 단순히 한 종류의 고양이만 내려오는게 아니라, 여러 타입의 고양이가 내려오고, 그에 맞게 난이도 구현도 가능하다.
Rigidbody 컴포넌트 bodytype - Kinematic
고양이는 음식에 닿으면 충돌 반응이 일어난다. (생선가게와의 충돌은 그냥 고양이가 일정 y위치값 이하로 내려오면 기능하는 함수로 대체)
음식은Rigidbody, Collider / 고양이는 Collider를 갖는다.
음식은 중력의 영향을 받지 않고 상승하는데 Rigidbody를 적용하면 오브젝트에 중력이 가해진다.
이때 Rigidbody-BodyType을 Dynamic(동역학)=>Kinematic(운동학)으로 변경하면, 오브젝트는 중력의 영향을 받지 않는다.
Kinematic 옵션을 가진 오브젝트는 OnColliderEnter(Collision coll){}함수로 동작하지 않고, Collider컴포넌트의 IsTrigger옵션 = True로 한 후, OnTriggerEnter(Collider coll){}함수로 동작한다.
다음은 Kinematic 옵션을 가진 음식의 Rigidbody컴포넌트와 그 음식에 충돌하는 고양이의 충돌함수 코드다.

private void OnTriggerEnter2D(Collider2D coll)
{
if (coll.gameObject.tag == "food")
{
} // 어떤 함수를 썼느냐가 이 문제의 핵심이다.
}
UI게이지 채우기
고양이의 게이지는 back, front로 나누며, back은 게이지의 바탕, front는 현재 수치를 나타낸다.
생성된 고양이의 front게이지 크기xyz는 0,1,1로 시작해, 고양이가 음식에 맞으면 찬다.
그렇다면 고양이와 음식 충돌 시 front오브젝트가 작동해야 한다.
고양이 하위 계층의 front를 찾는 코드는 다음과 같다. 찾은 front의 게이지가 차는 코드를 추기한다.
float full = 5.0f;
float energy = 0.0f;
private void OnTriggerEnter2D(Collider2D coll)
{
if (coll.gameObject.tag == "food")
{
energy += 1.0f;
Destroy(coll.gameObject);
gameObject.transform.Find("hungry/Canvas/front").transform.localScale = new Vector3(energy / full, 1.0f, 1.0f);
}
}
고양이와 음식이 닿으면 energy가 1차고, 음식은 사라지며, 고양이 오브젝트 하위 hungry/Canvas/front오브젝트의 위치를 찾아 그 LocalScal을 x방향 energy/full만큼 증가시킨다.
레벨 게이지 올리기
고양이를 쫓아낼 때 메인씬 우측 상단의 레벨 게이지가 일정량 증가하고, 게이지가 꽉 차면 레벨 게이지는 다시 0이 되며 동시에 바로 위 흰 고양이 이미지의 0부터 시작하는 텍스트가 1씩 증가한다.
다음의 위 동작을 GM에 작성한 코드다. 쫓겨나는 고양이가 생기는 함수(cat스크립트의 충돌 함수)에서 호출한다.
public Text levelText;
public GameObject levelFront; // 레벨게이지
int level = 0; // 쫓아낸 고양이 수가 일정 이상 올라가면 증가할 레벨
int cat = 0; // 쫓아낸 고양이 수
public void addCat()
{
cat += 1;
level = cat / 5;
levelText.text = level.ToString();
levelFront.transform.localScale = new Vector3((cat - level * 5) / 5.0f, 1.0f, 1.0f);
}
레벨의 수치화는 단순하다. 레벨 게이지의 증가와 다시 0으로 되는 코드(가장 하단)가 이 문제의 핵심이다.
강의 안보고 혼자 써본다고 (cat/5, 1, 1)부터 시작해 한참을 고민했다.
다음은 cat스크립트에서의 addCat()함수 호출 코드다.
float full = 5.0f;
float energy = 0.0f;
bool isFull;
private void OnTriggerEnter2D(Collider2D coll)
{
if (coll.gameObject.tag == "food")
{
/*if (energy < full)
{
energy += 1.0f;
Destroy(coll.gameObject);
gameObject.transform.Find("hungry/Canvas/front").transform.localScale = new Vector3(energy / full, 1.0f, 1.0f);
}*/
else
{
if (isFull == false)
{
GameManager.I.addCat();
gameObject.transform.Find("hungry").gameObject.SetActive(false);
gameObject.transform.Find("full").gameObject.SetActive(true);
isFull = true;
}
}
}
}
isFull변수는 왜 넣었는가? 넣지 않았을 때를 가정해보자.
고양이가 음식에 닿고, energy >= full 일 때, addCat을 실행하고 고양이 모습을 full로 바꾼다. 맞는 말인데 뭐가 이상한가?
full인 고양이가 사라질 때 까지 빌빌대는 시간동안은? 음식에 계속 닿고, energy >= full인 상태도 맞으니까 addCat함수 이하 모든 내용이 계속 실행될 것이다. 레벨 게이지가 올라가면 안 되는 상황인데 계속 올라가는 버그인 것이다.
이 버그를 고치기 위해 isFull변수가 사용되었다.
고양이가 음식에 닿고, energy >= full 일 때, 아직 isFull=false다. 고로 addCat함수 이하 내용은 실행되고 마지막에 isFull=true가 된다. 그럼 full인 고양이는 사라질 때 까지 빌빌대는 상태일때도 isFull=true인 채고, 음식에 닿아도 addCat은 실행되지 않는다.
아 이해 안 돼서 몇번이나 봤는데, 볼때 다 적어놓을걸. 작성할 때 되니까 전부 잊어버렸다.
난이도(레벨) 올리기
레벨이 올라갈 때 마다 생성되는 고양이도 많아지고, 다른 타입의 고양이도 생성된다.
미리 생성한 프리팹을 다수 복제하고, 그 프리팹이 서로 다른 타입이라는 구분을 주면 된다.
타입이 다르면 게임 난이도에 많은 변수를 줄 수 있다.
다음은 그냥이(0타입), 뚱냥이(1타입), 적냥이(2타입)고, 그에 따라 달라지는 속성을 작성한 코드다.
public class cat : MonoBehaviour
{
public int type;
void Start()
{
float x = Random.Range(-8.5f, 8.5f);
float y = 30.0f;
if (type == 1)
{
full = 10.0f;
}
transform.position = new Vector3(x, y, 0);
}
void Update()
{
if (energy < full)
{
if (type == 0)
{
transform.position += new Vector3(0, -0.04f, 0);
}
else if (type == 1)
{
transform.position += new Vector3(0, -0.02f, 0);
}
else if (type == 2)
{
transform.position += new Vector3(0, -0.08f, 0); //-0.1f하면 눈깜짝 새 내려옵니다..
}
}
}
}
타입에 따라 먹은 음식의 수가 달라지고, 내려오는 속도도 달라진다.
레벨이 높아짐에 따른 GM의 고양이 생성 코드도 추기한다.
void makeCat()
{
if (level <= 1)
{
float p = Random.Range(0, 10);
if (p < 3)
{
Instantiate(normalCat);
}
}
else
{
float p = Random.Range(0, 10);
if (p < 5)
{
Instantiate(normalCat);
}
}
if (level >= 3)
{
float p = Random.Range(0, 10);
if (p < 5)
{
Instantiate(fatCat);
}
}
if (level >= 4)
{
float p = Random.Range(0, 10);
if (p < 5)
{
Instantiate(pirateCat);
}
}
Instantiate(normalCat);
}
언제 어떤 고양이가 얼마나, 어떻게 내려오는지 생각해보자.
왜 energy =full인 고양이는 더이상 food에 안맞지?
당연하지.. energy <full일때만 food먹으면 destroy(충돌한food)라고 써놨으면서 왜 물어보냐
게임 만들 때 들었던 의문이다. 그럴수도 있지.
아닌가? 아닌거 같은데
스크립트에서 알트엔터 = 오류상세, 코드 제안사항 표시
유용한 기능이다. SceneManagement네임스페이즈 없이 SceneManager클래스 썼다가 나오는 오류, 이런 거에 대처할 만하다.
버튼 속성은 UI오브젝트에서만 작동. image나 다른 스프라이트에서는 불가
애먼 오브젝트에 버튼 컴포넌트 집어넣어서 괜한 고생하지 말자.
animator Controller컴포넌트가 이미 있는 오브젝트는 새로운 animation을 가질 수 없다.
기존의 완성된 animation을 가지고 있는 프리팹의 animation을 수정할 때는 해당 프리팹의 Animator컴포넌트를 삭제하고 새 animation을 다시 추가한다.
강의 제대로 안봐서 적냥이 만들 때 뭔 30분을 헛고생했다..
고양이 프리팹 생성할 때 고양이의 계층 구조를 제대로 파악하고, 애니메이션 추가 방법을 혼동하지 않도록 한다.
본문에 생략된 과정이 상당히 많다.
아 이거 혼자 다시 만들려고 하면 하루종일 걸리겠는데
'UNITY' 카테고리의 다른 글
| UNITY_20230808[팀 프로젝트_1] (0) | 2023.08.08 |
|---|---|
| UNITY_20230802[카드 짝 맞추기] (0) | 2023.08.03 |
| UNITY_TIL최적화를 위한 규칙 (0) | 2023.07.31 |
| UNITY_20230728[풍선 지키기] (0) | 2023.07.31 |
| UNITY_20230727[빗물 모으기] (0) | 2023.07.27 |