[Unity] 총알 시스템으로 알아보는 Object Pool

2026. 5. 7. 16:18·Unity,C#/Unity 정보
728x90

■ Object Pool

[ObjectPool]

게임 오브젝트를 생성하는 Instantiate() 메서드는 비용이 높은 연산이다. 이 메서드를 게임 플레이 중 반복적으로 호출하면 프레임 드롭이나 GC(Garbage Collection) 스파이크 등 다양한 성능 문제를 일으킬 수 있다.

 

이를 해결하기 위해 "필요할 때마다 생성하자"가 아닌, "미리 만들어 두고 꺼내 쓰자"라는 접근 방식을 취하는 것이 바로 Object Pool이다.

 

🤔Instantiate()가 비싼 이유

Unity에서 우리가 작성하는 코드는 C#이지만, Unity 엔진의 내부(코어)는 C++로 작성되어 있다. 즉, 우리가 다루는 C# 코드는 C++ 코어를 조작하는 리모컨 같은 역할을 한다.

 

Instantiate()를 호출하면, Unity는 하나의 게임 오브젝트를 위해 다음 과정을 거친다.

  1. C# 영역(매니지드 힙)에 C# 래퍼 객체를 생성한다.
  2. C++ 영역(네이티브 힙)에 실제 엔진 객체를 생성한다.
  3. C# 래퍼 객체 안에 C++ 본체의 주소(핸들)를 저장해 둘을 연결한다.
  4. C++ 본체를 엔진 시스템(씬 그래프, Transform 트리 등)에 등록한다.

여기서 핵심은, 이 과정이 컴포넌트 하나당, 자식 오브젝트 하나당 반복된다는 점이다.

 

C#-C++ 한 쌍을 만드는 비용 자체는 크지 않지만, 프리팹에 포함된 모든 컴포넌트와 모든 자식 오브젝트에 대해 곱해지기 때문에 전체 비용이 급격히 증가한다.

 

■ 간단한 게임 시스템 구현

위 영상처럼 플레이어가 마우스 방향을 바라보고, 바라보는 방향으로 총알을 발사하는 간단한 시스템을 만들어 볼 것이다.

Object Pool을 직접 구현하는 대신, Unity에서 제공하는 UnityEngine.Pool을 활용하여 제작한다.

 

1. Player.cs

using System;
using UnityEngine;
using UnityEngine.InputSystem;

public class Player : MonoBehaviour
{
    private Camera _camera;
    private Plane _aimPlane;

    private void Awake()
    {
        _camera = Camera.main;
        _aimPlane = new Plane(Vector3.up, transform.position.y);
    }

    private void Update()
    {
        var ray = _camera.ScreenPointToRay(Mouse.current.position.ReadValue());

        if (_aimPlane.Raycast(ray, out var dist) == true)
        {
            var hit = ray.GetPoint(dist);
        }
    }
}

[마우스 위치 구하기]

마우스의 월드 좌표(World Position)를 구하기 위해, 플레이어 위치의 Y값을 기준으로 수평 평면을 생성한다. 탑다운 시점에서는 마우스가 가리키는 바닥 위의 좌표를 알아야 하므로, 노멀을 Vector3.up(Y축)으로 설정해 바닥과 나란한 XZ 평면을 만든다. 플레이어가 이동하지 않으므로 Awake()에서 미리 생성해 둔다.

 

이후 카메라에서 마우스 방향으로 Ray를 발사하고, 이 Ray가 평면과 교차하는 지점을 마우스의 월드 좌표로 사용한다.

private void Update()
{
	var ray = _camera.ScreenPointToRay(Mouse.current.position.ReadValue());
	
	if (_aimPlane.Raycast(ray, out var dist) == true)
	{
		var hit = ray.GetPoint(dist);
		var dir = hit - transform.position;
		dir.y = 0;
		
		if (dir.sqrMagnitude > 0.001f)
		{
			transform.rotation = Quaternion.LookRotation(dir);
		}
	}
}

