Notice
Recent Posts
Recent Comments
Link
«   2025/07   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
Tags
more
Archives
Today
Total
관리 메뉴

1vdlrwnsv1 님의 블로그

탄피, 사운드 매니저, 오브젝트 풀 매니저 리팩토링 본문

Unity 최종

탄피, 사운드 매니저, 오브젝트 풀 매니저 리팩토링

1vdlrwnsv1 2025. 5. 7. 20:50

탄피부터

기존에는 탄피가 생성되는 EjectCasing() 함수 내부에서 Rigidbody 물리력을 직접 주었는데, 이 로직을 탄피 프리팹 자체에서 담당하도록 분리하고자 했다.
이를 통해 탄피 프리팹 자체가 자기 물리 처리 및 사운드를 제어하도록 설계함으로써 책임 분리를 달성

 

 

변경 전 구조

 

private void EjectCasing()
{
    GameObject casing = ObjectPoolManager.Instance.GetObject(...);
    Rigidbody rb = casing.GetComponent<Rigidbody>();

    // 여기서 직접 힘을 주고 토크를 적용함
    rb.AddExplosionForce(...);
    rb.AddTorque(...);
}

 

변경 후 구조 (방법 1)

1. ShellCasing.cs 스크립트를 프리팹에 추가

→ 탄피 오브젝트 자체가 자기 행동(물리/사운드)을 관리

 

public class ShellCasing : MonoBehaviour
{
    private Rigidbody rb;

    void Awake()
    {
        rb = GetComponent<Rigidbody>();
    }

    public void SetEjectData(float power, Vector3 position, Vector3 direction)
    {
        if (rb != null)
        {
            float finalPower = Random.Range(power * 0.7f, power);
            rb.AddExplosionForce(finalPower, position + direction, 1f);
            rb.AddTorque(new Vector3(0, Random.Range(100, 500), Random.Range(100, 1000)), ForceMode.Impulse);
        }
    }

    private void OnCollisionEnter(Collision other)
    {
        if (!other.gameObject.CompareTag("Gun"))
        {
            SoundManager.Instance.PlaySFXForName("Shell");
        }
    }
}

 

2. EjectCasing()에서는 생성만 하고, 물리 처리는 탄피가 알아서 함

 

private void EjectCasing()
{
    if (casingPrefab && casingExitLocation)
    {
        GameObject casing = ObjectPoolManager.Instance.GetObject(casingPrefab, casingExitLocation.position, casingExitLocation.rotation, 6f);

        float power = stat.Damage * 40f;
        var casingScript = casing.GetComponent<ShellCasing>();
        if (casingScript != null)
        {
            Vector3 offset = -casingExitLocation.right * 0.3f - casingExitLocation.up * 0.6f;
            casingScript.SetEjectData(power, casingExitLocation.position, offset);
        }
    }
}

 

 

ObjectPoolManager (오브젝트 풀 매니저) 리팩토링

주요 변경 사항 요약

  • Dictionary<GameObject, Queue<GameObject>>로 프리팹별 풀을 관리.
  • 반환된 오브젝트가 어디서 나왔는지 추적하기 위해 spawnedToPrefab 추가.
  • IPoolable 인터페이스 도입 → 오브젝트 재사용 시 콜백 처리 가능.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
/// <summary>
/// 사용법
/// ObjectPoolManager.Instance.GetObject(풀에서 가져올 프리펩, 위치, 회전, 반환될 시간);
/// /// 예)GameObject impact = ObjectPoolManager.Instance.GetObject(bulletImpactPrefab, hit.point, hitRotation, 5f);
/// 반환될 시간 -1f로 하면 ReturnToPool메서드를 호출해야만 반환됨
/// 예)GameObject impact = ObjectPoolManager.Instance.GetObject(bulletImpactPrefab, hit.point, hitRotation, -1f);
/// ObjectPoolManager.Instance.ReturnToPool(bullet.gameObject);
/// GetObject를 했는데 풀에 없으면 새로운 객체 생성
/// </summary>
public interface IPoolable
{
    void OnGetFromPool();      // 풀에서 꺼낼 때 호출
    void OnReturnToPool();     // 풀에 반환될 때 호출
}

public sealed class ObjectPoolManager : SingletonBehaviour<ObjectPoolManager>
{
    private readonly Dictionary<GameObject, Queue<GameObject>> pool = new();
    private readonly Dictionary<GameObject, GameObject> spawnedToPrefab = new();

