JS Canvas 移动动画循环

4

我在画布上渲染了一个对象。我想让这个对象沿着预设的路径循环移动。以下是我的代码:

// 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()每个

2个回答

1
你想要改变路径方向的条件,我将其更改为每个方向都有条件,类似于:
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>


这可能比我得到的更接近,但行为与我预期的不同。该对象应该每个字符串在“路径”数组中只移动一个瓷砖长度。因此,对于['down','up','left','right'],它应该仅向下移动16像素,向上移动16像素,向左移动16像素,然后向右移动16像素。 - jadle
无论我如何移动逻辑,它仍然是相同的... 您需要为每个方向设置条件。 - Helder Sepulveda
你对每个方向需要条件可能是正确的,但我还没想出逻辑,所以不确定。然而,这仍然没有解决我正在尝试解决的问题。你的条件将对象坐标与地图上特定坐标进行比较,并使用 tilesize。我希望目标地图坐标由 path 数组确定。感谢你花时间帮助我。 - jadle
你认为 originPoint = entities[id].xy; 这行代码是在做什么呢?我觉得它并没有实现你想要的功能。 - Helder Sepulveda
我的意图是让originPoint存储对象访问的上一个瓷砖的坐标。因此,当对象到达新的瓷砖(已经行驶了16px的一格)时,originPoint会更新为新的坐标,并且path数组的下一步就会发生。 - jadle
让我们在聊天中继续这个讨论 - Helder Sepulveda

0

既然有人已经解决了你的错误...

这不仅仅是解决问题,因为你面临的真正问题是复杂性,长而复杂的if语句使用数据结构以不同的方式表示相同的信息,使得难以发现逻辑上的简单错误。

此外,你还有一些不良的编程习惯,加剧了问题。

快速修复只会意味着你很快就会面临下一个问题。你需要以减少由于复杂性增加而导致的逻辑错误的方式编写代码。

风格

首先是风格。良好的风格非常重要。

不要将 null 赋给已声明的变量。JavaScript 不应该需要使用 null,违反此规则的例外是一些 C++ 程序员感染了 DOM API 的 null 返回值,因为他们不理解 JavaScipt(或者是一个残酷的玩笑),现在我们被困在了 null 中。 window 是默认的 this(全局的 this)并且很少需要使用。例如,window.requestAnimationFramerequestAnimationFrame 相同,window.onloadonload 相同。
不要用不准确、冗余和/或显而易见的注释来污染你的代码,使用良好的命名提供所需的信息。例如:
  • var map[]; 的注释为 // array of tile data,实际上它是一个带有数据的数组,谁会想到呢,所以注释可以是 // tiles,但是 map 是一个相当模糊的名称。删除注释并给变量命一个更好的名字。
  • // Static Globals 在某些变量上面。Javascript 没有静态类型,所以注释是错误的,"global's" 部分是 "duh..."。
使用 const 声明常量,将所有的魔术数字移动到顶部,并将它们定义为命名的 const。名称有意义,在某些代码中,数字几乎没有意义。
不要将监听器分配给事件名称,这是不可靠的,可以被劫持或覆盖。始终使用 addEventListener 分配事件监听器。
非常注意你的命名。例如,命名为 coordsToTile 的函数令人困惑,因为它并没有返回一个 tile,而是返回一个 tile 索引,请更改函数名称以匹配函数行为,或更改行为以匹配名称。
不要使用冗余的中间函数,例如:
  • 您的帧请求 requestAnimationFrame(function(){mainLoop()}); 应该跳过中间人并变成 requestAnimationFrame(mainLoop);
  • 您使用 Function.apply 来调用函数 window[entities[i].logic.func].apply(null, entities[i].logic.data);apply 用于将上下文 this 绑定到调用中,您在函数中不使用 this,因此不需要使用 apply。例如:window[entities[i].logic.func](...entities[i].logic.data); 顺便说一下,被迫使用括号符号来访问全局变量是数据结构不良的标志。你绝不能这样做。
