본문 바로가기
Unity 궁둥이 쪼물쪼물

005. 발사체(Projectile) 구현

by daco2020 2026. 4. 6.

마우스 클릭 시 화면 중앙(크로스헤어) 방향으로 총알을 발사하고,
적에 맞으면 죽는 기능을 구현한 과정을 기록한다.

 

 

1. 두 개의 스크립트 추가

PlayerShooting.cs 마우스 클릭 감지 → 총알 생성 → 발사 Player 오브젝트
Bullet.cs 날아가기 → 충돌 감지 → 사라지기 Bullet 프리팹

 

(프리팹은 밑에서 설명)

 

2. Fire 액션 추가 (Input Actions)

지난번 Move, Jump, Sprint를 만들었던 것처럼 Fire 액션을 추가했다.

 

설정은

  • Action 이름: Fire
  • Action Type: Button
  • Binding: Left Button [Mouse]

 

 

3. Bullet 프리팹 만들기

프리팹(Prefab)이란?

동일한 오브젝트를 다시 생성할 수 있도록 만든 오브젝트 템플릿(틀)을 의미한다. 여기서는 총알 하나를 만들어두고 발사할 때마다 복제된 총알이 나가도록 할 것이다.

만드는 과정

  1. Hierarchy → 3D Object → Sphere
  2. Scale: 0.2, 0.2, 0.2 (작은 총알 크기)
  3. Rigidbody 추가 → Use Gravity 해제
  4. Bullet.cs 스크립트 추가
  5. Hierarchy에서 Project 탭 Assets/Prefabs로 드래그 → 프리팹 생성!
  6. Hierarchy에서 원본 삭제

 

 

Use Gravity 해제 이유

Use Gravity 켜면:
  발사! → → → ↘ ↘ ↓ ↓  (포물선으로 떨어짐)

Use Gravity 끄면:
  발사! → → → → → → →  (직선으로 날아감)

 

일단 이번 프로젝트에서는 직선으로 그대로 날아가도록 해보았다.

 

 

 

4. PlayerShooting.cs - 발사 로직

Raycast (레이캐스트)

레이캐스트는 눈에 안 보이는 광선을 쏴서 어디를 겨냥하고 있는지 찾는 것이다.

 

카메라 ──── Ray(광선) ────→ 벽에 부딪힘 (hit.point = 조준 위치)
  👁️                        🎯

화면 중앙에서 앞으로 보이지 않는 선을 쏜다.
뭔가에 부딪히면 → 그 지점이 조준 위치
아무것도 없으면 → 100m 앞 허공

코드 흐름은 읽기 → 계산 → 적용

// === 1단계: 읽기 — 어디를 조준하는지 찾기 ===
Ray ray = Camera.main.ViewportPointToRay(new Vector3(0.5f, 0.5f, 0f));
// (0.5, 0.5) = 화면 정중앙

if (Physics.Raycast(ray, out hit))
    targetPoint = hit.point;       // 부딪힌 지점
else
    targetPoint = ray.GetPoint(100f); // 허공이면 100m 앞

// === 2단계: 계산 — 발사 방향 구하기 ===
Vector3 fireDirection = (targetPoint - firePoint.position).normalized;
// 004 문서에서 배운 벡터 뺄셈: 목표 - 나 = 나→목표 방향

// === 3단계: 적용 — 총알 생성 및 발사 ===
Vector3 spawnPosition = firePoint.position + fireDirection * 1.5f;
GameObject bullet = Instantiate(bulletPrefab, spawnPosition, Quaternion.LookRotation(fireDirection));
rb.linearVelocity = fireDirection * bulletSpeed;

 

 

Fire Point란?

"총알이 어디서 생성될지" 위치. Main Camera를 연결하면 카메라 위치에서 발사된다.

우측 하단에 Fire Point 가 있다

 

 

 

5. Bullet.cs - 총알의 동작

// 활성화되면 3초 후 자동 파괴 예약
private void OnEnable()
{
    Invoke(nameof(Deactivate), lifeTime);
}

