2D Game Feel - Object Pooling Issues

After completing the lecture and testing firing the gun, I’m getting the error below and some bullets are destroyed shortly after firing.

MissingReferenceException: The object of type ‘Bullet’ has been destroyed but you are still trying to access it.
Your script should either check if it is null or you should not destroy the object.
Gun+<>c.b__15_1 (Bullet bullet) (at Assets/Scripts/Gun/Gun.cs:63)
UnityEngine.Pool.ObjectPool`1[T].Get () (at <10871f9e312b442cb78b9b97db88fdcb>:0)
Gun.ShootProjectile () (at Assets/Scripts/Gun/Gun.cs:83)
Gun.Shoot () (at Assets/Scripts/Gun/Gun.cs:75)
Gun.Update () (at Assets/Scripts/Gun/Gun.cs:30)

I am able to recreate this consistently by just shooting the far away walls and then shooting the ground tiles nearby. I found another post by DustyDomino (2D game feel course object pooling bullet error) with the same issue. Tried changing the pools ActionOnDestroy from “Destroy(bullet)” to “Destroy(bullet.gameObject)”. This reduced the error frequency but it still can be consistently recreated.

I’ve tried setting the Pool collectionCheck to true, this gives a different set of errors about releasing objects that has already been released. I also tried changing the Pool’s actionOnDestroy to be a method checking if bullet != null. This seemed to reduce the errors but still didnt prevent them entirely. I’ve reverted both of these changes.

Any assistance would be appreciated to help understand what is happening. Please see gif/screenshot and code below.

Gif
ezgif-5-c6642d3d6d

Pic of holding down mouse button but showing a bunch of empty spots where bullets should be:

Gun Code

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

public class Gun : MonoBehaviour
{
    public static Action OnShoot;
    //public Transform BulletSpawnPoint => _bulletSpawnPoint;

    [SerializeField] private Transform _bulletSpawnPoint;
    [SerializeField] private Bullet _bulletPrefab;
    [SerializeField] private float _gunFireCD = .5f;

    private ObjectPool<Bullet> _bulletPool;
    private static readonly int FIRE_HASH = Animator.StringToHash("Fire");
    private Vector2 _mousePos;
    private float _lastFireTime = 0f;

    private Animator _animator;

    private void Awake() {
        _animator = GetComponent<Animator>();
    }
    
    private void Update()
    {
        Shoot();
        RotateGun();
    }

    private void Start() {
        CreateBulletPool();
    }

    private void OnEnable() {
        OnShoot += ShootProjectile;
        OnShoot += ResetLastFireTime;
        OnShoot += FireAnimation;
                
    }

    private void OnDisable() {
        OnShoot -= ShootProjectile;
        OnShoot -= ResetLastFireTime;
        OnShoot -= FireAnimation;

    }

    public void ReleaseBulletFromPool(Bullet bullet){
        _bulletPool.Release(bullet);

    }


    private void CreateBulletPool()
    {
        _bulletPool = new ObjectPool<Bullet>
            (
            () => {return Instantiate(_bulletPrefab); }, 
            bullet => { bullet.gameObject.SetActive(true); },
            bullet => { bullet.gameObject.SetActive(false); }, 
            bullet => { Destroy(bullet.gameObject); },
            false,
            20, 40
            );
    }


    private void Shoot()
    {
        if (Input.GetMouseButton(0) && Time.time >= _lastFireTime) {
            OnShoot?.Invoke();
        }
    }

    private void ShootProjectile()
    {
        
        //Bullet newBullet = Instantiate(_bulletPrefab, _bulletSpawnPoint.position, Quaternion.identity);
        Bullet newBullet = _bulletPool.Get();
        newBullet.Init(this, _bulletSpawnPoint.position, _mousePos);
    }

    private void FireAnimation()
    {
        _animator.Play(FIRE_HASH, 0, 0f);
    }

    private void ResetLastFireTime()
    {
        _lastFireTime = Time.time + _gunFireCD;

    }

    private void RotateGun()
    {
        _mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        //OLD, doesnt flip gun - Vector2 direction = _mousePos - (Vector2)PlayerController.Instance.transform.position;
        Vector2 direction = PlayerController.Instance.transform.InverseTransformPoint(_mousePos);

        float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
        transform.localRotation = Quaternion.Euler(0, 0, angle);
    }


}

Bullet Code

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

public class Bullet : MonoBehaviour
{
    [SerializeField] private float _moveSpeed = 10f;
    [SerializeField] private int _damageAmount = 1;

    private Vector2 _fireDirection;
    private Rigidbody2D _rigidBody;
    private Gun _gun;

    private void Awake()
    {
        _rigidBody = GetComponent<Rigidbody2D>();
    }

    private void FixedUpdate()
    {
        _rigidBody.velocity = _fireDirection * _moveSpeed;
    }

    public void Init(Gun gun, Vector2 bulletSpawnPos, Vector2 mousePos) {
        _gun = gun;
        transform.position = bulletSpawnPos;
        _fireDirection = (mousePos - bulletSpawnPos).normalized;
    }

    private void OnTriggerEnter2D(Collider2D other) {
        Health health = other.gameObject.GetComponent<Health>();
        health?.TakeDamage(_damageAmount);
        _gun.ReleaseBulletFromPool(this);
    }
}
1 Like

Hey - welcome to GameDev.Tv!

Well, the good news is that I copied your code into my (working) version, and I couldn’t replicate your error. So I don’t think its a code thing.

It kinda feels like an execution order thing, although I’m not sure how that tracks. It’s like a bullet is set to destroy, but before it is ( Unity - Manual: Order of execution for event functions (unity3d.com) then it is returned to the pool. This then leaves a null object in the pool.

There is a bug discussion about null objects in a pool ( Bugs in UnityEngine.Pool.ObjectPool - Unity Forum) so I’m wondering if you are using a bugged version and mine is a fixed version? I’m on 2023.1.19f1

1 Like

I had it originally running on 2022.3.8f1. I guess I wasn’t paying attention to the version when I created this project. I remade it on 2023.1.0f1 since I have that installed. It works on 2023.1.0f1 without any issues.

Thank you for the reply and assistance with figuring it out!

I did figure out an alternative solution which did fix the errors and I was planning on refining before finding out its a version issue. Most of its taken from a youtube video that goes over object pooling. Including code for reference, it breaks out the pool functions and each bullet gets created with a number for easier tracking in hierarchy. When the pool checking was enabled and also causing an error, that appears to have been from collisions with the bullet and the ground happening multiple times, since it was trying to return an object that was already returned. This was fixable with a bool on the bullet to disable the bullet after the first collision and reset again during init.

Bullet code (just for reference)

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

public class Bullet : MonoBehaviour
{
    [SerializeField] private float _moveSpeed = 10f;
    [SerializeField] private int _damageAmount = 1;

    private Vector2 _fireDirection;
    private Rigidbody2D _rigidBody;
    private Gun _gun;

    private bool _isReleased;

    private void Awake()
    {
        _rigidBody = GetComponent<Rigidbody2D>();
    }

    private void FixedUpdate()
    {
        _rigidBody.velocity = _fireDirection * _moveSpeed;
    }

    public void Init(Gun gun, Vector2 bulletSpawnPos, Vector2 mousePos) {
        _gun = gun;
        //transform.position = bulletSpawnPos;
        this.gameObject.SetActive(true);
        _isReleased = false;
        _fireDirection = (mousePos - bulletSpawnPos).normalized;

    }

    private void OnTriggerEnter2D(Collider2D other) {
        Health health = other.gameObject.GetComponent<Health>();
        health?.TakeDamage(_damageAmount);
        if(!_isReleased) {
            _gun.ReleaseBulletFromPool(this);
            _isReleased = true; 

        }
    }
}

Gun code

using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Cinemachine;
using Unity.Mathematics;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Pool;

public class Gun : MonoBehaviour
{
    public static Action OnShoot;
    //public Transform BulletSpawnPoint => _bulletSpawnPoint;

    [SerializeField] private Transform _bulletSpawnPoint;
    [SerializeField] private Bullet _bulletPrefab;
    [SerializeField] private float _gunFireCD = .5f;

    private ObjectPool<Bullet> _bulletPool;
    private static readonly int FIRE_HASH = Animator.StringToHash("Fire");
    private Vector2 _mousePos;
    private float _lastFireTime = 0f;

    private int _bulletCounter = 0;


    private CinemachineImpulseSource _impulseSource;
    private Animator _animator;

    private void Awake() {
        _impulseSource = GetComponent<CinemachineImpulseSource>();
        _animator = GetComponent<Animator>();
    }
    
    private void Start() {
        CreateBulletPool();
    }
    
    private void Update()
    {
        Shoot();
        RotateGun();
    }


    private void OnEnable() {
        OnShoot += ShootProjectile;
        OnShoot += ResetLastFireTime;
        OnShoot += FireAnimation;
        OnShoot += GunScreenShake;
                
    }

    private void OnDisable() {
        OnShoot -= ShootProjectile;
        OnShoot -= ResetLastFireTime;
        OnShoot -= FireAnimation;
        OnShoot -= GunScreenShake;

    }

    public void ReleaseBulletFromPool(Bullet bullet){
        _bulletPool.Release(bullet);

    }


    private void CreateBulletPool()
    {
        _bulletPool = new ObjectPool<Bullet>
        (
        CreateBullet, 
        null,
        OnPutBackInPool, defaultCapacity: 40        
        );

        // _bulletPool = new ObjectPool<Bullet>
        //     (
        //     () => { return Instantiate(_bulletPrefab); }, 
        //     bullet => { bullet.gameObject.SetActive(true) ; },
        //     bullet => { bullet.gameObject.SetActive(false); }, 
        //     bullet => { Destroy(bullet.gameObject); },
        //     true,
        //     30, 60
        //     );

    }

    private Bullet CreateBullet() {

        var projectile = Instantiate(_bulletPrefab);
        projectile.name = "Bullet " + _bulletCounter;
        _bulletCounter += 1;
        return projectile;


    }

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


    private void Shoot()
    {
        if (Input.GetMouseButton(0) && Time.time >= _lastFireTime) {
            OnShoot?.Invoke();
        }
    }

    private void ShootProjectile()
    {
        
        //Bullet newBullet = Instantiate(_bulletPrefab, _bulletSpawnPoint.position, Quaternion.identity);
        Bullet newBullet = _bulletPool.Get();
        newBullet.transform.position = _bulletSpawnPoint.position;
        newBullet.Init(this, _bulletSpawnPoint.position, _mousePos);
    }

    private void FireAnimation()
    {
        _animator.Play(FIRE_HASH, 0, 0f);
    }

    private void ResetLastFireTime()
    {
        _lastFireTime = Time.time + _gunFireCD;

    }

    private void GunScreenShake() {
        _impulseSource.GenerateImpulse();
    }

    private void RotateGun()
    {
        _mousePos = Camera.main.ScreenToWorldPoint(Input.mousePosition);
        //OLD, doesnt flip gun - Vector2 direction = _mousePos - (Vector2)PlayerController.Instance.transform.position;
        Vector2 direction = PlayerController.Instance.transform.InverseTransformPoint(_mousePos);

        float angle = Mathf.Atan2(direction.y, direction.x) * Mathf.Rad2Deg;
        transform.localRotation = Quaternion.Euler(0, 0, angle);
    }


}

2 Likes

Thanks for that. Very interesting.

A nice bool check to ensure only one collision is reported. Cool.

With the object pool, you have a create method, do nothing special on get and set the default Capacity. Those I get.

OnPutBackInPool - does that fire for returning to the pool, returning to a full pool, or both? If the first, how does it SetActive(True) and if the second, does that mean you stack up inactive objects as there is no destroy?

BTW: It’s harder, but I have managed to recreate the error in my version of Unity. Adding in the bool to check for collisions is the best fix. Just don’t do what I did and forget to reset the bool in Init. :slight_smile:

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms