Unity 사전캠프 10일차 - 게임 요소 간단하게 구현해보기

한예준·2025년 3월 28일

Unity - 투사체 시스템 구현

나는 오버워치2의 투사체 시스템을 2D Unity를 통해 직접 구현해보기로 했다. 땅 혹은 벽에 맞으면 사라지고, 적에게 맞으면 사라지며 데미지를 입히고, 아군에게 맞으면 회복시킬 수 있는 기능 등을 구현할 예정이다. 일단 오늘은 스페이스 바를 통해 발사하고, 아군에게 맞으면 관통하며, 적에게 맞으면 체력이 닳고, 이를 체력 UI를 통해 확인할 수 있도록 하겠다.


전체 구현 순서

  1. 플레이어 생성: 투사체를 발사할 주체를 생성하고 키 입력을 처리한다.
  2. 아군/적군 생성: 충돌 테스트용 유닛을 생성하고 체력 수치를 포함한 스크립트를 작성한다.
  3. 오브젝트 배치/바닥 구성: 유닛과 플레이어를 배치하고, 그들이 설 수 있는 바닥을 구성한다.
  4. 투사체 프리팹 생성: Rigidbody2D와 Collider2D를 가진 Bullet 오브젝트를 프리팹으로 만든다.
  5. 투사체 발사 스크립트 작성: Shooter 스크립트를 통해 스페이스바 입력 시 Bullet이 발사되도록 구현한다.
  6. 충돌 처리 및 데미지 전달: Bullet이 Enemy나 Ally와 충돌했을 때 데미지를 주거나 무시하는 로직을 구현한다.
  1. 체력 UI 구성: Slider와 HealthUI 스크립트를 활용하여 유닛 머리 위에 체력 UI를 고정해 표시한다.
  2. Ally 회복 기능 구현: Ally가 체력을 회복할 수 있도록 Heal 메서드를 추가한다.

Step 1. 플레이어 오브젝트 생성

  • Hierarchy에서 Create Empty → 이름: Player
  • 자식 오브젝트로 Sprite 추가 → 이름: PlayerSprite
  • Scale: 0.5 / Rigidbody2D: Kinematic, Gravity Scale: 0 / Collider2D 추가
public class PlayerController : MonoBehaviour
{
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Debug.Log("공격!");
        }
    }
}

Step 2. 아군 및 적군 오브젝트 생성

Ally

  • 2D Object > Sprite, 파란색, Collider 추가, Tag는 "Ally"

Enemy

  • 빨간색, Collider 추가, Tag는 "Enemy"
  • 데미지 처리 스크립트:
public class Enemy : MonoBehaviour
{
    public int hp = 3;
    public int maxHp = 3;
    public Slider hpSlider;

    void Start()
    {
        if (hpSlider != null)
        {
            hpSlider.maxValue = maxHp;
            hpSlider.value = hp;
        }
    }

    public void OnHit(Bullet bullet)
    {
        hp -= bullet.damage;

        if (hpSlider != null)
            hpSlider.value = hp;

        if (hp <= 0)
        {
            Destroy(gameObject);
        }
    }
}

public Slider hpSlider;?

  • 이후 해당 Enemy(또는 Ally) 유닛의 체력을 UI 상의 Slider로 표시하기 위한 연결 고리이다.
    GameManager에서 유닛이 생성될 때 Slider도 함께 생성되고, 해당 Slider의 참조가 이 변수에 할당된다.

Step 3. 오브젝트 배치 및 바닥 구성

  • Player: (-6, -2.5) / Ally: (-2, -2.5) / Enemy: (4, -2.5)
  • 바닥: 2D Object > Sprite, 이름: Ground, Scale: (20, 2), Y = -4
  • BoxCollider2D + Rigidbody2D(Static) 추가

