如何检测 2D 瓷砖游戏地图中的碰撞

how can I detect collision in a 2D tile game map

我制作了这个基本游戏,我画了一张地图和一个玩家,玩家可以移动到任何地方,但是我怎样才能让它在地图上的图块[1] 上时不会移动? 另外,当我尝试检查 player.x 是否大于 50 时,它可能会离开它,但如果我一次单击 2 个键,它就会通过 const context = document.querySelector("canvas").getContext("2d");

var rgb = 'rgb(' + Math.random()*256 + ',' + Math.random()*256 + ',' + Math.random()*256 + ','+Math.random() + ')';

document.onload = Loop();

var width = 1500;
var height = 800;

function Loop(){

  var width = 1500;
  var height = 800;

  context.canvas.height = height;
  context.canvas.width = width;

  this.interval = setInterval(Update, 1000/100);

}



const Player = function(x, y, w, h, color) {
  this.x = x; this.y = y; this.w = w; this.h = h;

  this.speedY = 0; this.speedX = 0;
  this.Draw = function(){
    context.fillStyle = this.color;
    context.fillRect(this.x, this.y, this.w, this.h);
  };
  this.Move = function(){
    this.x += this.speedX;
    this.y += this.speedY;
  };
};<code>

var player = new Player(100,100,50, 50, rgb);


var Key = {};
function Update(){
  context.clearRect(0, 0, width, height);
  Map();
  player.Draw();
  player.Move();

onkeydown = onkeyup = function(e){
  player.speedX = 0;
  player.speedY = 0;
  e = e || event;
  Key[e.keyCode] = e.type == 'keydown';
    if(Key[37] || Key[65]) {player.speedX -= 2}
    if(Key[38] || Key[87]) {player.speedY -= 2}
    if(Key[39] || Key[68]) {player.speedX += 2}
    if(Key[40] || Key[83]) {player.speedY += 2}
    if(Key[32]) {player.color = 'rgb(' + Math.random()*256 + ',' + Math.random()*256 + ',' + Math.random()*256 + ','+Math.random()*1 + ')';}
  };
}

var map = [
1, 1, 1, 1, 1,
1, 0, 0, 0, 1,
1, 0, 0, 0, 1,
1, 0, 0, 0, 1,
1, 1, 1, 1, 1
    ];

var row = 5;
var column = 5;


function Map(){
  for(let y = -1; y < column; y++){

    for(let x = -1; x < row; x++){
      switch(map[((y*row) + x)]) {
        case 0: context.fillStyle = player.color;
          break;
        case 1: context.fillStyle = "#ffffff";
          break;
        default: context.fillStyle = "#000000";
      }
    context.fillRect(x*50, y*50, 50, 50);

    }
  }
}

首先,查看您的代码,缺少一些实现基本碰撞检测所需的东西,它们是:

  1. he/she玩家当前移动的方向。这很重要,因为它允许确定碰撞检测的函数区分它正在检查碰撞的哪一侧(向上,向下、左或右)因为玩家一次只能与一侧发生碰撞。

  2. 图块的位置和大小。这一点也很重要,因为和第一点一样,玩家可以碰撞的瓷砖只有一侧,知道大小和位置可以根据玩家的大小和位置确定是否发生碰撞。

另外,既然你提到它是一个基本的游戏,下面的实现是一个基本的碰撞检测。如果你要制作一个更复杂更大的游戏,你应该尝试研究四叉树以获得更有效的碰撞检测: https://gamedevelopment.tutsplus.com/tutorials/quick-tip-use-quadtrees-to-detect-likely-collisions-in-2d-space--gamedev-374

现在是检测碰撞的函数,为了可读性和简洁性,p代表player对象,t代表tile对象。此函数 returns 玩家是否根据移动方向与方块碰撞。

function isColliding(p, t){
  if (p.direction == 'up') {
   return p.y +(p.height/2)-p.speedY< t.y + t.height && p.y > t.y
      && p.x + p.width > t.x && p.x < t.x + t.width;
  }
  if (p.direction == 'down') {
    return p.y + (p.height/2)+p.speedY > t.y && p.y < t.y
      && p.x + p.width > t.x && p.x < t.x + t.width;
  }
  if (p.direction == 'right') {
    return p.x + p.width+p.speedX > t.x && p.x < t.x
      && p.y +(p.height/2)> t.y && p.y + p.height < t.y +t.height+ (p.height / 2);
  }
  if (p.direction == 'left') {
    return p.x -p.speedX< t.x + t.width && p.x > t.x
      && p.y +(p.height/2)> t.y && p.y + p.height < t.y +t.height+ (p.height / 2);
  }
  return false;
}

您可能希望将其放入播放器移动功能中,以便在移动时不断检测瓷砖。为此,您需要修改按键检测,以便在每次不同的按键时,它都会更新玩家的方向,这是一个简单的示例:

document.onkeydown = function(event){
    if (event.keyCode == 87)
        player.up = true;
    else if (event.keyCode == 65)
        player.left = true;
    else if (event.keyCode == 83)
        player.down = true;
    else if (event.keyCode == 68)
        player.right = true;
}

