해당 글은 <유니티와 C#으로 배우는 게임 개발 교과서>를 따라하면서 공부하는 과정을 담았다.
▶초기 아트 에셋 제작

Transform에 있는 Position, Rotation, Scale은 각각 컴포넌트의 위치, 회전, 배율값(크기)을 의미한다.
책에서는 이 컴포넌트의 설정을 설명할 때 Trunk(Cylinder) P:[0,0,0] R:[0,0,0] S:[1,1,1] 의 형식으로 설명하는 듯.
3D Object - Sylinder (원기둥)
3D Object - Sphere (구)
--> 이 둘을 이용해 나무(나무 기둥-Trunk+잎-Sphere) 표현 + 사과 표현
--> 빈 오브젝트(AppleTree)를 만든 후, 이 둘(나무 기둥+잎)을 함께 묶어줌. 이후 질감 적용 후 프리팹으로 만들기.
3D Object - Cube
--> Basket 만들고 프리팹 지정.
▶Apple 기본 설정
Rigidbody 컴포넌트 추가
-> 게임 오브젝트가 물리 효과에 반응할 수 있게 함.
-> Apple에 Rigidbody 추가 후 재생 버튼을 클릭하면 Apple이 중력에 의해 밑으로 떨어지는 것을 확인할 수 있음.
Apple에 "Apple" 태그 추가
->이후 화면에 나오는 모든 Apple 게임 오브젝트의 배열을 얻는 작업을 할 때, 이에 특정한 태그를 지정하면 이 작업이 좀 더 쉬워짐.
-> Apple 인스펙터에서 Tag 팝업 메뉴를 열고 Add Tag를 선택, Apple 태그를 추가함.
--> 책에서는 Tags 목록을 펼치니까 Size랑 이 밖에 다른 것들이 나오고 값을 바로 수정할 수 있는 형태로 보여지는데 이상하게 내 버전에서는 안되는듯.. 내가 못 찾은 걸 수도 있으니 나중에 다시 찾아볼 것.


Apple을 prefab으로 만든 후, Apple 오브젝트는 Hierarchy 뷰에서 삭제함.
-> 나중에 나오게 연출할 것이기 때문.
▶카메라 설정

투시 카메라
-> 사람의 눈과 비슷하게 작동.
-> 카메라와 가까운 오브젝트는 크게 보이고 멀리 있는 오브젝트는 작게 보임.
-> Camera - Projection - Perspective

