3

Overview

Hi All! - I'm creating a side scroller space shooter (similar to games of old, still learning!) - and I'm wondering how to better manage my objects in order to prevent wasted resources!

So, In my code, I have created an IIFE for the Player and a constructor for projectiles. The player listens for clicks on the game's canvas and when it hears one, it creates a projectile and attaches it to an object within the Player. However, when a projectile reaches the right hand side of the screen, I want it to be destroyed, for it to be removed from the players projectiles object, and for all update and draw functions for the projectile to end. I've so far managed to stop it drawing and updating, but I haven't been able to remove it from the Players projectile object yet. Hopefully the code below will better demonstrate what I'm attempting to do.

Example

var Player = (function () {
    var width = 50;
    var height = 50;
    var x = 0;
    var y = 0;
    var projectiles = [];

    var update = function () {
        for (var p = 0; p < projectiles.length; p++) {
            if(!projectiles[p].destroyed)projectiles[p].update();
        }
    };

    var draw = function () {
        Canvas.context.fillStyle = 'white';
        Canvas.context.fillRect(x, y, width, height);

        for (var p = 0; p < projectiles.length; p++) {
            if(!projectiles[p].destroyed)projectiles[p].draw();
        }
    };

    Canvas.bindEvent('mousemove', function (e) {
        //x = e.pageX - Canvas.element.getBoundingClientRect().left;
        y = e.pageY - Canvas.element.getBoundingClientRect().top;
    });

    Canvas.bindEvent('click', function () {
        projectiles.push(new Projectile(width, y + (height / 2)));
    });

    return {
        draw: draw,
        update: update
    }
})();

var Projectile = function (x, y) {
    this.w = 10;
    this.h = 10;
    this.x = x;
    this.y = y;
    this.speed = 5;
    this.destroyed = false;

    this.update = function () {
        this.x += this.speed;

        if(this.x > Canvas.element.width){
            this.destroyed = true;
            this.x = 0;
            console.log('Projectile Destroyed!');
        }
    };

    this.draw = function(){
        Canvas.context.fillStyle = 'red';
        Canvas.context.fillRect(this.x, this.y, this.w, this.h);
    };
};

Js Fiddle

Here's my current code in a semi-working JS fiddle, so the code above can be viewed in context. If this question isn't clear, please let me know in the comments and I'll do my best to give clarification. Thanks all!

https://jsfiddle.net/tzzgwr1w/

Lewis
  • 3,479
  • 25
  • 40

4 Answers4

3

Try this code:

var update = function () {
    for (var p = arr.length-1; p >= 0; p--) {
        projectiles[p].update();
        if (projectiles[p].destroyed) projectiles.splice(p, 1);
    }
};

This is supposed to be a minor edit to the update function in your player function/class. After updating the projectile, it checks whether the projectile declares itself as destroyed, and removes it if it does. splice removes the element at index p.

Vincent
  • 3,965
  • 2
  • 22
  • 30
  • 1
    `destroyed` can be set to true inside the `update` function. This is the only answer that takes this into account. – A1rPun Mar 23 '16 at 13:56
  • @A1rPun yes, that mean object stay 1 tick more in memory, but this answer does not work because it miss some elements in the array when splice occur. – Hacketo Mar 23 '16 at 14:00
  • @Hacketo Good point. `for (var p = projectiles.length; p--;)` Vincent better make the loop backwards when using `splice`. – A1rPun Mar 23 '16 at 14:32
  • 1
    `.splice` is rather costly -- often 50%+ slower than manually rebuilding an array without the destroyed elements. Splice works but it's only an "OK" solution. A better solution is to simply ignore destroyed elements (and, if necessary, clean them up in bulk with a manual array rebuild when destroyedCount gets large). The best solution is to ignore destroyed elements but also to reuse the destroyed elements for new projectiles. – markE Mar 23 '16 at 19:03
3

