사과 받기 게임

Unity 3D에서 마우스 클릭으로 바구니를 이동하여 떨어지는 사과를 받고 폭탄을 피하는 게임을 구현합니다

Unity 3D에서 마우스 클릭으로 바구니를 이동시켜 하늘에서 떨어지는 사과를 받고 폭탄을 피하는 게임을 만들어봅니다. Raycast를 활용한 마우스 좌표 변환, 프리팹 동적 생성, 트리거 충돌 처리, UI 연동, 사운드 재생 등을 학습합니다.


게임 구조

이 게임은 네 가지 핵심 스크립트로 구성됩니다:

스크립트 역할
BasketController 바구니(플레이어) 이동, 아이템 충돌 처리, 사운드 재생
GameManager 타이머, 점수 관리, UI 업데이트
ItemController 떨어지는 아이템(사과/폭탄)의 이동 및 자동 삭제
ItemGenerator 사과/폭탄 프리팹을 일정 간격으로 랜덤 생성

프로젝트 폴더 구조

Assets/
├── 01.Scenes/       # 게임 씬
│   └── GameScene.unity
├── 02.Scripts/      # C# 스크립트
│   ├── BasketController.cs
│   ├── GameManager.cs
│   ├── ItemController.cs
│   └── ItemGenerator.cs
├── 03.Models/       # 3D 모델
│   ├── apple.fbx
│   ├── basket.fbx
│   ├── bomb.fbx
│   └── stage.fbx
├── 04.Sounds/       # 효과음
│   ├── get_se.mp3   # 사과 획득 효과음
│   └── damage_se.mp3 # 폭탄 피격 효과음
└── 05.Prefabs/      # 프리팹
    ├── applePrefab.prefab
    └── bombPrefab.prefab

BasketController - 바구니 컨트롤러

바구니(플레이어)의 이동과 아이템 충돌 처리를 담당하는 스크립트입니다.

전체 코드

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

public class BasketController : MonoBehaviour
{
    public AudioClip appleSound;
    public AudioClip bombSound;
    AudioSource audioSource;
    public GameObject gameManager;

    void Start()
    {
        audioSource = GetComponent<AudioSource>();
    }

    void Update()
    {
        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        RaycastHit hit;

        if (Input.GetMouseButtonDown(0))
        {
            if (Physics.Raycast(ray, out hit, Mathf.Infinity))
            {
                float x = Mathf.RoundToInt(hit.point.x);
                float z = Mathf.RoundToInt(hit.point.z);
                transform.position = new Vector3(x, 0, z);
            }
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.gameObject.CompareTag("Apple"))
        {
            audioSource.PlayOneShot(appleSound);
            gameManager.GetComponent<GameManager>().GetApple();
        }
        else
        {
            audioSource.PlayOneShot(bombSound);
            gameManager.GetComponent<GameManager>().GetBomb();
        }
        Destroy(other.gameObject);
    }
}

핵심 개념 설명

1. AudioClip과 AudioSource

public AudioClip appleSound;
public AudioClip bombSound;
AudioSource audioSource;

void Start()
{
    audioSource = GetComponent<AudioSource>();
}
타입(type) 설명
AudioClip 재생할 오디오 파일 (Inspector에서 연결)
AudioSource 오디오를 재생하는 컴포넌트
GetComponent<AudioSource>() 현재 오브젝트에 부착된 AudioSource 가져오기
  • AudioClip은 음악 파일 자체, AudioSource는 스피커 역할
  • Inspector에서 appleSoundbombSound에 각각 mp3 파일을 드래그하여 연결

2. Raycast를 활용한 마우스 클릭 위치 감지

Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
RaycastHit hit;

if (Input.GetMouseButtonDown(0))
{
    if (Physics.Raycast(ray, out hit, Mathf.Infinity))
    {
        float x = Mathf.RoundToInt(hit.point.x);
        float z = Mathf.RoundToInt(hit.point.z);
        transform.position = new Vector3(x, 0, z);
    }
}
코드 설명
Camera.main.ScreenPointToRay() 화면 좌표를 3D 공간의 Ray로 변환
Physics.Raycast(ray, out hit, Mathf.Infinity) Ray가 충돌한 지점 정보를 가져옴
hit.point Ray가 충돌한 3D 좌표
Mathf.RoundToInt() 정수(integer)로 반올림 (그리드 이동 효과)

