유니티 게임 프로토타입 연습 : 사과 받기

2021. 4. 4. 01:26·프로젝트

해당 글은 <유니티와 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 태그 추가
Apple에 "Apple" 태그 적용

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형으로 결과값을 반환

 

Physics - Layer Collision Matrix

게임 오브젝트 물리 충돌 조절

-> 레이어 설정

--> 인스펙터 - 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
'프로젝트' 카테고리의 다른 글
  • 유니티 게임 프로토타입 연습: 우주 슈팅 게임 (1)
  • 유니티 게임 프로토타입 연습: 성 부수기 (3) - 완성
  • 유니티 게임 프로토타입 연습 : 성 부수기 (2)
  • 유니티 게임 프로토타입 연습 : 성 부수기 (1)
김왈왈이
김왈왈이
  • 김왈왈이
    저장소
    김왈왈이
  • 전체
    오늘
    어제
  • 링크

    • GitHub
    • 분류 전체보기 (40)
      • TIL (1)
      • Spring (5)
      • Andriod (4)
      • Unity (2)
      • Java (1)
      • C++ (0)
      • HTML CSS JavaScript (1)
      • 프로젝트 (11)
      • 코딩테스트 (11)
      • 기타 (4)
  • 최근 글

  • 태그

    에러
    브루트포스 알고리즘
    silver iv
    구현
    게임프로토타입
    수학
    silver v
    C++
    UnassignedReferenceException
    그리디 알고리즘
    오류
    Quaternion.Euler()
    프로토타입
    [Serializable]
    게임개발
    유니티
    개발
    하위오브젝트반환
    유니티게임
    c#
    백준
    공격구현
    java
    단축키
    html
    springboot
    Unity
    경합상태오류
    게임
    자료구조
  • hELLO· Designed By정상우.v4.10.4
김왈왈이
유니티 게임 프로토타입 연습 : 사과 받기
상단으로

티스토리툴바