직교 카메라
-> 오브젝트가 카메라에서 어느정도 거리에 있는지에 상관없이 똑같은 크기로 보임.
-> 메인 카메라 Inspector에서 카메라 컴포넌트를 찾은 후, Projection을 Orthographic으로 변경하면 볼 수 있음.
현재 프로젝트에서는 직교카메라를 사용함.
씬 뷰 오른쪽 위 모서리 방향축(?) 아래에 Persp를 클릭하면 투시와 등각(직교, iso) 씬 뷰 사이를 전환할 수 있음.
유니티에서의 1 유닛(Unit) = 1미터 길이
▶스크립트 작업
스크립팅 레퍼런스 확인 방법
-> 1) 유니티의 메뉴 표시줄 - Help - Scripting Reference 선택 - 로컬로 저장된 레퍼런스 열림
-> 2) 찾아보려는 부분의 텍스트를 선택 - 메뉴 표시줄의 Help - Unity API Reference 선택 - 온라인으로 열림
--> 스크립팅 레퍼런스의 모든 코드 예제는 기본적으로 JS 코드를 보여줌.
--> 팝업 메뉴나 C# 버튼을 눌러 해당 언어로 전환 가능.
--> MonoBehaviour를 검색해 이 스크립트에서 기본 제공하는 모든 메서드 확인 가능.
변수 구성
// Apple을 인스턴스화 하기 위한 프리팹
public GameObject applePrefap;
// AppleTree가 움직이는 속도(미터/초)
public float speed = 1f;
// AppleTree가 움직일 수 있는 좌우 거리
public float leftAndRightEdge = 10f;
// AppleTree가 방향을 바꾸는 확률
public float chanceToChangeDirections = 0.1f;
// Apple이 인스턴스화 되는 속도
public float secondsBetweenAppleDrops = 1f;
이후 speed는 유니티 인스펙터에서 10으로 설정해주었다.
AppleTree의 이동 스크립트 (시간을 기반한 이동)
// 기본적인 이동
Vector3 pos = transform.position;   // pos에 AppleTree의 현재 위치 넣음
pos.x += speed * Time.deltaTime;    // Time.deltaTime : 마지막 프레임 이후 경과한 시간[초]
transform.position = pos;           // AppleTree를 새 위치로 이동
처음에 맨 마지막 줄(새 위치로 이동시키는 과정)에서 자꾸 오류가 나길래 뭔가 싶었는데 transform을 fransform으로 오타가 나서 발생했었음,,
transform.position을 값 위치에 두면 -> 할당된 게임 오브젝트의 현재 위치값을 불러오고 ( get{} )
변수 위치에 두면 -> 할당된 게임 오브젝트의 위치값을 변경할 수 있는듯 ( set{} )
transform.position.x += speed * Time.deltaTime;
위와 같이 한 번에 코드를 작성하면 안되는 이유
-> transform.position.x와 같은 position 속성의 하위 컴포넌트 값들은 읽기만 가능하다. 값을 설정할 수는 없음.
-> transform.position이 실제 필드가 아니라 get{}, set{}을 통해 필드처럼 사용할 수 있게 만든 속성이기 때문.
움직임을 시간 기반으로 구현시(Time.deltaTime 활용)
-> 게임이 실행되는 프레임 속도에 관계없이 동일한 속도로 움직이게 할 수 있음.
-> 25fps로 실행되는 게임의 경우 Time.deltaTime은 0.04f이며, 각 프레임은 4/100초 동안 재생됨.
--> 위 부분 아직 정확히 이해 못 했음,, 따로 찾아보는 게 좋을듯.
// 방향 바꾸기
// 일정 범위 내를 벗어나면 방향 바꿔줌
if (pos.x < -leftAndRightEdge || pos.x > leftAndRightEdge)
	speed = -speed;
예제에서는 오른쪽 이동/왼쪽 이동을 각각 if-else if로 Mathf.Abs()를 사용하여 나누었는데,
그냥 한 번에 합쳐주어도 될 것 같아 합쳐서 코드를 작성했다.

실행해보니 잘 작동된다. 일정 범위(leftAndRightEdge)를 벗어나면 speed의 부호를 바꿔주어 방향을 바꿔주는 방식.
// 방향 바꾸기
// 일정 범위를 벗어나면 방향 바꾸기
// 일정 확률에 따라 방향 바꾸기
if (pos.x < -leftAndRightEdge || pos.x > leftAndRightEdge || Random.value < chanceToChangeDirections)
	speed = -speed;
Random.value를 불러와 일정 확률에 따라 방향이 바뀌는 것도 추가했다.
--> Random.value : 0부터 1까지 임의의 float 값을 반환
근데 막상 실행해 보니 방향이 너무..............너무 자주 바뀌게 됐다.
chanceToChangeDirections 변수값을 인스펙터에서 0.005로 변경했다. 그나마 적당한듯.
그런데 한 프레임마다 이를 검사해 방향을 변경하기에, 각 컴퓨터마다 검사(호출) 속도가 다를 수 있다는 말을 봤다.
이를 해결하기 위해 일정 확률로 방향을 변경하는 코드는 FixedUpdate()로 옮겨주었다.
-> FixedUpdate()는 컴퓨터의 속도와 관계없이 초당 50회 호출된다.
void FixedUpdate()
{
    // 일정 확률에 따라 방향 바꾸기
    // Random.value : 0부터 1까지 임의의 float 값을 반환
    if (Random.value < chanceToChangeDirections)
    	speed *= -1;
}
FixedUpdate()로 옮겨준 후, chanceToChangeDirections를 0.01로 변경했다.
Apple 떨어뜨리기 작업
void Start()
{
    // 사과를 1초마다 하나씩 떨어뜨림
    // InvokeRepeating이 DropApple()을 처음 호출하기 전에 2초간 기다리도록 지정(두번째 인수)
    // 처음 대기 시간이 지나면 secondsBetweenAppleDrops초마다 DropApple()을 다시 호출함(세번째 인수)
    InvokeRepeating("DropApple", 2f, secondsBetweenAppleDrops);
}
void DropApple()
{
	// Instantiate() : 게임 실행 도중 게임 오브젝트 생성
    GameObject apple = Instantiate(applePrefap) as GameObject;
    apple.transform.position = transform.position;
}
as 연산자
-> 형변환이 가능하면 형변환을 된 값을 반환하고, 그렇지 않으면 null 값을 반환
-> 단, 참조 타입간의 캐스팅에만 사용 가능
is 연산자
-> 형변환이 가능한 여부를 boolean형으로 결과값을 반환

