在我的演示中,您可以看到玩家的相机稍微延迟地赶上了玩家的位置。它跟着他直到玩家最终停下来,逐渐缩短距离。
不幸的是,我注意到使用这款相机会导致像素显示不准确,或者模糊或来回摆动。如果您最小化屏幕高度,这在较大的屏幕上尤其明显。
如果您只是在
handleCamera
内返回目标位置,当然相机的功能就会消失,但这解决了问题。
我正在寻找一种解决方案来保留我的相机,但要删除不准确的像素。我已经尝试过包含一定的容差,但是效果很差。关于为什么会出现这种现象,有什么想法、技巧或经验吗?
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>
说明:单击应用程序的背景图像以获得焦点并使用“w”、“a”、“d”、“s”键移动角色。
由于绘制的字符和绘制的背景被像素分数偏移,您的字符似乎在抖动。当相机移动时,所产生的圆角有时会使背景朝一个方向(与角色相关)圆化,有时又朝另一个方向圆化。
要解决此问题,背景和字符不应偏移像素的一部分(或者它们应共享相同的偏移量)。为了实现这一目标,我想到了几种方法:
要舍入角色的坐标,只需更改:
context.drawImage(player[player.currentDirection], playerWidth * player.frame, 0, playerWidth, playerHeight, x, y, playerWidth, playerHeight);
至:
context.drawImage(player[player.currentDirection], playerWidth * player.frame, 0, playerWidth, playerHeight, Math.round(x), Math.round(y), playerWidth, playerHeight);
这使得角色在某些帧中移动不稳,角色偏移了一小部分像素,但它确实解决了抖动问题。
相反,将背景偏移与角色相同的像素偏移量可能是更好的主意。为此,请更改:
context.drawImage(background, 0, 0, backgroundWidth, backgroundHeight);
至:
let xOffset = x - Math.round(x);
let yOffset = y - Math.round(y);
context.drawImage(background, xOffset, yOffset, backgroundWidth, backgroundHeight);
全部放在一起,看起来像这样:
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));
let xOffset = x-Math.round(x);
let yOffset = y-Math.round(y);
context.drawImage(background, xOffset, yOffset, backgroundWidth, backgroundHeight);
context.drawImage(player[player.currentDirection], playerWidth * player.frame, 0, playerWidth, playerHeight, x, y, playerWidth, playerHeight);
// context.drawImage(player[player.currentDirection], playerWidth * player.frame, 0, playerWidth, playerHeight, Math.round(x), Math.round(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>