2
\$\begingroup\$

I am starting to experience frame rate issues in my game and I've tracked it down to my updateMobs function. I would like tips and code examples from you guys to improve the given code below, to make it more efficient FPS-wise. Can you also tell me why this speeds up my code and by how much, if possible?

var updateMobs = function() {
    for (var b = 0; b < mobsBlue.length; b++) {
        BM = mobsBlue[b]
        BM.x = BM.x - 1

        doCollision(BM, redBase, BM)
        doCollision(BM, debugPlayer, BM)

    }
    for (var r = 0; r < mobsRed.length; r++) {
        RM = mobsRed[r]
        RM.x = RM.x + 1

        doCollision(RM, blueBase, RM)
        doCollision(RM, debugPlayer, RM)

        for (var bb = 0; bb < mobsBlue.length; bb++) {
            BM = mobsBlue[bb]

            if (doCollision(RM, BM, collisionNull) == true) {
                BM.x = BM.x + 1
                RM.x = RM.x - 1
                if (BM.object.attackTime == 100 || RM.object.attackTime == 100) {
                    if (BM.object.armourType == 'light') {
                        BM.object.health = BM.object.health - RM.object.lightDamage
                    } 
                    if (BM.object.armourType == 'heavy') {
                        BM.object.health = BM.object.health - RM.object.heavyDamage
                    }
                    if (RM.object.armourType == 'light') {
                        RM.object.health = RM.object.health - BM.object.lightDamage
                    }
                    if (RM.object.armourType == 'heavy') {
                        RM.object.health = RM.object.health - BM.object.heavyDamage

                    }
                    if (BM.object.health <= 0) {
                        var BI = mobsBlue.indexOf(BM)
                        mobsBlue.splice(BI, 1)
                    }
                    if (RM.object.health <= 0) {
                        var RI = mobsRed.indexOf(RM)
                        mobsRed.splice(RI, 1)

                    }
                    BM.object.attackTime = 0
                    RM.object.attackTime = 0

                }
                BM.object.attackTime = BM.object.attackTime + 1
                RM.object.attackTime = RM.object.attackTime + 1
                console.log("blue " + BM.object.attackTime)
                console.log("red " + RM.object.attackTime)

            }
            console.log(RM.object.health)

            BM.Draw(ctx, false)
            RM.Draw(ctx, false)
        }

    }
}

Other included functions

var doCollision = function(rect1, rect2, objectToMove) {
    if (rect1.x + rect1.w > rect2.x &&
        rect1.x < rect2.x + rect2.w &&
        rect1.y + rect1.h > rect2.y &&
        rect1.y < rect2.y + rect2.h) {
        if (objectToMove === rect1) {
            moveOutside(objectToMove, rect2);
            return true
        } else if (objectToMove === rect2) {
            moveOutside(objectToMove, rect1);
            return true
        }
        return true
    };
};

var moveOutside = function(rectToMove, otherRect) {
    // Determine if the overlap is due more to x or to y,
    // then perform the appropriate move

    var moveOverOtherX = rectToMove.x + rectToMove.w - otherRect.x;
    var otherOverMoveX = otherRect.x + otherRect.w - rectToMove.x;

    var moveOverOtherY = rectToMove.y + rectToMove.h - otherRect.y;
    var otherOverMoveY = otherRect.y + otherRect.h - rectToMove.y;

    var minOver = Math.min(moveOverOtherX, otherOverMoveX, moveOverOtherY, otherOverMoveY);

    if (minOver == moveOverOtherX) {
        rectToMove.x = otherRect.x - rectToMove.w;
    } else if (minOver == otherOverMoveX) {
        rectToMove.x = otherRect.x + otherRect.w;
    } else if (minOver == moveOverOtherY) {
        rectToMove.y = otherRect.y - rectToMove.h;
    } else {
        rectToMove.y = otherRect.y + otherRect.h;
    }
}
var appendMob = function(mob, team) {
    var deployRange = window.innerHeight - 349
    var y = Math.floor((Math.random() * deployRange) + 150);
    if (mob == 'zombie') {
        var zombieObj = {
            health: 120,
            armourType: 'heavy',
            heavyDamage: 20,
            lightDamage: 5,
            attackTime: 0
        }
        if (team == 'red') {
            var zombie = new Rectangle(150, y, 30, 50)
            zombie.object = zombieObj
            zombie.colour = "red"
            mobsRed[mobsRed.length] = zombie
        }
        if (team == 'blue') {
            var zombie = new Rectangle(window.innerWidth - 180, y, 30, 50)
            zombie.object = zombieObj
            zombie.colour = "blue"
            mobsBlue[mobsBlue.length] = zombie
        };
    };
}

(this ones in another file)

this.Draw = function(ctx, hollow){
    ctx.fillStyle = this.colour;
    ctx.strokeStyle = this.colour;

    if (hollow == true){
        ctx.lineWidth = this.thickness
        ctx.strokeRect(this.x, this.y, this.w, this.h);
    };
    if (hollow == false){
        ctx.fillRect(this.x, this.y, this.w, this.h)
    };

};

(BM and RM are NOT global variables)

Also, I am only getting lag when there is 20 - 30+ items in both arrays mobsRed and mobsBlue. I want it so i can have 100 + items in both arrays and it still not lagging (if possible)

