Question

I'm creating a HTML5 minigame that uses collision-detection and I've recently discovered that it has a speed problem:

I think the reason of this problem is that...

Inside the 60fps ticker there are two forEach loops, one for the rects array and other for the lasers array. So, when there's 5 rects and 5 lasers in the canvas, it'll loop 5 times in the first forEach and five times in the second at each frame, and each forEach function has lots of ifs in it, making the game slow. How can I change that to something less CPU-intensive?

If you know a bigger speed problem in this minigame, feel free to help me to solve it too.


Here's my entire code:

I highly recommend you to see the JSFiddle instead of the code below, since there's more than 400 lines.

<!DOCTYPE html>
<html>

<head>
    <title>VelJS α</title>

    <!-- This app was coded by Tiago Marinho -->
    <!-- Do not leech it! -->

    <link rel="shortcut icon" href="http://i.imgur.com/Jja8mvg.png">

    <!-- EaselJS: -->
    <script src="http://static.tumblr.com/uzcr0ts/uzIn1l1v2/easeljs-0.7.1.min.js"></script>

    <script src="http://pastebin.com/raw.php?i=W4S2mtCp"></script>

    <!-- jQuery: -->
    <script src="http://code.jquery.com/jquery-1.11.0.min.js"></script>

    <script>
        (function () {

            // Primary vars (stage, circle, rects):
            var stage,
                circle, // Hero!
                rects = [], // Platforms
                lasers = [];
            // Velocity vars:
            var xvel = 0, // X Velocity
                yvel = 0, // Y Velocity
                xvelpast = 0,
                yvelpast = 0;
            // Keyvars (up, left, right, down):
            var up = false, // W or arrow up
                left = false, // A or arrow left
                right = false, // D or arrow right
                down = false; // S or arrow down
            // Other vars (maxvel, col, pause):
            var maxvel = 256, // Maximum velocity
                col = false, // Collision detection helper (returns true if collided side-to-side)
                pause = false;
            // Volatility vars (rmdir, pastrmdir):
            var rmdir = 0,
                pastrmdir = 0;

            // Main part (aka creating stage, creating circle, etc):

            function init() {
                stage = new createjs.Stage("canvas");

                // Creating circle:
                var circle = new createjs.Shape();
                circle.radius = 11;
                circle.graphics.beginFill("#fff").beginStroke("white").drawCircle(circle.radius - 0.5, circle.radius - 0.5, circle.radius);
                circle.width = circle.radius * 2;
                circle.height = circle.radius * 2;
                stage.addChild(circle);

                setTimeout(function () {

                    // newobj(W, H, X, Y)
                    newobj("laser", 3, 244, stage.canvas.width / 2 - 125, stage.canvas.height / 4 * 3 - 247);
                    newobj("rect", 125, 3, stage.canvas.width / 2 - 125, stage.canvas.height / 4 * 3 - 250);
                }, 250); // Wait until first tick finishes and stage is resized to 100%, then calculate the middle of canvas.

                // User Input (Redirect input to Input Handler):

                // Keydown:
                document.addEventListener("keydown", function (evt) {
                    if (evt.keyCode == 87 || evt.keyCode == 38) { // up
                        up = true;
                    }
                    if (evt.keyCode == 65 || evt.keyCode == 37) { // left
                        left = true;
                    }
                    if (evt.keyCode == 68 || evt.keyCode == 39) { // right
                        right = true;
                    }
                    if (evt.keyCode == 83 || evt.keyCode == 40) { // down
                        down = true;
                    }
                    if (evt.keyCode == 8 || evt.keyCode == 80) { // del/p
                        if (pause == false) {
                            xvelpast = xvel;
                            yvelpast = yvel;
                            pause = true;

                            var fadestep = 0;
                            for (var i = 1; i > 0; i -= 0.1) {
                                i = parseFloat(i.toFixed(1));
                                fadestep++;
                                fadeFill("circle", i, fadestep);
                                rects.forEach(function (rect) {
                                    fadeFill("rect", i, fadestep);
                                });
                            }
                        } else {
                            pause = false;
                            xvel = xvelpast;
                            yvel = yvelpast;
                            var fadestep = 0;
                            for (var i = 0; i <= 1; i += 0.1) {
                                i = parseFloat(i.toFixed(1));
                                fadestep++;
                                fadeFill("circle", i, fadestep);
                                rects.forEach(function (rect) {
                                    fadeFill("rect", i, fadestep);
                                });
                            }
                        }
                    }
                });
                // Keyup:
                document.addEventListener("keyup", function (evt) {
                    if (evt.keyCode == 87 || evt.keyCode == 38) { // up
                        up = false;
                    }
                    if (evt.keyCode == 65 || evt.keyCode == 37) { // left
                        left = false;
                    }
                    if (evt.keyCode == 68 || evt.keyCode == 39) { // right
                        right = false;
                    }
                    if (evt.keyCode == 83 || evt.keyCode == 40) { // down
                        down = false;
                    }
                });

                // Functions:

                // Fade beginFill to a lower alpha:
                function fadeFill(obj, i, t) {
                    setTimeout(function () {
                        if (obj == "circle") {
                            circle.graphics.clear().beginFill("rgba(255,255,255," + i + ")").beginStroke("white").drawCircle(circle.radius, circle.radius, circle.radius).endFill();
                        }
                        if (obj == "rect") {
                            for (var r = 0; r < rects.length; r++) {
                                rects[r].graphics.clear().beginFill("rgba(255,255,255," + i + ")").beginStroke("white").drawRect(0, 0, rects[r].width, rects[r].height).endFill();
                            }
                        }
                    }, t * 20);
                };
                // To create new rects:
                function newobj(type, w, h, x, y) {
                    if (type == "rect") {
                        var rect = new createjs.Shape();
                        rect.graphics.beginFill("#fff").beginStroke("white").drawRect(0, 0, w, h);
                        rect.width = w + 1;
                        rect.height = h + 1;
                        rect.y = Math.round(y) + 0.5;
                        rect.x = Math.round(x) + 0.5;
                        stage.addChild(rect);
                        rects.push(rect);
                    }
                    if (type == "laser") {
                        var laser = new createjs.Shape();
                        if (w >= h) {
                            laser.graphics.beginFill("#c22").drawRect(0, 0, w, 1);
                            laser.width = w;
                            laser.height = 1;
                        } else {
                            laser.graphics.beginFill("#c22").drawRect(0, 0, 1, h);
                            laser.width = 1;
                            laser.height = h;
                        }
                        laser.shadow = new createjs.Shadow("#ff0000", 0, 0, 5);
                        laser.y = Math.round(y);
                        laser.x = Math.round(x);
                        stage.addChild(laser);
                        lasers.push(laser);
                    }
                }
                // Collision recoil:
                function cls(clsdir) {
                    if (clsdir == "top") {
                        if (yvel <= 4) {
                            yvel = 0;
                        } else {
                            yvel = Math.round(yvel * -0.5);
                        }
                    }
                    if (clsdir == "left") {
                        if (xvel <= 4) {
                            xvel = 0;
                        } else {
                            xvel = Math.round(xvel * -0.5);
                        }
                    }
                    if (clsdir == "right") {
                        if (xvel >= -4) {
                            xvel = 0;
                        } else {
                            xvel = Math.round(xvel * -0.5);
                        }
                    }
                    if (clsdir == "bottom") {
                        if (yvel >= -4) {
                            yvel = 0;
                        } else {
                            yvel = Math.round(yvel * -0.5);
                        }
                    }
                    col = true;
                }
                // Die:
                function die() {
                    circle.alpha = 1;
                    createjs.Tween.get(circle).to({
                        alpha: 0
                    }, 250).call(handleComplete);

                    function handleComplete() {
                        circle.x = stage.canvas.width / 2 - circle.radius;
                        circle.y = stage.canvas.height / 2 - circle.radius;
                        createjs.Tween.get(circle).to({
                            alpha: 1
                        }, 250);
                        yvel = 0;
                        xvel = 0;
                        yvelpast = 0;
                        xvelpast = 0;
                    }
                    yvel = yvel/2;
                    xvel = xvel/2;
                }

                // Set Intervals:

                // Speed/Score:
                setInterval(function () {
                    if (pause == false) {
                        speed = Math.abs(xvel) + Math.abs(yvel);
                        $(".speed").html("Speed: " + speed);
                    } else {
                        speed = Math.abs(xvelpast) + Math.abs(yvelpast);
                        $(".speed").html("Speed: " + speed + " (Paused)");
                    }
                }, 175);

                // Tick:

                createjs.Ticker.on("tick", tick);
                createjs.Ticker.setFPS(60);

                function tick(event) {

                    // Input Handler:

                    if (up == true) {
                        yvel -= 2;
                    } else {
                        if (yvel < 0) {
                            yvel++;
                        }
                    }
                    if (left == true) {
                        xvel -= 2;
                    } else {
                        if (xvel < 0) {
                            xvel++;
                        }
                    }
                    if (right == true) {
                        xvel += 2;
                    } else {
                        if (xvel > 0) {
                            xvel--;
                        }
                    }
                    if (down == true) {
                        yvel += 2;
                    } else {
                        if (yvel > 0) {
                            yvel--;
                        }
                    }

                    // Volatility:

                    pastrmdir = rmdir;
                    rmdir = Math.floor((Math.random() * 20) + 1);
                    if (rmdir == 1 && pastrmdir != 4) {
                        yvel--;
                    }
                    if (rmdir == 2 && pastrmdir != 3) {
                        xvel--;
                    }
                    if (rmdir == 3 && pastrmdir != 2) {
                        xvel++;
                    }
                    if (rmdir == 4 && pastrmdir != 1) {
                        yvel++;
                    }

                    // Velocity limiter:

                    if (xvel > maxvel || xvel < maxvel * -1) {
                        (xvel > 0) ? xvel = maxvel : xvel = maxvel * -1;
                    }
                    if (yvel > maxvel || yvel < maxvel * -1) {
                        (yvel > 0) ? yvel = maxvel : yvel = maxvel * -1;
                    }

                    // Collision handler:
                    // xvel and yvel modifications must be before this!

                    rects.forEach(function (rect) { // Affect all rects

                        // Collision detection:
                        // (This MUST BE after every change in xvel/yvel)

                        // Next circle position calculation:
                        nextposx = circle.x + event.delta / 1000 * xvel * 30,
                        nextposy = circle.y + event.delta / 1000 * yvel * 30;
                        // Collision between objects (Rect and Circle):
                        if (nextposy + circle.height > rect.y && circle.y + circle.height < rect.y && circle.x + circle.width > rect.x && circle.x < rect.x + rect.width) {
                            cls("top");
                        }
                        if (nextposx + circle.width > rect.x && circle.x + circle.width < rect.x && circle.y + circle.height > rect.y && circle.y < rect.y + rect.height) {
                            cls("left");
                        }
                        if (nextposx < rect.x + rect.width && circle.x > rect.x + rect.width && circle.y + circle.height > rect.y && circle.y < rect.y + rect.height) {
                            cls("right");
                        }
                        if (nextposy < rect.y + rect.height && circle.y > rect.y + rect.height && circle.x + circle.width > rect.x && circle.x < rect.x + rect.width) {
                            cls("bottom");
                        }
                        rects.forEach(function (rect) {
                            // Check side-to-side collisions with other rects:
                            if (nextposy + circle.height > rect.y && circle.y + circle.height < rect.y && circle.x + circle.width > rect.x && circle.x < rect.x + rect.width) {
                                col = true;
                            }
                            if (nextposx + circle.width > rect.x && circle.x + circle.width < rect.x && circle.y + circle.height > rect.y && circle.y < rect.y + rect.height) {
                                col = true;
                            }
                            if (nextposx < rect.x + rect.width && circle.x > rect.x + rect.width && circle.y + circle.height > rect.y && circle.y < rect.y + rect.height) {
                                col = true;
                            }
                            if (nextposy < rect.y + rect.height && circle.y > rect.y + rect.height && circle.x + circle.width > rect.x && circle.x < rect.x + rect.width) {
                                col = true;
                            }
                        });

                        // Edge-to-edge collision between objects (Rect and Circle) - Note that this will not occur if a side-to-side collision occurred in the current frame!:
                        if (nextposy + circle.height > rect.y &&
                            nextposx + circle.width > rect.x &&
                            nextposx < rect.x + rect.width &&
                            nextposy < rect.y + rect.height &&
                            col == false) {
                            if (circle.y + circle.height < rect.y &&
                                circle.x + circle.width < rect.x) {
                                cls("top");
                                cls("left");
                            }
                            if (circle.y > rect.y + rect.height &&
                                circle.x + circle.width < rect.x) {
                                cls("bottom");
                                cls("left");
                            }
                            if (circle.y + circle.height < rect.y &&
                                circle.x > rect.x + rect.width) {
                                cls("top");
                                cls("right");
                            }
                            if (circle.y > rect.y + rect.height &&
                                circle.x > rect.x + rect.width) {
                                cls("bottom");
                                cls("right");
                            }
                        }
                        col = false;

                        // Stage collision:
                        if (nextposy < 0) { // Collided with TOP of stage. Trust me.
                            cls("bottom"); // Inverted clsdir is proposital!
                        }
                        if (nextposx < 0) {
                            cls("right");
                        }
                        if (nextposx + circle.width > stage.canvas.width) {
                            cls("left");
                        }
                        if (nextposy + circle.height > stage.canvas.height) {
                            cls("top");
                        }
                    });

                    // Laser collision handler:
                    lasers.forEach(function (laser) {
                        laser.alpha = Math.random() + 0.5;
                        nextposx = circle.x + event.delta / 1000 * xvel * 30,
                        nextposy = circle.y + event.delta / 1000 * yvel * 30;

                        if (nextposy + circle.height > laser.y && circle.y + circle.height < laser.y && circle.x + circle.width > laser.x && circle.x < laser.x + laser.width) {
                            circle.y = laser.y-circle.height;
                            die();
                        }
                        if (nextposx + circle.width > laser.x && circle.x + circle.width < laser.x && circle.y + circle.height > laser.y && circle.y < laser.y + laser.height) {
                            circle.x = laser.x-circle.width;
                            die();
                        }
                        if (nextposx < laser.x + laser.width && circle.x > laser.x + laser.width && circle.y + circle.height > laser.y && circle.y < laser.y + laser.height) {
                            circle.x = laser.x+laser.width;
                            die();
                        }
                        if (nextposy < laser.y + laser.height && circle.y > laser.y + laser.height && circle.x + circle.width > laser.x && circle.x < laser.x + laser.width) {
                            circle.y = laser.y+laser.height;
                            die();
                        }
                    });

                    // Velocity:
                    if (pause == true) {
                        xvel = 0;
                        yvel = 0;
                    }
                    circle.x += event.delta / 1000 * xvel * 20;
                    circle.y += event.delta / 1000 * yvel * 20;

                    // Stage.canvas 100% width and height:

                    stage.canvas.width = window.innerWidth;
                    stage.canvas.height = window.innerHeight;

                    // Update stage:

                    stage.update(event);
                }
                setTimeout(function () {
                    // Centre circle:
                    circle.x = stage.canvas.width / 2 - circle.radius;
                    circle.y = stage.canvas.height / 2 - circle.radius;

                    // Fade-in after loading:

                    $(".speed").css({
                        opacity: 1
                    });
                    $("canvas").css({
                        opacity: 1
                    });
                }, 500);
            }
            $(function () {
                init();
            });
        })();
    </script>
    <style>
        * {
            margin: 0;
        }
        html,
        body {
            -webkit-font-smoothing: antialiased;
            font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
            font-weight: 300;
            color: #fff;
            background-color: #181818
        }
        .build {
            position: absolute;
            bottom: 5px;
            right: 5px;
            color: rgba(255, 255, 255, 0.05)
        }
        canvas {
            -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
            filter: alpha(opacity=0);
            opacity: 0;
            position: absolute;
            top: 0;
            left: 0;
            -moz-transition: 5s ease;
            -o-transition: 5s ease;
            -webkit-transition: 5s ease;
            transition: 5s ease
        }
        .speed {
            -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";
            filter: alpha(opacity=0);
            opacity: 0;
            position: absolute;
            top: 5px;
            left: 5px;
            color: #fff;
            font-size: 16px;
            -moz-transition: 5s ease;
            -o-transition: 5s ease;
            -webkit-transition: 5s ease;
            transition: 5s ease
        }
        h2 {
            text-align: center;
            font-size: 22px;
            font-weight: 700
        }
        p {
            font-size: 16px;
            margin: 0
        }
    </style>
