You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

JavaScript像素风2D原生平滑移动实现优化求助

2D RPG八方向背景移动卡顿优化方案

问题背景

出于对游戏开发的兴趣,我决定不使用引擎从零实现相关逻辑。目前正着手优化一款2D RPG游戏的背景移动序列,借助各类辅助函数实现起来不算复杂,但并不满意现有效果。

此前仅有限地使用过canvas,提供的代码仍有优化空间,但更关注八方向系统的优化方案。实际移动的并非玩家而是背景,这导致移动时因速度和需跨越的像素数出现严重卡顿。

已有向量归一化的逻辑,但参考的优化指南帮助不大。为便于观察调低了移动速度,尝试过调整delta值或对数值取整,却导致像素画出现严重模糊。

请问有什么优化该demo的思路吗?


演示代码

JavaScript

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);

CSS

* {
  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;
}

HTML

<canvas id="game"></canvas>

优化思路

1. 修复八方向向量归一化逻辑

当前代码斜向移动时速度会异常(实际为speed的√2倍),且坐标更新逻辑存在冗余。替换为正确的向量归一化处理:

// 在handleWork中替换原有的angle和坐标更新代码
if (tempChange.x !== 0 || tempChange.y !== 0) {
    // 计算向量长度并归一化,确保八方向移动速度一致
    const length = Math.hypot(tempChange.x, tempChange.y);
    const normalizedX = tempChange.x / length;
    const normalizedY = tempChange.y / length;
    
    x += normalizedX * speed;
    y += normalizedY * speed;
}

2. 解决背景卡顿:整数化相机坐标

浮点数相机坐标会导致canvas亚像素渲染,引发像素画卡顿和模糊。修改相机处理逻辑并开启像素化渲染:

// 修改handleCamera函数,对结果取整
const handleCamera = (currentValue, destinationValue) => {
    const smoothed = currentValue * (1 - cameraSpeed) + destinationValue * cameraSpeed;
    return Math.round(smoothed);
};

// 在handleWork的clearRect之后添加
context.imageSmoothingEnabled = false;

3. 优化输入处理效率

当前keymap会重复存储按键,改用对象存储按键状态更高效:

// 替换原keymap定义
let keymap = {
    ArrowUp: false,
    ArrowDown: false,
    ArrowLeft: false,
    ArrowRight: false
};

// 替换keydown事件
window.addEventListener('keydown', event => {
    if (keymap.hasOwnProperty(event.key)) keymap[event.key] = true;
});

// 替换keyup事件
window.addEventListener('keyup', event => {
    if (keymap.hasOwnProperty(event.key)) keymap[event.key] = false;
});

// 在handleWork中读取按键状态
let tempChange = { x: 0, y: 0 };
if (keymap.ArrowRight) tempChange.x += 1;
if (keymap.ArrowLeft) tempChange.x -= 1;
if (keymap.ArrowUp) tempChange.y -= 1;
if (keymap.ArrowDown) tempChange.y += 1;

4. 分离逻辑更新与渲染

将逻辑计算和渲染操作拆分,避免固定时间步长循环耦合渲染逻辑:

// 新增独立渲染函数
const render = () => {
    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();
};

// 修改main函数,单独调用渲染
const main = (timestampMs) => {
    if (previousMs == undefined) previousMs = timestampMs;

    let delta = (timestampMs - previousMs) / 1000;
    while (delta >= step) {
        handleWork(); // 仅处理逻辑更新
        delta -= step;
    }
    previousMs = timestampMs - delta * 1000;
    
    render(); // 独立执行渲染
    requestAnimationFrame(main);
};

// 修改handleWork,移除渲染代码
const handleWork = () => {
    // 原有逻辑更新代码(保留坐标、相机计算)
    let tempChange = { x: 0, y: 0 };
    if (keymap.ArrowRight) tempChange.x += 1;
    if (keymap.ArrowLeft) tempChange.x -= 1;
    if (keymap.ArrowUp) tempChange.y -= 1;
    if (keymap.ArrowDown) tempChange.y += 1;

    if (tempChange.x !== 0 || tempChange.y !== 0) {
        const length = Math.hypot(tempChange.x, tempChange.y);
        const normalizedX = tempChange.x / length;
        const normalizedY = tempChange.y / length;
        x += normalizedX * speed;
        y += normalizedY * speed;
    }

    cameraX = handleCamera(cameraX, x);
    cameraY = handleCamera(cameraY, y);
};

5. 调整相机缓动参数

当前cameraSpeed = 0.1的缓动速度过慢,会导致视觉滞后。可将值调整为0.2~0.3,让相机更快跟上玩家位置,同时保持平滑过渡。


内容的提问来源于stack exchange,提问作者RoyBlunk

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 19:32:01