Jump to content

Alternative to using many TimerEvents to update Sprite


Carcigenicate
 Share

Recommended Posts

I'm creating a "arena" survival game, and have many AI tanks that follow and attack the player. I'm extending the Sprite object for all of my game "entities".

I'm using TimerEvents fairly frequently to automatically update different aspects of each enemy. My problem is, once I spawn even a small number of enemies (20-30ish), the number of TimerEvents in the main game timer starts to rise since each tank has around 3 TimerEvents to itself. I've realized having a TimerEvent for every single aspect of the tank seems to be slowing down my game, but I can't see a better alternative.

A couple partial examples of what I'm using them for (the "FREQ" delays are 100ms):

move(direction: MoveDirection) {
    this.moveTimer = this.tankSprite.game.time.events.loop(TankMoveManager.CHECK_DIRECTION_FREQ, () => this.updateVelocity(direction), this);
    this.moveDirection = direction;
}

private updateVelocity(direction: MoveDirection) {
    if (direction === MoveDirection.NOT_MOVING) return;

    let angle = direction === MoveDirection.FORWARD ?
        this.tankSprite.angle : TankMoveManager.reverseAngle(this.tankSprite.angle);

    this.tankSprite.body.velocity = this.tankSprite.game.physics.arcade.velocityFromAngle(angle, this.moveVelocity);
}

This is a part of every tank Sprite, including the player. Since tanks can really only move forward, I'm having the tank move forward, and periodically adjusting the velocity to account for the angle that it's facing. 

startAttacking() {
    if (this.firingTimer != null) {
        return;
    }

    this.host.setFiringAngle(null);
   
    this.firingTimer = this.host.game.time.events.loop(AlignedShotManager.DIRECTION_UPDATE_FREQ, () => {
        let angleToTarget = Phaser.Math.radToDeg(this.host.game.physics.arcade
            .angleToXY(this.host, this.target.x, this.target.y));

        if (Math.abs(this.host.angle - angleToTarget) < AlignedShotManager.TARGET_ANGLE_EPSILON) {
            this.host.startFiring();

        } else {
            this.host.stopFiring();
        }

    }, this);
}

This is in every enemy tank. It periodically checks if it's facing the target (the player), and starts firing if it is.

startFiring() {
    if (!this.firingTimer) {
        let shotDelay = this.gunProperties.delayBetweenShots;

        this.fireBullet();
        this.firingTimer = this.game.time.events.loop(shotDelay, () => this.fireBullet(), this);

    }
        
}

This is a part of the Gun class, which is also a part of every tank. I'm using the timer to continually fire bullets until it's told to stop firing.

 

All of the above methods have a "stop" variant that removes the TimerEvent from the Timer, and handles any cleanup. After testing, I've made sure I'm not leaking TimedEvents.

What can I do to prevent the number of TimerEvents from exploding and plugging up the Timer?

I thought of having a single TimerEvent per tank, and just adding new tasks on by chaining the callback, but that prevents me from stopping a single task; I could only kill all the tasks in the callback or none at all (I couldn't stop firing but continue moving).

 

Any guidance here would be greatly appreciated.

Link to comment
Share on other sites

I ended up creating a (poorly named) class to help deal with this. Basically, I'm relying on the Sprite's update method


type UpdateLooperCallback = () => void;

class UpdateLooper {

    private loopDelay: number;
    private loopCallback: UpdateLooperCallback;
    private context: any;

    private nextTime: number = null;

    private game: Phaser.Game;

    constructor(game: Phaser.Game, loopDelay: number, loopCallback: UpdateLooperCallback, context: any = null) {
        this.game = game;
        this.loopDelay = loopDelay;
        this.loopCallback = loopCallback;
        this.context = context;
    }

    setLoopDelay(delay: number) {
        this.loopDelay = delay;
    }

    testAndUpdate() {
        if (this.game.time.now > this.nextTime) {
            this.loopCallback.call(this.context);

            this.nextTime = this.calcNextTime();
        }
    }

    private calcNextTime(): number {
        return this.game.time.now + this.loopDelay;
    }
}

"testAndUpdate" is meant to be called from a Sprite's update method. It calls the callback if sufficient time has passed.

It could be improved by allowing the caller to supply a "now()" method themselves so the entire "game" object doesn't need to be passed in, but it's definitely an improvement from my old way.

Link to comment
Share on other sites

Besides the "creating too many timers" prolem, what problem are you seeing? Are you putting too much pressure on the GC? Does your game crash after you've played it for a while? Anything like that?

I think the "too many timers" thing might not be a problem, is what I'm saying. Or, at least, it doesn't seem to be a problem that's causing any problems if you get my drift. ( =

One thing to watch out for: arrow functions in ES6 make brand new functions every time which *will* put pressure on the GC and stop the world while the garbage gets taken out. Simple solution for that is to make named functions that exist somewhere and don't get created every run-through.

Link to comment
Share on other sites

 Share

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...