[캐릭터가 바라볼 방향 구하기]

마우스의 월드 좌표(hit)를 구했으니, 여기서 플레이어의 위치를 빼 플레이어에서 마우스 방향으로 향하는 방향 벡터(dir)를 구한다.

 

이때 마우스가 플레이어와 너무 가까우면 방향 벡터가 거의 0에 가까워져 회전이 불안정해질 수 있다. 이를 방지하기 위해 sqrMagnitude로 벡터의 제곱 크기를 검사하고, 일정 값 이상일 때만 Quaternion.LookRotation()으로 플레이어를 회전시켜 마우스 방향을 바라보게 한다.

  • sqrMagnitude는 벡터 크기의 제곱을 반환한다. magnitude와 달리 제곱근 연산을 생략하므로, 단순 대소 비교에서는 더 가볍다.

 

2. 총과 총알(Weapon, Bullet)

using System;
using UnityEngine;

public class Weapon : MonoBehaviour
{
    [SerializeField] private Bullet currentBullet;
    [SerializeField] private float fireDelay;
    
    private float _lastFireTime = float.NegativeInfinity;

    public void Fire()
    {
        if (Time.time - _lastFireTime < fireDelay) return;
        
        // TODO : Pool에서 총알을 얻어오기!
        _lastFireTime = Time.time;
    }
}

Weapon 클래스는 총의 역할을 담당한다.

 

발사할 총알과 발사 딜레이를 직렬화 필드로 선언하고, Fire()가 호출되면 사격 딜레이가 경과했는지 확인한다. 딜레이가 지났다면 풀에서 총알을 가져온 뒤 마지막 사격 시점을 갱신한다.

using UnityEngine;

public class Bullet : MonoBehaviour
{
    private Vector3 _dir;
    private float _velocity;

    private float _despawnTime;
    private const float LIFE_TIME = 5f;
    
    public void Init(Vector3 dir, Vector3 pos, float velocity)
    {
        _dir = dir.normalized;
        transform.position = pos;
        _velocity = velocity;

        _despawnTime = Time.time + LIFE_TIME;
    }
    
    private void Update()
    {
        transform.position += _dir * (_velocity * Time.deltaTime);

        if (Time.time >= _despawnTime)
        {
            // TODO : 사용한 총알을 다시 Pool로 돌려 재사용!
        }
    }
}

Bullet 클래스는 총알의 이동과 수명을 관리한다. Init()에서 날아갈 방향, 발사 위치, 속도를 파라미터로 받아 초기화하고, Update()에서 매 프레임 지정된 방향으로 이동한다. 일정 시간(LIFE_TIME)이 지나면 총알을 풀로 반환하여 재사용할 수 있도록 한다.

 

3. 입력 처리(Input Action)

기존 Input 클래스의 GetMouseButton() 대신, InputAction을 활용하여 마우스 좌클릭 입력 시 Weapon의 Fire()를 호출하도록 구현해 보자.

using System;
using UnityEngine;
using UnityEngine.InputSystem;

public class Player : MonoBehaviour
{
    [SerializeField] private Weapon equippedWeapon;

    private GameInput _gameInput;

    private void Awake()
    {
        _camera = Camera.main;
        _aimPlane = new Plane(Vector3.up, transform.position.y);
        
        _gameInput = new GameInput();
    }
    
    private void OnEnable()
    {
        _gameInput.Player.Enable();
       
        _gameInput.Player.Fire.performed += OnStartFire;
    }

    private void OnDisable()
    {
        _gameInput.Player.Disable();
        
        _gameInput.Player.Fire.performed -= OnStartFire;
    }

    private void OnDestroy()
    {
        _gameInput.Dispose();
    }

    private void OnStartFire(InputAction.CallbackContext _)
    {
        equippedWeapon.Fire();
    }
}

Input Action 에셋을 생성했으면, GameInput 타입의 변수를 선언할 수 있다. Awake()에서 new GameInput()으로 인스턴스를 초기화해야 사용할 수 있다.

 

