Apple Catching Game

Implement a game where you move a basket with mouse clicks to catch falling apples and avoid bombs in Unity 3D

Let's create a game in Unity 3D where you move a basket with mouse clicks to catch apples falling from the sky and avoid bombs. We'll learn about Raycast-based mouse coordinate conversion, dynamic prefab instantiation, trigger collision handling, UI integration, and sound playback.


Game Structure

This game consists of four core scripts:

Script Role
BasketController Basket (player) movement, item collision handling, sound playback
GameManager Timer, score management, UI updates
ItemController Movement and auto-deletion of falling items (apples/bombs)
ItemGenerator Random generation of apple/bomb prefabs at regular intervals

Project Folder Structure

Assets/
├── 01.Scenes/       # Game scenes
│   └── GameScene.unity
├── 02.Scripts/      # C# scripts
│   ├── BasketController.cs
│   ├── GameManager.cs
│   ├── ItemController.cs
│   └── ItemGenerator.cs
├── 03.Models/       # 3D models
│   ├── apple.fbx
│   ├── basket.fbx
│   ├── bomb.fbx
│   └── stage.fbx
├── 04.Sounds/       # Sound effects
│   ├── get_se.mp3   # Apple pickup sound
│   └── damage_se.mp3 # Bomb hit sound
└── 05.Prefabs/      # Prefabs
    ├── applePrefab.prefab
    └── bombPrefab.prefab

BasketController - Basket Controller

A script that handles basket (player) movement and item collision processing.

Complete Code

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);
    }
}

Key Concepts Explained

1. AudioClip and AudioSource

public AudioClip appleSound;
public AudioClip bombSound;
AudioSource audioSource;

void Start()
{
    audioSource = GetComponent<AudioSource>();
}
Type Description
AudioClip Audio file to play (connected in Inspector)
AudioSource Component that plays audio
GetComponent<AudioSource>() Get AudioSource attached to current object
  • AudioClip is the audio file itself, AudioSource acts as the speaker
  • Drag mp3 files to appleSound and bombSound in the Inspector

2. Detecting Mouse Click Position with 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);
    }
}
Code Description
Camera.main.ScreenPointToRay() Convert screen coordinates to Ray in 3D space
Physics.Raycast(ray, out hit, Mathf.Infinity) Get information about where Ray collided
hit.point 3D coordinates where Ray collided
Mathf.RoundToInt() Round to integer (grid movement effect)

What is RaycastHit? A struct that holds the result of Physics.Raycast(). It contains information such as hit.point (collision point), hit.collider (collided collider), and hit.normal (normal vector of collision surface).

Raycast Movement Process

Mouse Click (Screen Coordinates)
    ↓
Camera.main.ScreenPointToRay()
    ↓
Physics.Raycast() → 3D coordinates where floor was hit
    ↓
Mathf.RoundToInt() → Round to integer (grid movement)
    ↓
Change basket position

3. Trigger Collision Handling

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);
}
Code Description
OnTriggerEnter(Collider other) Called when another object enters the trigger collider
CompareTag("Apple") Compare tag to check if it's an apple
PlayOneShot(clip) Play audio clip once (allows overlapping sounds)
GetComponent<GameManager>() Reference to GameManager script
Destroy(other.gameObject) Immediately delete the collided item

OnTriggerEnter vs OnCollisionEnter OnTriggerEnter is called when a collider's Is Trigger is enabled. It only detects overlap without physical response (bouncing, etc.). OnCollisionEnter is called when a physical collision occurs.

CompareTag vs tag == CompareTag() performs better than gameObject.tag == "Apple". It uses internally optimized comparison instead of string comparison, and raises an error when using a non-existent tag to prevent mistakes.


GameManager - Game Manager

A script that manages the timer and score, and updates the UI.

Complete Code

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";
    }
}

Key Concepts Explained

1. UI Text Connection

using UnityEngine.UI;

public Text timerText;
public Text pointText;
  • Must use UnityEngine.UI namespace to access Text component
  • Drag Text objects under Canvas in the Inspector to connect

2. Time Management

float time = 30.0f;

void Update()
{
    time -= Time.deltaTime;
    if (timerText != null)
        timerText.text = time.ToString();
}
  • Subtract Time.deltaTime every frame to implement countdown
  • null check prevents errors when Text is not connected
  • ToString() converts number to string for UI display

3. Score System

int point = 0;

public void GetApple()
{
    point += 100;    // Apple: +100 points
}

public void GetBomb()
{
    point /= 2;      // Bomb: halve score
}
Item Score Change Operation
Apple +100 points point += 100
Bomb Halve score point /= 2
  • Declared as public methods so BasketController can call them
  • Bomb's point /= 2 halves the current score as a penalty

ItemController - Item Controller

A script that handles the movement and auto-deletion of falling items (apples, bombs).

Complete Code

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);
        }
    }
}

Key Concepts Explained

1. Item Falling

public float speed = -0.03f;

void Update()
{
    transform.Translate(0f, speed, 0f);
}
  • transform.Translate(): Move relative to current position
  • speed is negative so items move downward on Y-axis (falling)
  • Declared as public so ItemGenerator can adjust the speed

Translate vs Directly Changing position Translate() moves relative to the current position. Directly changing position moves to absolute coordinates. For simple movement, Translate() is more concise.

2. Auto-Deletion

public float minY = -1.0f;

if (transform.position.y < minY)
{
    Destroy(gameObject);
}
  • Automatically deletes items when they go below the screen (y < minY)
  • Without deletion, items accumulate in memory causing performance degradation
  • minY is declared as public so it can be adjusted in the Inspector

