본문 바로가기

내일 배움 캠프/따봉Ib

[따봉Ib 👍] ASTROWARD (2)

[Bullet 오브젝트 풀링]

게임에서 자주 생성되고 제거되는 오브젝트(예: 이펙트, 총알, 몬스터 등)는 매번 Instantiate/Destroy를 호출하면 성능에 큰 영향을 준다.

이를 해결하기 위해 오브젝트 풀링 기법을 적용하여 메모리 재사용성을 높이고 GC(가비지 컬렉션) 비용을 줄이는 것이 목적

 

구조 요약

    ● Pool 클래스: 하나의 프리팹을 위한 오브젝트 풀을 관리

    ● TowerPoolManager 클래스: 싱글톤 기반으로 여러 Pool을 관리

 

Pool 클래스 설명

    ● 역할: 하나의 프리팹(GameObject)에 대한 풀을 구성

    ● 핵심 변수

        ○ _prefab: 풀의 원형 프리팹

        ○ _pool: ObjectPool<T>로 실제 오브젝트 풀을 관리

        ○ Root: 생성된 풀 오브젝트들을 묶는 부모 transform

 

생성자

    ● 전달된 프리팹을 기반으로 오브젝트 풀을 초기화

 

오브젝트 생명주기 콜백

    ● OnCreate: 오브젝트를 처음 생성할 때 실행

    ● OnGet: 풀에서 꺼낼 때 실행 (SetActive(true))

    ● OnRelease: 풀에 반환할 때 실행 (SetActive(false))

    ● OnDestroy: 풀 자체를 제거할 때 실행

 

TowerPoolManager 클래스

    ● 싱글톤 패턴을 이용하여 어디서든 접근 가능

    ● _pools 딕셔너리로 프리팹 이름을 키로 각 Pool을 관리

    ● 주요 메서드

        ○ Pop(GameOjbect prefab)

            ○ 프리팹 이름으로 풀을 찾고 없으면 새로 생성

            ○ 해당 풀에서 오브젝트를 꺼내 반환

        ○ Push(GameObject go)

            ○ 오브젝트를 해당 이름의 풀로 반환

            ○ 이름이 일치하는 풀이 없으면 false 반환

 

장점

    ● 런타임 성능 향상: Instantiate/Destroy를 반복하지 않아 성능 저하 및 GC 비용 최소화

    ● 메모리 재사용: 이미 생성된 오브젝트를 재활용

    ● 확장 용이성: 여러 종류의 프리팹을 관리한느 구조 (TowerPoolManager)

 


 

[Bullet]

발사체(총알)가 적을 향해 날아가고 충돌 시 피해를 입힌 뒤 오브젝트 풀링 시스템으로 반환되는 투사체 로직을 담당

 

핵심 기능

    ● 이동: 지정된 방향으로 일정 속도로 이동

    ● 수명 관리: duration 이상 살아 있으면 자동 파괴

    ● 충돌 처리: 적(EnemyController)과 충돌 시 데미지 부여 후 파괴

    ● 풀 반환: 파괴 시 TowerPoolManager를 통해 오브젝트 풀에 반환

 

발사 정보 설정

    ● 총알의 시작 위치, 방향, 데미지를 설정

    ● transform.rotation을 설정해 시각적으로 발사 방향과 일치

public void SetInfo(Vector3 startPos, Vector3 dir, int damage)

 

이동 및 수명 제어

    ● 수명이 다 되었는지 확인하고 파괴

    ● 설정된 방향으로 일정 속도로 이동

private void Update()
{
    //일정 시간이 지나면 자연스럽게 파괴
    currentDuration += Time.deltaTime;
    if (currentDuration > duration)
    {
        DestroyProjectile();
    }
    
    //bullet 위치 이동
    transform.position += dir.normalized * (speed * Time.deltaTime);
}

 

충돌 처리

    ● targetLayer에 포함된 적과 충돌했을 경우 EnemyController를 가져와 데미지 부여 후 오브젝트 풀로 되돌림

private void OnTriggerEnter(Collider other)
{
    if (targetLayer.value == (targetLayer.value | (1 << other.gameObject.layer)))
    {
        EnemyController enemy = other.GetComponent<EnemyController>();
        if (enemy != null)
        {
            enemy.OnDamaged(damage);
            DestroyProjectile();
        }
    }
}

 

파괴 및 반환

    ● 실제로는 Destroy() 하지 않고 오브젝트 풀에 반환

private void DestroyProjectile()
{
    TowerPoolManager.Instance.Push(gameObject);
}

 

장점

    ● 성능 최적화: Destroy()가 아닌 풀링을 통해 GC 비용 절감

    ● 모듈화된 구조: SetInfo()만 호출하면 재사용 가능

    ● 정확한 충돌 감지: LayerMask 기반으로 타겟 필터링

    ● 비주얼 정렬: 방향 회전 자동 설정으로 자연스러운 발사

 


 

[타워 설치 시스템]

플레이어가 마우스로 타워를 설치할 수 있도록 돕는 시스템

프리뷰(반투명 타워)를 통해 설치 위치를 시각적으로 안내

충돌 여부와 자원을 기반으로 설치 가능 여부를 판단

설치 후 실제 타워를 배치하고 자원을 차감

 

주요 기능

    ● 타워 프리뷰 표시: 설치 전 마우스를 따라다니는 투명한 프리뷰 표시

    ● 충돌 판정: OverlapBox로 설치 가능 여부 계산

    ● 설치 회전: 마우스 휠로 프리뷰 회전

    ● 자원 확인 및 차감: ResourceController로 자원 체크 및 소비

    ● 타워 설치: 인게임 타워 실제 생성 및 리스트 등록

 