OnEnable()에서 Player 액션 맵을 활성화하고, Fire 액션의 performed 이벤트에 OnStartFire()를 등록한다. OnDisable()에서는 액션 맵을 비활성화하고 이벤트를 해제하며, OnDestroy()에서 Dispose()를 호출해 리소스를 정리한다.

  • OnStartFire()는 InputAction.CallbackContext를 파라미터로 받아야 하므로, Weapon의 Fire()를 직접 등록하지 않고 별도 메서드로 래핑 하여 호출한다.

 

■ ObjectPool 구현

[ObjectPool 흐름]

오브젝트 풀을 구현하기 위해선 먼저 풀을 관리할 Owner클래스를 만들어야 한다. 구현 사항마다 다르지만, 여기서는 풀에 쉽게 접근할 수 있도록 전역 클래스로 생성한다.

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;

public class BulletSpawner : MonoBehaviour
{
    public static BulletSpawner Instance { get; private set; }
    public IObjectPool<Bullet> BulletPool => _pool;
    
    private ObjectPool<Bullet> _pool;
		private Bullet _curBullet;
    
    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(Instance.gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }

    public void AddBullet(Bullet bullet)
    {
        _curBullet = bullet;

        _pool = new ObjectPool<Bullet>(
            CreateBullet,
            OnGetBullet,
            OnReleaseBullet,
            OnDestroyBullet,
            collectionCheck: true,
            defaultCapacity: 20,
            maxSize: 100
            );
    }

    private Bullet CreateBullet()
    {
        var obj = Instantiate(_curBullet, transform);
        return obj;
    }

    private void OnGetBullet(Bullet bullet)
    {
        bullet.gameObject.SetActive(true);
    }

    private void OnReleaseBullet(Bullet bullet)
    {
        bullet.gameObject.SetActive(false);
    }

    private void OnDestroyBullet(Bullet bullet)
    {
        Destroy(bullet.gameObject);
    }
    
}

클래스의 전체 형태는 위와 같다. 천천히 살펴보자.

 

먼저 풀 역할을 할 변수를 선언한다. 제네릭 타입 T에는 풀에서 관리할 오브젝트의 타입을 넣어주면 된다.

// 외부에서 접근할 Pool
public IObjectPool<Bullet> BulletPool => _pool;

// 내부에서 사용할 Pool    
private ObjectPool<Bullet> _pool;

IObjectPool<T>는 Unity가 제공하는 오브젝트 풀의 인터페이스로, Get()과 Release() 등 풀의 기본 동작을 정의한다. 외부에서는 이 인터페이스 타입으로 노출하여, 풀의 내부 구현을 감추고 꺼내기·반환하기만 가능하도록 제한한다.

 

ObjectPool<T>는 IObjectPool<T>를 구현한 구체 클래스다. Unity는 두 가지 구현체를 제공한다.

클래스 내부 구조 특징
ObjectPool<T> Stack (배열 기반) 가장 최근에 반환된 객체를 먼저 꺼낸다(LIFO). 메모리가 연속적이고 접근이 빠르다.
LinkedPool<T> LinkedList (노드 기반) 노드 단위로 할당되어 확장·축소에 유연하지만, 메모리 오버헤드가 크다. (자주 사용하지 않는 오브젝트를 풀링할 때 사용)

 

일반적으로는 ObjectPool<T>이 성능상 유리하므로, 이 글에서도 이를 사용한다.

public void AddBullet(Bullet bullet)
{
	_curBullet = bullet;

	_pool = new ObjectPool<Bullet>(
			CreateBullet,
			OnGetBullet,
			OnReleaseBullet,
			OnDestroyBullet,
			collectionCheck: true,
			defaultCapacity: 20,
			maxSize: 100
	);
}

ObjectPool<T>을 생성할 때 다음 파라미터를 전달한다.

