유니티 2D 프로젝트

- 3D와 유의미한 차이가 있진 않음

- 이미지 파일을 스프라이트 타입으로 임포트

- 기본 생성 카메라가 직교 모드 사용

- 라이팅 설정 중 일부가 비활성화

- 씬 창이 2D 뷰로 보임

 

 

2D 컴포넌트

대부분 Vector2로 동작하거나 Vector3로 동작하되 z값을 무시

오브젝트의 실제 위치값은 Vector3로 저장됨 (z값이 무의미할뿐)

 

 

Sprite

Sprite Sheet: 여러 이미지를 하나의 이미지 파일로 합친 것

Sprite mode

 

싱글 스프라이트 모드: 기본적으로 이 모드로 이미지를 가져옴. 하나의 스프라이트 에셋은 하나의 스프라이트를 표현

멀티플 스프라이트 모드: 하나의 스프라이트 에셋을 여러 개의 개별 스프라이트로 잘라 사용할 수 있음

 

멀티플 스프라이트 모드 변환 후 스프라이트 자르기

인스펙터 창의 Sprite Editor -> Type: Grid By Cell Size -> Slice -> Apply (Delete Existing: 이전 변경 내용 삭제)

 

 

2D Animation 제작

Animation 창 실행

 

Create 클릭

 

스프라이트 복수 선택 후 끌어넣기

 

Animation 창의 메뉴(점 세개) -> Show Sample Rate

 

1초당 스프라이트가 16번 바뀌도록 속도 조절

 

 

움직이는 배경

배경으로 쓸 사진 'background'가 있다고 하자. 플레이어가 제자리에서 달리고, 배경이 움직이게 하고 싶다.

배경을 움직이는 건 간단하다. transform.Translate를 통해 평행이동 시키면 된다. 그런데 우리는 배경을 '반복'해서 움직이게 하고 싶다.

이를 위해 background 사진을 두 개 이어붙여, 이전 사진이 넘어가면 곧이어 다음 사진이 스크롤링 되게 하면 된다. (크랭키 박스와 유사)

 

첫 번째 사진
두 번째 사진

 

구현은 간단하다! 사진의 너비를 콜라이더를 통해 파악하고, 두 번째 사진의 Position의 x값을 그 너비만큼 설정해주면 바로 이어서 스크롤링 될 것이다. (스크롤링되는 스크립트는 따로 작성이 필요하다.)

 

또한 사진 재생이 완료되면 다른 사진의 뒤쪽으로 순간이동해야 한다. 사진의 너비를 w라고 하자.

사진의 위치의 x값이 -w가 되면, 곧바로 +w의 위치로 이동시키는 스크립트를 작성하면 해결된다.

이렇게 배경 반복 재생 구현이 완료되었다~!

 

 


출처: 레트로의 유니티 게임 프로그래밍 에센스(개정판)

'Unity > 개념' 카테고리의 다른 글

[Unity] 4. 공간과 움직임  (5) 2024.09.11
[Unity] 3. transform: 방향, 크기, 회전  (1) 2024.03.29
[Unity] 2. C#  (1) 2023.12.30
[Unity] 1. 유니티 엔진 동작 원리  (0) 2023.12.25

유니티 공간

하나의 '세상' 내에 위치를 표현하는 여러 가지 좌표계(공간)이 존재

 

로컬/글로벌 버튼 안 보임 문제: Windows -> Layout -> Default

피봇 - 오브젝트의 실제 중심

센터 - 눈에 보이는 중심

ex. 눈에 보이는 사람의 중심은 배꼽이지만, 실제 중심은 발과 발 사이

 

피봇을 바꿀 수 있는 법은 없나? - 직접적으로 바꾸는 방법은 없음. 자식 오브젝트 이용. 다만 2D에서는 Sprite Editor를 통해 바꿀 수 있는 듯 함

 

공간에는 전역 공간, 지역 공간, 오브젝트 공간이 있다.

 

전역 공간: 게임 월드의 원점을 기준으로 위치 표현. 좌표계가 고정되어 있어 모든 오브젝트에 대해 회전의 기준이 동일. 이 좌표계를 전역 좌표계라 함.

지역 공간: 부모 오브젝트의 좌표계를 기준으로 위치, 회전, 크기를 표현. 전역 공간에서의 크기가 2인 큐브가 전역 크기가 1인 큐브를 자식으로 가진다면, 이 자식 큐브의 지역 공간에서의 크기는 0.5가 된다.