Step 4. 투사체 프리팹 생성

  • 이름: Bullet / Scale: (0.3, 0.3)
  • Rigidbody2D(Dynamic, Gravity Scale: 1) + CircleCollider2D(Is Trigger 체크)
  • Project 창으로 드래그하여 프리팹으로 저장 후 Hierarchy에서 삭제

Step 5. 투사체 발사 스크립트 작성

  1. Shooter라는 새 스크립트를 만든다.
  2. bulletPrefab, shootForce, offset 값을 인스펙터에서 조절할 수 있도록 public 변수로 선언한다.
  3. Update() 함수에서 스페이스바 입력을 감지한다.
  4. Fire() 함수를 통해 지정된 위치에 투사체를 생성하고 Rigidbody2D에 힘을 가해 오른쪽으로 발사한다.
public class Shooter : MonoBehaviour
{
    public GameObject bulletPrefab;
    public float shootForce = 50f;
    public Vector2 offset = new Vector2(1f, 0f);

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            Fire();
        }
    }

    void Fire()
    {
        Vector2 spawnPos = (Vector2)transform.position + offset;
        GameObject bullet = Instantiate(bulletPrefab, spawnPos, Quaternion.identity);
        Rigidbody2D rb = bullet.GetComponent<Rigidbody2D>();
        rb.AddForce(Vector2.right * shootForce, ForceMode2D.Impulse);
    }
}
  • 투사체는 플레이어 위치에서 약간 오른쪽에 생성되며, 오른쪽 방향으로 힘을 받아 날아간다.
  • AddForce
rb.AddForce(Vector2.right * shootForce, ForceMode2D.Impulse);
  • 이 코드는 Rigidbody2D에 순간적인 힘을 가해 투사체를 오른쪽으로 날려보냅니다.
    • Vector2.right: 오른쪽 방향 (1, 0) 벡터
    • shootForce: 힘의 크기. 값이 클수록 더 빠르게 날아갑니다.
    • ForceMode2D.Impulse: 단 한 번의 순간적인 힘을 적용 (총알, 점프 등에 적합)
    • 이 방식은 단순 위치 이동이 아닌 물리 기반 움직임을 구현하며, 중력 및 반동 등의 효과도 함께 적용될 수 있습니다.

Step 6. 충돌 처리 및 데미지 전달

  1. Bullet이라는 새 스크립트를 만든다.
  2. OnTriggerEnter2D를 통해 충돌 대상이 Enemy 또는 Ally인지 판단한다.
  3. Enemy라면 데미지를 주고 Bullet을 파괴한다.
  4. Ally라면 체력이 가득 찬 경우 관통하고, 아니라면 Bullet을 파괴한다.
  5. 기타 충돌 물체와는 무조건 충돌 시 파괴한다.
public class Bullet : MonoBehaviour
{
    public int damage = 1;

    void OnTriggerEnter2D(Collider2D collision)
    {
        if (collision.CompareTag("Enemy"))
        {
            Enemy enemy = collision.GetComponent<Enemy>();
            if (enemy != null)
            {
                enemy.OnHit(this);
                Destroy(gameObject);
                return;
            }
        }

        if (collision.CompareTag("Ally"))
        {
            Ally ally = collision.GetComponent<Ally>();
            if (ally != null && ally.hp < ally.maxHp)
            {
                Destroy(gameObject);
                return;
            }
            return; // 체력이 가득 찬 아군은 관통
        }

        Destroy(gameObject);
    }
}
  • 적 유닛은 충돌 시 데미지를 받고 Bullet은 파괴된다.
  • 체력이 가득 찬 아군은 관통하고, 그렇지 않으면 Bullet이 파괴된다.
  • 기타 모든 충돌 대상에는 Bullet이 사라진다.

