javascript - JS Canvas 运动动画循环

标签 javascript canvas html5-canvas

我有一个对象渲染到 Canvas 上。我试图让对象在一个循环中沿着设定的路径移动。这是我所拥有的:

// Canvas Element
var canvas = null;

// Canvas Draw
var ctx = null;

// Static Globals
var tileSize = 16,
    mapW = 10,
    mapH = 10;

// Instances of entities
var entities = [
  // A single entity that starts at tile 28, and uses the setPath() function
  {
    id: 0,
    tile: 28,
    xy: tileToCoords(28),
    width: 16,
    height: 24,
    speedX: 0,
    speedY: 0,
    logic: {
      func: 'setPath',
      // These are the parameters that go into the setPath() function
      data: [0, ['down', 'up', 'left', 'right'], tileToCoords(28), 0]
    },
    dir: {up:false, down:false, left:false, right:false}
  }
];

// Array for tile data
var map = [];

window.onload = function(){

  // Populate the map array with a blank map and 4 walls
  testMap();
  
  canvas = document.getElementById('save');
  ctx = canvas.getContext("2d");

  // Add all the entities to the map array and start their behavior
  for(var i = 0; i < entities.length; ++i){

    map[entities[i].tile].render.object = entities[i].id;

    if(entities[i].logic){        
      window[entities[i].logic.func].apply(null, entities[i].logic.data);
    }
  }

  drawGame(map);
  window.requestAnimationFrame(function(){
    mainLoop();
  });
};

function drawGame(map){
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // We save all the entity data for later so the background colors don't get rendered on top
  var tileObjData = [];

  for(var y = 0; y < mapH; ++y){
    for(var x = 0; x < mapW; ++x){

      var currentPos = ((y*mapW)+x);

      ctx.fillStyle = map[currentPos].render.base;
      ctx.fillRect(x*tileSize, y*tileSize, tileSize, tileSize);

      var thisObj = map[currentPos].render.object;

      if(thisObj !== false){

        thisObj = entities[thisObj];
        var originX = thisObj.xy.x;
        var originY = thisObj.xy.y;
        tileObjData.push(
          {
            id: thisObj.id,
            originX: originX, 
            originY: originY, 
            width: thisObj.width, 
            height: thisObj.height,
          }
        );
      }
    }
  }
  
  // Draw all the entities after the background tiles are drawn
  for(var i = 0; i < tileObjData.length; ++i){
    drawEntity(tileObjData[i].id, tileObjData[i].originX, tileObjData[i].originY, tileObjData[i].width, tileObjData[i].height);
  }
}

// Draws the entity data
function drawEntity(id, posX, posY, sizeX, sizeY){

  var offX = posX + entities[id].speedX;
  var offY = posY + entities[id].speedY;
  
  ctx.fillStyle = '#00F';
  ctx.fillRect(offX, offY + sizeX - sizeY, sizeX, sizeY);

  entities[id].xy.x = offX;
  entities[id].xy.y = offY;
}

// Redraws the canvas with the browser framerate
function mainLoop(){
  drawGame(map);

  for(var i = 0; i < entities.length; ++i){
    animateMove(i, entities[i].dir.up, entities[i].dir.down, entities[i].dir.left, entities[i].dir.right);
  }

  window.requestAnimationFrame(function(){
    mainLoop();
  });
}