JavaScript 有一些非官方的惯用风格,你应该尝试以这种风格编写 JS。您代码中的一些示例:
  • else 在同一行与闭合的 }
  • ifelseforfunction() 后空一格,并在 else、开放块 { 前空一格
  • id 和 index 不同,使用 idxindex 表示索引,使用 id 表示标识符

保持简单

数据结构越复杂,维护起来就越困难。

结构化

定义对象以封装和组织数据。

  • 一个全局配置对象,可转换为JSON格式并且可移植。它包含游戏中所有的魔法数字、默认值、类型描述等。

  • 创建一组全局实用程序,执行常见重复任务,例如创建坐标、方向列表。

  • 定义封装特定对象的设置和行为的对象。

  • 使用多态对象设计,意味着不同的对象使用命名的公共行为和属性。在示例中,所有可绘制对象都有一个名为draw的函数,该函数接受一个参数ctx,所有可以更新的对象都有一个名为update的函数。

示例

此示例是您代码的完全重写,并解决了您的问题。它可能有点高级,但只是一个示例,可以查看并获取一些提示。

所使用的对象的快速描述。

对象

  • config 是可移植的配置数据
  • testMap 是一个示例地图描述
  • tileMap 处理与地图相关的内容
  • Path 对象封装路径逻辑
  • Entity 对象是单个移动实体
  • Tile 对象表示单个瓦片
  • game 游戏状态管理器

游戏有各种状态,例如加载、介绍、游戏中、游戏结束等。如果您没有提前规划并创建一个健壮的状态管理器,那么从一个状态转移到另一个状态将会非常困难。

我已经包含了有限状态管理器的核心部分。状态管理器负责更新和渲染。它还负责所有状态更改。

setTimeout(() => game.state = "setup", 0); // Will start the game
const canvas = document.getElementById('save');
const ctx = canvas.getContext("2d");
const point = (x = 0, y = 0) => ({x,y});
const dirs = Object.assign(
    [point(0, -1), point(1), point(0,1), point(-1)], { // up, right, down, left
        "u": 0,  // defines index for direction string characters
        "r": 1,
        "d": 2,
        "l": 3,
        strToDirIdxs(str) { return str.toLowerCase().split("").map(char => dirs[char]) },
    }
);

const config = { 
    pathIdx: 28,
    pathTypes: {
        standard: "dulr",
        complex: "dulrldudlruldrdlrurdlurd",
    },
    tile: {size: 16},
    defaultTileName: "grass",
    entityTypes: {
        e: {
            speed: 1 / 32, // in fractions of a tile per frame
            color: "#00F",
            size: {x:16, y:24},
            pathName: "standard",
        },
        f: {
            speed: 1 / 16, // in fractions of a tile per frame
            color: "#08F",
            size: {x:18, y:18},
            pathName: "complex",
        },        
    },
    tileTypes: {
        grass: {
            style: {baseColor: "#0C3", object: false, sprite: false},
            state: {passable: true}
        },
        wall: {
            style: {baseColor: "#D35", object: false, sprite: false},
            state: {passable: false}
        },
    },
}
const testMap = {
    displayChars: {
        " " : "grass",  // what characters mean
        "#" : "wall", 
        "E" : "grass", // also entity spawn
        "F" : "grass", // also entity spawn
    },
    special: { // spawn enties and what not
        "E"(idx) { entities.push(new Entity(config.entityTypes.e, idx)) },
        "F"(idx) { entities.push(new Entity(config.entityTypes.f, idx)) }
    },
    map: // I double the width and ignor every second characters as text editors tend to make chars thinner than high
    //   0_1_2_3_4_5_6_7_8_9_ x coord
        "####################\n" +
        "##FF    ##        ##\n" +
        "##      ##        ##\n" +
        "##      ####      ##\n" +
        "##                ##\n" +
        "##  ####          ##\n" +
        "##                ##\n" +
        "##                ##\n" +
        "##              EE##\n" +
        "####################",
    //   0_1_2_3_4_5_6_7_8_9_ x coord
}
const entities = Object.assign([],{
    update() {
        for (const entity of entities) { entity.update() }
    },
    draw(ctx) {
        for (const entity of entities) { entity.draw(ctx) }
    },
});    
const tileMap = {
    map: [],
    mapToIndex(x, y) { return x + y  * tileMap.width },
    pxToIndex(x, y) { return x / config.tile.size | 0 + (y / config.tile.size | 0) * tileMap.width },
    tileByIdx(idx) { return tileMap.map[idx] },
    tileByIdxDir(idx, dir) { return tileMap.map[idx + dir.x + dir.y * tileMap.width] },
    idxByDir(dir) { return dir.x + dir.y * tileMap.width },
    create(mapConfig) {
        tileMap.length = 0;
        const rows = mapConfig.map.split("\n");
        tileMap.width = rows[0].length / 2 | 0;
        tileMap.height = rows.length;
        canvas.width = tileMap.width * config.tile.size;
        canvas.height = tileMap.height * config.tile.size;
        var x, y = 0;
        while (y < tileMap.height) {
            const row = rows[y];
            for (x = 0; x < tileMap.width; x += 1) {
                const char = row[x * 2];
                tileMap.map.push(new Tile(mapConfig.displayChars[char], x, y));
                if (mapConfig.special[char]) {
                    mapConfig.special[char](tileMap.mapToIndex(x, y));
                }
            }
            y++;
        }
    }, 
    update () {}, // stub
    draw(ctx) {
        for (const tile of tileMap.map) { tile.draw(ctx) }
    },
};
function Tile(typeName, x, y) {
    typeName = config.tileTypes[typeName] ? typeName : config.defaultTileName;
    const t = config.tileTypes[typeName];
    this.idx =  x + y * tileMap.width;
    this.coord = point(x * config.tile.size, y * config.tile.size);
    this.style =  {...t.style};
    this.state = {...t.state};    
}
Tile.prototype = {
    draw(ctx) {
        ctx.fillStyle = this.style.baseColor;
        ctx.fillRect(this.coord.x, this.coord.y, config.tile.size, config.tile.size);      
    }
};
function Path(pathName) {
    if (typeof config.pathTypes[pathName] === "string") { 
        config.pathTypes[pathName] = dirs.strToDirIdxs(config.pathTypes[pathName]);
    }
    this.indexes = config.pathTypes[pathName];
    this.current = -1;    
}
Path.prototype = {
    nextDir(tileIdx) {
        var len = this.indexes.length;
        while (len--) { // make sure we dont loop forever
            const dirIdx = this.indexes[this.current];
            if (dirIdx > - 1) {
                const canMove = tileMap.tileByIdxDir(tileIdx, dirs[dirIdx]).state.passable;
                if (canMove) { return dirs[dirIdx] }
            }
            this.current = (this.current + 1) % this.indexes.length;
        }
    }
};
function Entity(type, tileIdx) { 
    this.coord = point();
    this.move = point();
    this.color = type.color;
    this.speed = type.speed;
    this.size = {...type.size};
    this.path = new Path(type.pathName);
    this.pos = this.nextTileIdx = tileIdx;
    this.traveled = 1;  // unit dist between tiles 1 forces update to find next direction
}
Entity.prototype = {
    set dir(dir) {
        if (dir === undefined) { // dont move 
            this.move.y = this.move.x = 0;
            this.nextTileIdx = this.tileIdx;
        } else {
            this.move.x = dir.x * config.tile.size;
            this.move.y = dir.y * config.tile.size;
            this.nextTileIdx = this.tileIdx + tileMap.idxByDir(dir);
        }
    },
    set pos(tileIdx) {
        this.tileIdx = tileIdx;
        const tile = tileMap.map[tileIdx];
        this.coord.x = tile.coord.x + config.tile.size / 2;
        this.coord.y = tile.coord.y + config.tile.size / 2;
        this.traveled = 0;
    },
    draw(ctx) {
        const ox = this.move.x * this.traveled;
        const oy = this.move.y * this.traveled;
        ctx.fillStyle = this.color;        
        ctx.fillRect(ox + this.coord.x -  this.size.x / 2, oy + this.coord.y  -  this.size.y / 2, this.size.x, this.size.y)
    },
    update(){
        this.traveled += this.speed;
        if (this.traveled >= 1) {
              this.pos = this.nextTileIdx;
              this.dir = this.path.nextDir(this.tileIdx);
        }
    }
};
const game = {
    currentStateName: undefined,
    currentState: undefined,
    set state(str) {
        if (game.states[str]) {
            if (game.currentState && game.currentState.end) { game.currentState.end() }
            game.currentStateName = str;
            game.currentState = game.states[str];
            if (game.currentState.start) { game.currentState.start() }
        }
    },
    states: {
        setup: {
            start() { 
                tileMap.create(testMap);
                game.state = "play"; 
            },
            end() {
                requestAnimationFrame(game.render); // start the render loop
                delete game.states.setup; // MAKE SURE THIS STATE never happens again
            },
        },
        play: {
            render(ctx) {    
                tileMap.update();
                entities.update();
                tileMap.draw(ctx);
                entities.draw(ctx);
            }
        }
    },
    renderTo: ctx,
    startTime: undefined,
    time: 0,
    render(time) {
        
        if (game.startTime === undefined) { game.startTime = time }
        game.time = time - game.startTime;
        if (game.currentState && game.currentState.render) { game.currentState.render(game.renderTo) }
        requestAnimationFrame(game.render);
    }
};
body{
  background-color: #000;

}




canvas{
    image-rendering: pixelated;
    position: absolute;
    top: 0;
    left: 0;
    width: 400px;
    height: 400px;
}
<canvas id="save" width="200" height="200" style="z-index: 1;"></canvas>
  

请注意,有些运行状态尚未经过测试,可能会存在拼写错误。
还有,瓦片地图必须被墙壁包围以包含实体,否则它们将在尝试离开游戏区域时抛出异常。
该代码是设计用于片段中的。为了使其在标准页面中工作,在第一行之前添加 setTimeout(() => game.state = "setup", 0); ,在最后一行之后添加 addEventListener("load", () => { 和一行 });

哇,感谢您花费时间和精力制作这个!我以前从未见过 Object.assign,这将非常有帮助。您用于定义映射的方法也比我能想出的好得多。 - jadle

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