Step 6.5. 게임 매니저를 통한 유닛 및 UI 생성

  1. GameManager라는 빈 오브젝트에 붙일 스크립트를 생성한다.
  2. enemyPrefab, allyPrefab, sliderPrefab을 Inspector에 연결할 수 있도록 public으로 선언한다.
  3. Start()에서 각각의 유닛을 미리 지정된 위치에 생성한다.
  4. 각 유닛마다 Slider도 함께 Canvas 아래에 생성하고, HealthUI의 target을 해당 유닛으로 설정한다.
  5. Enemy 또는 Ally에 Slider를 연결하여 체력 UI가 반영되도록 한다.
public class GameManager : MonoBehaviour
{
    public GameObject enemyPrefab;
    public GameObject allyPrefab;
    public GameObject sliderPrefab;

    public Vector2 enemySpawnPosition = new Vector2(4, -2.5f);
    public Vector2 allySpawnPosition = new Vector2(-2, -2.5f);

    void Start()
    {
        SpawnUnit(enemyPrefab, enemySpawnPosition);
        SpawnUnit(allyPrefab, allySpawnPosition);
    }

    void SpawnUnit(GameObject unitPrefab, Vector2 position)
    {
        GameObject unit = Instantiate(unitPrefab, position, Quaternion.identity);

        Transform canvas = GameObject.Find("Canvas").transform;
        GameObject slider = Instantiate(sliderPrefab, canvas);

        slider.GetComponent<HealthUI>().target = unit.transform;

        if (unit.TryGetComponent<Enemy>(out var enemy))
        {
            enemy.hpSlider = slider.GetComponent<Slider>();
        }
        else if (unit.TryGetComponent<Ally>(out var ally))
        {
            ally.hpSlider = slider.GetComponent<Slider>();
        }
    }
}
  • 각 유닛은 생성 시 자동으로 Slider도 함께 생성되며, 체력 상태와 연결된다.

Step 7. 체력 UI 구성 (HealthUI.cs)

  1. Slider UI에 붙일 HealthUI.cs 스크립트를 만든다.
  2. target으로 지정된 오브젝트(유닛)의 머리 위 위치를 화면 좌표로 변환해 Slider에 적용한다.
  3. target이 수동으로 지정되지 않았다면 자동으로 부모 오브젝트를 따라가도록 한다.
  4. offset 값을 통해 Slider가 유닛의 머리 위에 고정되도록 위치를 조정한다.
public class HealthUI : MonoBehaviour
{
    public Transform target;
    public Vector3 offset = new Vector3(0, 2.0f, 0);

    void Start()
    {
        if (target == null && transform.parent != null)
            target = transform.parent;
    }

    void Update()
    {
        if (target == null) return;
        Vector3 screenPos = Camera.main.WorldToScreenPoint(target.position + offset);
        transform.position = screenPos;
    }
}
  • UI는 항상 target의 머리 위 위치에 고정되며, 화면 공간 기준으로 표시된다.

Step 8. Ally 유닛 체력 회복 기능 추가

  1. Ally.cs 스크립트를 생성하거나 기존에 추가한다.
  2. hp, maxHp, hpSlider 변수를 선언하고 초기화한다.
  3. Start() 함수에서 체력바의 초기 상태를 세팅한다.
  4. Heal() 함수로 외부에서 체력을 회복할 수 있도록 하고, 체력바도 갱신되게 한다.
public class Ally : MonoBehaviour
{
    public int hp = 5;
    public int maxHp = 5;
    public Slider hpSlider;

    void Start()
    {
        if (hpSlider != null)
        {
            hpSlider.maxValue = maxHp;
            hpSlider.value = hp;
        }
    }

    public void Heal(int amount)
    {
        hp = Mathf.Min(hp + amount, maxHp);
        if (hpSlider != null)
        {
            hpSlider.value = hp;
        }
    }
}
  • Heal()은 외부에서 호출되며, 체력 수치를 회복하고 Slider UI도 즉시 갱신된다.

최종적으로 현재까지 구현한 바는 다음과 같다.


Step 9. 투사체 시스템 구현 중 발생한 문제 및 해결 정리