파라미터 타입 설명
createFunc Func<T> 풀에 여유 객체가 없을 때 새 객체를 생성하는 콜백.
actionOnGet Action<T> 풀에서 객체를 꺼낼 때 호출되는 콜백. 주로 SetActive(true) 처리.
actionOnRelease Action<T> 객체를 풀로 반환할 때 호출되는 콜백. 주로 SetActive(false) 처리.
actionOnDestroy Action<T> 풀이 maxSize를 초과하여 객체를 수용할 수 없을 때 완전히 제거하는 콜백.
collectionCheck bool true로 설정하면 같은 객체를 중복 반환하는 실수를 방지한다.
defaultCapacity int 풀 내부 컬렉션(Stack)의 초기 할당 크기.
maxSize int 풀이 보관할 수 있는 최대 객체 수. 초과 시 actionOnDestroy가 호출된다.

 

여기서 중요한 점은, 이 파라미터들은 후처리 콜백 함수라는 것이다. 즉, 객체를 "어떻게 만들고, 어떻게 꺼내고, 어떻게 반환할지"의 핵심 로직은 Pool이 내부적으로 처리하고, 우리는 각 시점에 추가로 실행될 동작만 정의하면 된다.

  • collectionCheck, defaultCapacity, maxSize는 모두 선택적 파라미터로, 생략하면 기본값이 적용된다.
private Bullet CreateBullet()
{
	var obj = Instantiate(_curBullet, transform);
	return obj;
}

private void OnGetBullet(Bullet bullet)
{
	bullet.gameObject.SetActive(true);
}

private void OnReleaseBullet(Bullet bullet)
{
	bullet.gameObject.SetActive(false);
}

private void OnDestroyBullet(Bullet bullet)
{
	Destroy(bullet.gameObject);
}

Pool을 구현했으니, 이제 총알을 꺼내는 부분과 반환하는 부분을 마저 완성하면 된다.

public class Weapon : MonoBehaviour
{
    [SerializeField] private Bullet currentBullet;
    [SerializeField] private float fireDelay;
    
    private BulletSpawner _bulletSpawner;
    private float _lastFireTime = float.NegativeInfinity;

    private void Start()
    {
        _bulletSpawner = BulletSpawner.Instance;
        _bulletSpawner.AddBullet(currentBullet);
    }

    public void Fire()
    {
        if (Time.time - _lastFireTime < fireDelay) return;
        
        // Pool에서 총알을 얻어온 뒤, 초기화 함수 호출
        var bullet = _bulletSpawner.BulletPool.Get();
        bullet.Init(transform.forward, transform.position, 10f, 
	        _bulletSpawner.BulletPool);
        
        _lastFireTime = Time.time;
    }
}

Weapon은 Start()에서 BulletSpawner 싱글톤에 접근하고, AddBullet()으로 풀을 초기화한다.

 

Fire()가 호출되면 BulletPool.Get()으로 풀에서 총알을 꺼낸 뒤, Init()을 통해 발사 방향, 위치, 속도, 그리고 돌아갈 풀의 참조를 함께 전달한다.

public class Bullet : MonoBehaviour
{
    private IObjectPool<Bullet> _pool;
    
    private Vector3 _dir;
    private float _velocity;

    private float _despawnTime;
    private const float LIFE_TIME = 5f;
    
    public void Init(Vector3 dir, Vector3 pos, float velocity, IObjectPool<Bullet> pool)
    {
        _dir = dir.normalized;
        transform.position = pos;
        _velocity = velocity;
        _pool = pool;

        _despawnTime = Time.time + LIFE_TIME;
    }
    
    private void Update()
    {
        transform.position += _dir * (_velocity * Time.deltaTime);

        if (Time.time >= _despawnTime)
        {
            _pool.Release(this);
        }
    }
}

총알이 어느 풀로 돌아가야 하는지 알아야 하므로, Init() 시점에 돌아갈 풀의 참조를 IObjectPool<Bullet> 타입으로 저장한다. 이후 수명(LIFE_TIME)이 만료되면 _pool.Release(this)를 호출해 자기 자신을 풀로 반환한다.

 