게임 오브젝트 물리 충돌 조절
-> 레이어 설정
--> 인스펙터 - Layer 옆의 팝업 메뉴 - Add Layer - Layer 8~31에 원하는 레이어 추가
--> 메뉴 표시줄 - Edit - Project Settings - Physics - 아래쪽에 Layer Collision Matrix에서 레이어 간의 충돌 여부 설정
--> 각 오브젝트에 알맞은 레이어 할당
// 사과가 벗어나는 범위 설정
public static float bottomY = -20f;
void Update()
{
    if (transform.position.y < bottomY)
    	Destroy(this.gameObject);   // 사과 삭제
}
이후 떨어진 사과 인스턴스를 삭제하기 위해 Destroy() 사용 -> Apple 프리팹에 스크립트 적용
바구니 스크립트
void Update()
{
    // Input에서 마우스의 현재 화면 위치를 얻음
    Vector3 mousePos2D = Input.mousePosition;
    // 카메라의 z 위치만큼 3D 공간에서 마우스를 앞쪽으로 전진
    mousePos2D.z = -Camera.main.transform.position.z;
    // 2D 화면 공간의 지점을 3D 게임 세계 공간으로 변환
    Vector3 mousePos3D = Camera.main.ScreenToWorldPoint(mousePos2D);
    // 마우스의 x 위치로 이 바구니의 x 위치를 설정
    Vector3 pos = this.transform.position;
    pos.x = mousePos3D.x;
    this.transform.position = pos;
}
// OnCollisionEnter 메소드는 다른 게임 오브젝트가 Basket과 충돌할 때마다 호출됨
void OnCollisionEnter(Collision coll)
{
    // 이 바구니가 무엇과 충돌했는지 확인
    // 바구니와 충돌한 오브젝트의 참조를 포함한 정보가 전달됨
    GameObject collidedWith = coll.gameObject;
    if (collidedWith.tag == "Apple")
    	Destroy(collidedWith);
}
바구니 이동(마우스 포인터 기준) 및 바구니와 사과 충돌시 사과 삭제 구현
-> Basket에 스크립트 연결
public GameObject basketPrefab;
public int numBaskets = 3;
public float basketBottomY = -14f;
public float basketSpacingY = 2f;
void Start()
{
    for (int i = 0; i < numBaskets; i++)
    {
        GameObject tBasketGo = Instantiate(basketPrefab) as GameObject;
        Vector3 pos = Vector3.zero;     // (0,0,0)의 의미인듯..?
        pos.y = basketBottomY + (basketSpacingY * i);
        tBasketGo.transform.position = pos;
    }
}
-> 메인 카메라에 게임 매니저 연결
점수 구현
예제에서는 GUI Text로 이를 구현하라고 적혀있었는데, 버전이 달라서 그런지 GUI Text가 보이지 않았다.
대신 UI 메뉴 내에 있는 Text가 이를 대신할 수 있을 것 같아 시도해봤다.