1. Slider 및 Canvas 관련 문제

  • 문제 상황: 유닛 위에 체력바를 띄우기 위해 Slider를 생성했지만 화면에 표시되지 않거나, Slider만 프리팹으로 만들지 Canvas까지 포함해야 하는지 혼란이 있었음.
  • 해결 방법: Canvas 전체를 프리팹으로 구성하고, 그 하위에 Slider 오브젝트를 포함시켜 함께 프리팹화함. 이후 GameManager에서 Slider는 해당 Canvas 하위에 생성되도록 처리하여 UI가 정확하게 출력되도록 함.

2. HealthUI의 target 지정 오류

  • 문제 상황: GameManager에서 생성된 Slider가 유닛과 연결되지 않아 HealthUI 스크립트의 target이 null 상태로 남아있음. 그 결과 Slider 위치가 잘못 표시되거나 전혀 보이지 않음.
  • 해결 방법: Slider가 생성될 때 해당 유닛의 Transform을 target으로 명시적으로 지정. slider.GetComponent<HealthUI>().target = unit.transform 코드를 통해 유닛과 Slider를 정확히 연결함.

3. Bullet이 보이지 않는 문제

  • 문제 상황: Bullet이 생성되었지만 Sprite Renderer가 빠져 있어 화면에 렌더링되지 않음.
  • 해결 방법: Bullet 프리팹에 Sprite Renderer 컴포넌트를 추가하고, 적절한 스프라이트 이미지를 할당하여 시각적으로 명확하게 확인되도록 설정함.

4. 충돌 처리 후 Bullet이 제거되지 않음

  • 문제 상황: Bullet이 Enemy 또는 Ally와 충돌해도 사라지지 않거나, 체력이 가득 찬 Ally에 계속 충돌하면서 의도하지 않은 처리가 발생함.
  • 해결 방법: Bullet의 OnTriggerEnter2D 함수 내에서 Tag를 기준으로 Enemy와 Ally를 구분하여 처리. Ally의 경우 체력이 가득 찼을 때는 Bullet이 관통하고, 그렇지 않으면 Destroy되도록 로직을 분기함.

5. 바닥(Ground) 충돌 처리 미구현

  • 문제 상황: Bullet이 바닥에 떨어졌을 때 아무런 반응 없이 화면에 남아있는 문제 발생.
  • 해결 방법: 바닥 오브젝트와 충돌 시 파티클 이펙트를 발생시키고 Bullet을 제거하는 기능을 추가할 예정. Ground 태그 또는 특정 레이어를 활용해 충돌 처리를 구현할 계획임.

이후 구현 계획

  1. Ally 자동 회복 시스템
    일정 시간 경과 또는 특정 이벤트(예: 투사체 적중) 발생 시 체력이 자동 회복되도록 한다.

  2. UI 정보 확장
    체력 외에도 유닛의 이름, 상태 효과, 속성 등을 Slider UI 상단 또는 하단에 표시하도록 개선한다.

  3. 데미지 이펙트 및 애니메이션
    Bullet 또는 유닛이 데미지를 입었을 때 피격 이펙트나 애니메이션이 재생되도록 한다.

  4. 투사체 조준 시스템
    Player가 마우스 방향 또는 자동 타겟팅을 기반으로 투사체를 발사하도록 개선한다.

  5. 바닥 충돌 처리
    Bullet이 일정 시간 경과 또는 바닥(Ground)과 충돌했을 때 파티클 또는 효과와 함께 소멸하도록 처리한다.

쉽지 않다. 강의 내용을 되짚어 보면서 이것저것 갖다붙이고, 구글링도 해가면서 구현 방법을 찾았는데, 오류도 많이 발생했다. 하지만, 실제로 구현되는 걸 보고 다시 스크립트들을 보니 이해가 되는 부분도 있었다. 이런 식으로 직접 만들어 보는 방식이 확실히 도움이 되는 것 같다.

0개의 댓글