1
\$\begingroup\$

I'm new to building games on canvas, and I'm trying to create one. But I encountered a problem with sprite movements. My sprites move is smooth, but sometimes it starts to "jump". FPS is stable 60 frames per seconds.

Here is minimal implementation to play with: https://codepen.io/antonmedv/pen/ROjqXw

Code:

var __extends = (this && this.__extends) || (function () {
    var extendStatics = function (d, b) {
        extendStatics = Object.setPrototypeOf ||
            ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
            function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
        return extendStatics(d, b);
    };
    return function (d, b) {
        extendStatics(d, b);
        function __() { this.constructor = d; }
        d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
    };
})();
var Sprite = /** @class */ (function () {
    function Sprite(img, width, height) {
        this.width = width;
        this.height = height;
        var image = new Image();
        image.src = img;
        this.image = image;
    }
    return Sprite;
}());
function drawSprite(c, sprite) {
    c.drawImage(sprite.image, -sprite.width / 2, -sprite.height / 2);
}
function vector(x, y) {
    return { x: x, y: y };
}
function copy(a) {
    return vector(a.x, a.y);
}
var nil = function () { return vector(0, 0); };
function divide(a, b) {
    if (b == 0) {
        return nil();
    }
    return vector(a.x / b, a.y / b);
}
function negative(a) {
    return vector(-a.x, -a.y);
}
function add() {
    var list = [];
    for (var _i = 0; _i < arguments.length; _i++) {
        list[_i] = arguments[_i];
    }
    var x = 0, y = 0;
    for (var _a = 0, list_1 = list; _a < list_1.length; _a++) {
        var v = list_1[_a];
        x += v.x;
        y += v.y;
    }
    return vector(x, y);
}
function multiply(a, x) {
    return vector(a.x * x, a.y * x);
}
function rad(grad) {
    return grad * Math.PI / 180;
}
function angel(a, b) {
    var x = Math.atan2(a.y, a.x) - Math.atan2(b.y, b.x);
    if (x > Math.PI) {
        x -= 2 * Math.PI;
    }
    else if (x <= -Math.PI) {
        x += 2 * Math.PI;
    }
    return x;
}
function norm(v) {
    return Math.sqrt(Math.pow(v.x, 2) + Math.pow(v.y, 2));
}
function dot(a, b) {
    return a.x * b.x + a.y * b.y;
}
function normalize(a) {
    return divide(a, norm(a));
}
var bodies = [];
var Body = /** @class */ (function () {
    function Body(sprite) {
        this.sprite = sprite;
        this.mass = 1;
        this.position = nil();
        this.velocity = nil();
        this.force = nil();
    }
    return Body;
}());
var Plane = /** @class */ (function (_super) {
    __extends(Plane, _super);
    function Plane(sprite, forward, normal) {
        var _this = _super.call(this, sprite) || this;
        _this.forward = forward;
        _this.normal = normal;
        _this.angle = 0;
        _this.elevator = 0;
        _this.thrust = 0;
        return _this;
    }
    return Plane;
}(Body));
function render(c, width, height, dpr) {
    c.clearRect(0, 0, width, height);
    for (var _i = 0, bodies_1 = bodies; _i < bodies_1.length; _i++) {
        var body = bodies_1[_i];
        c.save();
        if (body.position.x < 0) {
            body.position.x = width / dpr;
        }
        if (body.position.x > width / dpr) {
            body.position.x = 0;
        }
        if (body.position.y > height / dpr) {
            body.position.y = 0;
        }
        if (body.position.y < 0) {
            body.position.y = height / dpr;
        }
        c.translate(body.position.x, body.position.y);
        c.rotate(body.angle);
        drawSprite(c, body.sprite);
        c.restore();
    }
}
function timestamp() {
    return window.performance && window.performance.now ? window.performance.now() : Date.now();
}
void function main() {
    var canvas = document.querySelector('#canvas');
    var c = canvas.getContext('2d');
    function setupCanvas() {
        canvas.style.width = window.innerWidth + 'px';
        canvas.style.height = window.innerHeight + 'px';
        var dpr = window.devicePixelRatio || 1;
        var rect = canvas.getBoundingClientRect();
        canvas.width = rect.width * dpr;
        canvas.height = rect.height * dpr;
        c.scale(dpr, dpr);
        return dpr;
    }
    var dpr = setupCanvas();
    var plane1 = createPlane();
    attachControls(plane1, {
        thrustLevers: { up: 'ArrowUp', down: 'ArrowDown' },
        elevator: { up: 'ArrowLeft', down: 'ArrowRight' },
        fire: 'Space',
    });
    bodies.push(plane1);
    var now, dt = 0, last = timestamp(), step = 1 / 60;
    function frame() {
        now = timestamp();
        dt = dt + Math.min(1, (now - last) / 1000);
        while (dt > step) {
            dt = dt - step;
            simulation(step * 4);
        }
        render(c, canvas.width, canvas.height, dpr);
        last = now;
        requestAnimationFrame(frame);
    }
    requestAnimationFrame(frame);
}();
function simulation(dt) {
    for (var _i = 0, bodies_2 = bodies; _i < bodies_2.length; _i++) {
        var body = bodies_2[_i];
        if (body instanceof Plane) {
            computeForceOnPlane(body, dt);
        }
        var acceleration = divide(body.force, body.mass);
        body.velocity.x += acceleration.x * dt;
        body.velocity.y += acceleration.y * dt;
        body.position.x += body.velocity.x * dt;
        body.position.y += body.velocity.y * dt;
    }
}
function computeForceOnPlane(plane, dt) {
    plane.forward = vector(Math.sin(-plane.angle - Math.PI / 2), Math.cos(-plane.angle - Math.PI / 2));
    plane.normal = vector(Math.sin(-plane.angle + Math.PI), Math.cos(-plane.angle + Math.PI));
    var c = 1 - norm(plane.velocity) / (100 * 1.5);
    plane.angle += c * plane.elevator * dt;
    var v = norm(plane.velocity);
    var a = angel(plane.forward, plane.velocity);
    var gravity = vector(0, plane.mass * 9.81);
    var thrust = multiply(plane.forward, plane.thrust * 0.9);
    var lift = multiply(plane.normal, v * trim(a, rad(50)));
    var drag = multiply(normalize(negative(plane.velocity)), Math.pow(2, (v - 100)));
    plane.force = add(gravity, thrust, lift, drag);
}
function trim(a, max) {
    if (a < max && a > -max) {
        return a;
    }
    return 0;
}
function createPlane() {
    var plane = new Plane(createPlaneSprite(), vector(-1, 0), vector(0, -1));
    plane.position = vector(100, 100);
    plane.thrust = 10;
    plane.radius = 20;
    return plane;
}
function attachControls(plane, controls) {
    document.addEventListener('keydown', function (event) {
        switch (event.code) {
            case controls.thrustLevers.up:
                plane.thrust += 5;
                if (plane.thrust > 10) {
                    plane.thrust = 10;
                }
                break;
            case controls.thrustLevers.down:
                plane.thrust -= 5;
                if (plane.thrust < 0) {
                    plane.thrust = 0;
                }
                break;
            case controls.elevator.up:
                plane.elevator = -1;
                break;
            case controls.elevator.down:
                plane.elevator = 1;
                break;
            case controls.fire:
                fire(world, plane);
                break;
            default:
                return;
        }
        event.preventDefault();
    }, true);
    document.addEventListener('keyup', function (event) {
        switch (event.code) {
            case controls.elevator.up:
            case controls.elevator.down:
                plane.elevator = 0;
                break;
            default:
                return;
        }
        event.preventDefault();
    }, true);
}
function createPlaneSprite() {
    return new Sprite('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4KPHN2ZyB3aWR0aD0iNDBweCIgaGVpZ2h0PSIzMHB4IiB2aWV3Qm94PSIwIDAgNDAgMzAiIHZlcnNpb249IjEuMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICAgIDwhLS0gR2VuZXJhdG9yOiBTa2V0Y2ggNTMuMiAoNzI2NDMpIC0gaHR0cHM6Ly9za2V0Y2hhcHAuY29tIC0tPgogICAgPHRpdGxlPmJpcGxhbmU8L3RpdGxlPgogICAgPGRlc2M+Q3JlYXRlZCB3aXRoIFNrZXRjaC48L2Rlc2M+CiAgICA8ZGVmcz4KICAgICAgICA8bGluZWFyR3JhZGllbnQgeDE9IjUwJSIgeTE9IjAlIiB4Mj0iNTAlIiB5Mj0iMTAwJSIgaWQ9ImxpbmVhckdyYWRpZW50LTEiPgogICAgICAgICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjRDAzNzM3IiBvZmZzZXQ9IjAlIj48L3N0b3A+CiAgICAgICAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiNBMzE5MTkiIG9mZnNldD0iMTAwJSI+PC9zdG9wPgogICAgICAgIDwvbGluZWFyR3JhZGllbnQ+CiAgICAgICAgPGxpbmVhckdyYWRpZW50IHgxPSI1MCUiIHkxPSIwJSIgeDI9IjUwJSIgeTI9IjEwMCUiIGlkPSJsaW5lYXJHcmFkaWVudC0yIj4KICAgICAgICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzhERDAzNyIgb2Zmc2V0PSIwJSI+PC9zdG9wPgogICAgICAgICAgICA8c3RvcCBzdG9wLWNvbG9yPSIjQTM5OTE5IiBvZmZzZXQ9IjEwMCUiPjwvc3RvcD4KICAgICAgICA8L2xpbmVhckdyYWRpZW50PgogICAgPC9kZWZzPgogICAgPGcgaWQ9ImJpcGxhbmUiIHN0cm9rZT0ibm9uZSIgc3Ryb2tlLXdpZHRoPSIxIiBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPgogICAgICAgIDxwYXRoIGQ9Ik0xMCwxMC45ODcwMTI5IEwxMCw3IiBpZD0iTGluZS0zIiBzdHJva2U9IiM5Nzk3OTciIHN0cm9rZS1saW5lY2FwPSJzcXVhcmUiPjwvcGF0aD4KICAgICAgICA8cGF0aCBkPSJNMTgsMTAuNSBMMTUsNi41IiBpZD0iTGluZS00IiBzdHJva2U9IiM5Nzk3OTciIHN0cm9rZS1saW5lY2FwPSJzcXVhcmUiPjwvcGF0aD4KICAgICAgICA8cGF0aCBkPSJNMTAsMTAuNSBMMTUsNS41IiBpZD0iTGluZS01IiBzdHJva2U9IiM5Nzk3OTciIHN0cm9rZS1saW5lY2FwPSJzcXVhcmUiPjwvcGF0aD4KICAgICAgICA8cGF0aCBkPSJNNS4wMjI4ODkyOSwxNSBMMi41LDE1IiBpZD0iTGluZS0yIiBzdHJva2U9IiMwMDAwMDAiIHN0cm9rZS1saW5lY2FwPSJzcXVhcmUiPjwvcGF0aD4KICAgICAgICA8cGF0aCBkPSJNNi43NjIxNDY2OCwxMC45ODcwMTI5IEwxNy4yMTA3MzM1LDEwLjk4NzAxMjkgTDE4LjYyOTE3Myw4Ljk3Nzg0ODg2IEwyMi43NDI2NDczLDguOTc3ODQ4ODYgTDI2Ljc0MDU0NTIsMTMuNTgyODE1NCBMMzAuMjA0OTIzLDEzLjU4MjgxNTQgTDMzLjg1MjAwNTIsNy42OTcxNjczMyBMMzYuOTUyNzI1NSw3LjY5NzE2NzMzIEwzOC4yNzQyNjc0LDguOTc3ODQ4ODYgTDM4LjI3NDI2NzQsMTYuMjU4NzgxOSBDMzAuMjg2NjQzOCwxOS4zNDg0NzY2IDIyLjM1MTM2MDQsMjAuODkzMzIzOSAxNC40Njg0MTczLDIwLjg5MzMyMzkgTDcsMTkuMjg4MTkwOCBMNS4wNTcxMDA3NSwxNy45NTgzODAxIEM1LjAxMTQ4NTQ3LDE0LjE3MTA2NjcgNS4wMTE0ODU0NywxMi4xNzM1NzcxIDUuMDU3MTAwNzUsMTEuOTY1OTExMiBDNS4xMDI3MTYwMywxMS43NTgyNDU0IDUuNjcxMDY0NjcsMTEuNDMxOTQ1OSA2Ljc2MjE0NjY4LDEwLjk4NzAxMjkgWiIKICAgICAgICAgICAgICBpZD0iUGF0aCIgc3Ryb2tlPSIjMDAwMDAwIiBzdHJva2Utd2lkdGg9IjEuNSIgZmlsbD0idXJsKCNsaW5lYXJHcmFkaWVudC0xKSI+PC9wYXRoPgogICAgICAgIDxwYXRoIGQ9Ik02Ljc2MjE0NjY4LDEwLjk4NzAxMjkgTDkuMjE4MTMwOTksMTAuOTg3MDEyOSBMOS4yMTgxMzA5OSwxOS4yODgxOTA4IEw2Ljc2MjE0NjY4LDE5LjI4ODE5MDggTDUuMDIyODg5MjksMTcuODc2MTI4MyBDNS4wMDAwODE2NSwxNC4xNDM2NDk0IDUuMDExNDg1NDcsMTIuMTczNTc3MSA1LjA1NzEwMDc1LDExLjk2NTkxMTIgQzUuMTAyNzE2MDMsMTEuNzU4MjQ1NCA1LjY3MTA2NDY3LDExLjQzMTk0NTkgNi43NjIxNDY2OCwxMC45ODcwMTI5IFoiCiAgICAgICAgICAgICAgaWQ9IlBhdGgiIHN0cm9rZT0iIzAwMDAwMCIgZmlsbD0idXJsKCNsaW5lYXJHcmFkaWVudC0yKSI+PC9wYXRoPgogICAgICAgIDxjaXJjbGUgaWQ9Ik92YWwiIHN0cm9rZT0iIzAwMDAwMCIgZmlsbD0iI0MyRDE5OCIgY3g9IjEyLjUiIGN5PSIyMy41IiByPSIyLjUiPjwvY2lyY2xlPgogICAgICAgIDxyZWN0IGlkPSJSZWN0YW5nbGUiIHN0cm9rZT0iIzAwMDAwMCIgZmlsbD0iI0Q4RDhEOCIgeD0iNy41IiB5PSI0LjUiIHdpZHRoPSIxMCIgaGVpZ2h0PSIyIiByeD0iMSI+PC9yZWN0PgogICAgICAgIDxwYXRoIGQ9Ik0zLjUsNyBMMy41LDIzLjUiIGlkPSJMaW5lIiBzdHJva2Utb3BhY2l0eT0iMC40OTc0ODY4ODgiIHN0cm9rZT0iIzAwMDAwMCIKICAgICAgICAgICAgICBzdHJva2UtbGluZWNhcD0ic3F1YXJlIj48L3BhdGg+CiAgICA8L2c+Cjwvc3ZnPgo=', 40, 30);
}
body {
  margin: 0;
  padding: 0;
  width: 100vw;
  height: 100vh;
  overflow: hidden;
  background: #9be2fe;
}
<canvas id="canvas"></canvas>

Movements usually look smooth, but sometimes they are really junky.

Here are the things I'm using:

  • clearRect
  • requestAnimationFrame
  • time delta with performance.now
  • constant time steps for physics simulation

I profiled it, but everything looks smooth in the report (60 fps, each frame has little Javascript, low CPU). Also tried using round x,y for drawImage, still junk movements. If turn off all physics and set const acceleration = vector(-1, 0) sprite movements again jumps from time to time.

How do I make the movements consistently smooth?

\$\endgroup\$
  • \$\begingroup\$ RequestAnimationFrame passes a similar value to performance.now to the called function, why not use that? \$\endgroup\$ – Bálint Apr 16 at 10:05
  • \$\begingroup\$ Please include in the question itself a minimal complete verifiable example of the problem. Code on an external site can change or become unavailable, making the question less useful for future visitors. \$\endgroup\$ – DMGregory Apr 16 at 11:06

Your Answer

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

Browse other questions tagged or ask your own question.