</head>

<body>
    <p class="speed"></p>
    <p class="build">α256</p>
    <canvas id="canvas">
        <h2>Your browser doesn't support Canvas.</h2>
        <p>Switch to <b>Chrome 33</b>, <b>Firefox 27</b> or <b>Safari 7</b>.</p>
    </canvas>
</body>

</html>

JSFiddle

Was it helpful?

Solution

The game logic isn't really a problem, the reason it's slow is because you "create" a new canvas every tick by setting the width and height:

stage.canvas.width = window.innerWidth;
stage.canvas.height = window.innerHeight;

So, even if you set the canvas width and height to the same values they had, under the hood pretty much a new canvas is constructed. If you remove the lines above from the game loop it should run smoothly.

Just set the canvas width and height once and then listen for window resize and set it when the browser window changes size.

OTHER TIPS

Running logic in a tick can be expensive, as is updating the canvas each frame. If you can, a lower framerate could be advisable - since it often isn't necessary to run at 60fps. If you want to keep refreshing the canvas at that rate, and you have any particularly expensive functions, such as pathfinding, collision, etc - you could always decouple that from your update loop, so it isn't running quite as often.

Note that stage updating can be very expensive, especially with vectors. If you can, find a solution that lets you cache vector content as bitmaps, and update the vector as little as possible. In your case, since you are just fading the fill - you might separate the fill from the outline, cache them separately, and then use alpha on the fill object. If you have a lot of shapes, you can reuse the caches between them, and you will see huge performance gains.

Best of luck!

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top