// Sets the speed, direction, and collision detection of an entity
function animateMove(id, up, down, left, right){

  var prevTile = entities[id].tile;

  if(up){

    var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y};
    var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y};

    if(!map[coordsToTile(topLeft.x, topLeft.y - 1)].state.passable || !map[coordsToTile(topRight.x, topRight.y - 1)].state.passable){
      entities[id].speedY = 0;
    }
    else{
      entities[id].speedY = -1;
    }
  }
  else if(down){

    var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1};
    var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1};

    if(!map[coordsToTile(bottomLeft.x, bottomLeft.y + 1)].state.passable || !map[coordsToTile(bottomRight.x, bottomRight.y + 1)].state.passable){
      entities[id].speedY = 0;
    }
    else{
      entities[id].speedY = 1;
    }
  }
  else{
    entities[id].speedY = 0;
  }

  if(left){

    var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1};
    var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y};

    if(!map[coordsToTile(bottomLeft.x - 1, bottomLeft.y)].state.passable || !map[coordsToTile(topLeft.x - 1, topLeft.y)].state.passable){
      entities[id].speedX = 0;
    }
    else{
      entities[id].speedX = -1;
    }
  }
  else if(right){

    var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1};
    var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y};

    if(!map[coordsToTile(bottomRight.x + 1, bottomRight.y)].state.passable || !map[coordsToTile(topRight.x + 1, topRight.y)].state.passable){
      entities[id].speedX = 0;
    }
    else{
      entities[id].speedX = 1;
    }
  }
  else{
    entities[id].speedX = 0;
  }

  entities[id].tile = coordsToTile(entities[id].xy.x + (entities[id].width / 2), entities[id].xy.y + (tileSize / 2));
  map[entities[id].tile].render.object = id;

  if(prevTile !== entities[id].tile){
    map[prevTile].render.object = false;
  }
}

//////////////////////////////////////
// THIS IS WHERE I'M HAVING TROUBLE //
//////////////////////////////////////
// A function that can be used by an entity to move along a set path
// id = The id of the entity using this function
// path = An array of strings that determine the direction of movement for a single tile
// originPoint = Coordinates of the previous tile this entity was at. This variable seems to be where problems happen with this logic. It should get reset for every tile length moved, but it only gets reset once currently.
// step = The current index of the path array 
function setPath(id, path, originPoint, step){

  // Determine if the entity has travelled one tile from the origin
  var destX = Math.abs(entities[id].xy.x - originPoint.x);
  var destY = Math.abs(entities[id].xy.y - originPoint.y);

  if(destX >= tileSize || destY >= tileSize){
    // Go to the next step in the path array
    step = step + 1;
    if(step >= path.length){
      step = 0;
    }
    // Reset the origin to the current tile coordinates
    originPoint = entities[id].xy;
  }
  
  // Set the direction based on the current index of the path array
  switch(path[step]) {

    case 'up':
      entities[id].dir.up = true;
      entities[id].dir.down = false;
      entities[id].dir.left = false;
      entities[id].dir.right = false;
      break;

    case 'down':
      entities[id].dir.up = false;
      entities[id].dir.down = true;
      entities[id].dir.left = false;
      entities[id].dir.right = false;
      break;

    case 'left':
      entities[id].dir.up = false;
      entities[id].dir.down = false;
      entities[id].dir.left = true;
      entities[id].dir.right = false;
      break;

    case 'right':
      entities[id].dir.up = false;
      entities[id].dir.down = false;
      entities[id].dir.left = false;
      entities[id].dir.right = true;
      break;
  };

  window.requestAnimationFrame(function(){
    setPath(id, path, originPoint, step);
  });
}

// Take a tile index and return x,y coordinates
function tileToCoords(tile){

  var yIndex = Math.floor(tile / mapW);
  var xIndex = tile - (yIndex * mapW);

  var y = yIndex * tileSize;
  var x = xIndex * tileSize;
  return {x:x, y:y};
}

// Take x,y coordinates and return a tile index
function coordsToTile(x, y){

  var tile = ((Math.floor(y / tileSize)) * mapW) + (Math.floor(x / tileSize));
  return tile;
}

