Мой интерес к разработке игр привел меня к идее реализовать логику с нуля, без движка.
Наконец, я дошёл до оптимизации последовательности движений фона в ролевой 2D-игре. В принципе, реализация не оказалась особенно сложной из-за многочисленных справочных функций, но меня не полностью устраивает.
До сих пор я работал с холстом лишь ограниченно, поэтому приведенный здесь код, безусловно, можно оптимизировать. Однако меня интересует, как можно оптимизировать 8-направленную систему.
В конечном итоге перемещается не пользователь, а фон. Это приводит к массовым рывкам из-за скорости движения и заданного количества пикселей, которые необходимо преодолеть.
По сути, у меня есть логика, позволяющая нормализовать векторы, но замеченные мною руководства по оптимизации не очень полезны.
Чтобы было понятнее, я минимизировал скорость движения. Я уже поигрался с некоторыми значениями дельты или попытался округлить некоторые значения, но это закончилось очень плохо в виде размытой пиксельной графики.
Есть идеи, как оптимизировать эту демо-версию?
const game = document.querySelector('#game');
const context = game.getContext('2d');
const background = new Image();
background.src = 'https://i.imgur.com/82hX6qh.png';
const player = new Image();
player.src = 'https://i.imgur.com/LrL69By.png';
const backgroundHeight = 480;
const backgroundWidth = 840;
const playerHeight = 16;
const playerWidth = 12;
const initialGameWidth = 240;
const initialGameHeight = playerWidth * 10;
const cameraSpeed = 0.1;
const step = 1 / 60;
let x = backgroundWidth / 2; // Player Starting X
let y = backgroundHeight / 2; // Player Starting Y
let speed = 1;
let keymap = [];
let previousMs;
let cameraX = x;
let cameraY = y;
game.width = initialGameWidth;
game.height = initialGameHeight;
const handleScreens = () => {
const scale = window.innerHeight / initialGameHeight;
game.style.scale = scale;
const gameHeight = Math.round(window.innerHeight / scale);
const gameWidth = Math.round(window.innerWidth / scale);
game.height = gameHeight % 2 ? gameHeight + 1 : gameHeight;
game.width = gameWidth % 2 ? gameWidth + 1 : gameWidth;
};
handleScreens();
window.addEventListener('resize', () => handleScreens());
window.addEventListener('keydown', event => {
let index = keymap.indexOf(event.key);
if (index > -1) {
keymap.splice(index, 1);
}
keymap.push(event.key);
});
window.addEventListener('keyup', event => {
let index = keymap.indexOf(event.key);
if (index > -1) {
keymap.splice(index, 1);
}
});
const handleCamera = (currentValue, destinationValue) => {
return currentValue * (1 - cameraSpeed) + destinationValue * cameraSpeed;
};
const handleWork = () => {
let tempChange = { x: 0, y: 0 };
[...keymap].forEach(direction => {
switch (direction) {
case 'ArrowRight':
tempChange.x = 1;
break;
case 'ArrowLeft':
tempChange.x = -1;
break;
case 'ArrowUp':
tempChange.y = -1;
break;
case 'ArrowDown':
tempChange.y = 1;
break;
}
});
let angle = Math.atan2(tempChange.y, tempChange.x);
if (tempChange.x !== 0) {
x += Math.cos(angle) * speed;
}
if (tempChange.y !== 0) {
y += Math.sin(angle) * speed;
}
cameraX = handleCamera(cameraX, x);
cameraY = handleCamera(cameraY, y);
//context.imageSmoothingEnabled = false;
context.clearRect(0, 0, game.width, game.height);
context.save();
context.translate(-cameraX + (game.width / 2) - (playerWidth / 2), -cameraY + (game.height / 2) - (playerHeight / 2));
context.drawImage(background, 0, 0, backgroundWidth, backgroundHeight);
context.drawImage(player, 0, 0, playerWidth, playerHeight, x, y, playerWidth, playerHeight);
context.restore();
};
const main = (timestampMs) => {
if (previousMs == undefined) {
previousMs = timestampMs;
}
let delta = (timestampMs - previousMs) / 1000;
while (delta >= step) {
handleWork();
delta -= step;
}
previousMs = timestampMs - delta * 1000;
requestAnimationFrame(main);
};
requestAnimationFrame(main);
* {
margin: 0;
padding: 0;
}
body {
background-color: #222;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
height: 100vh;
width: 100vw;
}
#game {
-ms-interpolation-mode: nearest-neighbor;
image-rendering: -moz-crisp-edges;
image-rendering: pixelated;
}
<canvas id = "game"></canvas>
@synnøve При работе с большими экранами, например 1920x1080, смещение становится более заметным. Движение пикселей нечеткое. Если посмотреть на мой прокомментированный код, будет размытие. С моим текущим кодом, особенно диагонали, очень лестничные. Я видел некоторую реализацию с добавлением значения дельты в движении для большей «детализации», но я не знаю, подойдет ли этот вариант.
Если скорость равна 7, карта будет смещаться на 7 пикселей за раз, что может выглядеть немного заикающимся при частоте кадров 24 кадра в секунду. Итак, первое, что вы можете сделать, это установить FPS на 60 и скорость немного ниже.
const step = 1 / 60;
const speed = 3;
Он по-прежнему немного заикается, но это также может быть вызвано тем, что браузер/процессор выполняет другие действия, которые задерживают вызовы requestAnimationframe
.
Вы также можете использовать translate
, чтобы переместить карту. Вы можете перевести весь холст, кроме персонажа игрока.
ctx.save();
ctx.translate(...); // movement
// draw the map here
ctx.restore();
// draw the player here
const game = document.querySelector('#game');
const context = game.getContext('2d');
const background = new Image();
background.src = 'https://i.imgur.com/Kjrpd2v.png';
const player = new Image();
player.src = 'https://i.imgur.com/PIT6zqs.png';
const initialGameWidth = 640;
const initialGameHeight = 360;
const step = 1 / 60;
let x = 3360 / 2;
let y = 1920 / 2;
let speed = 3;
let keymap = [];
let previousMs;
game.width = initialGameWidth;
game.height = initialGameHeight;
const handleScreens = () => {
let scale = window.innerHeight / initialGameHeight;
game.style.scale = scale < 1 ? 1 : scale;
console.info(game.style.scale)
game.height = window.innerHeight % 2 ? window.innerHeight - 1 : window.innerHeight;
game.width = window.innerWidth % 2 ? window.innerWidth - 1 : window.innerWidth;
};
window.addEventListener('resize', () => handleScreens());
handleScreens();
window.addEventListener('keydown', event => {
let index = keymap.indexOf(event.key);
if (index > -1) {
keymap.splice(index, 1);
}
keymap.push(event.key);
});
window.addEventListener('keyup', event => {
let index = keymap.indexOf(event.key);
if (index > -1) {
keymap.splice(index, 1);
}
});
const handleWork = (delta) => {
let tempChange = { x: 0, y: 0 };
[...keymap].forEach(direction => {
switch (direction) {
case 'ArrowRight':
tempChange.x = 1;
break;
case 'ArrowLeft':
tempChange.x = -1;
break;
case 'ArrowUp':
tempChange.y = -1;
break;
case 'ArrowDown':
tempChange.y = 1;
break;
}
});
let angle = Math.atan2(tempChange.y, tempChange.x);
if (tempChange.x !== 0) {
x += Math.cos(angle) * speed;
}
if (tempChange.y !== 0) {
y += Math.sin(angle) * speed;
}
// const mapX = Math.floor((game.width / 2) - 24 - x) + delta;
// const mapY = Math.floor((game.height / 2) - 32 - y) + delta;
const mapX = (game.width / 2) - 24 - x;
const mapY = (game.height / 2) - 32 - y;
context.clearRect(0, 0, game.width, game.height);
context.drawImage(background, 0, 0, 3360, 1920, mapX, mapY, 3360, 1920);
context.drawImage(player, 0, 0, 48, 64, (game.width / 2) - 24, (game.height / 2) - 32, 48, 64);
};
const main = (timestampMs) => {
if (previousMs == undefined) {
previousMs = timestampMs;
}
let delta = (timestampMs - previousMs) / 1000;
while (delta >= step) {
handleWork(delta);
delta -= step;
}
previousMs = timestampMs - delta * 1000;
requestAnimationFrame(main);
};
requestAnimationFrame(main);
* {
margin: 0;
padding: 0;
}
body {
background-color: #222;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
height: 100vh;
width: 100vw;
}
#game {
-ms-interpolation-mode: nearest-neighbor;
image-rendering: -moz-crisp-edges;
image-rendering: pixelated;
}
<canvas id = "game"></canvas>
Вы правы, на это немного приятнее смотреть. Часто ли в играх с пиксельной графикой выставляют 60 FPS? Согласно определенной документации и форумам, неписаным стандартом является 24FPS. Однако остается проблема размытия фона из-за нормализации диагонального вектора. Есть ли здесь какой-то подход?
Холст может интерпретировать только целые числа, поэтому он интерполирует десятичные числа. Эта интерполяция гарантирует, что пиксели будут размыты, поскольку делается попытка отобразить промежуточное состояние двух пикселей.
Одной из причин этого является масштабирование с помощью CSS, который только увеличивает размер элемента. Исходные пиксели все еще присутствуют, просто они воспринимаются как более крупные.
Однако, как только вы начинаете масштабировать непосредственно через холст, пиксели не только воспринимаются крупнее, но и улучшается интерполяция, поскольку теперь она имеет доступ к промежуточным пикселям. Это позволяет четко отображать промежуточные состояния.
Это можно реализовать через холст следующим образом:
context.imageSmoothingEnabled = false;
const scale = window.innerHeight / initialGameHeight;
context.scale(scale, scale);
Вы ищете своего рода слайд на клавиатуре? Не совсем уверен, что означает плавность, поскольку мне это (с точки зрения FPS) кажется хорошим.