    /// <summary>
    /// 제너릭 방식: 원하는 컴포넌트 타입으로 반환
    /// </summary>
    public T GetObject<T>(T prefab, Vector3 position, Quaternion rotation, float autoReturnTime = -1f)
        where T : Component
    {
        GameObject key = prefab.gameObject;

        if (!pool.ContainsKey(key))
            pool[key] = new Queue<GameObject>();

        GameObject obj = pool[key].Count > 0 ? pool[key].Dequeue() : Instantiate(key);
        obj.transform.SetPositionAndRotation(position, rotation);
        obj.SetActive(true);

        if (obj.TryGetComponent<IPoolable>(out var poolable))
            poolable.OnGetFromPool();

        spawnedToPrefab[obj] = key;

        if (autoReturnTime > 0f)
            StartCoroutine(ReturnAfterSeconds(obj, autoReturnTime));

        return obj.GetComponent<T>();
    }

    /// <summary>
    /// GameObject 방식 (기존 방식 유지)
    /// </summary>
    public GameObject GetObject(GameObject prefab, Vector3 position, Quaternion rotation, float autoReturnTime = -1f)
    {
        return GetObject(prefab.transform, position, rotation, autoReturnTime).gameObject;
    }

    /// <summary>
    /// 반환
    /// </summary>
    public void ReturnToPool(GameObject obj)
    {
        if (!spawnedToPrefab.TryGetValue(obj, out var prefab))
        {
            Debug.LogWarning("Trying to return unknown pooled object. Destroyed.");
            Destroy(obj);
            return;
        }

        if (obj.TryGetComponent<IPoolable>(out var poolable))
            poolable.OnReturnToPool();

        obj.SetActive(false);
        pool[prefab].Enqueue(obj);
        spawnedToPrefab.Remove(obj);
    }

    /// <summary>
    /// 일정 시간 후 자동 반환
    /// </summary>
    private IEnumerator ReturnAfterSeconds(GameObject obj, float seconds)
    {
        yield return new WaitForSeconds(seconds);

        if (obj != null && obj.activeSelf)
            ReturnToPool(obj);
    }
}

 

  • 풀에 없으면 새로 생성됨 (Instantiate 호출됨).
  • ReturnToPool 호출 전에 GetObject로 생성된 애만 반환 가능함.
    • 안 그러면 spawnedToPrefab에 없어서 Destroy됨.

SoundManager

 

Before: 기존 SoundManager의 한계

항목내용
단점 - Dictionary<string, AudioClip>만 사용하여 인스펙터에서 직접 관리 불가
- Inspector에서 효과음을 등록할 수 없음 (코드로만 가능)
- 효과음 이름 오타 시 런타임까지 알 수 없음
- PlaySFX()가 중복되어 혼란 유발
사용성 - 오디오 클립을 코드로만 등록해야 해서 협업 시 디자이너와의 분업에 제약
구조 - AudioClip을 직접 다루는 방식으로 확장성과 관리성이 떨어짐

After: 개선한 SoundManager 핵심 변경점

1. SoundEffectData 클래스로 시리얼라이즈 가능하게 구성

[System.Serializable]
public class SoundEffectData {
    public string name;
    public AudioClip clip;
}

 

  • 목적: 인스펙터에서 리스트 형태로 등록 가능하게 하여 시각적으로 확인 및 관리
  •  이름과 클립을 명시적으로 매칭

List<SoundEffectData> → Dictionary로 변환

public List<SoundEffectData> sfxList;
private Dictionary<string, AudioClip> soundEffects;

 

장점:

  • 인스펙터에서 등록 + 런타임에 빠른 조회
  • InitializeSFXDictionary()로 변환
private void InitializeSFXDictionary() {
    foreach (var sfx in sfxList) {
        if (!soundEffects.ContainsKey(sfx.name)) {
            soundEffects[sfx.name] = sfx.clip;
        }
    }
}

 

3. 메서드 명확화 (PlaySFX  PlaySFXForName, PlaySFXForClip)

public void PlaySFXForName(string soundName);
public void PlaySFXForClip(AudioClip clip);

 

  • 이름만으로 or 직접 AudioClip으로 호출 가능하게 분리
  • 호출부에서 직관성 향상

4. null 체크 및 디버그 메시지 강화

if (clip == null) {
    Debug.LogWarning("재생할 클립이 null");
    return;
}

 

디버깅 편의성 증가

 

최종 비교 요약

항목                                       개선 전                                                     개선 후

 

효과음 등록 방식 Dictionary 직접 등록 Inspector에서 List<SoundEffectData>로 등록
클립 조회 방식 문자열 기반 딕셔너리 only Inspector 등록 후 자동 변환
메서드 네이밍 중복 / 혼동 있음 (PlaySFX) PlaySFXForName, PlaySFXForClip로 명확화
예외처리 및 디버깅 부족한 편 Null 체크 및 디버그 메시지 강화
사용 편의성 프로그래머 위주 디자이너와 협업 가능

느낀 점

  • 구조를 조금만 바꿔도 유지보수성과 협업 편의성이 개선됨
  • Serializable과 Inspector-friendly한 설계가 유니티에서는 정말 중요하다.
  • 불필요한 중복 로직을 줄이고 메서드를 명확히 나누면 실수도 줄어든다