37일차) [Unity] 총알 피격 시각 효과 구현 및 Dictionary 학습 정

엄기태·2026년 2월 24일

Unity

목록 보기
11/13

1. 구현 목표

  • 과제 1: 총알 피격 위치에 시각 효과 구현
  • 세부 사항: * 총알이 맞은 지점에 피격 자국(Bullet Hole) 생성
  • 동시에 연기 및 폭발 이펙트(Particle Effect) 출력

2. 구현 결과

  • 결과물 영상:

📋 주요 소스코드

PoolManager.cs

using System.Collections.Generic;
using UnityEngine;

public enum PoolKey
{
	BULLET,
	MONSTER,
	MUZZLEFLASH,
	EXPLOSION,
	BULLETHOLE
}

public class PoolManager : MonoBehaviour
{
	public static PoolManager instance { get; private set; }

	[System.Serializable]
	public struct PoolData
	{
		public PoolKey key;
		public Transform parentGroup;
		public GameObject prefab;
		public int count;
	}

	[SerializeField] private List<PoolData> _poolList;

	private Dictionary<PoolKey, Queue<GameObject>> _poolDictionary = new();

	private void Awake()
	{
		if (instance != null && instance != this)
		{
			Destroy(gameObject);
			return;
		}
		instance = this;
	}

	void Start()
	{
		CreatAllPool();
	}

	private void CreatAllPool()
	{
		foreach (var data in _poolList)
		{
			Queue<GameObject> queue = new Queue<GameObject>();

			for (int i = 0; i < data.count; i++)
			{
				GameObject obj = Instantiate(data.prefab, data.parentGroup);
				obj.SetActive(false);
				queue.Enqueue(obj);
			}
			_poolDictionary.Add(data.key, queue);
		}
	}

	public GameObject Get(PoolKey key)
	{
		if (_poolDictionary.ContainsKey(key) == false)
			{
				Debug.LogWarning(key + "가 적절하지 않습니다.");
				return null;
			}

		if (_poolDictionary[key].Count > 0)
			{
				GameObject obj = _poolDictionary[key].Dequeue();
				obj.SetActive(true);
				return obj;
			}

			GameObject newObj = Instantiate(_poolList[(int)key].prefab);
			newObj.SetActive(true);
			return newObj;
	}

	public void Return(GameObject obj, PoolKey key)
	{
		if (_poolDictionary.ContainsKey(key) == false)
		{
			Debug.LogWarning(key + "가 적절하지 않습니다.");
			return;
		}
		obj.SetActive(false);
		_poolDictionary[key].Enqueue(obj);
	}
}

PooledParticle.cs

using System.Collections;
using UnityEngine;

public class PooledParticle : MonoBehaviour
{
	private ParticleSystem _particle;
	private Coroutine _DoneRoutine;

	public PoolKey currentKey;
    private void Awake()
    {
        _particle = GetComponent<ParticleSystem>();
    }

    void OnEnable()
    {
		if (_DoneRoutine != null)
		{
			StopCoroutine(_DoneRoutine);
		}
		_particle.Clear();
		_particle.Play();

		_DoneRoutine = StartCoroutine(DoneRoutine());
    }

	IEnumerator DoneRoutine()
	{
		while (_particle != null && _particle.IsAlive(true))
		{
			yield return null;
		}

		PoolManager.instance.Return(gameObject, currentKey);
	}

	public void RestartAt(Transform firePosition)
	{
		transform.SetPositionAndRotation(firePosition.position, firePosition.rotation);
	}
}

BulletController.cs (수정 사항)

private void OnCollisionEnter(Collision collision)
{
    // 총알 충돌 이펙트
    if (collision.gameObject.CompareTag("Player") || collision.gameObject.CompareTag("Enemy"))
    {
        GameObject explosion = PoolManager.instance.Get(PoolKey.EXPLOSION);
        explosion.GetComponent<PooledParticle>().RestartAt(this.transform);
        PoolManager.instance.Return(gameObject, PoolKey.BULLET);
    }
    // 총알 충돌 + 총알 구멍 이펙트
    else if (collision.gameObject.CompareTag("Wall"))
    {
        GameObject explosion = PoolManager.instance.Get(PoolKey.EXPLOSION);
        explosion.GetComponent<PooledParticle>().RestartAt(this.transform);
        GameObject bulletHole = PoolManager.instance.Get(PoolKey.BULLETHOLE);
        bulletHole.GetComponent<PooledParticle>().RestartAt(this.transform);
        PoolManager.instance.Return(gameObject, PoolKey.BULLET);
    }
}

3. 구현 과정

  1. PoolManager 확장: PoolKey 열거형에 MUZZLEFLASH, EXPLOSION, BULLETHOLE을 추가하여 관리 범위를 넓혔다.
  2. PooledParticle 활용: public PoolKey currentKey를 선언하여 인스펙터에서 프리팹별로 맞는 키를 할당하고, 파티클 종료 시 자동으로 풀에 반환되도록 구현했다.
  3. 충돌 로직 구현: BulletController에서 충돌 감지 시 PoolManager를 통해 이펙트를 생성하고, 특히 벽(Wall) 충돌 시에는 총알 구멍 이펙트를 추가로 소환하도록 구성했다.

4. 문제 해결 방법 (Troubleshooting)

  • 문제점: OnTriggerEnter 사용 시 충돌 지점이 물체 안쪽에 잡혀 총알 구멍 이펙트가 벽에 파묻혀 보이지 않는 현상 발생.
  • 시도: 처음에는 RayCast를 사용하려 했으나, 모든 총알에 개별적으로 레이를 쏘는 것은 연산 비용이 크고 코드가 복잡해질 것이라 판단했다.
  • 해결: 충돌 이벤트를 OnCollisionEnter로 수정함으로써 물리적인 충돌 지점 정보를 더 정확하고 간단하게 가져와 해결했다.

5. Dictionary 학습 정리

📘 Dictionary란?

  • Key와 Value의 쌍으로 데이터를 관리하는 자료구조다.
  • Key를 통해 데이터의 양과 상관없이 매우 빠르게 값을 찾을 수 있다.
  • 중복된 Key를 허용하지 않으며, 다양한 자료형을 지원한다.

🏗️ 내부 구조

  • 해시 테이블(Hash Table): 내부적으로 해시 테이블 구조를 사용한다. Key를 해시 함수를 통해 해시 코드로 변환하고, 이를 인덱스 주소로 활용한다.
  • 버킷(Bucket)과 Entries: 실제 데이터는 Entries에 저장되며, 버킷은 해당 데이터의 주소를 가리킨다.
  • 해시 충돌: 서로 다른 키가 같은 인덱스에 배정될 경우, 연결 고리를 만드는 Chaining 방식 등으로 해결한다.

🛠️ 주요 메서드

메서드설명
Add(key, value)키와 값을 추가한다.
ContainsKey(key)특정 키의 존재 여부를 확인한다.
TryGetValue(key, out value)키를 확인하고 값을 안전하게 가져온다.
Remove(key)특정 키와 값을 삭제한다.

6. 회고 및 아이디어

  • 아이디어: 많은 수의 아이템이나 몬스터를 관리할 때 foreach로 전수 조사하는 것보다 Dictionary를 사용하는 것이 성능 면에서 훨씬 압도적일 것 같다.
  • 어려운 점: private Dictionary<PoolKey, Queue<GameObject>>와 같은 복합적인 문법이 아직 낯설다. 자동완성에 의존하기보다 구조를 완전히 이해하도록 노력해야겠다.

profile
코딩 학습 공간

0개의 댓글