// Generate a map array with a blank map and 4 walls
function testMap(){
  for(var i = 0; i < (mapH * mapW); ++i){

    // Edges

    if (
      // top
      i < mapW || 
      // left
      (i % mapW) == 0 || 
      // right
      ((i + 1) % mapW) == 0 || 
      // bottom
      i > ((mapW * mapH) - mapW)
    ) {

      map.push(
        {
          id: i,
          render: {
            base: '#D35',
            object: false,
            sprite: false
          },
          state: {
            passable: false
          }
        },
      );

    }
    else{

      // Grass

      map.push(
        {
          id: i,
          render: {
            base: '#0C3',
            object: false,
            sprite: false
          },
          state: {
            passable: true
          }
        },
      );

    }
  }
}
<!DOCTYPE html>
<html>
<head>

  <style>

    body{
      background-color: #000;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #FFF;
      font-size: 18px;
      padding: 0;
      margin: 0;
    }

    main{
      width: 100%;
      max-width: 800px;
      margin: 10px auto;
      display: flex;
      align-items: flex-start;
      justify-content: center;
      flex-wrap: wrap;
    }

    .game{
      width: 1000px;
      height: 1000px;
      position: relative;
    }

    canvas{
      image-rendering: -moz-crisp-edges;
      image-rendering: -webkit-crisp-edges;
      image-rendering: pixelated;
      image-rendering: crisp-edges;
    }

    .game canvas{
      position: absolute;
      top: 0;
      left: 0;
      width: 800px;
      height: 800px;
    }

  </style>
  
</head>
<body>
  
  <main>
    <div class="game">
      <canvas id="save" width="200" height="200" style="z-index: 1;"></canvas>
    </div>
  </main>

</body>
</html>

问题出在 setPath() 函数上,更具体地说,我认为它出在 originPoint 变量上。这个想法是 setPath() 将对象每个 path 字符串移动一个瓦片,而 originPoint 应该是最后访问的瓦片的坐标(所以只有当对象坐标距 originPoint 一个瓦片长度时,它才会更新)。现在它只会第一次更新然后停止。希望有人能指出我在这里做错了什么。

最佳答案

你改变路径方向的条件我把它改成每个方向都有条件,比如:

