JS Canvas运动动画循环

时间:2019-08-31 02:42:47

标签: javascript canvas html5-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的长度)。现在,它只会在第一次更新,然后停止。希望有人可以指出我在这里做错了。

2 个答案:

答案 0 :(得分: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>

答案 1 :(得分:0)

由于有人已经解决了您的错误...

这不仅仅是解决问题的方法,因为您面临的实际问题是复杂性,如果语句使用数据结构以不同的方式表示相同的信息,则很难理解逻辑中的简单错误。

最重要的是,您有一些不良的风格习惯使问题更加复杂。

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

样式

第一种样式。好的风格很重要

  • 请勿将null分配给已声明的变量。 JavaScript不需要使用null,该规则的例外是某些C ++编码器不了解JavaScipt(或者是一个残酷的笑话),因此用null返回值感染了DOM API,现在我们陷入了null问题

  • window是默认this(全局this),很少需要。例如,window.requestAnimationFrame仅与requestAnimationFrame相同,而window.onloadonload相同

  • 不要用不正确,多余和/或明显的注释来污染您的代码,请使用良好的命名来提供所需的信息。例如:

    • var map[];的注释为// array of tile data,实际上,它是一个数组,其中包含可能会猜到的数据,因此注释可以为// tiles,但map是一个相当模棱两可的名字。删除注释,并给变量起一个更好的名字。
    • 某些变量上方的注释// Static Globals。 Javascript没有静态类型,因此注释是错误的,并且“ global's”部分是“ duh ...”
  • 使用const声明常量,将所有幻数移到顶部并将其定义为名为const。名称具有含义,某些代码中的数字几乎没有含义。

  • 不为事件名称分配侦听器,它不可靠,可以被劫持或覆盖。始终使用addEventListener分配事件监听器

  • 请务必谨慎命名。例如,名为coordsToTile的函数令人困惑,因为它不返回图块,它返回图块索引,或者更改函数名称以匹配函数的行为,或者更改行为以匹配名称。

    < / li>
  • 不要使用冗余的中间函数,例如:

    • 您的框架请求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);

      BTW被迫使用括号表示法来访问全局变量,这表明数据结构不良。您永远都不要这样做。

  • JavaScript具有非正式的惯用风格,您应尝试以这种风格编写JS。代码中的一些示例

    • else与结束}
    • 在同一行
    • ifelseforfunction()else之前的空格,打开块{
    • ID和索引不同,请使用idxindex作为索引,使用id作为标识符

保持简单

使数据结构越复杂,维护它们就越困难。

结构化

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

  • 一个全局配置对象,该对象是可转换的,即可以与JSON相互转换。它包含所有魔术数字,默认值,类型说明以及游戏中不需要的内容。

  • 创建一组全局公用程序,以执行常见的重复任务,即创建坐标和方向列表。

  • 定义对象,该对象封装仅特定于该对象的设置和行为。

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

示例

此示例是对代码的完整重写并解决了问题。它可能有点高级,但是它只是一个示例,尽管可以了解一些技巧。

所用对象的简要说明。

对象

  • config是可传输的配置数据
  • testMap是地图说明示例
  • tileMap会映射相关内容
  • Path对象封装路径逻辑
  • Entity反对一个移动的实体
  • Tile代表单个图块的对象
  • game游戏状态管理器

游戏具有状态,例如加载,介绍,inPlay,gameOver等。如果您不提前计划并创建可靠的状态管理器,您会发现很难从一种状态转移到另一种状态

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

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", () = {行,并在最后一行之后添加});