如何在画布中创建障碍物

7
我正在尝试制作一个简单的平台游戏。我使用的代码如下所示。

window.onload = function(){
 var canvas = document.getElementById('game');
 var ctx = canvas.getContext("2d");

 var rightKeyPress = false;
 var leftKeyPress = false;
 var upKeyPress = false;
 var downKeyPress = false;
 var playerX = canvas.width / 2;
 var playerY = -50;
 var dx = 3;
 var dy = 3;
 var dxp = 3;
 var dyp = 3;
 var dxn = 3;
 var dyn = 3;
 var prevDxp = dxp;
 var prevDyp = dyp;
 var prevDxn = dxn;
 var prevDyn = dyn;
 var playerWidth = 50;
 var playerHeight = 50;
 var obstacleWidth = 150;
 var obstacleHeight = 50;
 var obstaclePadding = 10;
 var G = .98;
 var currentVelocity = 0;
 var obstacles = [];
 var imageLoaded = false;

 document.addEventListener("keyup",keyUp,false);
 document.addEventListener("keydown",keyDown,false);

 function keyDown(e){
  if(e.keyCode == 37){
   leftKeyPress = true;
   if(currentVelocity > 2){
    currentVelocity -= .1;
   }
  }
  if(e.keyCode == 38){
   upKeyPress = true;
  }
  if(e.keyCode == 39){
   rightKeyPress = true;
   if(currentVelocity < 2){
    currentVelocity += .1;
   }
  }
  if(e.keyCode == 40){
   downKeyPress = true;
  }
 }
 function keyUp(e){
  if(e.keyCode == 37){
   leftKeyPress = false;
  }
  if(e.keyCode == 38){
   upKeyPress = false;
  }
  if(e.keyCode == 39){
   rightKeyPress = false;
  }
  if(e.keyCode == 40){
   downKeyPress = false;
  }
 }
 function createObstacles(){
  for(x=0;x < 4;x++){
   var obX = (200 * x) + Math.round(Math.random() * 150);
   var obY = 50 + Math.round(Math.random() * 400);
   obstacles.push({"x":obX,"y":obY});
  }
 }
 createObstacles();
 function drawObstacles(){
  ctx.beginPath();
  for(x=0;x < 4;x++){
   var obX = obstacles[x].x;
   var obY = obstacles[x].y;
   ctx.rect(obX,obY,obstacleWidth,obstacleHeight)
  } 
  ctx.fillStyle = "grey";
  ctx.fill();
  ctx.closePath();
 }
 function initPlayer(){
  ctx.beginPath();
  ctx.rect(playerX,playerY,50,50);
  ctx.fillStyle="orange";
  ctx.fill();
  ctx.closePath();
 }
 function KeyPressAndGravity(){
  checkObstacleCollision();
  playerX += currentVelocity;
  if(rightKeyPress && playerX + 50 < canvas.width){
   playerX += dxp;
  }
  if(leftKeyPress && playerX > 0){
   playerX -= dxn;
  }
  if(upKeyPress && playerY > 0){
   playerY -= dyn;
  }
  if(downKeyPress && playerY + 50 < canvas.height){
   playerY += dyp;
  }
  if(playerY+50 < canvas.height){
   playerY += G;
  }
  if(playerX <= 0){
   currentVelocity = 0;
  }else if(playerX + 50 >= canvas.width){
   currentVelocity = 0;
  }
  dxp = prevDxp;
  dyp = prevDyp;
  dxn = prevDxn;
  dyn = prevDyn;
  G = .98;
  if(currentVelocity != 0){
   if(currentVelocity > 0){
    currentVelocity -= .01;
   }else{
    currentVelocity += .01;
   }
  }
 }
  /*-----------------------------------------------------------
  -------------------------------------------------------------
  -------------------------------------------------------------
  ---------------------------Check this part-------------------
  -------------------------------------------------------------
  -------------------------------------------------------------
  -------------------------------------------------------------
  ------------------------------------------------------------*/
 function checkObstacleCollision(){
  var obLen = obstacles.length;
  for(var x=0;x<obLen;x++){
   var obX = obstacles[x].x;
   var obY = obstacles[x].y;
   if((playerX + playerWidth > obX && playerX + playerWidth < obX + obstacleWidth || playerX > obX && playerX < obX + obstacleWidth) && playerY + playerHeight > obY - obstaclePadding && playerY + playerHeight < obY){
    dyp = 0;
    G = 0;
   }else if((playerX + playerWidth > obX && playerX + playerWidth < obX + obstacleWidth || playerX > obX && playerX < obX + obstacleWidth) && playerY > obY + obstacleHeight && playerY < obY + obstacleHeight + obstaclePadding){
    dyn = 0;
   }else if(playerX + playerWidth > obX - obstaclePadding && playerX + playerWidth < obX && ((playerY + playerHeight > obY && playerY + playerHeight < obY + obstacleHeight) || (playerY > obY && playerY < obY + obstacleHeight))){
    dxp = 0;
   }else if(playerX  > obX + obstacleWidth && playerX < obX + obstacleWidth + obstaclePadding && ((playerY + playerHeight > obY && playerY + playerHeight < obY + obstacleHeight) || (playerY > obY && playerY < obY +  obstacleHeight))){
    dxn = 0;
   }

  }
 }
 function draw(){
  ctx.clearRect(0,0,canvas.width,canvas.height);
  initPlayer();
  KeyPressAndGravity();
  drawObstacles();
 }

 setInterval(draw,15);
}
<canvas id="game" width="1000" height="600" style="border:1px solid #000;"></canvas>