설치 준비

    ● 프리뷰 오브젝트를 생성하고 반투명 머터리얼 적용

    ● 기존 프리뷰는 제거하고 새로 생성

    ● UI에서 호출되는 인터페이스

public void SetSelectedTower(int prefabIndex)
{
    if (prefabIndex < 0 || prefabIndex >= towerPrefabList.Count)
    {
        Debug.Log("잘못된 프리팹 인덱스");
        return;
    }

    if (currentPreview != null)
    {
        Destroy(currentPreview);
    }

    selectedPrefab = towerPrefabList[prefabIndex];
    currentPreview = Instantiate(previewPrefabList[prefabIndex]);
    previewRenderers = currentPreview.GetComponentsInChildren<Renderer>();
    SetMaterialTransparent();
}

 

설치 처리 흐름

    ● 프리뷰 위치 이동

        ○ 마우스 위치를 Raycast로 추적 → transform.position 적용

Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
if (Physics.Raycast(ray, out RaycastHit hit, 100f, LayerMask.GetMask("Ground")))
{
    //1. 위치 이동 (마우스 위치)
    Vector3 pos = hit.point;
    currentPreview.transform.position = pos;

    ● 프리뷰 회전

        ○ 마우스 휠 입력 → previewRotationY 축적 → Quaternion.Euler 적용

//2. 회전 (마우스 휠)
float scroll = Input.mouseScrollDelta.y;
if (scroll != 0)
{
    previewRotationY += scroll * 15f;
    currentPreview.transform.rotation = Quaternion.Euler(0, previewRotationY, 0);
}

    ● 충돌 여부 판단

        ○ Physics.OverlapBox()를 통해 현재 프리뷰가 buildableLayer와 겹치는지 검사

//3. 충돌 판정
Vector3 halfSize = GetBoundsExtents();
Vector3 centerPos = pos + Vector3.up * halfSize.y;
//현재 centerPos 위치에 halfSize의 박스 크기를 만들어 buildableLayer에 속한 기존 오브젝트들과 겹치면 true를 반환
Collider[] overlapped = Physics.OverlapBox(centerPos, halfSize, Quaternion.identity, buildableLayer);

canPlace = true;

foreach (Collider col in overlapped)
{
    if (col.gameObject == currentPreview || col.transform.IsChildOf(currentPreview.transform))
    {
        continue;
    }

    canPlace = false;
}

    ● 프리뷰 색상 업데이트

        ○ 설치 가능 → 초록색, 불가능 → 빨간색

//프리뷰 색상 설정 (설치 가능: 초록, 설치 불가능: 빨강)
SetPreviewColor(canPlace ? Color.green : Color.red);

    ● 마우스 클릭 시 설치

        ○ 자원 확인 → 설치 위치에 타워 생성 → 자원 차감

//4. 타워 설치
if (Input.GetMouseButtonDown(0) && canPlace)
{
    //자원 비교 
    if (CanBuild())
    {
        //자원 소모
        SpendResource();
     
        //타워 건설
        BuildTower(pos);
    }
}

 

자원 확인과 소비

    ● 설치하려는 타워의 TowerData → TowerLevelData를 참조

    ● UpgradeCost 기준으로 자원이 충분한지 판단

private bool CanBuild()
{
    towerData = selectedPrefab.GetComponent<Tower>().towerData;
    towerLevelData = towerData.towerLevelDatas[0];
    
    foreach (UpgradeCost cost in towerLevelData.upgradeCosts)
    {
        if (playerResource.GetResourceAmount(cost.itemType) < cost.amount)
        {
            return false;
        }
    }
    return true;
}

    ● 충분하면 자원 소비

private void SpendResource()
{
    foreach (UpgradeCost cost in towerLevelData.upgradeCosts)
    {
        playerResource.SpendResource(cost.itemType, cost.amount);
    }
}

 

타워 설치

    ● 실제 타워 프리팹을 설치 위치에 인스턴스화

private void BuildTower(Vector3 pos)
{
    AudioManager.Instance.PlaySFX(SFX.TOWER);
    GameObject go = Instantiate(selectedPrefab, pos, currentPreview.transform.rotation);
    towers.Add(go.GetComponent<Tower>());
    Destroy(currentPreview);
    currentPreview = null;
    selectedPrefab = null;
}

 

설치 영역 충돌 체크

    ● 설치 위치에 대한 Bounds 크기를 렌더러 기준으로 계산

    ● OverlapBox()에 활용하여 설치 가능 여부 판단

private Vector3 GetBoundsExtents()
{
    Bounds bounds = new Bounds(currentPreview.transform.position, Vector3.zero);
    foreach (Renderer rend in previewRenderers)
    {
        bounds.Encapsulate(rend.bounds);
    }
    return bounds.extents;
}

 

시각적 피드백: 프리뷰 색상

    ● 설치 가능 시 초록색, 불가능 시 빨간색으로 반투명 컬러 적용

private void SetPreviewColor(Color color)
{
    foreach (Renderer rend in previewRenderers)
    {
        foreach (Material mat in rend.materials)
        {
            mat.color = new Color(color.r, color.g, color.b, 0.5f); // 반투명
        }
    }
}

 

장점

    ● 유저 피드백 우수: 프리뷰 + 색상으로 직관적인 설치 가능 여부를 전달

    ● 자원 시스템과 통합: 타워 설치 시 자원 조건 확인 및 차감

    ● 유연한 구조: 다양한 종류의 타워 및 프리뷰 모델 대응 가능

    ● 충돌 방지 정확성: OverlapBox로 타워 간 간섭 없이 설치 가능