You could remove the destroyed one in the update method, but you 'll need to loop from the end of the array.

var update = function () {
    for (var p = projectiles.length - 1; p >= 0; p--) {
        if(!projectiles[p].destroyed)projectiles[p].update();
        else projectiles.splice(p,1);
    }
};

Suppose you have a basic loop from 0 to X. If you remove an element at index 0 the array will shift, that mean the object at index 1 will be at index 0. But the next loop will do i++ then the object that is at index 0 will not be checked.

Hacketo
  • 4,978
  • 4
  • 19
  • 34
3

Using splice is a little waste of resource here. I would recommend a linear sweep on the array. The following algorithm will filter the projectiles array in place:

function removeDestroyeds(arr) {
  for (var i=0, j=0; j < arr.length; j++) {
    if (!arr[j].destroyed) {
      arr[i++] = arr[j];
    }
  }
  arr.length = i;
}

....

var update = function () {
    for (var p = 0; p < projectiles.length; p++) {
        projectiles[p].update();
    }
    removeDestroyeds(projectiles);
};

var draw = function () {
    Canvas.context.fillStyle = 'white';
    Canvas.context.fillRect(x, y, width, height);

    for (var p = 0; p < projectiles.length; p++) {
        projectiles[p].draw();
    }
};

....

Update:

@Hacketo and I made a quick benchmark: http://jsperf.com/splice-nosplice/11

Tamas Hegedus
  • 28,755
  • 12
  • 63
  • 97
  • Good point -- splice is costly, upvote. But even better, just ignore destroyed projectiles and reuse their array element for new projectiles. – markE Mar 23 '16 at 19:04
  • @Hacketo Interesting. First I saw your benchmark and I was like: "oh he didn't restore the array state so after the first iteration it measures with no deletion", which is true, but if I use a new array in every iteration the splice method seems to be still faster. I have think about it, I always thought splice was O(n) – Tamas Hegedus Mar 24 '16 at 11:52
  • @TamasHegedus the bench was not really relevant I made a mistake when writing it (misspelled distoyed in object creation). Forget about it, your solution is faster :) – Hacketo Mar 24 '16 at 12:04
  • @TamasHegedus btw the preparation code is executed before each test so the array remain the same before each one, or am I wrong ? – Hacketo Mar 24 '16 at 12:12
  • @Hacketo The preparation code is run before the bench loop, so the iterating part uses the same array instance. So, by the way if destroyed projectiles are very rare (0-1 projectile per model update), then splice is indeed faster. – Tamas Hegedus Mar 24 '16 at 12:25
  • @Hacketo `filter in place × 9,342 (58 samples)` means that a test loop of 9342 iterations were run 58 times. The preparation code thus runs once per 9342 iterations – Tamas Hegedus Mar 24 '16 at 12:29
1

In your update loop, you can use .splice() to remove a destroyed projectile. In order for this to not skip items in your projectiles array, you need to iterate in reverse.

for (p = projectiles.length-1; p >= 0; p--) {
    if(projectiles[p].destroyed) {
        projectiles.splice(p, 1);
    } else {
        projectiles[p].update();
    }
}
forgivenson
  • 4,394
  • 2
  • 19
  • 28
  • Hi, thanks for answering! - I'm unclear on what you mean about skipping projectiles if not looping in reverse? – Lewis Mar 23 '16 at 13:46
  • 2
    Lets say you have 5 projectiles in the array, and the third one (index = 2) is destroyed, so you want to remove it. When you remove it using `.splice(2,1)`, the item immediately following the third item (it has index = 3), will now move to fill the empty slot left by the removed item, and its index will change to 2. In the next iteration of the loop, `p` will equal 3, so the item that got moved to index 2 will never be checked. – forgivenson Mar 23 '16 at 13:55
  • If you start at the end of the array, any items that get moved have already been checked, so you don't need to worry about skipping any. – forgivenson Mar 23 '16 at 13:56