问题在于有时候“玩家”的速度很快,就像下面的图片一样,可以穿过障碍物。我该如何阻止这种情况发生?

enter image description here

所以我的要求是,当玩家到达障碍物时,应该停止而不是通过它。


5
游戏开发中经常会遇到这个问题。您需要计算玩家在下一帧将出现的位置,并检查是否穿过了碰撞器,如果是,则使其碰撞。此外,我不会为每个碰撞器都检查碰撞,而是只会检查那些在特定距离内的碰撞器,否则您可能很快就会遇到性能问题。 - Jonas Grumann
2
你在 Stack Overflow 上拥有超过 8K 的声望,却仍然不知道如何创建 MCVE 吗? - hindmost
1
这是一个非常广泛的主题,涵盖了完整的书籍,你在搜索中应该会遇到像边界框或边界圆、宽相位和窄相位等一大堆其他术语。如果你想在 Stack Exchange 网络上找到答案,也许一个好的开始就是 GameDev SE 上的 碰撞引擎是如何工作的? - GolezTrol
谢谢 @GolezTrol,我肯定会看一下的。 - Akshay
我在我的2D浏览器游戏中使用一个名为PhaserJs的游戏引擎。它的源代码托管在Github上。虽然其他人提到了解决问题的好方法,但查看Phaser中可用的物理引擎可能有所帮助,如果您想自己开发。编辑:我相信Arcade系统是核心,其他系统是单独的库。但我可能错了。 - ste2425
显示剩余5条评论
3个回答

5

当测试移动速度快的物体的碰撞时,会出现一些复杂情况。

你必须确定在移动过程中,玩家和障碍物是否在任何时间点相交——即使在移动结束时,玩家已经超越了障碍物。因此,你必须考虑到玩家从移动开始到结束所经过的完整路径。

enter image description here ... enter image description here

然后,您可以通过检查玩家的轨迹是否与障碍物相交来检查玩家在移动过程中是否曾与障碍物相交。

enter image description here

一个相对高效的测试快速移动物体碰撞的方法
1. 定义连接玩家起始矩形与结束矩形最靠近的三个顶点的三条线段。

enter image description here

对于与障碍物相交的任何三条线路,计算线段到障碍物的距离。选择起始顶点和障碍物之间距离最短的线路。

enter image description here enter image description here

  1. Calculate the "x" & "y" distances of the selected line segment.

    var dx = obstacleIntersection.x - start.x;
    var dy = obstacleIntersection.y - start.y;
    
  2. Move the player from their starting position by the distance calculated in #3. This results in the player moving to the spot where it first collided with the obstacle.

    player.x += dx;
    player.y += dy;
    

enter image description here

代码和演示:

代码中的有用函数:

  • setPlayerVertices 确定连接玩家起始矩形最靠近玩家结束矩形的3条线段。

  • hasCollided 找到从玩家起始位置到障碍物碰撞点的最短线段。

  • line2lineIntersection 找到两条线之间的交点(如果有)。这用于测试从#1开始到结束的线段与组成障碍物矩形的任何4条线段之间是否存在交点。 归属: 此函数改编自Paul Bourke有用的交点论文

enter image description here

这里有示例代码和演示,展示如何在障碍物的碰撞点处停止播放器:

var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
    var BB=canvas.getBoundingClientRect();
    offsetX=BB.left;
    offsetY=BB.top;        
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
window.onresize=function(e){ reOffset(); }

var isDown=false;
var startX,startY,dragging;

ctx.translate(0.50,0.50);
ctx.textAlign='center';
ctx.textBaseline='middle';

var pts;
var p1={x:50,y:50,w:25,h:25,fill:''};
var p2={x:250,y:250,w:25,h:25,fill:''};
var ob={x:100,y:150,w:125,h:25,fill:''};
var obVertices=[
    {x:ob.x,y:ob.y},
    {x:ob.x+ob.w,y:ob.y},
    {x:ob.x+ob.w,y:ob.y+ob.h},
    {x:ob.x,y:ob.y+ob.h}
];
var s1,s2,s3,e1,e2,e3,o1,o2,o3,o4;

draw();

$("#canvas").mousedown(function(e){handleMouseDown(e);});
$("#canvas").mousemove(function(e){handleMouseMove(e);});
$("#canvas").mouseup(function(e){handleMouseUpOut(e);});
$("#canvas").mouseout(function(e){handleMouseUpOut(e);});


function draw(){
    ctx.clearRect(0,0,cw,ch);
    //
    ctx.lineWidth=4;
    ctx.globalAlpha=0.250;
    ctx.strokeStyle='blue';
    ctx.strokeRect(ob.x,ob.y,ob.w,ob.h);
    ctx.globalAlpha=1.00;
    ctx.fillStyle='black';
    ctx.fillText('obstacle',ob.x+ob.w/2,ob.y+ob.h/2);
    //
    ctx.globalAlpha=0.250;
    ctx.strokeStyle='gold';
    ctx.strokeRect(p1.x,p1.y,p1.w,p1.h);
    ctx.strokeStyle='purple';
    ctx.strokeRect(p2.x,p2.y,p2.w,p2.h);
    ctx.fillStyle='black';
    ctx.globalAlpha=1.00;
    ctx.fillText('start',p1.x+p1.w/2,p1.y+p1.h/2);
    ctx.fillText('end',p2.x+p2.w/2,p2.y+p2.h/2);
}


function handleMouseDown(e){
  // tell the browser we're handling this event
  e.preventDefault();
  e.stopPropagation();
  
  startX=parseInt(e.clientX-offsetX);
  startY=parseInt(e.clientY-offsetY);

  // Put your mousedown stuff here
  var mx=startX;
  var my=startY;
  if(mx>p1.x && mx<p1.x+p1.w && my>p1.y && my<p1.y+p1.h){
      isDown=true;
      dragging=p1;
  }else if(mx>p2.x && mx<p2.x+p2.w && my>p2.y && my<p2.y+p2.h){
      isDown=true;
      dragging=p2;
  }
}

function handleMouseUpOut(e){
  // tell the browser we're handling this event
  e.preventDefault();
  e.stopPropagation();
  // Put your mouseup stuff here
  isDown=false;
  dragging=null;
}

function handleMouseMove(e){
  if(!isDown){return;}
  // tell the browser we're handling this event
  e.preventDefault();
  e.stopPropagation();

  mouseX=parseInt(e.clientX-offsetX);
  mouseY=parseInt(e.clientY-offsetY);

  // Put your mousemove stuff here
  var dx=mouseX-startX;
  var dy=mouseY-startY;
  startX=mouseX;
  startY=mouseY;
  //
  dragging.x+=dx;
  dragging.y+=dy;
  //
  draw();
  //
  setPlayerVertices(p1,p2);
  var c=hasCollided(obVertices);
  if(c.dx){
      ctx.strokeStyle='gold';
      ctx.strokeRect(p1.x+c.dx,p1.y+c.dy,p1.w,p1.h);
      ctx.fillStyle='black';
      ctx.fillText('hit',p1.x+c.dx+p1.w/2,p1.y+c.dy+p1.h/2);
      line(c.s,c.i,'red');
  }
}

