1vdlrwnsv1 님의 블로그
탄피, 사운드 매니저, 오브젝트 풀 매니저 리팩토링 본문
탄피부터
기존에는 탄피가 생성되는 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한 설계가 유니티에서는 정말 중요하다.
- 불필요한 중복 로직을 줄이고 메서드를 명확히 나누면 실수도 줄어든다
'Unity 최종' 카테고리의 다른 글
Unity + Firebase: 랭킹 UI 비동기 처리 방법 3가지 정리 (ContinueWith vs Coroutine vs async/await) (0) | 2025.05.28 |
---|---|
Unity3D 적 탐지 시스템 구현 (0) | 2025.04.30 |
Unity 클래스 구조 변경 (0) | 2025.04.28 |
unity 오브젝트 풀링 매니저 (0) | 2025.04.25 |
유니티 PlayerPref를 이용한 사운드 값, 마우스 감도 값 저장 불러오기 (0) | 2025.04.23 |