2D 画布 |拍照流畅无抖动

问题描述 投票:0回答:1

在我的演示中,您可以看到玩家的相机稍微延迟地赶上了玩家的位置。它跟着他直到玩家最终停下来,逐渐缩短距离。

不幸的是,我注意到使用这款相机会导致像素显示不准确,或者模糊或来回摆动。如果您最小化屏幕高度,这在较大的屏幕上尤其明显。

如果您只是在

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”键移动角色。

javascript math canvas game-development game-engine
1个回答
0
投票

抖动

由于绘制的字符和绘制的背景被像素分数偏移,您的字符似乎在抖动。当相机移动时,所产生的圆角有时会使背景朝一个方向(与角色相关)圆化,有时又朝另一个方向圆化。

同步坐标系

要解决此问题,背景和字符不应偏移像素的一部分(或者它们应共享相同的偏移量)。为了实现这一目标,我想到了几种方法:

  • 您只能将角色的坐标增加整数值 - 这限制了引擎的功能,因此我们将放弃这个。
  • 绘图时,可以将角色坐标四舍五入为整数值
  • 绘制时,可以将背景坐标偏移与字符相同的小数偏移量

对绘制的角色坐标进行四舍五入

要舍入角色的坐标,只需更改:

  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>

最新问题
© www.soinside.com 2019 - 2025. All rights reserved.