Colliders not being removed when calling sprite destroy()

I’m attempting to create a zombie survival game. I’m having a problem where my game gets more laggy, the more bullets I fire.

My code is structured as follows (documented beneath - may be easier to follow)

import WeaponSystem from "../../../common/modules/WeaponSystem";
import { ExtendedNengiTypes } from "../../../common/types/custom-nengi-types";
import Phaser from "phaser";
import BotGraphicServer from "./BotGraphicServer";
import BulletGraphicServer from "./BulletGraphicServer";
import BulletEntity from "../../../common/entity/BulletEntity";
import ClientHudMessage from "../../../common/message/ClientHudMessage";

export default class PlayerGraphicServer extends Phaser.Physics.Arcade.Sprite{

    weaponSystem: WeaponSystem
    rotation = 0
    speed: number
    bulletGraphics: Map<number, BulletGraphicServer>
    health = 100
    totalBullets = 0

    constructor(
        scene: Phaser.Scene,
        private worldLayer: Phaser.Tilemaps.StaticTilemapLayer,
        private nengiInstance: ExtendedNengiTypes.Instance,
        private client: ExtendedNengiTypes.Client,
        xStart: number,
        yStart: number,
        public associatedEntityId: number
    ) {
        super(scene, xStart, yStart, "player");
        this.bulletGraphics = new Map();
        this.speed = 1000;

        this.associatedEntityId = associatedEntityId;

        scene.add.existing(this);
        scene.physics.add.existing(this);

        this.setSize(50,50)
        this.setDisplaySize(50, 50)

        console.log("Setting up collision with world");
        scene.physics.add.collider(this, worldLayer);
        this.body.immovable = true

        setInterval(() => {
            this.updateHud()
        }, 200)
    }

    // Sent message to the client who owns this player, with their player information
    updateHud() {
        this.nengiInstance.message(new ClientHudMessage(
            this.health,
            "~",
            "Shredder",
        ), this.client);
    }

    fire(bots: any ) {
        // Set on cooldown - will check soon
        // this.weaponSystem.fire()

        const bulletEntity = new BulletEntity(this.x, this.y, this.rotation +  1.57079633);
        this.nengiInstance.addEntity(bulletEntity);

        // We now have a bullet created, that has a link to the entity so we can update it easily
        const bulletGraphic = new BulletGraphicServer(this.scene, this.worldLayer, bulletEntity.nid, this.x, this.y, Phaser.Math.RadToDeg(this.rotation), bots, this.processBulletHit);
        this.bulletGraphics.set(bulletGraphic.associatedEntityId, bulletGraphic);

        // Debug bullet creation lag
        this.totalBullets++
        console.log(this.totalBullets)

        setTimeout(() => {
            this.deleteBullet(bulletGraphic.associatedEntityId);
        }, 3000);
    }

    deleteBullet = (entityId: number) => {
        const bulletEntity = this.nengiInstance.getEntity(entityId);

        if (!bulletEntity) {
            // console.log("Trying to delete a bullet which doesn't exist any longer (may have already been cleared after collission)");
            return;
        }

        this.nengiInstance.removeEntity(bulletEntity);

        // Delete server copy
        let bullet = this.bulletGraphics.get(entityId);

        // CALL THIS FIRST TO IMPROVE PERFORMANCE
        bullet.removeColliders()
        bullet.destroy(false);

        this.bulletGraphics.delete(entityId);
        console.log(this.bulletGraphics.size)

    }

    processBulletHit = (bullet: any, hitObj: any) => {
        // console.log(`Bullet hit an object ${bullet.associatedEntityId} hit zombie ${hitObj.name}`);

        if (hitObj.type === "BOT") {
            hitObj.takeDamage(bullet.associatedEntityId);
        }

        this.deleteBullet(bullet.associatedEntityId);
    }

    processMove = (command: any) => {
         // Removed to simplify
    }