还有另一个关于每次玩家移动(用户按下按键)的简单示例:

const Player= function(/*Param stuff*/){
  /*Property stuff*/
  //tileArray is the array (or object, your choice) of all the current tiles in the map
  this.move=function(tileArray){
    //Go through all tiles to see if player is colliding with any of them
    for(var t in tileArray){
      if(this.up){
        if(isColliding(this, tileArray[t]){
          //functionality for when player collides
        }else{
          //functionality for when player doesn't collide
        }
      }
      //check if player is going down, left, etc
    }
  }
}

这些只是如何实施检测的示例。您应该将其用作相对于您的代码功能如何实现它的参考,因为我没有根据您发布的内容编写它。

PS.

确保在用户停止按键后也将方向转换为 false。

<!doctype html>
<html>
 <head>
  <meta charset="utf-8">
  <style>
   body {
    background-color: black;
   }
   
   canvas {
    display: block;
    margin: auto;
    border: solid 1px white;
    border-radius: 10px;
   }
   
   script {
    display: none;
   }
  </style>
 </head>
 
 <body>
  <canvas id="canvas"></canvas>
  <script type="application/javascript">
  
  void function() {
   
   "use strict";
   
   // Classes
   function Camera(x,y) {
    this.x = x || 0.0;
    this.y = y || 0.0;
   }
   
   Camera.prototype = {
    set: function(x,y) {
     this.x = x || 0.0;
     this.y = y || 0.0;
    },
    
    pan: function(x,y) {
     this.x += x || 0.0;
     this.y += y || 0.0;
    }
   };
   
   var nextID = 0;
   
   function Tile(colour) {
    this.id = nextID++;
    this.colour = colour || "black";
   }
   
   function Map(width,height) {
    this.width = width || 1;
    this.height = height || 1;
    this.map = [];
    this.map.length = this.height;
    
    for (var y = 0; y < this.height; ++y) {
     this.map[y] = [];
     this.map[y].length = width;
     
     for (var x = 0; x < this.width; ++x) {
      this.map[y][x] = Math.random() < 0.2 ?
       this.TILE_WALL:
       this.TILE_GRASS;
     }
     
     this.map[y][0] = this.TILE_WALL;
     this.map[y][this.width - 1] = this.TILE_WALL;
    }
    
    for (var x = 0; x < this.width; ++x) {
     this.map[0][x] = this.TILE_WALL;
     this.map[this.height - 1][x] = this.TILE_WALL;
    }
   }
   
   Map.prototype = {
    TILE_WIDTH: 32.0,
    TILE_HEIGHT: 32.0,
    INV_TILE_WIDTH: 0.0,
    INV_TILE_HEIGHT: 0.0,
   
    TILE_AIR: new Tile("#00000000"),
    TILE_GRASS: new Tile("#00AA00FF"),
    TILE_WALL: new Tile("#555555FF"),
   
    set: function(x,y,tile) {
     this.map[y][x] = tile;
    },
   
    scaleX: function(x) {
     return (x * this.INV_TILE_WIDTH) | 0;
    },
   
    scaleY: function(y) {
     return (y * this.INV_TILE_HEIGHT) | 0;
    },
   
    isColliding: function(x,y) {
     return x > -1 && x < this.width
      && y > -1 && y < this.height
      && this.map[y][x].id > 1;
    },
   
    render: function(ctx,camera) {
     for (var y = 0; y < this.height; ++y) {
      for (var x = 0; x < this.width; ++x) {
       var tile = this.map[y][x];
       var _x = x * this.TILE_WIDTH - camera.x;
       var _y = y * this.TILE_HEIGHT - camera.y;
       
       ctx.fillStyle = tile.colour;
       ctx.fillRect(_x,_y,this.TILE_WIDTH - 1,this.TILE_HEIGHT - 1);
      }
     }
    }
   };
   
   Map.prototype.INV_TILE_WIDTH = 1.0 / Map.prototype.TILE_WIDTH;
   Map.prototype.INV_TILE_HEIGHT = 1.0 / Map.prototype.TILE_HEIGHT;
   
   function Player(x,y) {
    this.x = x || 0.0;
    this.y = y || 0.0;
    this.dx = 0.0;
    this.dy = 0.0;
    this.isUp = false;
    this.isDown = false;
    this.isLeft = false;
    this.isRight = false;
   }
   
   Player.prototype = {
    WIDTH: 20.0,
    HEIGHT: 20.0,
    
    ACCELERATION: 1.0,
    DEACCELERATION: 0.5,
    MAX_SPEED: 3.0,
    
    tick: function(map) {
     // Movement
     if (this.isUp) {
      this.dy -= this.ACCELERATION;
      
      if (this.dy < -this.MAX_SPEED) {
       this.dy = -this.MAX_SPEED;
      }
     } else if (this.dy < 0.0) {
      this.dy += this.DEACCELERATION;
      
      if (this.dy > 0.0) {
       this.dy = 0.0;
      }
     }
     
     if (this.isDown) {
      this.dy += this.ACCELERATION;
      
      if (this.dy > this.MAX_SPEED) {
       this.dy = this.MAX_SPEED;
      }
     } else if (this.dy > 0.0) {
      this.dy -= this.DEACCELERATION;
      
      if (this.dy < 0.0) {
       this.dy = 0.0;
      }
     }
     
     if (this.isLeft) {
      this.dx -= this.ACCELERATION;
      
      if (this.dx < -this.MAX_SPEED) {
       this.dx = -this.MAX_SPEED;
      }
     } else if (this.dx < 0.0) {
      this.dx += this.DEACCELERATION;
      
      if (this.dx > 0.0) {
       this.dx = 0.0;
      }
     }
     
     if (this.isRight) {
      this.dx += this.ACCELERATION;
      
      if (this.dx > this.MAX_SPEED) {
       this.dx = this.MAX_SPEED;
      }
     } else if (this.dx > 0.0) {
      this.dx -= this.DEACCELERATION;
      
      if (this.dx < 0.0) {
       this.dx = 0.0;
      }
     }
     
     // Collision
     if (this.dx !== 0.0) {
      var minY = map.scaleY(this.y);
      var maxY = map.scaleY(this.y + this.HEIGHT);
      var minX = 0;
      var maxX = 0;
      
      if (this.dx < 0.0) {
       minX = map.scaleX(this.x + this.dx);
       maxX = map.scaleX(this.x);
      } else {
       minX = map.scaleX(this.x + this.WIDTH);
       maxX = map.scaleX(this.x + this.WIDTH + this.dx);
      }
      
      loop:
      for (var y = minY; y <= maxY; ++y) {
       for (var x = minX; x <= maxX; ++x) {
        if (map.isColliding(x,y)) {
         this.x = this.dx < 0.0 ?
          (x + 1) * map.TILE_WIDTH:
          x * map.TILE_WIDTH - this.WIDTH - 1;
        
         this.dx = 0.0;
         break loop;
        }
       }
      }
     }
     
     if (this.dy !== 0.0) {
      var minX = map.scaleX(this.x);
      var maxX = map.scaleX(this.x + this.WIDTH);
      var minY = 0;
      var maxY = 0;
      
      if (this.dy < 0.0) {
       minY = map.scaleY(this.y + this.dy);
       maxY = map.scaleY(this.y);
      } else {
       minY = map.scaleY(this.y + this.HEIGHT);
       maxY = map.scaleY(this.y + this.HEIGHT + this.dy);
      }
      
      loop:
      for (var y = minY; y <= maxY; ++y) {
       for (var x = minX; x <= maxX; ++x) {
        if (map.isColliding(x,y)) {
         this.y = this.dy < 0.0 ?
          (y + 1) * map.TILE_HEIGHT:
          y * map.TILE_HEIGHT - this.HEIGHT - 1;
        
         this.dy = 0.0;
         break loop;
        }
       }
      }
     }
     
     this.x += this.dx;
     this.y += this.dy;
    },
    
    render: function(ctx,camera) {
     camera.set(this.x,this.y);
    
     ctx.lineWidth = 1;
     ctx.strokeStyle = "black";
     ctx.fillStyle = "darkred";
     ctx.beginPath();
     ctx.rect(this.x - camera.x,this.y - camera.y,this.WIDTH,this.HEIGHT);
     ctx.fill();
     ctx.stroke();
    }
   };
   
   // Variables
   var canvasWidth = 180;
   var canvasHeight = 160;
   var canvas = null;
   var ctx = null;
   var camera = null;
   var map = null;
   var player = null;
   
   // Functions
   function onKeyDown(e) {
    switch(e.key.toUpperCase()) {
     case "W": player.isUp = true; break;
     case "S": player.isDown = true; break;
     case "A": player.isLeft = true; break;
     case "D": player.isRight = true; break;
    }
   }
   
   function onKeyUp(e) {
    switch(e.key.toUpperCase()) {
     case "W": player.isUp = false; break;
     case "S": player.isDown = false; break;
     case "A": player.isLeft = false; break;
     case "D": player.isRight = false; break;
    }
   }
   
   function loop() {
    // Tick
    player.tick(map);
    
    // Render
    ctx.fillStyle = "gray";
    ctx.fillRect(-canvasWidth >> 1,-canvasHeight >> 1,canvasWidth,canvasHeight);
    
    map.render(ctx,camera);
    player.render(ctx,camera);
    
    //
    requestAnimationFrame(loop);
   }
   
   // Entry point (first to execute)
   onload = function() {
    canvas = document.getElementById("canvas");
    canvas.width = canvasWidth;
    canvas.height = canvasHeight;
    
    ctx = canvas.getContext("2d");
    ctx.translate(canvasWidth >> 1,canvasHeight >> 1);
    
    camera = new Camera(0.0,0.0);
    map = new Map(10,10);
    player = new Player(40.0,40.0);
    
    map.set(1,1,map.TILE_GRASS);
    
    addEventListener("keydown",onKeyDown);
    addEventListener("keyup",onKeyUp);
    
    loop();
   }
   
  }();
  
  </script>
 </body>
</html>