if ((entities[id].dir.left  && entities[id].xy.x <= tileSize) ||    
  (entities[id].dir.right && entities[id].xy.x >= tileSize*8) || 
  (entities[id].dir.up    && entities[id].xy.y <= tileSize) ||
  (entities[id].dir.down  && entities[id].xy.y >= tileSize*8)) {

originPoint 只是您应该做的引用:

originPoint = JSON.parse(JSON.stringify(entities[id].xy));

查看下面的工作代码

// Canvas Element
var canvas = null;

// Canvas Draw
var ctx = null;

// Static Globals
var tileSize = 16,
    mapW = 10,
    mapH = 10;

// Instances of entities
var entities = [
  // A single entity that starts at tile 28, and uses the setPath() function
  {
    id: 0,
    tile: 28,
    xy: tileToCoords(28),
    width: 16,
    height: 24,
    speedX: 0,
    speedY: 0,
    logic: {
      func: 'setPath',
      // These are the parameters that go into the setPath() function
      data: [0, ['down', 'left', 'down', 'left', 'up', 'left', 'left', 'right', 'up', 'right', 'down','right', "up"], tileToCoords(28), 0]
    },
    dir: {up:false, down:false, left:false, right:false}
  }
];

// Array for tile data
var map = [];

window.onload = function(){

  // Populate the map array with a blank map and 4 walls
  testMap();
  
  canvas = document.getElementById('save');
  ctx = canvas.getContext("2d");

  // Add all the entities to the map array and start their behavior
  for(var i = 0; i < entities.length; ++i){

    map[entities[i].tile].render.object = entities[i].id;

    if(entities[i].logic){        
      window[entities[i].logic.func].apply(null, entities[i].logic.data);
    }
  }

  drawGame(map);
  window.requestAnimationFrame(function(){
    mainLoop();
  });
};

function drawGame(map){
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // We save all the entity data for later so the background colors don't get rendered on top
  var tileObjData = [];

  for(var y = 0; y < mapH; ++y){
    for(var x = 0; x < mapW; ++x){

      var currentPos = ((y*mapW)+x);

      ctx.fillStyle = map[currentPos].render.base;
      ctx.fillRect(x*tileSize, y*tileSize, tileSize, tileSize);

      var thisObj = map[currentPos].render.object;

      if(thisObj !== false){

        thisObj = entities[thisObj];
        var originX = thisObj.xy.x;
        var originY = thisObj.xy.y;
        tileObjData.push(
          {
            id: thisObj.id,
            originX: originX, 
            originY: originY, 
            width: thisObj.width, 
            height: thisObj.height,
          }
        );
      }
    }
  }
  
  // Draw all the entities after the background tiles are drawn
  for(var i = 0; i < tileObjData.length; ++i){
    drawEntity(tileObjData[i].id, tileObjData[i].originX, tileObjData[i].originY, tileObjData[i].width, tileObjData[i].height);
  }
}

// Draws the entity data
function drawEntity(id, posX, posY, sizeX, sizeY){

  var offX = posX + entities[id].speedX;
  var offY = posY + entities[id].speedY;
  
  ctx.fillStyle = '#00F';
  ctx.fillRect(offX, offY + sizeX - sizeY, sizeX, sizeY);

  entities[id].xy.x = offX;
  entities[id].xy.y = offY;
}

// Redraws the canvas with the browser framerate
function mainLoop(){
  drawGame(map);

  for(var i = 0; i < entities.length; ++i){
    animateMove(i, entities[i].dir.up, entities[i].dir.down, entities[i].dir.left, entities[i].dir.right);
  }

  window.requestAnimationFrame(function(){
    mainLoop();
  });
}

// Sets the speed, direction, and collision detection of an entity
function animateMove(id, up, down, left, right){

  var prevTile = entities[id].tile;

  if(up){

    var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y};
    var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y};

    if(!map[coordsToTile(topLeft.x, topLeft.y - 1)].state.passable || !map[coordsToTile(topRight.x, topRight.y - 1)].state.passable){
      entities[id].speedY = 0;
    }
    else{
      entities[id].speedY = -1;
    }
  }
  else if(down){

    var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1};
    var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1};

    if(!map[coordsToTile(bottomLeft.x, bottomLeft.y + 1)].state.passable || !map[coordsToTile(bottomRight.x, bottomRight.y + 1)].state.passable){
      entities[id].speedY = 0;
    }
    else{
      entities[id].speedY = 1;
    }
  }
  else{
    entities[id].speedY = 0;
  }

  if(left){

    var bottomLeft = {x: entities[id].xy.x, y: entities[id].xy.y + entities[id].width - 1};
    var topLeft = {x: entities[id].xy.x, y: entities[id].xy.y};

    if(!map[coordsToTile(bottomLeft.x - 1, bottomLeft.y)].state.passable || !map[coordsToTile(topLeft.x - 1, topLeft.y)].state.passable){
      entities[id].speedX = 0;
    }
    else{
      entities[id].speedX = -1;
    }
  }
  else if(right){

    var bottomRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y + entities[id].width - 1};
    var topRight = {x: entities[id].xy.x + entities[id].width - 1, y: entities[id].xy.y};

    if(!map[coordsToTile(bottomRight.x + 1, bottomRight.y)].state.passable || !map[coordsToTile(topRight.x + 1, topRight.y)].state.passable){
      entities[id].speedX = 0;
    }
    else{
      entities[id].speedX = 1;
    }
  }
  else{
    entities[id].speedX = 0;
  }

  entities[id].tile = coordsToTile(entities[id].xy.x + (entities[id].width / 2), entities[id].xy.y + (tileSize / 2));
  map[entities[id].tile].render.object = id;

  if(prevTile !== entities[id].tile){
    map[prevTile].render.object = false;
  }
}

//////////////////////////////////////
// THIS IS WHERE I'M HAVING TROUBLE //
//////////////////////////////////////
// A function that can be used by an entity to move along a set path
// id = The id of the entity using this function
// path = An array of strings that determine the direction of movement for a single tile
// originPoint = Coordinates of the previous tile this entity was at. This variable seems to be where problems happen with this logic. It should get reset for every tile length moved, but it only gets reset once currently.
// step = The current index of the path array 