function setPlayerVertices(p1,p2){
    var tl1={x:p1.x,      y:p1.y};
    var tl2={x:p2.x,      y:p2.y};
    var tr1={x:p1.x+p1.w, y:p1.y};
    var tr2={x:p2.x+p2.w, y:p2.y};
    var br1={x:p1.x+p1.w, y:p1.y+p1.h};
    var br2={x:p2.x+p2.w, y:p2.y+p2.h};
    var bl1={x:p1.x,      y:p1.y+p1.h};
    var bl2={x:p2.x,      y:p2.y+p2.h};
    //
    if(p1.x<=p2.x && p1.y<=p2.y){
        s1=tr1; s2=br1; s3=bl1;
        e1=tr2; e2=br2; e3=bl2;
        o1=0; o2=1; o3=3; o4=0;
    }else if(p1.x<=p2.x && p1.y>=p2.y){
        s1=tl1; s2=tr1; s3=br1;
        e1=tl2; e2=tr2; e3=br2;
        o1=2; o2=3; o3=3; o4=0;
    }else if(p1.x>=p2.x && p1.y<=p2.y){
        s1=tl1; s2=br1; s3=bl1;
        e1=tl2; e2=br2; e3=bl2;
        o1=0; o2=1; o3=1; o4=2;
    }else if(p1.x>=p2.x && p1.y>=p2.y){
        s1=tl1; s2=tr1; s3=bl1;
        e1=tl2; e2=tr2; e3=bl2;
        o1=1; o2=2; o3=2; o4=3;
    }
}

function hasCollided(o){
    //
    var i1=line2lineIntersection(s1,e1,o[o1],o[o2]);
    var i2=line2lineIntersection(s2,e2,o[o1],o[o2]);
    var i3=line2lineIntersection(s3,e3,o[o1],o[o2]);
    var i4=line2lineIntersection(s1,e1,o[o3],o[o4]);
    var i5=line2lineIntersection(s2,e2,o[o3],o[o4]);
    var i6=line2lineIntersection(s3,e3,o[o3],o[o4]);
    //
    var tracks=[];
    if(i1){tracks.push(track(s1,e1,i1));}
    if(i2){tracks.push(track(s2,e2,i2));}
    if(i3){tracks.push(track(s3,e3,i3));}
    if(i4){tracks.push(track(s1,e1,i4));}
    if(i5){tracks.push(track(s2,e2,i5));}
    if(i6){tracks.push(track(s3,e3,i6));}
    //
    var nohitDist=10000000;
    var minDistSq=nohitDist;
    var halt={dx:null,dy:null,};
    for(var i=0;i<tracks.length;i++){
        var t=tracks[i];
        var testdist=t.dx*t.dx+t.dy*t.dy;
        if(testdist<minDistSq){
            minDistSq=testdist;
            halt.dx=t.dx;
            halt.dy=t.dy;
            halt.s=t.s;
            halt.i=t.i;
        }
    }
    return(halt);
}
//
function track(s,e,i){
    dot(s);dot(i);line(s,i);line(i,e);
    return({ dx:i.x-s.x, dy:i.y-s.y, s:s, i:i });
}


function line2lineIntersection(p0,p1,p2,p3) {
    var unknownA = (p3.x-p2.x) * (p0.y-p2.y) - (p3.y-p2.y) * (p0.x-p2.x);
    var unknownB = (p1.x-p0.x) * (p0.y-p2.y) - (p1.y-p0.y) * (p0.x-p2.x);
    var denominator  = (p3.y-p2.y) * (p1.x-p0.x) - (p3.x-p2.x) * (p1.y-p0.y);        
    // Test if Coincident
    // If the denominator and numerator for the ua and ub are 0
    //    then the two lines are coincident.    
    if(unknownA==0 && unknownB==0 && denominator==0){return(null);}
    // Test if Parallel 
    // If the denominator for the equations for ua and ub is 0
    //     then the two lines are parallel. 
    if (denominator == 0) return null;
    // If the intersection of line segments is required 
    // then it is only necessary to test if ua and ub lie between 0 and 1.
    // Whichever one lies within that range then the corresponding
    // line segment contains the intersection point. 
    // If both lie within the range of 0 to 1 then 
    // the intersection point is within both line segments. 
    unknownA /= denominator;
    unknownB /= denominator;
    var isIntersecting=(unknownA>=0 && unknownA<=1 && unknownB>=0 && unknownB<=1)
    if(!isIntersecting){return(null);}
    return({
        x: p0.x + unknownA * (p1.x-p0.x),
        y: p0.y + unknownA * (p1.y-p0.y)
    });
}