RaycastHit이란? Physics.Raycast()의 결과를 담는 구조체(struct)입니다. hit.point(충돌 지점), hit.collider(충돌한 콜라이더), hit.normal(충돌 면의 법선 벡터) 등의 정보를 포함합니다.

Raycast 이동 과정

마우스 클릭 (화면 좌표)
    ↓
Camera.main.ScreenPointToRay()
    ↓
Physics.Raycast() → 바닥과 충돌한 3D 좌표
    ↓
Mathf.RoundToInt() → 정수로 반올림 (그리드 이동)
    ↓
바구니의 position 변경

3. 트리거 충돌 처리

private void OnTriggerEnter(Collider other)
{
    if (other.gameObject.CompareTag("Apple"))
    {
        audioSource.PlayOneShot(appleSound);
        gameManager.GetComponent<GameManager>().GetApple();
    }
    else
    {
        audioSource.PlayOneShot(bombSound);
        gameManager.GetComponent<GameManager>().GetBomb();
    }
    Destroy(other.gameObject);
}
코드 설명
OnTriggerEnter(Collider other) 트리거 콜라이더에 다른 오브젝트가 진입 시 호출
CompareTag("Apple") 태그를 비교하여 사과인지 확인
PlayOneShot(clip) 오디오 클립을 한 번 재생 (중복 재생 가능)
GetComponent<GameManager>() GameManager 스크립트 참조
Destroy(other.gameObject) 충돌한 아이템 즉시 삭제

OnTriggerEnter vs OnCollisionEnter OnTriggerEnter는 콜라이더의 Is Trigger가 활성화된 경우 호출됩니다. 물리적 반응(튕기기 등) 없이 겹침만 감지합니다. OnCollisionEnter는 물리적 충돌이 발생할 때 호출됩니다.

CompareTag vs tag == CompareTag()gameObject.tag == "Apple"보다 성능이 좋습니다. 문자열(string) 비교 대신 내부적으로 최적화된 비교를 수행하며, 존재하지 않는 태그를 사용하면 에러(error)를 발생시켜 실수(float)를 방지합니다.


GameManager - 게임 매니저

타이머와 점수를 관리하고 UI를 업데이트하는 스크립트입니다.

전체 코드

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

public class GameManager : MonoBehaviour
{
    public Text timerText;
    public Text pointText;
    float time = 30.0f;
    int point = 0;

    public void GetApple()
    {
        point += 100;
    }

    public void GetBomb()
    {
        point /= 2;
    }

    void Update()
    {
        time -= Time.deltaTime;
        if (timerText != null)
            timerText.text = time.ToString();
        if (pointText != null)
            pointText.text = point.ToString() + " Point";
    }
}

핵심 개념 설명

1. UI Text 연결

using UnityEngine.UI;

public Text timerText;
public Text pointText;
  • UnityEngine.UI 네임스페이스(namespace)를 사용해야 Text 컴포넌트에 접근 가능
  • Inspector에서 Canvas 하위의 Text 오브젝트를 드래그하여 연결

2. 시간 관리

float time = 30.0f;

void Update()
{
    time -= Time.deltaTime;
    if (timerText != null)
        timerText.text = time.ToString();
}
  • Time.deltaTime을 매 프레임 빼서 카운트다운 구현
  • null 체크로 Text가 연결되지 않았을 때의 에러(error) 방지
  • ToString()으로 숫자를 문자열(string)로 변환하여 UI에 표시

3. 점수 시스템

int point = 0;

public void GetApple()
{
    point += 100;    // 사과: +100점
}

public void GetBomb()
{
    point /= 2;      // 폭탄: 점수 절반
}
아이템 점수 변화 연산
사과 +100점 point += 100
폭탄 점수 절반 point /= 2
  • public 메서드(method)로 선언하여 BasketController에서 호출 가능
  • 폭탄의 point /= 2는 페널티로 현재 점수를 반으로 줄임

ItemController - 아이템 컨트롤러

떨어지는 아이템(사과, 폭탄)의 이동과 자동 삭제를 담당하는 스크립트입니다.

전체 코드

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

public class ItemController : MonoBehaviour
{
    public float speed = -0.03f;
    public float minY = -1.0f;

    void Update()
    {
        transform.Translate(0f, speed, 0f);

        if (transform.position.y < minY)
        {
            Destroy(gameObject);
        }
    }
}

핵심 개념 설명

1. 아이템 낙하

public float speed = -0.03f;