오브젝트 공간: 오브젝트 자신의 좌표계(오브젝트 좌표계)가 기준. 오브젝트가 회전하면 오브젝트의 x축과 월드의 x축이 다를 수 있음.

 

전역 공간과 지역 공간은 알겠다. 부모 오브젝트가 없다면 두 공간이 서로 같다는 것도 이해했다.

근데 지역 공간이랑 오브젝트 공간은 뭐가 다르지?

 

오브젝트 공간은 항상 자신이 기준이 된다. 따라서 위치도, 회전도 항상 0이 된다.

공간 모드를 로컬로 설정하면, 씬 화면에는 오브젝트 좌표계가 표현된다.

인스펙터 창에 표시되는 오브젝트의 위치, 회전, 스케일(크기)은 모두 지역 공간에서 측정된 값이다.

유니티는 편의상 지역 공간과 오브젝트 공간을 합쳐 지역 공간으로 부르고 있다. (따라서 공간 모드를 로컬로 설정하면

평행이동에 대해서만 오브젝트 공간을 사용한다.

 

그렇군!

지역 공간만을 사용했다면 오브젝트를 x축 방향으로 움직엿을 때 '부모 오브젝트의 x축' 방향으로 움직여야 한다. 그러나 그렇게 되면 결국 모두 전역 공간의 x축 방향으로 움직이지 않겠는가? 그래서 오브젝트 공간의 개념을 썼다~

 

Transform 타입 제공 함수

평행이동 스크립트

Translate(): 입력으로 Vector3을 받고, 입력값만큼 해당 트랜스폼 컴포넌트를 이동시킴. 지역 공간 기준 평행이동(오브젝트 공간)

Translate(Vector3, Space.World) - 전역 공간 설정 가능

Translate(Vector3, Space.Self) - 지역 공간 설정

 

회전 스크립트

Rotate(): 입력으로 Vector3을 받아 입력값만큼 더 회전. 역시나 지역 공간 기준 회전

Rotate(Vector3, Space.World)

Rotate(Vector3, Space.Self)

 

벡터 연산으로 평행이동 구현

벡터의 속기(shorthand): 모두 크기가 1인 방향벡터

- Vector3.forward == new Vector3(0, 0, 1)

- Vector3.back == new Vector3(0, 0, -1)

- Vector3.right == new Vector3(1, 0, 0)

- Vector3.left == new Vector3(-1, 0, 0)

- Vector3.up == new Vector3(0, 1, 0)

- Vector3.down == new Vector3(0, -1, 0)

 

트랜스폼의 방향

- transform.forward: 전역 공간에서의 앞쪽 (z축 

- transform.right

- transform.up

=> 반대 방향은 -1 곱해서 사용

 

벡터 연산으로 평행이동

  • 자신의 앞쪽으로 평행이동: transform.position += transform * transform.up * 1;
    -> transform.Translate(new Vector3(0, 1, 0)와 동일
  • 전역 공간 앞쪽으로 평행이동: transform.position += Vector3.up * 1;
    -> transform.Translate(new Vector3(0, 1, 0), Space.World)와 동일

 

 


출처: 레트로의 유니티 게임 프로그래밍 에센스(개정판)

'Unity > 개념' 카테고리의 다른 글

[Unity] 5. 2D 게임  (1) 2024.09.25
[Unity] 3. transform: 방향, 크기, 회전  (1) 2024.03.29
[Unity] 2. C#  (1) 2023.12.30
[Unity] 1. 유니티 엔진 동작 원리  (0) 2023.12.25

문제 상황

오브젝트에 콜라이더 붙여줬는데 통과함 - Convex 켜줬더니 됨.

 

결론부터 말하자면 Mesh Collider는 또 다른 Mesh Collider와 충돌할 수 없다! Convex를 키면 이 문제가 해결된다.

그래서 Convex가 뭔데? 라는 의문점에서 시작해보는 글

 

Convex가 뭘까?

 

Convex는 '볼록한'이라는 뜻으로, Mesh Collider 컴포넌트에 존재한다. Mesh Collider는 뭔데! 이 참에 알아보기로 한다.

 

Mesh Collider

Mesh Collider는 기본 Collider에 비해 오브젝트의 모양과 일치하고, 그만큼 프로세서 부하도 크다.

Mesh Collider는 추가 당시 Mesh Filter의 Mesh 기반으로 생성된다.(Mesh Filter의 Mesh를 바꾼다고 자동 업데이트 되진 않는다. Reset 해도 안 바뀐다.)

 

Cube의 Mesh를 Capsule로 바꾼 후 Mesh Collider 추가, 다시 Cube Mesh로 바꾼 모습

 

Mesh Collider와 기본 Collider 비교

Capsule Mesh일 때의 Mesh Collider
Capsule Mesh일 때의 Capsule Collider

 

기본적으로 Collider는 Mesh 기반으로 생성되는 듯하다! Cube Mesh인 상태로 Capsule Collider를 추가하면 높이가 1로 생성되고, Capsule Mesh인 상태로 추가하면 높이가 2로 생성된다.

Cube Mesh일 때의 Capsule Collider

 

Convex

 

그래서 Convex는 뭐냐! 이 설정을 키면 말 그대로 '볼록'해진다. 원본 메쉬와 유사하지만 움푹 파인 부분이 모두 채워지게 된다.

다음 후라이팬 에셋의 예시를 보자.

 

Convex Off

 

Convex On

 

물체가 통과돼서 Convex를 켰었는데, 어쩐지 고기가 미끄러졌다. 이유를 알게 된,,,

참고로 Convex Mesh의 폴리곤은 255개로 제한된다고 한다.

 

이걸 켜야지만 Mesh Collider간의 충돌이 가능한데, 최적화의 문제가 가장 주된 이유다.

Concave(오목한) Mesh는 두 점을 잇는 직선이 물체 외부에 있을 수도 있기 때문에, 충돌 감지 연산량이 훨씬 많고 복잡하다. 와그렇겠네

따라서 두 점을 잇는 모든 직선이 물체 내에 있는 Convex Mesh를 쓰는 것이다!

Concave여도 trigger는 가능하다고 한다. 충돌 이벤트 처리는 성능 문제를 크게 유발하지 않기 때문이다.

 

더보기

GPT 짱

 

 

https://docs.unity3d.com/kr/2022.3/Manual/CollidersOverview.html

 

충돌 소개 - Unity 매뉴얼

Unity는 게임 오브젝트에 연결되어 물리적 충돌을 목적으로 게임 오브젝트의 모양을 정의하는 콜라이더로 게임 오브젝트 간의 충돌을 처리합니다.콜라이더는 보이지 않으며 게임 오브젝트의 메

docs.unity3d.com

 

https://learnandcreate.tistory.com/567

 

유니티에서 메시 콜라이더 사용하기(mesh collider, convex)

유니티에서 메시 콜라이더 사용하기(mesh collider, convex) 오브젝트의 메시와 유사한 형태의 콜라이더를 생성하여 기본적인 콜라이더(primitives - box collider, sphere collider, capsure collider)보다 정확한 충

learnandcreate.tistory.com

 

 


 

'Unity > 정리' 카테고리의 다른 글

[Unity] Script: 게임 오브젝트 이동  (4) 2024.08.04

분명 트랜스폼의 포지션은 떨어지는 중인데 오브젝트는 안 움직이는 상황 (중력 적용 중)

 

 

 

옆에 있는 Static 표시 때문이었다 (...)

 


 

게임 오브젝트를 움직이는 방법은 크게 두 가지가 있다.

  1. Transform 컴포넌트 이용
  2. 물리엔진 이용

Transform 컴포넌트를 이용하는 방법은 transform.position을 통해 물체의 위치를 직접 옮기는 반면,

물리엔진을 이용하는 방법은 물체에 힘을 가해 이동시킨다.

 

Input 받아오기


어떤 방법을 이용하든 우선 키보드의 인풋을 받아와야 한다. Input을 받는 방식은 두 가지가 있다:

  1. Input Manager (legacy)
  2. Input System (new)

Input System을 쓰는 법은 모르기 때문에 Input Manager을 쓸 것이다.

화살표 키 또는 wasd를 누르면 사용자가 움직이는 것을 가정했을 때, w와 s, 화살표의 상/하 키는 Vertical, a와 d, 화살표의 좌/우 키는 Horizontal로 인식해 플레이어를 움직이도록 한다.

Edit > Project Settings > Input Manager

Input Manager에 들어가보면 설정이 돼있을 것이다.

 

 

이제 C# 스크립트에서 Input을 감시해야 하는데, 플레이어가 화살표 키(또는 wasd)를 언제 누를지 모르기 때문에, Update 함수 내에서 감시할 것이다.

 

private float v;
private float h;

void Update()
{
    v = Input.GetAxis("Vertical");
    h = Input.GetAxis("Horizontal");
}

 

여기서 axis는 축이란 뜻으로, GetAxis는 파라미터로 입력된 문자열과 동일한 Input Axis의 현재 값을 가져오며, -1.0f ~ +1.0f 사이의 값을 반환한다. 만약 플레이어가 d 키를 누르고 있으면 h의 값은 0에서 +1.0f로 서서히 변할 것이고, s 키를 누르고 있으면 v의 값이 0에서 -1.0f로 서서히 변할 것이다.

Input 클래스는 말 그대로 플레이어의 입력을 처리해주는 클래스로, 키 입력 감지, 마우스 입력 감지, 축 입력 처리 등의 기능을 제공한다.

더보기

GPT의 친절한 설명

 

 

Transform 컴포넌트 이용


Transform 컴포넌트를 이용하는 방식도 여러가지지만, 가장 쉬운 방법은 transform.position을 직접 업데이트하는 것이다. (C# 스크립트에서 Transform과 transform은 다름에 유의하자.)

 

private float speed = 3.0f;

private float v;
private float h;

void Update()
{
    float h = Input.GetAxis("Horizontal");
    float v = Input.GetAxis("Vertical");
       
    transform.position += (Vector3.forward * v) + (Vector3.right * h) * speed * Time.deltaTime;
}

 

transform.position은 Vector3의 형태로, (x축의 위치, y축의 위치, z축의 위치)의 형식이다. h와 v를 직접적으로 새로운 Vector3에 넣어서 transform.position에 더해도 되겠지만, 전진 방향(z축 양의 방향)의 Vector3.forward와 우측 방향(x축의 양의 방향)의 Vector3.right를 이용하면 번거로움을 줄일 수 있다. Vector3.forward는 사실상 (0, 0, 1) 이며, Vector3.right은 (1, 0, 0) 이다.

 

speed의 값을 조절하며 속도를 조절할 수 있으며, Time.deltaTime은 컴퓨터마다 다른 사양에 의해 Update의 주기가 달라짐을 보완하고자 곱하는 값이다.

 

 

 

물리 엔진 이용


물리 엔진을 이용하는 방법은 스크립트가 부착될 게임 오브젝트, 즉 움직일 오브젝트에 Rigidbody 컴포넌트가 있어야 한다.

이 방식은 transform 컴포넌트를 이용하는 방식과 달리 오브젝트의 '위치'를 직접적으로 변경시키는 것이 아닌 '힘'을 가해 움직인다.

 

transform을 이용할 때는 -1.0과 1.0 사이의 값을 반환하는 GetAxis의 값을 직접적으로 현재 오브젝트의 위치에 더해주었다면,

물리 엔진을 이용하면 GetAxis를 통해 얻은 두 값을 이용해 벡터를 생성하고, 이 벡터의 방향과 크기를 통해 힘의 방향과 크기를 구한다.

 

private float speed = 5.0f;
private Rigidbody rb;

private float h;
private float v;

void Start()
{
    rb = GetComponent<Rigidbody>();
}


void Update()
{
    h = Input.GetAxis("Horizontal");
    v = Input.GetAxis("Vertical");

    Vector3 movement = new Vector3(h, 0.0f, v);
    
    rb.AddForce(movement * speed);
}

 

Rigidbody 컴포넌트를 이용하기 때문에 Start 함수에서 Rigidbody를 가져오고, h, v의 값을 이용해 새로운 벡터를 만들어준다. 이후 Rigidbody의 AddForce를 통해 물체에 힘을 가해주게 된다. 위 코드에서 speed를 이용해 힘의 크기를 조정해주었는데, 속도에 영향을 주긴 하나 편의상 이름을 speed로 했을뿐 실제 속도는 아니다.

 

 

 


틀린 내용 있으면 알려주세요 !!

'Unity > 정리' 카테고리의 다른 글

[Unity] Object 통과 현상 - Mesh Collider의 Convex  (1) 2024.08.09

벡터


게임에서 벡터의 정의: 공간상의 화살표. 방향, 크기를 가짐.

유니티: Vector2(x, y), Vector3(x, y, z), Vector4(x, y, z, w) - 원소 개별 접근/수정 가능. 구조체로 선언(값 타입)

 

위치

상대 위치: 현재 좌표에서 (x, y)만큼 이동

절대 위치: 게임 세상 속 좌표가 (x, y) - 화살표의 시작점이 원점 (0, 0)이라는 특수한 전제

 

크기

X = (x, y, z)

벡터의 크기 |X| = (x^2 + y^2 + z^2)^0.5

Vector3.magnitude;

 

속도

(x, y, z) = (정규화된 방향 벡터) * (속력)

 

벡터의 덧셈

A+B: A만큼 이동한 상태에서 B만큼 더 이동

 

벡터의 뺄셈

B-A: A에서 B까지의 방향, 거리

 

스칼라 곱

Vector3 * c (c는 스칼라);

 

벡터 정규화(방향 벡터 만들기)

Vector3.normalized;

 

벡터의 내적(점 연산)

상대 벡터를 자신의 지평선으로 끌어내렸을 때(투여되었을 때) 가지는 길이(내적 결과가 스칼라)

 

자신과 상대방 사이의 각이 벌어질수록 투영된 길이가 짧아지는 현상 이용 => 둘 사이의 각도 알 수 있음

Vector3.Dot(a, b) > 0: 각도 < 90도

Vector3.Dot(a, b) < 0: 각도 > 90도

Vector3.Dot(a, b)  == 0: 각도 == 90도

 

게임에서 두 물채 사이의 각도, 플레이어의 시선 방향/이동 방향 사이의 각도 등 쉽게 파악할 수 있음

 

벡터의 외적(벡터 곱, 교차 곱)

두 벡터 모두에 수직인 벡터(외적 결과가 벡터)

 

Vector3.Cross(a, b) == C,  Vector3.Cross(a, b)  == -C

 

a와 b가 평면 L에 있다고 하면, 벡터 C는 평면 L과 수직(노말 벡터, 법선 벡터)

: C를 정규화하면 평면 L의 방향으로 사용 가능

 

Vector3 응용

  • 두 점 사이 거리: (목적 위치 - 현재 위치).magnitude; / Vector3.Distance(현재 위치, 목적 위치);
  • 향하는 방향: (목적 위치 - 현재 위치).normalized;
  • 새 위치: 현재 위치 + 향하는 방향 * 이동 거리

 

쿼터니언


오일러각

3차원 공간을 3개의 각으로 나타냄. x, y, z축을 한 축씩 회전함 => 짐벌락 현상 발생 가능성!

 

짐벌락

축을 하나씩 회전하다 보면 두 축이 합쳐지는 현상 발생 => 한 축의 자유도 상실

 

쿼터니언

x, y, z 외에 w를 가짐. (사원수)

'한 번에' 회전 => 짐벌락 현상 X

유니티 코드 상에서 직접 생성/수정 X => Vector3 타입의 오일러각 등을 사용해 쿼터니언 생성하는 메소드 제공

 

쿼터니언 생성

Quaternion rotation = Quaternion.Euler(Vector3);

 

회전을 오일러각으로 가져오기

Vector3 eulerRotation = rotation.eulerAngles;

 

현재 회전에서 더 회전하기

Quaternion a = Quaternion.Euler(Vector3);

Quaternion b = Quaternion.Euler(Vector3);

 

Quaternion rotation = a*b; (행렬 연산)

 

 


출처: 레트로의 유니티 게임 프로그래밍 에센스(개정판)

'Unity > 개념' 카테고리의 다른 글

[Unity] 5. 2D 게임  (1) 2024.09.25
[Unity] 4. 공간과 움직임  (5) 2024.09.11
[Unity] 2. C#  (1) 2023.12.30
[Unity] 1. 유니티 엔진 동작 원리  (0) 2023.12.25

Game Over Panel

게임 오버 UI에서 텍스트의 배경이 되는 사각형이 자꾸 움직임.

사각형의 position은 안 바뀌는데 Canvas의 position이 게임 실행마다 유동적이라 그런 거 같음.

pivot이랑 anchor와도 관련이 있지 않나 싶은데 정확한 이유는 잘 모르겠음. ㅠ

그래서 그냥 Panel 만들어서 Gameover Text를 Panel의 자식으로 만들어버림.

오히려 처음 원하던 그림이라 괜찮다고 생각함!

(하도 했더니 수박 게임 개잘함)

 

게임 종료시 과일 터트리기

게임 종료되면 과일이 터지면서, 과일 종류에 따른 보너스 점수를 부여하고 싶었음.

근데 왜인지 자꾸 오류가 뜨는..ㅠㅠ

 

[ PopFruits.cs ]

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PopFruits : MonoBehaviour
{
    private GameObject fruits;
    private int children;

    private GameObject score;

    public int bonus = 0;

    void Awake()
    {
        fruits = GameObject.Find("Fruits");
        score = GameObject.Find("Score");
    }

    public void Pop()
    {
        children = fruits.transform.childCount;
        do
        {
            Transform kid = fruits.transform.GetChild(children - 1);

            switch (kid.name)
            {
                case "01_Cherry(Clone)": bonus += 1; break;
                case "02_Strawberry(Clone)": bonus += 2; break;
                case "03_Grape2(Clone)": bonus += 4; break;
                case "04_Orange(Clone)": bonus += 6; break;
                case "05_Apple(Clone)": bonus += 8; break;
                case "06_Pear(Clone)":  bonus += 10; break;
                case "07_Lemon(Clone)": bonus += 12; break;
                case "08_Peach(Clone)": bonus += 14; break;
                case "09_Pineapple(Clone)": bonus += 16; break;
                case "10_Coconut(Clone)": bonus += 18; break;
                case "11_Watermelon(Clone)": bonus += 20; break;
                default: break;
            }
            
            Destroy(kid.gameObject);
            children--;

        } while (children > 1);

    }
}

 

원래는

public void Pop()
    {
        do
        {
            children = fruits.transform.childCount;
            Transform kid = fruits.transform.GetChild(children - 1);

            switch (kid.name)
            {
                case "01_Cherry(Clone)": bonus += 1; break;
                case "02_Strawberry(Clone)": bonus += 2; break;
                case "03_Grape2(Clone)": bonus += 4; break;
                case "04_Orange(Clone)": bonus += 6; break;
                case "05_Apple(Clone)": bonus += 8; break;
                case "06_Pear(Clone)":  bonus += 10; break;
                case "07_Lemon(Clone)": bonus += 12; break;
                case "08_Peach(Clone)": bonus += 14; break;
                case "09_Pineapple(Clone)": bonus += 16; break;
                case "10_Coconut(Clone)": bonus += 18; break;
                case "11_Watermelon(Clone)": bonus += 20; break;
                default: break;
            }
            
            Destroy(kid.gameObject);

        } while (children > 1);

    }

이렇게 하려 했는데, 찾아보니까 childCount가 프레임 단위로 실행된다는 말이 있어서 함수 내에서 children을 줄이도록 수정함.

 

몇 번 수정해봤지만..! 지금으로서는 알아내기 힘든 원인일 거라 생각해 좀 미뤄두기로 함

나중에 좀 더 발전된 실력을 가지고 구현해보도록 하자!

 


 

과일 합치기, 위치 이동/낙하, 점수 UI, 게임 오버까지 구현한 상태 

 

<구현해야 할 기능>

  1. 같은 과일끼리 충돌하면 다음 단계 과일로 합쳐지는 기능
  2. 클릭시 원하는 위치로 이동 및 낙하
  3. 이전 과일 낙하 후 새 과일 생성
  4. 점수 UI
  5. 게임 진행도에 따른 과일 생성 확률 조정

[ MakeFruit2.cs ] 수정

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MakeFruit2 : MonoBehaviour
{
    private GameObject fruit;

    [SerializeField] private GameObject fruits;
    [SerializeField] private GameObject cherry;
    [SerializeField] private GameObject strawberry;
    [SerializeField] private GameObject grape;
    [SerializeField] private GameObject orange;
    [SerializeField] private GameObject apple;

    private GameObject instance;

    private int fruitNumber = 0;
    private int fruitCount = 0;
    private int boundaryPoint = 50;

    private bool drop = false;

    void Awake()
    {
        instance = Instantiate(cherry, fruits.transform.position, transform.rotation);
        instance.transform.parent = fruits.transform;
        drop = true;

        fruitCount = 1;
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            MoveFruit();
            drop = true;
        }
    }
    
    void Make()
    {
        fruitNumber = Random.Range(1, 15);

        if ( fruitCount <= boundaryPoint ) { Front(); }
        if ( fruitCount > boundaryPoint ) { Back(); }

        instance = Instantiate(fruit, fruits.transform.position, transform.rotation);
        instance.transform.parent = fruits.transform;

        fruitCount++;
    }

    void Front()
    {
        switch (fruitNumber)
        {
            case 1: case 2: case 3: case 4:
                fruit = cherry;
                break;
            case 5: case 6: case 7: case 8:
                fruit = strawberry;
                break;
            case 9: case 10: case 11:
                fruit = grape;
                break;
            case 12: case 13:
                fruit = orange;
                break;
            case 14:
                fruit = apple;
                break;
            default:
                break;
        }
    }

    void Back()
    {
        switch (fruitNumber)
        {
            case 1:
                fruit = cherry;
                break;
            case 2: case 3:
                fruit = strawberry;
                break;
            case 5: case 6: case 7:
                fruit = grape;
                break;
            case 8: case 9: case 10: case 11:
                fruit = orange;
                break;
            case 12: case 13: case 14:
                fruit = apple;
                break;
            default:
                break;
        }
    }

    private void MoveFruit()
    {
        Vector3 pos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        float clampedX = Mathf.Clamp(pos.x, -3.5f, 3.5f);		// 게임 컨테이너 내에서만 이동

        Vector3 clamPos = new Vector3(clampedX, instance.transform.position.y, 0);

        instance.transform.position = clamPos;

        if (instance.GetComponent<Rigidbody2D>() == null)
        {
            instance.AddComponent<Rigidbody2D>();
        }

        StartCoroutine("DropWait");
    }

    IEnumerator DropWait()
    {
        yield return new WaitForSeconds(1f);	// 낙하 대기 1초로 수정
        if (drop)				// 이전 과일이 낙하한 경우에만 새 과일 생성
        {
            drop = false;
            Make();
        }
    }
}

 

게임 진행도에 따른 과일 생성 확률

전반부(Front) 후반부(Back)로 나눠서 과일 50개 생성되는 때까지는 전반부, 그 이후는 후반부로 나눴다.

전반부에선 작은 과일이 더 빈번하게, 후반부에선 비교적 큰 과일이 더 빈번하게 생성되도록 코드를 수정했다.

 

과일 생성 위치

게임 화면에서 생성 범위 바깥을 클릭하면 과일이 외부에서 떨어져서 Clamp해주었다.

 

또 두 번이나 그 이상 클릭하면 과일이 계속 이동해서, 여러번 클릭해도 처음 위치에서 떨어질 수 있게

       

       Vector3 clamPos = new Vector3(clampedX, 0, 0);
       instance.transform.position += clamPos;  에서

       

       Vector3 clamPos = new Vector3(clampedX, instance.transform.position.y, 0);
       instance.transform.position = clamPos;  로 바꾸었다.

 

과일 생성 중복 현상 해결

클릭을 너무 빨리하면 과일이 떨어지지 못하고 겹쳐서 생성만 되는 현상이 발생했는데, 이를 해결하기 위해 drop 변수를 만들어서 이전 과일이 떨어졌을 때만 새 과일을 생성하도록 코드를 추가했다.

 

생성 속도도 너무 빠른가 싶어 0.5초에서 1초로 바꾸었다(DropWait)

 


 

과일 합치기, 위치 이동/낙하, 점수 UI까지 구현한 상태 

 

<구현해야 할 기능>

  1. 같은 과일끼리 충돌하면 다음 단계 과일로 합쳐지는 기능
  2. 클릭시 원하는 위치로 이동 및 낙하
  3. 이전 과일 낙하 후 새 과일 생성
  4. 점수 UI
  5. 게임 진행도에 따른 과일 생성 확률 조정

[ EndGame.cs ]

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using UnityEngine.SceneManagement;

public class EndGame : MonoBehaviour
{
    private float stayCount = 0;
    
    private bool gameover = false;

    public GameObject gameoverText;
    public TMP_Text recordText;

    private GameObject scoreText;
    private Score sco;

    private Color originalColor;
    private Color changeColor;
    private SpriteRenderer renderer;

    void Awake()
    {
        scoreText = GameObject.Find("ScoreText (TMP)");
        sco = scoreText.GetComponent<Score>();
        
        renderer = GetComponent<SpriteRenderer>();
        originalColor = new Color(255f / 255f, 133f / 255f, 240f / 255f);
        changeColor = new Color(250f / 255f, 136f / 255f, 0f / 255f);
    }

    void OnTriggerStay2D(Collider2D collider)
    {
        renderer.color = Color.cyan;

        stayCount += Time.deltaTime;
        Debug.Log("stayCount: " + stayCount);
    }

    void OnTriggerExit2D(Collider2D collider)
    {
        renderer.color = originalColor;
        stayCount = 0;
    }

    void Update()
    {
        if ( stayCount > 3 )
        {
            End();
        }
        
        if( gameover )
        {
            Debug.Log("Game Over");
            if (Input.GetKeyDown(KeyCode.R))
            {
                SceneManager.LoadScene("SampleScene");
            }
        }
    }

    public void End()           
    {
        gameover = true;

        gameoverText.SetActive(true);

        int score = sco.GetScore();

        float bestScore = PlayerPrefs.GetFloat("BestScore");     // 키 존재하지 않을시 기본값(0) 반환

        if (score > bestScore)
        {
            bestScore = score;
            PlayerPrefs.SetFloat("BestScore", bestScore);     // 로컬에 파일로 값 저장(키-값)
        }

        recordText.text = "Best Record: " + (int)bestScore;

    }
}

 

점수 UI 구현했으니, 게임 오버 화면 만들 거임

 

전에 교재에 있는 Dodge 게임 만들 때 썼던 코드 재탕해서 비교적 빨리 구현함!

(재탕: 게임 종료시 재시작 화면/최고기록 표시, 최고기록 저장, R 누르면 재시작)

 

생성 라인에 3초 이상 닿으면 게임이 끝나게 만들었는데,

가끔 3초 이상 머물렀는데 stayCount가 3초까지 안 가기도 함

왜 그런지 진짜 도저히 모르겠음..

색이 바뀌는 걸 보면 충돌은 제대로 된 게 맞는데 ㅠ (생성 라인에 닿으면 색깔이 바뀌게 만들었음)

나중에 알아내면 밝히는 걸로...!

 

또, rgb로 색깔 지정하려면 255로 나눠야한다고 함 (0~1 사이의 값으로 표현)

 

가끔 3초가 제대로 인식 안 되는 거 빼곤 얼렁뚱땅 완성!

 


 

 

과일 합치기, 클릭시 위치 이동 + 낙하 까지 구현한 상태 

 

<구현해야 할 기능>

  1. 같은 과일끼리 충돌하면 다음 단계 과일로 합쳐지는 기능
  2. 클릭시 원하는 위치로 이동 및 낙하
  3. 이전 과일 낙하 후 새 과일 생성
  4. 점수 UI
  5. 게임 진행도에 따른 과일 생성 확률 조정

[ Score.cs ]

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

public class Score : MonoBehaviour
{
    private int score = 0;
    private int prevCount = 0;
    private int presCount = 0;

    private GameObject fruits;

    public TMP_Text txt;

    void Awake()
    {
        score = 0;
        prevCount = 0;
        presCount = 0;

        fruits = GameObject.Find("Fruits");
        txt = GetComponent<TMP_Text>();
    }

    private void CheckInstance()
    {
        Transform instance = fruits.transform.GetChild(presCount - 1);	// Fruits의 마지막 인덱스 확인

        switch (instance.name)
        {
            case "02_Strawberry": score += 2; break;		// "02_Strawberry(Clone)"
            case "03_Grape2": score += 4; break;		// "03_Grape2(Clone)"
            case "04_Orange": score += 6; break;		// "04_Orange(Clone)"
            case "05_Apple": score += 8; break;			// "05_Apple(Clone)"
            case "06_Pear": score += 10; break;			// "06_Pear(Clone)"
            case "07_Lemon": score += 12; break;		// "07_Lemon(Clone)"
            case "08_Peach": score += 14; break;		// "08_Peach(Clone)"
            case "09_Pineapple": score += 16; break;		// "09_Pineapple(Clone)"
            case "10_Coconut": score += 18; break;		// "10_Coconut(Clone)"
            case "11_Watermelon": score += 20; break;		// "11_Watermelon(Clone)"
            default: break;
        }

    }

    void Update()
    {
        presCount = fruits.transform.childCount;

        if ( presCount < prevCount )
        {
            CheckInstance();
        }

        txt.text = "Score: " + score;

        StartCoroutine("RatingScore");		// 없어도 됨
        prevCount = presCount;
    }
    
    IEnumerator RatingScore()				// 없어도 됨
    {
        yield return new WaitForSeconds(0.5f);
    }
}

 

점수 측정을 위해선 합친 결과 어떤 과일이 만들어졌는지 알아야 해서 MakeFruit2와 Combine 코드 수정:

- 생성되는 모든 과일들을 Fruits 게임 오브젝트의 자식으로 설정

  => 합성으로 만들어진 과일은 항상 Fruits의 마지막 자식 오브젝트로 추가됨!

  => Fruits의 마지막 인덱스를 확인

- MakeFruit2와 Combine에서 instance 만드는 코드 뒤에 instance.transform.parent = fruits.transform; 추가

  => 생성되는 모든 과일들이 Fruits의 자식 오브젝트로 설정

 

처음엔 점수 변경이 안 됐음!

너무 빨리 측정돼서 계속 presCount == prevCount인가? 그래서 CheckInstance가 실행이 안 되나?

싶어서 RatingScore 추가 => 그래도 안 됨

 

Debug.Log 로 instance.name과 score 확인 => instance.name이 "02_Strawberry(Clone)" 와 같이 뜸

(Clone)이 문제였군!

 

나중에 RatingScore 빼봤는데 잘됨

ㅠ진짜 (Clone)이 문제였던 거임..

 

또, txt를 public으로 선언하거나 GetComponent으로 연결해야 함 그냥 txt를 텍스트 게임 오브젝트에 추가만 하면 텍스트 변경 안 됨

+ Recent posts