function dot(pt){
    ctx.beginPath();
    ctx.arc(pt.x,pt.y,3,0,Math.PI*2);
    ctx.closePath();
    ctx.fill();
}

function line(p0,p1,stroke,lw){
    ctx.beginPath();
    ctx.moveTo(p0.x,p0.y);
    ctx.lineTo(p1.x,p1.y);
    ctx.lineWidth=lw || 1;
    ctx.strokeStyle=stroke || 'gray';
    ctx.stroke();
}
body{ background-color: ivory; }
#canvas{border:1px solid red; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Drag start & end player position rects<br>The shortest segment intersecting the obstacle is red.<br>The repositioned player is shown on the obstacle.</h4>
<canvas id="canvas" width=400 height=400></canvas>


1
@markE 很棒的回答。我只想对你从线截距和轨迹返回值的方式提出一点建议。与其为每个调用创建一个新对象(内存分配和GC开销),不如将返回结构作为可选的最后一个参数添加,例如 function lineIntcept(p1,p2,p3,p4,point){ if(point === undefined){ point = {x:null,y:null};} 然后 point.x = //answer x; point.y = //answer.y; return point; 如果提供了点,这将节省大量每次调用的CPU时间。这只是一个微不足道的观点,因为答案很好。 - Blindman67
谢谢 :-) 是的,通过引用参数传递值更有效率 -- 特别是因为碰撞测试很可能经常被调用并且有多个障碍物。 - markE

1
你正在经历的通常被称为隧道效应。
有很多不同的解决方法,但最简单的方法通常是保存上一个位置并执行以下操作。

A

为每个元素计算一个新的更大的碰撞框,其中包含元素的上一个位置和它的新位置。可以将其想象为一个包含元素两次的框。一次代表它的上一个位置(LP),另一次代表它的新位置(NP),

 ------------
|| LP |      |
||____|      |
|       ____ |
|      | NP ||
|______|____||

现在,如果您使用这个新盒子来检查碰撞,它将考虑到已行进的路径以避免隧道效应。这可能会在右上角和左下角创建意外的碰撞,但这是一个简单的实现,权衡可能值得。

B

检查从上一个位置到新位置所走过的路径中的每一步是否发生了碰撞。如果您的元素自上一帧以来移动了5个像素,则需要为每个像素(或最小可接受的碰撞距离)检查一次碰撞。

 ____
| LP |      
|____||      
  ---- |___ 
   |___|NP |
      |____|

这当然会增加碰撞检测的数量并对性能产生影响。在这里,您可以研究四叉树来弥补性能损失。
前进时,有许多更优雅和先进的解决方案,但是这个主题太广泛了,无法在这里给出完整的答案。
希望这有所帮助!

谢谢,我正在尝试它,我几乎已经成功让它工作了。 - Akshay

1

嗯,几个月前我制作了一个“碰撞计算器”,所以您可以自由更改和使用下面的代码 :) 为了更好地解释:

  • p_x是上一个玩家位置x加上他的宽度
  • p_y是上一个玩家位置y加上他的高度
  • p_x_m是上一个玩家位置x
  • p_y_m是上一个玩家位置y
  • y_m是新玩家位置(他的y-某个值)
  • x_m是新玩家位置(他的x-某个值)
  • y_p是新玩家位置(他的y+某个值+他的高度)
  • y_p_m是新玩家位置(他的y+某个值)
  • x_p是新玩家位置(他的x+某个值+他的宽度)
  • x_p_m是新玩家位置(他的x+某个值)
  • w_x是墙壁位置x
  • w_y是墙壁位置y
  • w_w是墙壁宽度
  • w_h是墙壁高度

  • pressedKeys是一个字符串,告诉我们玩家按下了哪些键(例如 "was" 或 "wd" 或 "ad" 等)

  • this.walls是一个带有墙壁的变量(例如如果我有4堵墙,那么数组看起来像这样[false,'s',false,false],因为我用"s"键触碰了第二堵墙)。