■ 시스템 개선

총알이 한 종류뿐이라면 위 코드에 문제가 없지만, 여러 종류의 총알이 존재하면 문제점이 드러난다.

 

현재 AddBullet()을 호출하면 기존 풀을 덮어쓰고 새 풀을 생성한다. 이 경우 기존 풀의 참조가 사라져, 이미 발사된 총알이 반환될 풀을 잃게 된다.

 

또한 총알을 꺼낼 때마다 Init()에 방향, 위치, 속도, 풀 참조 등 모든 정보를 매번 전달하고 있어 비효율적이다.

 

1. BulletData

using UnityEngine;

[CreateAssetMenu(fileName = "BulletData", menuName = "Weapon/BulletData")]
public class BulletData : ScriptableObject
{
    public float Damage => damage;
    public float Velocity => velocity;
    public float LifeTime => lifeTime;
    public Bullet BulletPrefab => bulletPrefab;

    [SerializeField] private float damage;
    [SerializeField] private float velocity;
    [SerializeField] private float lifeTime;
    [SerializeField] private Bullet bulletPrefab;
}

총알마다 서로 다른 정보를 저장하기 위해 BulletData ScriptableObject를 만든다.

 

프리팹, 데미지, 속도, 수명 등 총알의 고정 데이터를 에셋으로 관리하면, 총알을 꺼낼 때마다 매번 값을 넘길 필요 없이 데이터 에셋 하나로 참조할 수 있다.

 

2. Bullet

using UnityEngine;
using UnityEngine.Pool;

public abstract class Bullet : MonoBehaviour
{
    protected BulletData BulletData;
    
    private IObjectPool<Bullet> _pool;
    
    private Vector3 _dir;
    private float _curTime = 0f;

    protected abstract bool OnHit(GameObject other);
    
    public void Init(BulletData bulletData, Vector3 dir, Vector3 pos)
    {
        BulletData = bulletData;
        _dir = dir;
        _curTime = 0f;
        
        transform.position = pos;
        transform.rotation = Quaternion.LookRotation(dir);
    }

    public void SetPool(IObjectPool<Bullet> pool)
    {
        _pool = pool;
    }
    
    private void Update()
    {
        transform.position += _dir * (BulletData.Velocity * Time.deltaTime);
        _curTime += Time.deltaTime;

        if (_curTime >= BulletData.LifeTime)
        {
            _pool.Release(this);
        }
    }

    private void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Enemy"))
        {
            if(OnHit(other.gameObject)) _pool.Release(this);
        }
    }
}

기존 Bullet을 추상 클래스로 변경하여, 다양한 종류의 총알(예: 폭발탄, 관통탄)을 상속으로 확장할 수 있도록 한다.

  • OnHit()을 추상 메서드로 선언해 충돌 시 동작을 자식 클래스에서 정의하도록 한다. 반환값이 true이면 총알을 풀로 반환하고, false이면 반환하지 않아 관통 등의 동작을 구현할 수 있다.

또한 풀 참조를 Init()이 아닌 SetPool()로 분리했다. 풀 참조는 총알이 생성될 때 한 번만 설정하면 되지만, Init()은 풀에서 꺼낼 때마다 호출되므로 매번 풀을 다시 넘길 필요가 없기 때문이다.

using UnityEngine;

public class NormalBullet : Bullet
{
    protected override bool OnHit(GameObject other)
    {
			  // TODO : 일반 총알이 충돌했을 때 동작 구현
        return true;
    }
}

 

3. BulletSpawner

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Pool;

public class BulletSpawner : MonoBehaviour
{
    public static BulletSpawner Instance { get; private set; }
    private Dictionary<BulletData, IObjectPool<Bullet>> _pool;
    
