HTML5 Canvas Javascript - 如何使用'translate3d'创建可移动的带有瓷砖的画布?

5
我在可移动的画布上遇到了问题,该画布会随着“玩家”在地图上移动而调整。由于每秒绘制600个瓷砖60次非常低效,所以我改用translate3d,并且仅当玩家穿过一个完整的瓷砖时才进行draw - 但它一直出现故障,不能平稳移动。如何正确实现这一点?

const ctx = canvas.getContext('2d');
canvas.height = 200;
canvas.width = 600;
const tileSize = canvas.height/6;
const MAIN = {position:{x: 120, y: 120}};
const canvasRefresh = {x: 0, y: 20};
document.body.onmousemove = e => MAIN.position = {x: e.clientX, y: e.clientY};
const tiles = {x: 20, y: 20}

function update(){
    moveMap();
    requestAnimationFrame(update);
}
function drawMap(){
    for(var i = 0; i < tiles.x; i++){
        for(var j = 0; j < tiles.y; j++){
            ctx.fillStyle = ['black', 'green','orange'][Math.floor((i+j+canvasRefresh.x1+canvasRefresh.y1)%3)];
            ctx.fillRect(tileSize * i, tileSize * j, tileSize, tileSize);
        }
    }
}
function moveMap(){
    const sector = {
        x: Math.round(-MAIN.position.x % tileSize),
        y: Math.round(-MAIN.position.y % tileSize)
    };
    const x2 = Math.floor(MAIN.position.x/tileSize);
    const y2 = Math.floor(MAIN.position.y/tileSize);
    if(canvasRefresh.x1 != x2 || canvasRefresh.y1 != y2){
        canvasRefresh.x1 = x2;
        canvasRefresh.y1 = y2;
        requestAnimationFrame(drawMap);
    }
    $('#canvas').css({
        transform: "translate3d(" + sector.x + "px, " + sector.y + "px, 0)"
    });
}
update();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<canvas id=canvas></canvas>


1
你考虑过使用游戏引擎吗?有很多开源的选择: https://github.com/collections/javascript-game-engines - Helder Sepulveda
1
我有过使用,但我不想冒限制的风险,我喜欢使用原始代码的自由,让我的游戏更加独特 :) - Vardan Betikyan
@Darth 谢谢您的编辑,代码片段完美地描述了我的问题! - Vardan Betikyan
我认为你不会遇到任何限制(如果有,那些引擎都是开源的,你可以添加新功能或修复错误),但你所得到的是一个非常精细的引擎,多年来已经由游戏行业的许多开发人员进行了优化。 - Helder Sepulveda
1
在嵌套的 requestAnimationFrame(drawMap) 中使用似乎很奇怪 - 因为你已经在 RAF 回调中了,所以只需调用 drawMap() 而不是触发一个单独的回调。 - ggorlen
显示剩余2条评论
2个回答

1

有几件事情正在发生:

立即调用drawMap而不是使用requestAnimationFrame

正如ggorlen在评论中提到的那样,多次在更新周期中使用requestAnimationFrame是一种不寻常的做法。当您使用requestAnimationFrame时,您正在调用下一帧更新的函数,这意味着会有一帧地图没有重新绘制,从而导致轻微的闪烁。相反,如果您立即调用它,它将为该帧重新绘制地图。此外,最好将所有绘画和更新合并为一个requestAnimationFrame的调用,因为这使得更新顺序更清晰。

因此,您应将requestAnimationFrame(drawMap);更改为drawMap();

使用非整数找到余数

模算术(即%运算符)通常使用整数。在MAIN.position.x % tileSize的情况下,它偶尔会出现故障,因为tileSize不是整数(200/6)。要使用非整数数字查找余数,我们可以使用自定义函数:

function remainder(a, b) {
  return a - Math.floor(a / b) * b;
}

使用我们的新函数替换模数算术的实例(例如,将MAIN.position.x % tileSize更改为remainder(MAIN.position.x, tileSize))。

Math.roundMath.floor

最后,您可能想使用Math.floor而不是Math.round,因为Math.round对于范围在(-1,0)和(0,1)之间的值都返回0,而Math.floor返回-1和0。

使用容器和CSS隐藏画布的移动部分

您可能希望使用包含div和相应的CSS来隐藏正在重新绘制的画布的边缘:

在HTML中:

<div class="container">
<canvas id=canvas></canvas>
</div>

在CSS中:
.container {
  width: 560px;
  height: 160px;
  overflow: hidden;
}

全部在一起

全部在一起的样子如下:

const ctx = canvas.getContext('2d');
canvas.height = 200;
canvas.width = 600;
const tileSize = canvas.height/6;
const MAIN = {position:{x: 120, y: 120}};
const canvasRefresh = {x: 0, y: 20};
document.body.onmousemove = e => MAIN.position = {x: e.clientX, y: e.clientY};
const tiles = {x: 20, y: 20}