    // Update the entity, with the local positions of all bullets this player has
    preUpdate() {
        this.bulletGraphics.forEach((bullet) => {
            const associatedEntity = this.nengiInstance.getEntity(bullet.associatedEntityId);

            if (!associatedEntity) {
                console.log("Trying to update positions of bullet graphic, but cannot find an entity");
                return;
            }

            associatedEntity.x = bullet.x;
            associatedEntity.y = bullet.y;
            associatedEntity.rotation = bullet.rotation;
        });
    }

    public takeDamage(damagerEntityId: number) {
               // Removed to simplify
    }
}

Bullet

import BotGraphicServer from "./BotGraphicServer";
import Phaser from "phaser";

export default class BulletGraphicServer extends Phaser.Physics.Arcade.Sprite{

    // sprite: Phaser.Physics.Arcade.Sprite

    rotation: number = 0
    colliders:any[] = []


    constructor(
        scene: Phaser.Scene,
        worldLayer: Phaser.Tilemaps.StaticTilemapLayer,

        public associatedEntityId: number,
        startX: number,
        startY: number,
        angle: number,
        bots: BotGraphicServer[],
        cb: any
    ) {
        super(scene, startX, startY, "bullet");
        this.type = "BULLET";

        this.scene.add.existing(this);
        this.scene.physics.add.existing(this);

        this.setSize(10, 20)
        this.setDisplaySize(10, 20)

        this.colliders.push(this.scene.physics.add.collider(this, worldLayer, cb))
        this.body.immovable = true

        // this.associatedEntityId = associatedEntityId
        const vec = scene.physics.velocityFromAngle(angle, 250);

        this.setVelocityX(vec.x);
        this.setVelocityY(vec.y);

        // Hacky way to fix our (rotate extra 90 degree) sprite, until we edit :)
        this.rotation = Phaser.Math.DegToRad(angle) + 1.57079633

        bots.forEach((bot: any) => {
            this.colliders.push(this.scene.physics.add.collider(this, bot, cb))
        });
    }

    preUpdate = () => {
        // console.log("Running pre-update")
    }


    public removeColliders() {

        console.log(`Removing ${this.colliders.length} colliders`)
        this.colliders.forEach((c) => {
            this.scene.physics.world.removeCollider(c)
        })

    }

}

Here’s an overview of what happens -

  1. The fire() method is called with a list of all current bots, when the client clicks on the screen. the Player class is responsible for creating a bullet and storing it in the bulletGraphics map (using the ID of an “entity” we create - used for networking).

  2. The Bullet class accepts this ID we used as a key, a collision callback method, a worldLayer, and a map of all zombies (and a few other bits we don’t need to worry about). It attaches a collider to my worldlayer (created as this.map.createStaticLayer("LevelOneWorld", tileset, 0, 0); in the level code, and also loops every zombie - adding a collider with the callback provided.

  3. Upon the bullet either hitting a worldLayer object (E.G a wall) or a zombie - the callback function I provided into the Bullet class, is invoked, and handled in the Player class

  4. The Player class calls the damage method if the collision was with a zombie, and then calls deleteBullet(). This method looks up the bullet from the map we originally created, and calls destroy() on the sprite instance

The problem I’m having, is after spawning 200 zombies, and then firing my gun, my game begins lagging hard after 1500 bullets (this test was done firing at a wall, which as previously mentioned will call destroy() ( on the bullet instance when hit).

If I add the following code to my BulletGraphicServer, where I manually store all collisions, and then remove then every time I delete a bullet - I’ve tested 3000 bullets and the lag doesn’t seem to appear. However my game does lag hard as I run through that loop, so calling removeCollider isn’t really usable in the real world.

This seems to point to either a misunderstanding on my behalf, on how destroy() works (I thought it will remove the sprite from the scene physics update), or a bug in my code.

Here’s a screenshot of the Player, Bullets + Zombies if it helps visualize the problem -

Does anyone have any ideas?

Use collider groups and sprite pools?

Destroying a game object doesn’t remove it from any colliders it belongs to. The collider will skip the destroyed object, but it’s still there.

If possible you might try to reorganize into a single collider for each type, e.g. all bots vs. all bullets, using physics groups. Groups edit themselves automatically when a member is destroyed/removed, so they work well in colliders.