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?