그리고 그대로 GUIText 타입으로 변수를 생성하려 하니 역시나 오류가 발생했다.
using UnityEngine.UI;
public Text scoreT;
그래서 위와 같이 UnityEngine.UI를 불러온 후, Text 타입으로 진행하니 오류가 해결됐다.
UI.Text를 사용하라는 뜻이 먼저 UI 패키지(?)를 불러온 후 쓰라는 말인듯
Basket의 충돌 메소드에 점수 증가 추가, HighScore 비교 및 변경
// scoreT의 텍스트를 int로 구문 분석
int score = int.Parse(scoreT.text);
// 사과를 받은 점수를 추가
score += 100;
// 점수를 다시 문자열로 바꾸고 표시
scoreT.text = score.ToString();
// 기록 점수와 최고 기록을 비교, 변경
if (score > HighScore.score)
	HighScore.score = score;
HighScore 스크립트
public static int score = 1000;
void Update()
{
    Text tx = this.GetComponent<Text>();
    tx.text = "High Score : " + score;      // ToString()이 암시적으로 호출됨
}
씬 로드
-> Application.LoadLevel("씬 이름"); 도 먹히기는 하는데 더이상 업데이트 되지 않는 함수라고 함
-> using UnityEngine.SceneManagement 추가 후, SceneManager.LoadScene()을 사용하자.
--> 인수로는 index 혹은 씬 이름을 넣을 수 있음(String)
바구니 리스트 - Start 메소드에서 초기 바구니 추가
basketList = new List<GameObject>();
for (int i = 0; i < numBaskets; i++)
{
    GameObject tBasketGo = Instantiate(basketPrefab) as GameObject;
    Vector3 pos = Vector3.zero;     // (0,0,0)의 의미인듯..?
    pos.y = basketBottomY + (basketSpacingY * i);
    tBasketGo.transform.position = pos;
    basketList.Add(tBasketGo);
}
그 후 Basket의 참조를 얻어 사과를 못 잡으면 바구니가 하나씩 사라지는 것으로 구현함 (초기에 바구니 3개로 시작)
// Apple 스크립트
// 사과가 벗어나는 범위 설정
public static float bottomY = -20f;
void Update()
{
    if (transform.position.y < bottomY)
    {
        Destroy(this.gameObject);   // 사과 삭제
        // 메인 카메라의 ApplePicker 컴포넌트에 대한 참조를 얻음
        ApplePicker apScript = Camera.main.GetComponent<ApplePicker>();
        // apScript의 공용 AppleDestroyed() 메소드를 호출
        apScript.AppleDestroyed();
    }
}
// 메인카메라에 있는 게임 매니저 - ApplePicker 스크립트
public void AppleDestroyed()
{
    // 사과 태그를 이용해 모든 사과 tAppleArray 배열에 저장
    GameObject[] tAppleArray = GameObject.FindGameObjectsWithTag("Apple");
    // 사과 모두 삭제
    // tAppleArray 안의 모든 요소를 한 턴에 하나씩 순서대로 tGo에 집어넣음
    foreach (GameObject tGo in tAppleArray)
    	Destroy(tGo);
    // 바구니 하나를 삭제함
    // basketList에서 마지막 바구니의 인덱스를 얻음
    int basketIndex = basketList.Count - 1;
    // 해당 Basket 게임 오브젝트의 참조를 얻음
    GameObject tBasketGo = basketList[basketIndex];
    // 리스트에서 해당 바구니를 제거, 게임 오브젝트를 삭제함
    basketList.RemoveAt(basketIndex);
    Destroy(tBasketGo);
    // 게임을 재시작, HighScore.score에는 영향을 주지 않음
    if (basketList.Count == 0)
        SceneManager.LoadScene("_Scene_0");
        // Application.LoadLevel("_Scene_0");
}
최종 결과물


'프로젝트' 카테고리의 다른 글
| 유니티 게임 프로토타입 연습 : 우주 슈팅 게임 (2) (0) | 2021.05.02 | 
|---|---|
| 유니티 게임 프로토타입 연습: 우주 슈팅 게임 (1) (0) | 2021.04.25 | 
| 유니티 게임 프로토타입 연습: 성 부수기 (3) - 완성 (0) | 2021.04.18 | 
| 유니티 게임 프로토타입 연습 : 성 부수기 (2) (0) | 2021.04.10 | 
| 유니티 게임 프로토타입 연습 : 성 부수기 (1) (0) | 2021.04.08 |