代码:

if(
    pressedKeys.indexOf("s")>-1 &&
    (
        (                                                                                   //      P
            p_y>w_y&&p_y<(w_y+w_h)&&x_p_m>w_x && x_p-5>w_x && x_m<w_x                       //      +----
        ) ||                                                                                //      |

        (                                                                                   //      P
            y_p>w_y&&p_y<(w_y+w_h) && x_p-5>w_x && x_p<=(w_x+w_w)                           //  +--------+
        ) ||                                                                                //  |        |

        (                                                                                   //      P
            y_p>w_y&&p_y<(w_y+w_h) && x_p>(w_x+w_w)&&p_x_m<(w_x+w_w) && x_m+5<(w_x+w_w)     //  ----+
        )                                                                                   //      |
    )
)
{
    if(this.walls[i] == false)
        this.walls[i] = "";
    this.walls[i] += "s";
}
if(
    pressedKeys.indexOf("d")>-1 &&
    (
        (                                                                                   //      P+----
            p_x>w_x&&p_x<(w_x+w_w)&&y_p_m>w_y && y_p-5>w_y && y_m<w_y                       //       |
        ) ||                                                                                //       |

        (                                                                                   //       |
            x_p>w_x&&p_x<(w_x+w_w) && y_p-5>w_y && y_p<=(w_y+w_h)                           //      P|
        ) ||                                                                                //       |

        (                                                                                   //       |
            x_p>w_x&&p_x<(w_x+w_w) && y_p>(w_y+w_h)&&p_y_m<(w_y+w_h) && y_m+5<(w_y+w_h)     //       |
        )                                                                                   //      P+----
    )
)
{
    if(this.walls[i] == false)
        this.walls[i] = "";
    this.walls[i] += "d";
}
if(
    pressedKeys.indexOf("w")>-1 &&
    (
        (                                                                                   //      |
            y_m<(w_y+w_h)&&y_p-5>w_y && x_p-5>w_x && x_m<w_x &&x_p_m>w_x                    //      +----
        ) ||                                                                                //      P

        (                                                                                   //  |        |
            y_m<(w_y+w_h)&&y_p-5>w_y && x_p-5>w_x && x_p<=(w_x+w_w)                         //  +--------+
        ) ||                                                                                //      P    

        (                                                                                   //      |
            y_m<(w_y+w_h)&&y_p-5>w_y && x_p>(w_x+w_w)&&p_x_m<(w_x+w_w) && x_m+5<(w_x+w_w)   //  ----+
        )                                                                                   //      P
    )
)
{
    if(this.walls[i] == false)
        this.walls[i] = "";
    this.walls[i] += "w";
}
if(
    pressedKeys.indexOf("a")>-1 &&
    (
        (                                                                                   //  ----+P
            x_m<(w_x+w_w)&&x_p-5>w_x && y_p-5>w_y && y_m<w_y &&y_p_m>w_y                    //      |
        ) ||                                                                                //      |

        (                                                                                   //      |
            x_m<(w_x+w_w)&&x_p-5>w_x && y_p-5>w_y && y_p<=(w_y+w_h)                         //      |P
        ) ||                                                                                //      |    

        (                                                                                   //      |
            x_m<(w_x+w_w)&&x_p-5>w_x && y_p>(w_y+w_h)&&p_y_m<(w_y+w_h) && y_m+5<(w_y+w_h)   //      |P
        )                                                                                   //  ----+P
    )
)
{
    if(this.walls[i] == false)
        this.walls[i] = "";
    this.walls[i] += "a";
}

代码右侧的注释展示了玩家碰撞的方式。

这段代码百分之百有效,每次我想检查碰撞时都会使用它。

希望它能有所帮助 :)


嗨,感谢您的帮助,您的代码很好(有点复杂),我会尝试它。 - Akshay

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