// 뭔가에 부딪히면
private void OnCollisionEnter(Collision collision)
{
    // Player는 무시 (자기 자신을 쏘면 안 되니까)
    if (collision.gameObject.CompareTag("Player")) return;

    // Enemy면 죽이기 (STEP 1에서 만든 메서드!)
    Enemy enemy = collision.gameObject.GetComponent<Enemy>();
    if (enemy != null)
        enemy.Die();

    // 총알 파괴
    Deactivate();
}

총알의 생명주기

생성 → 날아감 → 뭔가에 부딪힘 → 사라짐
                  또는
              3초 지남 → 자동으로 사라짐

 

사용자가 마우스 좌클릭을 하면 다음처럼 오브젝트가 복제되어 생성된다!

 

 

 

6. 이슈 기록

이슈 1. 화면에 총알이 안 보이고 플레이어가 떠오름

원인: 총알이 카메라 위치(Player 안쪽)에서 생성되면서 Player의 CharacterController와 충돌 → 서로 밀어냄

해결:

  1. 총알 생성 위치를 카메라 앞 1.5m로 이동: firePoint.position + fireDirection * 1.5f
  2. Bullet.cs에서 Player 태그와 충돌 시 무시: CompareTag("Player") return

 

이슈 2. 총알이 벽을 뚫고 지나감 (터널링)

원인: 총알이 너무 빨라서 프레임 사이에 벽을 통과

프레임1: ●─────벽─────
프레임2: ─────벽─────●  (벽을 뛰어넘음!)

 

해결: Bullet 프리팹의 Rigidbody에서 Collision Detection Discrete  Continuous로 변경

 

  • Discrete(이산): 프레임마다 위치만 확인 → 총알이 빠르면 통과될 수 있음
  • Continuous(연속): 프레임 사이 경로도 확인 → 정확하게 충돌 감지

 

이슈 3. 총알이 적(오리)을 통과함

원인: Enemy 오브젝트에 Collider가 없었다!

 

 

충돌 판정이 일어나려면 Collider(충돌 영역)가 있어야 한다.
벽(Cube)은 만들 때 자동으로 Box Collider가 붙지만, 외부 모델은 자동으로 안 붙는 경우가 있다.

 

해결: Enemy에 Capsule Collider 오리 크기에 맞게 조절하여 추가

 

 

7. 충돌 판정의 조건 정리

유니티에서 두 오브젝트가 충돌하려면 다음의 조건을 충족해야 한다.

오브젝트 A: Collider 필요
오브젝트 B: Collider + Rigidbody 필요 (둘 중 하나에 Rigidbody가 있어야 함)

 

여기에 더해, 둘 중 하나라도 Is Trigger = true면 물리 충돌(밀림/튕김)은 발생 하지 않는다. Is Trigger 는 "이 콜라이더는 물리 계산에서 제외하고 이벤트만 받을게"라는 뜻이기 때문이다. 

 

단, Trigger여도 겹쳤다는 사실 자체는 감지된다. 하나라도 Trigger면 '충돌'이 아니라 '통과 + 이벤트'로 바뀌는 셈.

 

이번 프로젝트의 경우에는 다음과 같다.

  • Bullet: Sphere Collider + Rigidbody ✅
  • Enemy: Capsule Collider ✅ (Bullet에 Rigidbody가 있으니 OK)
  • 벽(Cube): Box Collider ✅ (자동 생성됨)

 

 

8. 이번 작업에서 배운 것 요약

  1. Raycast = 보이지 않는 광선으로 조준 위치를 찾는 기술
  2. Instantiate = 프리팹을 복제해서 씬에 생성
  3. 총알 생성 위치를 Player 바깥으로 해야 자기 충돌 방지
  4. Collision Detection: Continuous = 빠른 물체의 터널링 방지
  5. 충돌에는 양쪽 모두 Collider가 필요 (+ 한쪽은 Rigidbody 필수)
  6. 외부 모델은 Collider가 자동으로 안 붙을 수 있으므로 확인 후 추가해주자.
  7. CompareTag()로 특정 대상과의 충돌을 무시할 수 있다

 

 

지금은 총알을 Instantiate()로 매번 복제 생성하고 Destroy()로 파괴한다. 다음에는 총알을 매번 생성하는게 아니라 "미리 만들어두고 재사용"하는 방식으로 성능을 개선해보자.