void Update()
{
    transform.Translate(0f, speed, 0f);
}
  • transform.Translate(): 현재 위치에서 상대적으로 이동
  • speed가 음수이므로 Y축 아래 방향으로 이동 (낙하)
  • public으로 선언하여 ItemGenerator에서 속도 조절 가능

Translate vs position 직접 변경 Translate()은 현재 위치를 기준으로 상대 이동합니다. position을 직접 변경하면 절대 좌표로 이동합니다. 단순한 이동에는 Translate()이 더 간결합니다.

2. 자동 삭제

public float minY = -1.0f;

if (transform.position.y < minY)
{
    Destroy(gameObject);
}
  • 아이템이 화면 아래로 벗어나면(y < minY) 자동 삭제
  • 삭제하지 않으면 메모리(memory)에 계속 누적되어 성능 저하 발생
  • minYpublic으로 선언하여 Inspector에서 조절 가능

ItemGenerator - 아이템 생성기

사과와 폭탄을 일정 간격으로 랜덤 위치에 생성하는 스크립트입니다.

전체 코드

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

public class ItemGenerator : MonoBehaviour
{
    public GameObject applePrefab;
    public GameObject bombPrefab;
    float spawn = 1.0f;
    float delta = 0f;

    public int ratio = 2;
    float speed = -0.03f;

    public void SetParameter(float spawn, int ratio, float speed)
    {
        this.spawn = spawn;
        this.ratio = ratio;
        this.speed = speed;
    }

    void Update()
    {
        delta += Time.deltaTime;
        if (delta > spawn)
        {
            delta = 0;
            GameObject item;
            int dice = Random.Range(1, 11);
            if (dice <= ratio)
            {
                item = Instantiate(bombPrefab);
            }
            else
            {
                item = Instantiate(applePrefab);
            }
            int x = Random.Range(-1, 2);
            int z = Random.Range(-7, -5);
            item.transform.position = new Vector3(x, 7, z);
            item.GetComponent<ItemController>().speed = speed;
        }
    }
}

핵심 개념 설명

1. 스폰 타이머 구현

float spawn = 1.0f;  // 생성 간격 (초)
float delta = 0f;     // 경과 시간 누적

void Update()
{
    delta += Time.deltaTime;
    if (delta > spawn)
    {
        delta = 0;
        // 아이템 생성 ...
    }
}
  • delta에 매 프레임 Time.deltaTime을 누적
  • deltaspawn 간격을 초과하면 아이템 생성
  • 생성 후 delta를 0으로 초기화

코루틴 대신 Update() 타이머를 사용하는 이유 간단한 반복 생성은 Update()에서 시간을 누적하는 방식이 직관적입니다. 더 복잡한 대기 로직이 필요하면 CoroutineWaitForSeconds()를 사용할 수 있습니다.

2. 확률 기반 아이템 선택

public int ratio = 2;

int dice = Random.Range(1, 11);  // 1~10 랜덤
if (dice <= ratio)
{
    item = Instantiate(bombPrefab);   // 폭탄 생성
}
else
{
    item = Instantiate(applePrefab);  // 사과 생성
}
  • Random.Range(1, 11): 1부터 10까지의 정수(integer) 랜덤 생성
  • ratio = 2일 때: 폭탄 20% (1,2), 사과 80% (3~10)
  • ratio를 변경하여 난이도 조절 가능
ratio 값 폭탄 확률 사과 확률
1 10% 90%
2 20% 80%
5 50% 50%
8 80% 20%

3. 랜덤 위치에 아이템 배치

int x = Random.Range(-1, 2);    // -1, 0, 1 중 랜덤
int z = Random.Range(-7, -5);   // -7, -6 중 랜덤
item.transform.position = new Vector3(x, 7, z);
  • X축: -1 ~ 1 범위에서 랜덤 배치
  • Y축: 7 (높은 위치에서 시작하여 낙하)
  • Z축: -7 ~ -5 범위에서 랜덤 배치

4. 생성된 아이템의 속도 설정

item.GetComponent<ItemController>().speed = speed;
  • 생성된 아이템의 ItemController에 접근하여 낙하 속도 설정
  • SetParameter() 메서드(method)로 생성 간격, 비율, 속도를 한번에 변경 가능

5. SetParameter 메서드(method)

public void SetParameter(float spawn, int ratio, float speed)
{
    this.spawn = spawn;
    this.ratio = ratio;
    this.speed = speed;
}
  • 외부에서 난이도 파라미터를 동적으로 변경할 수 있는 메서드(method)
  • this 키워드로 매개변수(variable)(parameter)와 멤버 변수(variable)를 구분
  • 게임 진행에 따라 난이도를 점점 올릴 때 활용