    private void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(Instance.gameObject);
        }
        else
        {
            Destroy(gameObject);
            return;
        }
        
        _pool = new Dictionary<BulletData, IObjectPool<Bullet>>();
    }

    public Bullet GetBullet(BulletData data)
    {
        var pool = GetOrCreateBullet(data);
        return pool.Get();
    }
    
    private IObjectPool<Bullet> GetOrCreateBullet(BulletData data)
    {
        if(_pool.TryGetValue(data, out var bulletPool))
        {
            return bulletPool;
        }
        
        IObjectPool<Bullet> pool = null;
        
        pool = new ObjectPool<Bullet>(
            createFunc: () => 
            {
                var obj = Instantiate(data.BulletPrefab, transform);
                obj.SetPool(pool);
                return obj;
            },
            OnGetBullet,
            OnReleaseBullet,
            OnDestroyBullet,
            defaultCapacity: 10,
            maxSize: 100
            );
        
        _pool.Add(data, pool);
        return pool;
    }
    
    private void OnGetBullet(Bullet bullet)
    {
        bullet.gameObject.SetActive(true);
    }

    private void OnReleaseBullet(Bullet bullet)
    {
        bullet.gameObject.SetActive(false);
    }

    private void OnDestroyBullet(Bullet bullet)
    {
        Destroy(bullet.gameObject);
    }
  
}

BulletSpawner의 핵심 변경점은 풀의 자료구조다. 기존의 단일 ObjectPool<Bullet> 대신, Dictionary<BulletData, IObjectPool<Bullet>>로 변경하여 총알 종류별로 독립된 풀을 관리한다.

 

GetBullet()이 호출되면 내부의 GetOrCreateBullet()에서 해당 BulletData를 Key로 딕셔너리를 조회한다. 이미 풀이 존재하면 그대로 반환하고, 없으면 새 풀을 생성한 뒤 딕셔너리에 등록한다.

IObjectPool<Bullet> pool = null;
        
pool = new ObjectPool<Bullet>(
	createFunc: () => 
	{
		var obj = Instantiate(data.BulletPrefab, transform);
		obj.SetPool(pool);
		return obj;
	},
	OnGetBullet,
	OnReleaseBullet,
	OnDestroyBullet,
	defaultCapacity: 10,
	maxSize: 100
	);

여기서 주목할 부분은 createFunc 내부에서 pool 변수를 클로저로 캡처하고 있다는 점이다.

 

pool을 먼저 null로 선언한 뒤 ObjectPool을 생성하면, 람다가 pool 변수 자체를 캡처하므로 실제 총알이 생성되는 시점에는 이미 유효한 풀 참조를 갖게 된다. 이를 통해 Init()에서 매번 풀을 전달하는 대신, 생성 시 한 번만 SetPool()로 등록하여 불필요한 반복을 제거할 수 있다.

 

4. Player & Weapon

using System;
using UnityEngine;

public class Weapon : MonoBehaviour
{
    [SerializeField] private float fireDelay;
    
    private BulletData _loadedBullet;
    private float _lastFireTime = float.NegativeInfinity;
    
    public void Fire()
    {
        if (Time.time - _lastFireTime < fireDelay) return;
        
        var bullet = BulletSpawner.Instance.GetBullet(_loadedBullet);
        bullet.Init(_loadedBullet, transform.forward, transform.position);
        
        _lastFireTime = Time.time;
    }

    public void SwapBullet(BulletData bullet)
    {
        _loadedBullet = bullet;
        
    }
}
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.InputSystem;

public class Player : MonoBehaviour
{
    [SerializeField] private Weapon equippedWeapon;
    [SerializeField] private List<BulletData> inventory;

    private int _curInventoryIndex = 0;
    private GameInput _gameInput;
    private Camera _camera;
    private Plane _aimPlane;

    private void Awake()
    {
        _camera = Camera.main;
        _aimPlane = new Plane(Vector3.up, transform.position.y);
        
        _gameInput = new GameInput();
        
        equippedWeapon.SwapBullet(inventory[_curInventoryIndex]);
    }

