2주 차 프로젝트에서는 <풍선을 지켜라> 게임을 제작했다.
[ 기본 씬 구성 ]
Square, Circle, Text 오브젝트를 이용해서 게임 기본 씬을 구성한다.
풍선 애니메이션
색이 번갈아가면서 나타나게 하기 위해서 Balloon_Idle 애니메이션을 생성하고 Balloon 오브젝트에 드래그 앤 드롭한다.
애니메이션 창을 열어 왼쪽 상단의 녹화 버튼을 누르면, 오브젝트에 변화가 생길 때마다 변동 사항이 기록이 된다. 프레임 0에서는 맨 처음 상태가 기록이 되었고, 프레임 20에서는 연보라색으로 색상을 바꿔주었다. 프레임 40에서는 다시 흰색으로 변경하고 녹화를 종료하면, 두 가지 색상이 번갈아가면서 나타나게 된다.
왼쪽 상단의 토글을 눌러 Create New Clip으로 Balloon_Die 애니메이션을 생성한다. 녹화 버튼을 누르고 프레임 20에서 크기를 살짝 키우고 붉은색으로 변경한다. 터지는 애니메이션은 한 번만 실행되면 되기 때문에 Loop Time 체크 박스를 해제한다.
Balloon 오브젝트를 더블클릭하면 애니메이터 컨트롤러가 뜬다. Balloon_Idle에서 우클릭 후 Make Trasition을 선택해서 화살표를 Balloon_Die로 연결한다.
왼쪽 상단에 Parameters로 이동한다. + 버튼을 눌러서 불리언 타입의 파라미터를 생성하고 이름은 isDie로 한다.
화살표 클릭 후 Conditions에서 isDie가 true일 때 애니메이션이 전환되게 해 준다.
[ 풍선, 쉴드, 장애물 설정 ]
마우스 포인터 추적
Shield 오브젝트가 마우스 포인터 위치에 따라서 움직일 수 있도록 하는 스크립트를 작성한다. Shield 스크립트를 생성해서 Shield 오브젝트에 드래그 앤 드롭한다.
void Update()
{
Vector2 mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
transform.position = mousePos;
}
현재 게임은 2D이므로 Z 값을 제외한 X, Y의 값만 필요하기 때문에, 좌표를 Vector2 타입(X, Y)으로 사용한 mousePos 변수를 생성한다. 그리고 메인 카메라 화면에서 입력받은 마우스 위치 정보를 게임 월드의 좌표로 변환한 값을 할당한다. 이 변수를 Shield 오브젝트의 transform 컴포넌트를 참조하여 위치 정보를 입력한다.
중력, 충돌 적용
하늘에서 떨어지는 장애물인 Square 오브젝트를 생성한 뒤, Rigidbody 2D를 통해 중력을 적용하고 Box Collider 2D를 통해 충돌을 적용한다.
장애물과 부딪혀서 이벤트를 발생시키는 Balloon 오브젝트와 Shield 오브젝트에도 Circle Collider 2D 컴포넌트를 추가한다.
랜덤 위치, 크기
void Start()
{
//랜덤 위치
float x = Random.Range(-3.0f, 3.0f);
float y = Random.Range(3.0f, 5.0f);
transform.position = new Vector2(x, y);
//랜덤 크기
float size = Random.Range(0.5f, 1.5f);
transform.localScale = new Vector2 (size, size);
}
Square 스크립트에서 랜덤한 위치와 랜덤한 크기의 오브젝트를 생성하는 코드를 작성한다.
public GameObject Square;
오브젝트 반복 생성을 위해 Square 오브젝트를 프리팹화한다. 그리고 GameManager 오브젝트와 스크립트를 생성하고, Square 변수에 Sqaure 프리팹을 드래그 앤 드롭한다.
반복 생성
void Start()
{
InvokeRepeating("MakeSquare", 0f, 1f);
}
void MakeSquare()
{
Instantiate(Square);
}
오브젝트가 반복해서 생성되게 하기 위해 GameManager 스크립트에서 작업한다. MakeSquare 메서드에서 Square 프리팹이 생성되는 코드를 작성한다. 그리고 게임이 실행됐을 때 1초 간격으로 Square 프리팹이 생성되는 것을 반복시켜 준다.
장애물 파괴
반복해서 생성된 Square(Clone)들은 화면 밖으로 나가도 사라지지 않는다. 게임이 종료되기 전까지 계속해서 증가하기 때문에, 화면 밖으로 나가면 Square 오브젝트가 사라지도록 한다.
void Update()
{
if (transform.position.y < -6)
{
Destroy(gameObject);
}
}
Square 스크립트로 이동한다. Square 오브젝트가 화면 밖으로 완전히 나갔을 때 Y의 값이 -6이므로, y가 -6보다 작다면 이 오브젝트를 파괴하는 코드를 작성한다. 그러면 화면 밖으로 나간 클론들은 모두 사라지는 것을 확인할 수 있다.
[ 시간 설정 ]
using UnityEngine.UI;
public class GameManager : MonoBehaviour
{
public Text timeTxt;
float time = 0.0f;
void Update()
{
time += Time.deltaTime;
timeTxt.text = time.ToString("N2");
}
}
GameManager 스크립트에서 UnityEngine.UI 네임스페이스를 불러온 뒤, Text 컴포넌트를 가져오기 위해 timeTxt 변수에 Text 오브젝트를 드래그 앤 드롭한다.
Update 메서드에서 time 변수에 0부터 델타 타임을 더해준다. timeTxt의 text 컴포넌트를 참조하여 time 변수에 할당된 시간 데이터를 소수점 둘째 자리까지 할당한다.
[ 게임 종료 ]
게임 종료 UI
EndPanel 오브젝트로 게임이 종료 됐을 때 나타나는 UI를 모두 그룹화한다.
Image를 생성하고 Shadow 컴포넌트를 추가해 게임 화면에서 나타나는 것과 같이 만들어준다.
게임 종료창 UI를 다음과 같이 만든다. 게임이 종료됐을 때에만 나타나야 하므로, 체크박스는 해제한다.
만약 텍스트가 이미지에 깔려서 보이지 않는다면, 하이어아키 창에서 오브젝트의 순서를 바꾸면 된다. UI 오브젝트 순서는 아래에 있는 것이 화면의 가장 위에 나타난다.
게임 종료 표시
public static GameManager instance;
private void Awake()
{
if (instance == null)
{
instance = this;
}
}
다른 스크립트에서 GameObject를 참조할 수 있도록 싱글톤을 사용한다. Awake 메서드에서 instance가 null값으로 비어있으면 그때 instance에 this를 할당한다.
public GameObject endPanel;
public Text nowScore;
bool isPlay = true;
void Update()
{
if (isPlay)
{
time += Time.deltaTime;
timeTxt.text = time.ToString("N2");
}
}
public void GameOver()
{
isPlay = false;
Time.timeScale = 0.0f;
nowScore.text = time.ToString("N2");
endPanel.SetActive(true);
}
UI 요소를 할당하기 위해 endPanel과 nowScore 변수를 선언하고, 각 EndPanel, NowScore 오브젝트를 드래그 앤 드롭한다.
bool 타입의 isPlay 변수를 통해서 게임이 실행 중인지를 추적해서 시간 정확성을 높인다. 기본 값을 true로 두고 Update 메서드에 게임이 실행되고 있을 때에만 시간이 흐르도록 한다.
GameOver 메서드에서 게임이 실행되지 않을 때 시간이 멈추게 한다. 그리고 EndPanel을 활성화해서 게임 종료 UI를 화면에 띄우고, NowScore 텍스트 컴포넌트를 참조하여 게임 종료 시점의 시간이 표시되게 한다.
private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.gameObject.CompareTag("Player"))
{
GameManager.instance.GameOver();
}
}
Square 오브젝트와 Balloon 오브젝트가 충돌했을 때 게임을 종료하기 위해, 먼저 Balloon 오브젝트에 Player 태그를 달아준다.
Square 스크립트로 넘어와서, Square 오브젝트가 다른 오브젝트와 충돌했을 때 호출되는 OnCollisionEnter2D 메서드를 작성한다. collision 매개변수를 통해 충돌한 오브젝트가 Player 태그를 가지고 있다면, GameManager의 GameOver 메서드를 호출하여 게임을 종료한다.
풍선 터지는 애니메이션
public Animator anim;
public void GameOver()
{
anim.SetBool("isDie", true);
Invoke("TimeStop", 0.5f);
}
void TimeStop()
{
Time.timeScale = 0.0f;
}
Animator 컴포넌트를 참조하기 위해 anim 변수에 Balloon 오브젝트를 할당한다.
SetBool 메서드를 활용해서 애니메이터 컨트롤러에서 생성했던 불리언 타입의 isDie 파라미터를 가져온다. isDie 파라미터가 true라면 isDie 애니메이션으로 전환된다.
애니메이션이 실행됐을 때 적용되는 시간이 있는데, 게임 시간을 0으로 설정하면 애니메이션이 적용되기 전에 시간이 멈춰버린다. 그래서 게임 시간을 멈추는 코드를 TimeStop 메서드에 따로 넣어준다. 그리고 Invoke 함수를 활용해서 TimeStop 메서드가 0.5초 뒤에 실행되도록 함으로써 애니메이션이 보이게 한다.
다시 시작
using UnityEngine.SceneManagement;
public class RetryBtn : MonoBehaviour
{
public void Retry()
{
SceneManager.LoadScene("MainScene");
}
}
다시 시작 버튼을 클릭했을 때 게임이 다시 실행될 수 있도록 RetryBtn 버튼과 스크립트를 생성한다. Retry 메서드를 호출했을 때 MainScene이 로드되는 코드를 작성한다.
cf. 현재 코드까지 실행했을 때 위와 같은 에러가 발생했다. 하이어아키에서 최상단의 씬의 이름을 확인해 보니 SampleScene으로 되어있었다. 그래서 해당 프로그램에서는 MainScene 대신 SampleScene으로 수정했고, 이후 프로그램이 정상 작동되는 것을 확인했다. 위와 같은 에러가 발생했을 때는 현재 씬의 이름을 확인해 보자.
RetryBtn 오브젝트에 RetryBtn 스크립트를 드래그 앤 드롭하고, 버튼 컴포넌트에서 Retry 메서드를 호출한다.
Time.timeScale = 1.0f;
GameManager 스크립트의 Awake 메서드에 게임 내 시간을 실시간으로 맞춰줌으로써, 게임을 다시 실행했을 때 정상적으로 시간이 흘러가게 한다.
[ 최고 점수 저장 ]
GameOver 메서드에 최고 점수를 저장하는 로직을 구현한다.
- 최고 점수가 존재한다면
- 최고 점수 < 현재 점수라면
- 현재 점수 저장 및 표시
- 최고 점수 > 현재 점수라면
- 최고 점수 표시
- 최고 점수 < 현재 점수라면
- 최고 점수가 없다면
- 현재 점수 저장 및 표시
데이터 저장과 관련된 클래스는 PlayerPrefs이다. 데이터는 키-값 쌍으로 저장되며, 게임이 재시작되거나 장치가 재부팅되어도 유지된다. 주로 소량의 데이터를 저장할 때 사용되며, 크거나 민감한 데이터를 저장하기에는 적합하지 않다.
PlayerPrefs.SetInt("Level", 5);
int level = PlayerPrefs.GetInt("Level", 1);
데이터를 저장하는 메서드는 Set__(string key, __ value)이다. 밑줄에는 자료형이 들어가며 int, float, string 등의 데이터가 될 수 있다.
데이터를 불러오는 메서드는 Get__(string key)이다. 밑줄에는 자료형이 들어간다. key 매개변수 뒤에는 defaultValue 매개변수를 입력할 수 있다. key 값이 존재하지 않으면 defaultValue 값이 반환된다. defaultValue 매개변수가 없으면 기본값인 빈 문자열("")을 반환한다.
if (PlayerPrefs.HasKey("PlayerScore"))
{
int score = PlayerPrefs.GetInt("PlayerScore");
}
데이터를 확인하는 메서드는 HasKey(string key)이다. 지정된 key가 PlayerPrefs에 존재하는지 확인하고, 그 결과를 불리언 값으로 변환한다. key가 존재하면 true, 존재하지 않으면 false를 반환한다. 조건문에서 true 값을 가진다면 코드가 실행된다.
PlayerPrefs.DeleteKey("Level");
PlayerPrefs.DeleteAll();
지정된 key에 저장된 데이터를 PlayerPrefs에서 삭제하는 메서드는 DeleteKey(string key)이다. key는 매개변수명이므로 string 값만 적을 수 있다.
PlayerPrefs에 저장된 모든 데이터를 삭제하는 메서드는 DeleteAll()이다.
public Text bestScoreTxt;
string key = "bestscore";
public void GameOver()
{
isPlay = false;
Time.timeScale = 0.0f;
nowScore.text = time.ToString("N2");
if (PlayerPrefs.HasKey(key))
{
float best = PlayerPrefs.GetFloat(key);
if (best < time)
{
PlayerPrefs.SetFloat(key, time);
bestScoreTxt.text = time.ToString("N2");
}
else
{
bestScoreTxt.text = best.ToString("N2");
}
}
else
{
PlayerPrefs.SetFloat(key, time);
bestScoreTxt.text = time.ToString("N2");
}
endPanel.SetActive(true);
}
코드 오류 가능성을 줄여 안정성을 위해 bestscore를 key 변수에 할당한다.
PlayerScore에 key가 존재한다면, 최고 시간과 현재 시간을 비교한다. 먼저 best 변수를 생성하고 key 값을 가져와서 할당한다. 현재 시간이 최고 시간보다 더 크다면, key 변수에 현재 시간을 저장하고 bestScore 텍스트에 할당한다. 최고 시간이 현재 시간보다 더 크다면, 최고 시간을 bestScore에 할당한다.
PlayerScore에 key가 존재하지 않는다면, key 변수에 현재 시간을 저장하고 bestScore 텍스트에 할당한다.
[ 회고 ]
2주 차 강의는 주로 복습 차원에서 이전에 사용했던 코드들을 많이 사용했다. 개념이나 코드의 구성을 이해해 놓으니까 확실히 제작할 때 어려움이 덜 했다. 덕분에 오늘 하루 하나의 프로젝트를 제작할 수 있었지만, 컨디션 조절 실패로 그 이상은 공부하지 못했다. 현재로서는 이번 주 주말까지 포함해서 하루에 한 주차씩 프로젝트를 만드는 것을 목표로 하고 있다.
'Coding > Unity' 카테고리의 다른 글
[내일배움캠프 17일차 TIL] 게임 엔진, PPU, 계층 구조, Input.GetAxis, 직렬화 (0) | 2024.05.08 |
---|---|
[내일배움캠프 5일차 TIL] 복습, 체력바, isKinematic, isTrigger, 레벨 (1) | 2024.04.19 |
[내일배움캠프 3일차 TIL] UI, 싱글톤, 점수 적용, 게임 오버, 다시 시작 (0) | 2024.04.17 |
[내일배움캠프 2일차 TIL] 오브젝트 중력, 충돌, 파괴, 랜덤, 반복, 클래스 (0) | 2024.04.16 |
[내일배움캠프 1일차 TIL] 오브젝트, 애니메이션, 스크립트, 캐릭터 움직이기 (0) | 2024.04.15 |