function update(){
    moveMap();
    requestAnimationFrame(update);
}
function drawMap(){
    for(var i = 0; i < tiles.x; i++){
        for(var j = 0; j < tiles.y; j++){
            ctx.fillStyle = ['black', 'green','orange'][Math.floor((i+j+canvasRefresh.x1+canvasRefresh.y1)%3)];
            ctx.fillRect(tileSize * i, tileSize * j, tileSize, tileSize);
        }
    }
}
function remainder(a, b) {
  return a - Math.floor(a / b) * b;
}
function moveMap(){
    const sector = {
        x: Math.floor(-remainder(MAIN.position.x, tileSize)),
        y: Math.floor(-remainder(MAIN.position.y, tileSize))
    };
    const x2 = Math.floor(MAIN.position.x/tileSize);
    const y2 = Math.floor(MAIN.position.y/tileSize);
    if(canvasRefresh.x1 != x2 || canvasRefresh.y1 != y2){
        canvasRefresh.x1 = x2;
        canvasRefresh.y1 = y2;
        drawMap();
    }
    $('#canvas').css({
        transform: "translate3d(" + sector.x + "px, " + sector.y + "px, 0)"
    });
}
update();
.container {
  width: 560px;
  height: 160px;
  overflow: hidden;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="container">
<canvas id=canvas></canvas>
</div>


你说得对,谢谢!使用requestAnimationFrame()是主要问题所在!在我接受你的答案之前,我想听听你的意见...实际上,我已经创建了一个非常大的画布,并绘制了所有的瓷砖(100x100),现在我只需移动画布而不需要重新绘制。你认为这样更有效率,还是你的方法更好? - Vardan Betikyan
1
我认为这应该取决于瓷砖的大小(最终取决于画布)-如果它们是32px x 32px(3200px x 3200px画布),那么就可以了,但如果瓷砖更大,比如256px x 256px(25600px x 25600px画布),那么可能对浏览器来说太大或占用了很多内存。 - Steve
1
对于地图大小,这取决于您的目标平台 - 我建议在目标平台上测试各种实现。虽然我认为这些尺寸应该适用于现代硬件。另请参阅https://dev59.com/0m025IYBdhLWcg3wUEOP以获取最大约束条件。 - Steve
1
为了效率,这是一个有点棘手的问题。一般来说,相对于通过画布重新绘制每个帧,翻译会更加高效,但通常您更关心每帧的延迟。如果您事先绘制整个地图,则可能会导致一些初始化时间。如果您只偶尔重绘地图,则可能会在重绘地图时引入卡顿。在回答的特定情况下,当您绘制的地图与您在画布上绘制的地图大小相同时,我认为卡顿将不存在或不可察觉。 - Steve
1
完美!非常感谢。 - Vardan Betikyan
显示剩余2条评论

0

根据上面的评论显示您只想在现有代码基础上平滑移动画布,而没有修改计划,您尝试过为您的canvas元素添加缓动过渡效果吗?

canvas { transition: all 1500ms cubic-bezier(0.250, 0.100, 0.250, 1.000); transition-timing-function: cubic-bezier(0.250, 0.100, 0.250, 1.000); /* ease (default) */ }

const ctx = canvas.getContext('2d');
canvas.height = 200;
canvas.width = 600;
const tileSize = canvas.height/6;
const MAIN = {position:{x: 120, y: 120}};
const canvasRefresh = {x: 0, y: 20};
document.body.onmousemove = e => MAIN.position = {x: e.clientX, y: e.clientY};
const tiles = {x: 20, y: 20}

function update(){
    moveMap();
    requestAnimationFrame(update);
}
function drawMap(){
    for(var i = 0; i < tiles.x; i++){
        for(var j = 0; j < tiles.y; j++){
            ctx.fillStyle = ['black', 'green','orange'][Math.floor((i+j+canvasRefresh.x1+canvasRefresh.y1)%3)];
            ctx.fillRect(tileSize * i, tileSize * j, tileSize, tileSize);
        }
    }
}
function moveMap(){
    const sector = {
        x: Math.round(-MAIN.position.x % tileSize),
        y: Math.round(-MAIN.position.y % tileSize)
    };
    const x2 = Math.floor(MAIN.position.x/tileSize);
    const y2 = Math.floor(MAIN.position.y/tileSize);
    if(canvasRefresh.x1 != x2 || canvasRefresh.y1 != y2){
        canvasRefresh.x1 = x2;
        canvasRefresh.y1 = y2;
        requestAnimationFrame(drawMap);
    }
    $('#canvas').css({
        transform: "translate3d(" + sector.x + "px, " + sector.y + "px, 0)"
    });
}
update();
canvas {
  transition: all 1500ms cubic-bezier(0.250, 0.100, 0.250, 1.000); /* ease (default) */
  transition-timing-function: cubic-bezier(0.250, 0.100, 0.250, 1.000); /* ease (default) */
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<canvas id=canvas></canvas>

我个人不会移动画布本身,而是通过在元素内部添加一行/列来改变其方向,并在相反的方向上删除方块。然而,这应该解决你在问题中提出的问题。


为什么不移动画布本身?我的目标是移动画布(硬件加速)和画布中的元素,最小化 CPU 使用并使其平稳滚动。在您的示例代码片段中,瓷砖以块状/断断续续的方式移动。需要平滑。想象一下在二维地图上移动城市中的“玩家”。 - Vardan Betikyan
我的原始示例代码已被编辑。每个瓦片都是来自2D矩阵中的map[x][y](基于玩家坐标)的单独图像。 - Vardan Betikyan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接