ItemGenerator - Item Generator

A script that generates apples and bombs at random positions at regular intervals.

Complete Code

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;
        }
    }
}

Key Concepts Explained

1. Spawn Timer Implementation

float spawn = 1.0f;  // Spawn interval (seconds)
float delta = 0f;     // Accumulated elapsed time

void Update()
{
    delta += Time.deltaTime;
    if (delta > spawn)
    {
        delta = 0;
        // Create item ...
    }
}
  • Accumulate Time.deltaTime in delta every frame
  • Create item when delta exceeds spawn interval
  • Reset delta to 0 after creation

Why Use Update() Timer Instead of Coroutine? For simple repeated generation, accumulating time in Update() is intuitive. For more complex waiting logic, you can use Coroutine with WaitForSeconds().

2. Probability-Based Item Selection

public int ratio = 2;

int dice = Random.Range(1, 11);  // Random 1~10
if (dice <= ratio)
{
    item = Instantiate(bombPrefab);   // Create bomb
}
else
{
    item = Instantiate(applePrefab);  // Create apple
}
  • Random.Range(1, 11): Generate random integer from 1 to 10
  • When ratio = 2: Bomb 20% (1,2), Apple 80% (3~10)
  • Adjust difficulty by changing ratio
ratio Value Bomb Chance Apple Chance
1 10% 90%
2 20% 80%
5 50% 50%
8 80% 20%

3. Placing Items at Random Positions

int x = Random.Range(-1, 2);    // Random among -1, 0, 1
int z = Random.Range(-7, -5);   // Random among -7, -6
item.transform.position = new Vector3(x, 7, z);
  • X-axis: Random placement in range -1 to 1
  • Y-axis: 7 (starts high and falls down)
  • Z-axis: Random placement in range -7 to -5

4. Setting Speed of Generated Items

item.GetComponent<ItemController>().speed = speed;
  • Access ItemController of generated item to set falling speed
  • SetParameter() method allows changing spawn interval, ratio, and speed at once

5. SetParameter Method

public void SetParameter(float spawn, int ratio, float speed)
{
    this.spawn = spawn;
    this.ratio = ratio;
    this.speed = speed;
}
  • Method to dynamically change difficulty parameters from external scripts
  • this keyword distinguishes between parameters and member variables
  • Useful for gradually increasing difficulty as the game progresses

Unity Setup Checklist

Apple Prefab

  • Place apple.fbx model in scene
  • Add Collider component (check Is Trigger)
  • Add ItemController script
  • Set Tag to "Apple"
  • Save as prefab (drag to Project window)

Bomb Prefab

  • Place bomb.fbx model in scene
  • Add Collider component (check Is Trigger)
  • Add ItemController script
  • Tag should NOT be "Apple" (default "Untagged")
  • Save as prefab

Basket Object

  • Place basket.fbx model in scene
  • Add Collider component (check Is Trigger)
  • Add AudioSource component
  • Add BasketController script
  • Connect appleSound, bombSound audio clips in Inspector
  • Connect gameManager object in Inspector

Stage Object

  • Place stage.fbx model in scene
  • Add Collider component (for Raycast floor detection)

Game Manager

  • Create empty GameObject and name it "GameManager"
  • Add GameManager script
  • Connect timerText, pointText UI in Inspector

Item Generator

  • Create empty GameObject
  • Add ItemGenerator script
  • Connect applePrefab, bombPrefab prefabs in Inspector

UI Setup

  • Create Canvas
  • Add Timer Text object
  • Add Point Text object

Tag Setup

  • Edit > Project Settings > Tags and Layers
  • Add "Apple" tag
  • Apply "Apple" tag to apple prefab

Script Interactions

ItemGenerator
    │ Instantiate()
    ▼
ItemController ←── speed setting
    │ Falling movement
    ▼
BasketController (OnTriggerEnter)
    │ CompareTag("Apple")
    ├── Apple → GameManager.GetApple() → +100 points
    └── Bomb  → GameManager.GetBomb()  → Halve score
                    │
                    ▼
              GameManager
              (UI Update: time, score)

Key Concepts Summary

OnTriggerEnter vs OnCollisionEnter

Item OnTriggerEnter OnCollisionEnter
Condition Collider's Is Trigger enabled Is Trigger disabled
Physical Response None (overlap detection only) Yes (bouncing, pushing, etc.)
Parameter Collider other Collision collision
Use Case Item pickup, area entry Physical collision (walls, floors, etc.)

PlayOneShot vs Play

Method Description
PlayOneShot(clip) Play clip once, allows multiple simultaneous sounds
Play() Play clip set on AudioSource, only one at a time

Random.Range Integer vs Float

Call Return Range
Random.Range(1, 11) (int) 1, 2, 3, ..., 10 (excludes 11)
Random.Range(1.0f, 11.0f) (float) 1.0 ~ 11.0 (includes 11.0)

Summary

Item Content
Basket Movement Physics.Raycast() + Mathf.RoundToInt()
Item Falling transform.Translate(0, speed, 0)
Item Creation Instantiate(prefab) + Random.Range()
Collision Detection OnTriggerEnter() + CompareTag()
Sound Playback AudioSource.PlayOneShot(clip)
Score Management Call GameManager's public methods
UI Update Text.text = value.ToString()
Timer time -= Time.deltaTime
Probability Control Random.Range(1, 11) + ratio comparison
Difficulty Adjustment SetParameter(spawn, ratio, speed)

By combining these four scripts, you can implement a 3D casual game where you move a basket with mouse clicks to catch falling apples and avoid bombs.


← Back to Contents