\$\endgroup\$
  • \$\begingroup\$ The vast majority of what's happening in this function is probably in doCollision() and Draw(), can you provide those? Can you also clarify why you're using global variables (is that intentional?) for BM and RM? Also, is this function being called every frame? \$\endgroup\$ – Eric Blade Mar 21 '15 at 22:21
  • \$\begingroup\$ Edited the question \$\endgroup\$ – Colourfit Mar 21 '15 at 22:24
2
\$\begingroup\$

Limit the number of loops

Sure, JavaScript is fast, but loops are your biggest enemy especially when they start to nest. As far as I have experienced, a 10k item loop will start to choke your system. Nesting 2 loops is worse, and nesting more is even more terrible.

A tip I once followed is to design your data to avoid such. In cases where I had 2D coordinate systems, I just used a flat list. It may result to more calculations, but at least those calculations aren't as slow as N^2.

Use requestAnimationFrame

Called rAF, it ensures that your frames aim for 60fps. Browsers will try to maintain 60fps and if the browser chokes, it drops frames instead of lagging the game. This also means...

Separate state from rendering

Logic should only modify state. Render should only pick up and render data. Should rendering be slow, at worst the browser can drop frames, but not hinder state updates. Same for the other way around. If the state changes lag behind, rendering will lag not because state prevented a render, but because it's rendering stale data.

Bitwise operations over Math functions

Math functions are slow compared to their bitwise counterparts. Math.floor(N) can be substituted by N | 0, as flooring the number is similar to just dropping the decimals.

console.log is slow

While your most favorite dev tools is the most handy, it's also slow. Try breakpoints instead.

Reduce conditions

Conditions, especially those daisy-chained one after the other are slow. Either simplify the logic, or break them into blocks. For instance, the worst case is that the engine tries to run 3 conditions before reaching the last block.

if (minOver == moveOverOtherX) {
    rectToMove.x = otherRect.x - rectToMove.w;
} else if (minOver == otherOverMoveX) {
    rectToMove.x = otherRect.x + otherRect.w;
} else if (minOver == moveOverOtherY) {
    rectToMove.y = otherRect.y - rectToMove.h;
} else {
    rectToMove.y = otherRect.y + otherRect.h;
}

Also, this code poses a problem. Your armor is only 1 type, but this code executes conditions for each type. I'd try minimizing that.

if (BM.object.armourType == 'light') {
  BM.object.health = BM.object.health - RM.object.lightDamage
} 
if (BM.object.armourType == 'heavy') {
  BM.object.health = BM.object.health - RM.object.heavyDamage
}
if (RM.object.armourType == 'light') {
  RM.object.health = RM.object.health - BM.object.lightDamage
}
if (RM.object.armourType == 'heavy') {
  RM.object.health = RM.object.health - BM.object.heavyDamage
}

// Try:
BM.object.health = BM.object.health - RM.object[BM.object.armor + 'Damage'];
RM.object.health = RM.object.health - BM.object[BM.object.armor + 'Damage'];
\$\endgroup\$
3
\$\begingroup\$

It's a bit hard to answer, because the code is not runnable, so cannot be practically tested, profiled or refactored. At first sight, I think that there are some things you could consider changing (see below) - but the relevancy of these remains dubious at best, as I could not verify my hypotheses with working code. I hope this helps, anyway.

1. Managing the pools of mobs

You write

var BI = mobsBlue.indexOf(BM);
mobsBlue.splice(BI, 1);

and

var RI = mobsRed.indexOf(RM);
mobsRed.splice(RI, 1);

The indexOf calls seem unneeded, as the indices of your mobs are already available in the outer for loops (namely the variables r and bb).

The splicing of an array is an expensive operation. If you can work with a finite length for your pools of mobs, then it's better to define an array of this length, and nullify the slot where your mobs die: mobsRed[r] = null, then testing mobsRed[r] for null before applying any further logic.

2. Managing the board

Loops of loops often indicate a design issue. In your case, you could consider using a board object, made of vertices. Each vertex would contain a reference to a game object (such as a mob), and would provide methods to access neighboring vertices. In other words, you could use something like:

for(var mobIndex = 0; mobIndex < redMobs.length; mobIndex++) {
  var redMob = redMobs[mobIndex];
  var vertex = redMob.getCurrentVertex();
  var left = vertex.getLeft();
  if (!left.isEmpty()) {
    var collidedObject = left.getObject();
    // and so forth
  }
}

When moving an object, you update both the target vertex to contain the object, and the object to reference the vertex. Doing so, you would eventually reduce the overall number of iterations.

3. Other

Finally, there are many style issues with your code: not always finishing your statements with semicolons, comparing boolean variables to explicit boolean values, using abbreviated variables names, using global variables (BM and RM are global since they're not declared with the var keyword), and from a design point of view, exposing mob mechanic internals rather than encapsulating them into methods. Although these should not have any significant impact on the performance, I think that one should always try and write good style.

\$\endgroup\$
  • \$\begingroup\$ The code given is not my entire code. \$\endgroup\$ – Colourfit Mar 22 '15 at 1:23
  • \$\begingroup\$ I know that. What I meant is that the portion of code that you have provided is not runnable, profilable and testable. Actually, it might even be a design thing, as in most apps, components should be well encapsulated and unit-testable. Anyway, this degrades the quality of the answer I have provided, as it is based more on "impressions" rather than "facts". I do understand that providing the full code base might not be practical for you, though, and I hope that what I've written will somehow be of use to you. \$\endgroup\$ – Elegie Mar 22 '15 at 3:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.