Unity 설정 체크리스트(list)

사과 프리팹 (Apple Prefab)

  • apple.fbx 모델을 씬에 배치
  • Collider 컴포넌트 추가 (Is Trigger 체크)
  • ItemController 스크립트 추가
  • Tag를 "Apple"로 설정
  • 프리팹으로 저장 (Project 창으로 드래그)

폭탄 프리팹 (Bomb Prefab)

  • bomb.fbx 모델을 씬에 배치
  • Collider 컴포넌트 추가 (Is Trigger 체크)
  • ItemController 스크립트 추가
  • Tag는 "Apple"이 아닌 다른 값 (기본 "Untagged")
  • 프리팹으로 저장

바구니 오브젝트 (Basket)

  • basket.fbx 모델을 씬에 배치
  • Collider 컴포넌트 추가 (Is Trigger 체크)
  • AudioSource 컴포넌트 추가
  • BasketController 스크립트 추가
  • Inspector에서 appleSound, bombSound 오디오 클립 연결
  • Inspector에서 gameManager 오브젝트 연결

스테이지 오브젝트 (Stage)

  • stage.fbx 모델을 씬에 배치
  • Collider 컴포넌트 추가 (Raycast 바닥 감지용)

게임 매니저 (GameManager)

  • 빈 GameObject 생성 후 이름을 "GameManager"로 설정
  • GameManager 스크립트 추가
  • Inspector에서 timerText, pointText UI 연결

아이템 생성기 (ItemGenerator)

  • 빈 GameObject 생성
  • ItemGenerator 스크립트 추가
  • Inspector에서 applePrefab, bombPrefab 프리팹 연결

UI 설정

  • Canvas 생성
  • Timer Text 오브젝트 추가
  • Point Text 오브젝트 추가

태그 설정

  • Edit > Project Settings > Tags and Layers
  • "Apple" 태그 추가
  • 사과 프리팹에 "Apple" 태그 적용

스크립트 간 상호작용

ItemGenerator
    │ Instantiate()
    ▼
ItemController ←── speed 설정
    │ 낙하 이동
    ▼
BasketController (OnTriggerEnter)
    │ CompareTag("Apple")
    ├── 사과 → GameManager.GetApple() → +100점
    └── 폭탄 → GameManager.GetBomb() → 점수 절반
                    │
                    ▼
              GameManager
              (UI 업데이트: 시간, 점수)

주요 개념 정리

OnTriggerEnter vs OnCollisionEnter

항목 OnTriggerEnter OnCollisionEnter
조건 콜라이더의 Is Trigger 활성화 Is Trigger 비활성화
물리 반응 없음 (겹침만 감지) 있음 (튕기기, 밀기 등)
매개변수(variable)(parameter) Collider other Collision collision
용도 아이템 획득, 영역 진입 물리적 충돌 (벽, 바닥 등)

PlayOneShot vs Play

메서드(method) 설명
PlayOneShot(clip) 클립을 한 번 재생, 동시에 여러 소리 재생 가능
Play() AudioSource에 설정된 클립 재생, 한 번에 하나만

Random.Range 정수(integer) vs 실수(float)

호출 반환(return) 범위
Random.Range(1, 11) (정수(integer)) 1, 2, 3, ..., 10 (11 미포함)
Random.Range(1.0f, 11.0f) (실수(float)) 1.0 ~ 11.0 (11.0 포함)

요약

항목 내용
바구니 이동 Physics.Raycast() + Mathf.RoundToInt()
아이템 낙하 transform.Translate(0, speed, 0)
아이템 생성 Instantiate(prefab) + Random.Range()
충돌 감지 OnTriggerEnter() + CompareTag()
사운드 재생 AudioSource.PlayOneShot(clip)
점수 관리 GameManagerpublic 메서드(method) 호출
UI 업데이트 Text.text = value.ToString()
타이머 time -= Time.deltaTime
확률 제어 Random.Range(1, 11) + ratio 비교
난이도 조절 SetParameter(spawn, ratio, speed)

이 네 가지 스크립트를 조합하면 마우스 클릭으로 바구니를 이동시켜 떨어지는 사과를 받고 폭탄을 피하는 3D 캐주얼 게임을 구현할 수 있습니다.


← 목차로 돌아가기