    private void Update()
    {
        var ray = _camera.ScreenPointToRay(Mouse.current.position.ReadValue());

        if (_aimPlane.Raycast(ray, out var dist) == true)
        {
            var hit = ray.GetPoint(dist);
            var dir = hit - transform.position;
            dir.y = 0;

            if (dir.sqrMagnitude > 0.001f)
            {
                transform.rotation = Quaternion.LookRotation(dir);
            }
        }
    }

    private void OnEnable()
    {
        _gameInput.Player.Enable();
        
        _gameInput.Player.Fire.performed += OnStartFire;
        _gameInput.Player.Reload.started += OnStartReload;
    }

    private void OnDisable()
    {
        _gameInput.Player.Disable();
        
        _gameInput.Player.Fire.performed -= OnStartFire;
        _gameInput.Player.Reload.started -= OnStartReload;
    }

    private void OnDestroy()
    {
        _gameInput.Dispose();
    }

    private void OnStartFire(InputAction.CallbackContext _)
    {
        equippedWeapon.Fire();
    }

    private void OnStartReload(InputAction.CallbackContext _)
    {
        _curInventoryIndex = (_curInventoryIndex + 1) % inventory.Count;
        equippedWeapon.SwapBullet(inventory[_curInventoryIndex]);
    }
}

기존에는 Weapon이 직렬화된 Bullet 프리팹을 직접 들고 있었지만, 총알 종류가 여러 가지로 늘어나면서 구조를 변경했다. Player에 BulletData 리스트로 간단한 인벤토리를 구성하고, 장전 키(Reload)를 누르면 인벤토리의 다음 총알 데이터를 Weapon에 전달하도록 했다.

 

Weapon은 더 이상 특정 총알에 종속되지 않으며, SwapBullet()을 통해 언제든 장전된 총알을 교체할 수 있다. Fire() 호출 시에는 현재 장전된 BulletData를 기반으로 BulletSpawner에서 해당 종류의 총알을 꺼내 발사한다.

최종 결과

 

728x90

'Unity,C# > Unity 정보' 카테고리의 다른 글

[Unity, C#] 델리게이트에 대한 모든것(Action, Func, Lambda)  (0) 2026.05.07
[Unity, C#] SOLID 원칙  (1) 2025.11.21
[Unity] UI Toolkit 기본 사용법  (0) 2025.11.21
[Unity, C#] ScriptableObject - 스크립터블 오브젝트  (6) 2025.08.02
[Unity, C#] EditorWindow - 커스텀 에디터 윈도우  (1) 2025.08.02
'Unity,C#/Unity 정보' 카테고리의 다른 글
  • [Unity, C#] 델리게이트에 대한 모든것(Action, Func, Lambda)
  • [Unity, C#] SOLID 원칙
  • [Unity] UI Toolkit 기본 사용법
  • [Unity, C#] ScriptableObject - 스크립터블 오브젝트
브라더스톤
브라더스톤
유티니, C#과 관련한 여러 정보를 끄적여둔 블로그입니다. Email : dkavmdk98@gmail.com
  • 브라더스톤
    젊은 프로그래머의 슬픔
    브라더스톤
  • 전체
    오늘
    어제
    • 개발 노트 (57)
      • Unity,C# (32)
        • Unity 정보 (9)
        • 알고리즘 (11)
        • 자료구조 (3)
        • 절차적생성(PCG) (9)
      • 게임수학 (16)
      • C++ (8)
        • 자료구조 (8)
      • 게임 (1)
        • 리치마작 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    CustomWindow
    커스텀 윈도우
    원근투영행렬
    c++
    자료구조
    pcg
    UI Toolkit
    절차적던전생성
    알고리즘
    절차적지형생성
    정렬알고리즘
    게임수학
    unity
    이진공간분할
    외적
    BSP
    PerlinNoise
    최단경로찾기
    C#
    스택
  • 최근 댓글

  • 최근 글

  • 250x250
  • hELLO· Designed By정상우.v4.10.3
브라더스톤
[Unity] 총알 시스템으로 알아보는 Object Pool
상단으로

티스토리툴바