【问题标题】:how can I detect collision in a 2D tile game map如何检测 2D 瓷砖游戏地图中的碰撞
【发布时间】:2018-07-05 18:39:02
【问题描述】:

我制作了这个基本游戏,其中我画了一张地图和一个玩家,玩家可以在任何地方移动,但我怎样才能让它在地图中的图块 [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);

    }
  }
}

【问题讨论】:

    标签: javascript canvas 2d tile


    【解决方案1】:

    <!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>

    【讨论】:

    • 哇,物理学非常流畅;感觉就像一场真正的比赛。太糟糕了,代码是用非常规的 JS 编写的,比如 void、按位操作、循环中的预增量等。地图类和碰撞逻辑的变量命名很差(xy 而不是 column 和 @ 987654326@、INV_TILE_WIDTHnextId 等),一些死代码,没有 cmets。我必须对其进行逆向工程才能理解其中的魔力。这是my Codepen,带有更现代的 ES6+
    【解决方案2】:

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

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

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

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

    现在这是检测碰撞的函数,为了可读性和简洁性,p代表玩家对象,t代表瓷砖对象。此函数根据玩家的移动方向返回玩家是否与瓷砖发生碰撞。

    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
        }
      }
    }
    

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

    附言。

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

    【讨论】:

    • 是 tileArray 像:map = [...]
    • 是的,tileArray 基本上是您的地图之一。你可以这样做,这样数字就可以代表不同的瓷砖对象。
    • 没问题!如果这解决了您的问题,请确保接受此作为答案,因为这是 javascript 游戏开发中的常见问题。谢谢!
    • 好的,所以我添加了碰撞检测,一切运行良好,所以我认为下一步是让地图滚动,但我做到了,但是....瓷砖碰撞不再正常工作了就像地图滚动绘图时一样,但碰撞检测没有继承链接hastebin.com/adunogobiz.js
    • 如果你能提供一些可以执行和测试的东西,那就太好了,因为如果没有看到碰撞检测出了什么问题,就很难确定问题出在哪里。另外,我不明白您所说的地图滚动是什么意思。我假设当玩家移动时地图在移动。如果我错了,请纠正我。
    猜你喜欢
    • 2013-01-26
    • 1970-01-01
    • 2012-01-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多