function setPath(id, path, originPoint, step){
  if ((entities[id].dir.left  && entities[id].xy.x <= originPoint.x - tileSize) ||    
      (entities[id].dir.right && entities[id].xy.x >= originPoint.x + tileSize) || 
      (entities[id].dir.up    && entities[id].xy.y <= originPoint.y - tileSize) ||
      (entities[id].dir.down  && entities[id].xy.y >= originPoint.y + tileSize)) {
    // Go to the next step in the path array
    step = step + 1;
    if(step >= path.length){
      step = 0;
    }
    // Reset the origin to the current tile coordinates
    originPoint = JSON.parse(JSON.stringify(entities[id].xy));
  }
  
  // Set the direction based on the current index of the path array
  switch(path[step]) {

    case 'up':
      entities[id].dir.up = true;
      entities[id].dir.down = false;
      entities[id].dir.left = false
      entities[id].dir.right = false;
      break;

    case 'down':
      entities[id].dir.up = false;
      entities[id].dir.down = true;
      entities[id].dir.left = false;
      entities[id].dir.right = false;
      break;

    case 'left':
      entities[id].dir.up = false;
      entities[id].dir.down = false;
      entities[id].dir.left = true;
      entities[id].dir.right = false;
      break;

    case 'right':
      entities[id].dir.up = false;
      entities[id].dir.down = false;
      entities[id].dir.left = false;
      entities[id].dir.right = true;
      break;
  };

  window.requestAnimationFrame(function(){
    setPath(id, path, originPoint, step);
  });
}

// Take a tile index and return x,y coordinates
function tileToCoords(tile){

  var yIndex = Math.floor(tile / mapW);
  var xIndex = tile - (yIndex * mapW);

  var y = yIndex * tileSize;
  var x = xIndex * tileSize;
  return {x:x, y:y};
}

// Take x,y coordinates and return a tile index
function coordsToTile(x, y){

  var tile = ((Math.floor(y / tileSize)) * mapW) + (Math.floor(x / tileSize));
  return tile;
}

// Generate a map array with a blank map and 4 walls
function testMap(){
  for(var i = 0; i < (mapH * mapW); ++i){

    // Edges

    if (
      // top
      i < mapW || 
      // left
      (i % mapW) == 0 || 
      // right
      ((i + 1) % mapW) == 0 || 
      // bottom
      i > ((mapW * mapH) - mapW)
    ) {

      map.push(
        {
          id: i,
          render: {
            base: '#D35',
            object: false,
            sprite: false
          },
          state: {
            passable: false
          }
        },
      );

    }
    else{

      // Grass

      map.push(
        {
          id: i,
          render: {
            base: '#0C3',
            object: false,
            sprite: false
          },
          state: {
            passable: true
          }
        },
      );

    }
  }
}
<!DOCTYPE html>
<html>
<head>

  <style>

    body{
      background-color: #000;
      display: flex;
      align-items: center;
      justify-content: center;
      color: #FFF;
      font-size: 18px;
      padding: 0;
      margin: 0;
    }

    main{
      width: 100%;
      max-width: 800px;
      margin: 10px auto;
      display: flex;
      align-items: flex-start;
      justify-content: center;
      flex-wrap: wrap;
    }

    .game{
      width: 1000px;
      height: 1000px;
      position: relative;
    }

    canvas{
      image-rendering: -moz-crisp-edges;
      image-rendering: -webkit-crisp-edges;
      image-rendering: pixelated;
      image-rendering: crisp-edges;
    }

    .game canvas{
      position: absolute;
      top: 0;
      left: 0;
      width: 800px;
      height: 800px;
    }

  </style>
  
</head>
<body>
  
  <main>
    <div class="game">
      <canvas id="save" width="200" height="200" style="z-index: 1;"></canvas>
    </div>
  </main>

</body>
</html>

关于javascript - JS Canvas 运动动画循环,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57734806/

相关文章:

javascript - 更新祖父组件的状态

javascript - 从 XHR 请求接收 OAuth2 token

javascript - 如何清除 Canvas : HTML5 中的特定行

javascript - 在基于 javascript 的 Canvas 游戏上上下移动

html - 为什么我的 html canvas 游戏不能在 firefox 上运行?

javascript - D3 : Hyperlinks in tree diagram

javascript - 循环遍历字符串中的 html 标签并将内部文本添加到数组中

javascript - 在 JavaScript Canvas 中沿直线移动一个点

javascript - 一段时间后 Canvas 开始滞后

javascript - 如何使用 javascript/jquery 将 3 个 Canvas HTML 元素合并到 1 个图像文件中?