4

Background: Over the last week I've been working on a game that is essentially multi-directional Tron, using Canvas and JavaScript. I opted not to clear the Canvas every frame so that my little line segments leave a trail. For collision detection, I use this function:

// 8 sensors for collision testing, positioned evenly around the brush point
var detectionRadius = this.width / 2 + 1; //points just outside the circumference
var counter = 0;
var pixelData;
for (var i = 0; i < 16; i += 2) {
    //collisionPixels[] is an array of 8 (x, y) offsets, spaced evenly around the center of the circle
    var x = this.x + collisionPixels[i] * detectionRadius;
    var y = this.y + collisionPixels[i + 1] * detectionRadius;
    pixelData = context.getImageData(x,y,1,1).data; //pixel data at each point
    if (pixelData[3] != 0) {
        counter++;
    }
}
if (counter > 4) {
    this.collision();
}

The purpose here is to get the alpha values of 8 pixels around the brushpoint's surface; alpha values of 0 are just on the background. If the number of colliding pixels, out of the total 8, is greater than 4 (this is including the trail behind the player) then I call the collision() method. This function actually works really well (and this IS inside a function, so these declarations are local).

The problem is that context.getImageData() skyrockets my memory usage, and after 3 or 4 games tanks the framerate. Cutting just that line out and assigning pixelData some other value makes everything run very smoothly, even while doing the other computations.

How do I fix this memory leak? And, if there's a less convoluted way to do collision detection of this type, what is it?

EDIT: at request, here is my loop:

function loop() {
    now = Date.now();
    delta = now - lastUpdate;
    lastUpdate = now;
    if (!paused) {
        for (var i = 0; i < numPlayers; i++) {
            if (players[i].alive) {
                players[i].update(delta);
                players[i].render();
            }
        }
    }
    requestAnimationFrame(loop);
}

EDIT 2: So I tried Patrick's UInt8ClampedArrays idea:

//8 sensors for collision testing, positioned evenly around the brush point
    var detectionRadius = this.width / 2 + 1;
    var counter = 0;
    for (var i = 0; i < 16; i += 2) {
        var x = this.x + collisionPixels[i] * detectionRadius;
        var y = this.y + collisionPixels[i + 1] * detectionRadius;
        //translate into UInt8ClampedArray for data
        var index = (y * canvas.width + x) * 4 + 3; //+3 so we're at the alpha index
        if (canvasArray[index] != 0) {
            counter++;
        }
    }

And, at the top of my loop I added a new global variable, updated once per frame:

var canvasArray = context.getImageData(0,0,canvas.width,canvas.height).data;

Hope I did that right. It works, but the memory and framerate still get worse each round you play. Going to upload some heap snapshots.

EDIT 3:

Snapshot 1: https://drive.google.com/open?id=0B-8p3yyYzRjeY2pEa2Z5QlgxRUk&authuser=0

Snapshot 2: https://drive.google.com/open?id=0B-8p3yyYzRjeV2pJb1NyazY3OWc&authuser=0

Snapshot 1 is after the first game, 2 is after the second.

EDIT 4: Tried capping the framerate:

function loop() {
    requestAnimationFrame(loop);

    now = Date.now();
    delta = now - lastUpdate;
    //lastUpdate = now;

    if (delta > interval) {
        lastUpdate = now;
        if (!paused) {
            for (var i = 0; i < numPlayers; i++) {
                if (players[i].alive) {
                    players[i].update(delta);
                    players[i].render();
                }
            }
        }
    }
}

Where

interval = 1000 / fps;

It delays the eventual performance hit, but memory is still climbing with this option.

EDIT 5: While I'm sure there must be a better way, I found a solution that works reasonably well. Capping the framerate around 30 actually worked in terms of long-term performance, but I hated the way the game looked at 30 FPS.. so I built a loop that had an uncapped framerate for all updating and rendering EXCEPT for collision handling, which I updated at 30 FPS.

function loop() {
    requestAnimationFrame(loop);

    now = Date.now();
    delta = now - lastUpdate;
    lastUpdate = now;

    if (!paused) {
        for (var i = 0; i < numPlayers; i++) {
            if (players[i].alive) {
                players[i].update(delta);
                players[i].render();
            }
        }
        if (now - lastCollisionUpdate > collisionInterval) {
            canvasData = context.getImageData(0, 0, context.canvas.width, context.canvas.height).data;
            for (var i = 0; i < numPlayers; i++) {
                if (players[i].alive) {
                    if (players[i].detectCollisions()) {
                        players[i].collision();
                    }
                }
            }
            lastCollisionUpdate = now;
        }
        canvasData = null;
    }
}

Thanks for the answers.. a lot of your ideas found their way into the final(?) product, and I appreciate that. Closing this thread.

  • What does "skyrockets" mean, more or less? Can you provide a runnable example which shows the issue? – Oriol May 02 '15 at 22:12
  • I don't see any reason for memory leak in this code, but the code is not efficient so I suspect the problem is more that your game is very tight time wise leaving little time for GC. Could you show us how you invoke the loop (e.g. are you using setTimeout/Interval, requestAnimationFrame ...)? –  May 02 '15 at 23:00
  • @KenFyrstenberg Edited into the description; it's requestAnimationFrame(). I'll be trying a framerate cap later tonight to see if that gives GC time. – Jack Britton May 02 '15 at 23:21
  • This won't fix your problem per se, but you are redeclaring some vars unnecessarily. I'd define x, y, and index outside the loop. – Jared Smith May 03 '15 at 01:55

2 Answers2

0

Is there some point at which you could call context.getImageData(0, 0, context.canvas.width, context.canvas.height).data so that you can use that single UInt8ClampedArray instead of however many you're using? Also when you're done with the image data (the ImageData that is, not the TypedArray inside it), you could try calling delete on it, though I'm not certain if that will deallocate the memory.

Patrick Roberts
  • 49,224
  • 10
  • 102
  • 153
  • I've tried running delete, and it does not deallocate the memory. But the first idea sounds promising: not too familiar with UInt8ClampedArrays, so I'll get back to you on it. – Jack Britton May 02 '15 at 23:24
  • @JackBritton Your ``pixelData`` is a ``UInt8ClampedArray``, just to clarify. – Patrick Roberts May 02 '15 at 23:38
0

While I'm sure there must be a better way, I found a solution that works reasonably well. Capping the framerate around 30 actually worked in terms of long-term performance, but I hated the way the game looked at 30 FPS.. so I built a loop that had an uncapped framerate for all updating and rendering EXCEPT for collision handling, which I updated at 30 FPS.

//separate update cycle for collision detection
var collisionFPS = 30;
var lastCollisionUpdate;
var collisionInterval = 1000 / collisionFPS;
var canvasData;

function loop() {
    requestAnimationFrame(loop);

    now = Date.now();
    delta = now - lastUpdate;
    lastUpdate = now;

    if (!paused) {
        for (var i = 0; i < numPlayers; i++) {
            if (players[i].alive) {
                players[i].update(delta);
                players[i].render();
            }
        }
        if (now - lastCollisionUpdate > collisionInterval) {
            canvasData = context.getImageData(0, 0, context.canvas.width, context.canvas.height).data;
            for (var i = 0; i < numPlayers; i++) {
                if (players[i].alive) {
                    if (players[i].detectCollisions()) {
                        players[i].collision();
                    }
                }
            }
            lastCollisionUpdate = now;
        }
        canvasData = null;
    }
}

Might not be the best solution, but it's consistent.