In my demo, you can see that the player’s camera catches up with the player’s position with a slight delay. It follows him until the player finally stops, gradually reducing the distance.
Unfortunately, I noticed that using this camera causes the pixels to be displayed inaccurately and either blur or wobble back and forth. This is especially noticeable on larger screens if you minimize the screen height.
If you simply return the target position within handleCamera
, the functionality of the camera disappears, of course, but this solves the problem.
I am looking for a solution to keep my camera, but to remove the inaccurate pixels. I have already tried to include a certain tolerance, but this worked rather poorly. Any ideas, tips or experience as to why this phenomenon occurs?
const game = document.querySelector('#game');
const context = game.getContext('2d');
const background = new Image();
background.src = 'https://i.imgur.com/Ti1uecQ.png';
const player = {
down: new Image(),
up: new Image(),
left: new Image(),
right: new Image(),
currentDirection: 'down',
frame: 0,
maxFrames: 4,
idle: true,
};
player.down.src = 'https://i.imgur.com/cx6ag4V.png';
player.up.src = 'https://i.imgur.com/oZNeLGC.png';
player.left.src = 'https://i.imgur.com/yU2GBiF.png';
player.right.src = 'https://i.imgur.com/F69aMwq.png';
const backgroundHeight = 480;
const backgroundWidth = 840;
const playerHeight = 16;
const playerWidth = 12;
const initialGameHeight = playerHeight * 10;
const fps = 60;
const speed = 64;
const dodgeSpeed = speed * 2;
const cameraSpeed = speed / 8;
const step = 1 / fps;
const keymap = [];
const keyAssignments = {
up: ['ArrowUp', 'w'],
left: ['ArrowLeft', 'a'],
right: ['ArrowRight', 'd'],
down: ['ArrowDown', 's'],
dodge: [' ']
};
const dodgeSteps = 20;
const dodgeDelaySteps = 40;
let dodgeStep = 0;
let dodgeDelayStep = 0;
let x = backgroundWidth / 2; // Player Starting X
let y = backgroundHeight / 2; // Player Starting Y
let cameraX = x;
let cameraY = y;
let previousMs = 0;
let scale = 1;
let keyListenerPaused = false;
let lastTempChange = null;
const handleScreens = () => {
game.height = Math.round(window.innerHeight / 2) * 2;
game.width = Math.round(window.innerWidth / 2) * 2;
scale = Math.round(window.innerHeight / initialGameHeight);
document.documentElement.style.setProperty('--scale', scale);
};
const handleKey = (key) => {
if (keyListenerPaused) return;
}
const handleCamera = (currentValue, destinationValue, delta) => {
// return destinationValue;
let currentCameraSpeed = cameraSpeed * delta;
if (Math.abs(currentValue - destinationValue) < currentCameraSpeed) {
return destinationValue;
}
return +parseFloat(currentValue * (1 - currentCameraSpeed) + destinationValue * currentCameraSpeed).toPrecision(15);
};
const handlePlayerMovement = (delta) => {
let currentSpeed = speed;
let tempChange = { x: 0, y: 0 };
let dodgePressed = false;
if (!keyListenerPaused && dodgeStep === 0) {
player.idle = true;
keymap.forEach(direction => {
if (keyAssignments.right.includes(direction)) {
tempChange.x = 1;
player.currentDirection = 'right';
player.idle = false;
}
if (keyAssignments.left.includes(direction)) {
tempChange.x = -1;
player.currentDirection = 'left';
player.idle = false;
}
if (keyAssignments.up.includes(direction)) {
tempChange.y = -1;
player.currentDirection = 'up';
player.idle = false;
}
if (keyAssignments.down.includes(direction)) {
tempChange.y = 1;
player.currentDirection = 'down';
player.idle = false;
}
if (keyAssignments.dodge.includes(direction)) {
dodgePressed = true;
}
});
}
if (dodgeStep > 0) {
if (dodgeStep < dodgeSteps * delta * 5) {
dodgeStep += delta;
currentSpeed = dodgeSpeed;
tempChange = lastTempChange;
dodgeDelayStep = 0;
} else {
dodgeStep = 0;
dodgeDelayStep += delta;
}
} else {
if (dodgePressed && dodgeDelayStep === 0) {
if (tempChange.x !== 0 || tempChange.y !== 0) {
dodgeStep += delta;
currentSpeed = dodgeSpeed;
lastTempChange = tempChange;
dodgeDelayStep = 0;
}
dodgeDelayStep+=delta;
} else {
if (dodgeDelayStep > 0) {
if (dodgeDelayStep < dodgeDelaySteps * delta * 5) {
dodgeDelayStep += delta;
} else {
dodgeDelayStep = 0;
}
}
}
}
let angle = Math.atan2(tempChange.y, tempChange.x);
if (tempChange.x !== 0) {
x += Math.cos(angle) * currentSpeed * delta;
}
if (tempChange.y !== 0) {
y += Math.sin(angle) * currentSpeed * delta;
}
x = +parseFloat(x).toPrecision(15);
y = +parseFloat(y).toPrecision(15);
cameraX = handleCamera(cameraX, x, delta);
cameraY = handleCamera(cameraY, y, delta);
};
let savedDelta = 0;
const draw = (delta) => {
context.imageSmoothingEnabled = false;
context.clearRect(0, 0, game.width, game.height);
context.save();
context.scale(scale, scale);
context.translate(-cameraX - (playerWidth / 2) + (game.width / 2 / scale), -cameraY - (playerHeight / 2) + (game.height / 2 / scale));
context.drawImage(background, 0, 0, backgroundWidth, backgroundHeight);
context.drawImage(player[player.currentDirection], playerWidth * player.frame, 0, playerWidth, playerHeight, x, y, playerWidth, playerHeight);
context.restore();
if (player.idle) {
player.frame = 0;
return;
}
if ((delta + savedDelta) * 1000 >= fps * 2) {
player.frame++;
savedDelta = 0;
if (player.frame === player.maxFrames) {
player.frame = 0;
}
}
savedDelta += delta;
};
const main = (timestampMs) => {
if (previousMs === 0) {
previousMs = timestampMs;
}
const delta = +parseFloat((timestampMs - previousMs) / 1000).toPrecision(15);
handlePlayerMovement(delta);
draw(delta);
previousMs = timestampMs;
requestAnimationFrame(main);
};
window.addEventListener('keydown', event => {
if (event.metaKey) {
keymap.splice(0, keymap.length);
return;
}
let index = keymap.indexOf(event.key);
if (index > -1) {
keymap.splice(index, 1);
}
keymap.push(event.key);
handleKey(event.key);
});
window.addEventListener('keyup', event => {
let index = keymap.indexOf(event.key);
if (index > -1) {
keymap.splice(index, 1);
}
});
window.addEventListener("blur", _ => {
keymap.splice(0, keymap.length);
});
window.addEventListener('resize', () => handleScreens());
handleScreens();
requestAnimationFrame(main);
* {
margin: 0;
padding: 0;
}
body {
overflow: hidden;
image-rendering: pixelated;
height: 100vh;
}
<canvas id="game"></canvas>