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 |
AudioClipis the audio file itself,AudioSourceacts as the speaker- Drag mp3 files to
appleSoundandbombSoundin 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 ashit.point(collision point),hit.collider(collided collider), andhit.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
OnTriggerEnteris called when a collider'sIs Triggeris enabled. It only detects overlap without physical response (bouncing, etc.).OnCollisionEnteris called when a physical collision occurs.
CompareTag vs tag ==
CompareTag()performs better thangameObject.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.UInamespace to accessTextcomponent - 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.deltaTimeevery frame to implement countdown nullcheck prevents errors when Text is not connectedToString()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
publicmethods soBasketControllercan call them - Bomb's
point /= 2halves 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 positionspeedis negative so items move downward on Y-axis (falling)- Declared as
publicsoItemGeneratorcan adjust the speed
Translate vs Directly Changing position
Translate()moves relative to the current position. Directly changingpositionmoves 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
minYis declared aspublicso 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.deltaTimeindeltaevery frame - Create item when
deltaexceedsspawninterval - Reset
deltato 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
CoroutinewithWaitForSeconds().
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
ItemControllerof 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
thiskeyword 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
Collidercomponent (check Is Trigger) - Add
ItemControllerscript - Set Tag to "Apple"
- Save as prefab (drag to Project window)
Bomb Prefab
- Place bomb.fbx model in scene
- Add
Collidercomponent (check Is Trigger) - Add
ItemControllerscript - Tag should NOT be "Apple" (default "Untagged")
- Save as prefab
Basket Object
- Place basket.fbx model in scene
- Add
Collidercomponent (check Is Trigger) - Add
AudioSourcecomponent - Add
BasketControllerscript - Connect
appleSound,bombSoundaudio clips in Inspector - Connect
gameManagerobject in Inspector
Stage Object
- Place stage.fbx model in scene
- Add
Collidercomponent (for Raycast floor detection)
Game Manager
- Create empty GameObject and name it "GameManager"
- Add
GameManagerscript - Connect
timerText,pointTextUI in Inspector
Item Generator
- Create empty GameObject
- Add
ItemGeneratorscript - Connect
applePrefab,bombPrefabprefabs 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.