如何对两个部分透明图像进行像素完美的碰撞检测

Ear*_*ith 2 javascript jquery canvas collision-detection

基本上我想做的是,当屏幕上的两个角色接触并且有人按下按钮时,就会夺走他们的生命值。我唯一不知道的是如何检测它们何时接触。

$(document).ready(function(){


var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
canvas.width = 1000;
canvas.height = 600;
document.body.appendChild(canvas);

var kGroundHeight = 500;

/*var upKey = 38;
var downKey = 40;
var leftKey = 37;
var rightKey = 39;
*/


var render = function() {
  gravity();
  gravity1();
  
 
  
  context.clearRect(0, 0, canvas.width, canvas.height);
  
    context.fillRect(0,kGroundHeight,canvas.width,10);

  context.drawImage(kirby, kirbyObject.x, kirbyObject.y);
  
    context.drawImage(link, linkObject.x, linkObject.y);

};




var main = function() {

  render();
  window.requestAnimationFrame(main);
};

main();
});




var linkReady = false;
var link = new Image();
link.onLoad = function() {
  
  linkReady = true;
};


linkObject = {};
link.src= "https://vignette1.wikia.nocookie.net/zelda/images/1/18/Link_(Sprite)_The_Legend_of_Zelda.png/revision/latest?cb=20130117162823";

linkObject.x = 200;
linkObject.y = 200;






var keys = {};




$(document).keydown(function(e) {
    console.log(e);
 
   move1(e.keyCode);
  
    if (keys[87] && keys[65]) {
       linkObject.y -=50;
       linkObject.x -=50;
      
        
    }else if(keys[87] && keys[68]){
      
      linkObject.y -=50;
      linkObject.x +=50;
      
      
    }else if(keys[83] && keys[68]){
      
      linkObject.y +=50;
      linkObject.x +=50;
      
      
    }else if(keys[83] && keys[65]){
      
      linkObject.y +=50;
      linkObject.x -=50;
    }
      
    keys[e.keyCode] = true;
}).keyup(function(e) {
  
   keys[e.keyCode] = false;
      
    
   
});

var upKey1 = 87;
var downKey1 = 83;
var leftKey1 = 65;
var rightKey1 = 68;
var attackKey1 = 75; 


var move1 = function(key) {
  
  if (key == upKey1) {
    linkObject.y -= 50;
  }else if(key == downKey1){
    
      var kGroundHeight = 500;

     if(linkObject.y + link.height ==  kGroundHeight){
      
      linkObject.y +=0;
    }else{
      
      linkObject.y +=50;
      
    }
    
   
    //console.log("down");
        console.log(linkObject.y);

    
  }else if(key == leftKey1){
    
    linkObject.x -=50;
  }else if(key == rightKey1 ){
    
    linkObject.x +=50;
  }
  // MORE DIRECTIONS!!!
};




var gravity1 = function() {
    var kGravityScale = 1;
  var kGroundHeight = 500;
  
  if (linkObject.y + link.height ==  kGroundHeight) {
    
    linkObject.y += 0;
    
  }else{
        linkObject.y += kGravityScale;
//console.log(link.width);
  }
};


var attack = function(a){
  
  
  
};



 var kGroundHeight = 500;


var keys = {};




$(document).keydown(function(e) {
    console.log(e);
 
   move(e.keyCode);
  
    if (keys[38] && keys[37]) {
       kirbyObject.y -=50;
       kirbyObject.x -=50;
      
        
    }else if(keys[38] && keys[39]){
      
      kirbyObject.y -=50;
      kirbyObject.x +=50;
      
      
    }else if(keys[40] && keys[39]){
      
      kirbyObject.y +=50;
      kirbyObject.x +=50;
      
      
    }else if(keys[40] && keys[37]){
      
      kirbyObject.y +=50;
      kirbyObject.x -=50;
    }
      
    keys[e.keyCode] = true;
}).keyup(function(e) {
  
   keys[e.keyCode] = false;
      
    
   
});


var kirbyReady = false;
var kirby = new Image();
kirby.onLoad = function() {
  kirbyReady = true;
};

kirbyObject = {};
kirby.src = "https://vignette3.wikia.nocookie.net/spritechronicles/images/5/5c/Kirby.png/revision/latest?cb=20101010225540";




kirbyObject.x = 300;
kirbyObject.y = 100;



var upKey = 38;
var downKey = 40;
var leftKey = 37;
var rightKey = 39;
var attackKey = 32; 



var move = function(key) {
  
  if (key == upKey) {
    kirbyObject.y -= 50;
  }else if(key == downKey){
    
      var kGroundHeight = 500;

     if(kirbyObject.y + kirby.height ==  kGroundHeight){
      
      kirbyObject.y +=0;
    }else{
      
      kirbyObject.y +=50;
      
    }
    
   
    //console.log("down");
        console.log(kirbyObject.y);

    
  }else if(key == leftKey){
    
    kirbyObject.x -=50;
  }else if(key == rightKey ){
    
    kirbyObject.x +=50;
  }
  // MORE DIRECTIONS!!!
};




var gravity = function() {
    var kGravityScale = 1;
  var kGroundHeight = 500;
  
  if (kirbyObject.y + kirby.height ==  kGroundHeight) {
    
    kirbyObject.y += 0;
    
  }else{
        kirbyObject.y += kGravityScale;

  }
};
Run Code Online (Sandbox Code Playgroud)

Bli*_*n67 5

径向周长测试

通过使用一组极坐标定义每个精灵的形状,可以实现几乎像素完美的快速碰撞。每个坐标描述了距中心的距离(中心是任意的,但必须位于精灵内部)以及沿该方向距中心最远像素的中心的方向。坐标数(n)由最外层像素的周长决定。我不知道这是否已经被描述过,因为我现在才想到它,所以它的实际稳健性需要测试。

这个概念

下图显示了基本概念。

极地碰撞

精灵 (1.) 覆盖极坐标和外部边界圆 (2.) 将每个坐标索引为 0-15 (3.) 提取检查碰撞所需的信息。绿色 (a) 是每个精灵的角度原点,黄线 P 是 A 和 B 之间的向量,标记为距角度原点 (4.) 的角度

要获取坐标,您需要访问像素信息,这可以在生产过程中完成并作为代码添加,或者在游戏设置过程中完成。它将产生类似的数据结构

var sprite = {
    ...
    collisionData : {
        maxRadius : 128,
        minRadius : 20,
        coords : [128,30,50, ... ],
    }
}
Run Code Online (Sandbox Code Playgroud)

片段 1.

现在您已将每个精灵描述为进行测试所需的一组极坐标。

假设精灵有一个位置(x,y,以像素为单位),一个旋转(r,以弧度为单位)和一个比例(s 具有方形纵横比)。

positionData = {  // position data structure
   x : 100,           // x pos
   y : 100,           // y pos
   r : Math.PI * 1.2, // rotation
   s : 1,             // scale
}
Run Code Online (Sandbox Code Playgroud)

片段 2.

碰撞测试

对于两个精灵,其中pA、 和pB引用精灵位置数据(片段 2) 和cAcB引用每个精灵的碰撞数据(片段 1),我们首先进行距离测试,检查最大半径和最小半径。如果发生碰撞,代码将返回 true。

const TAU = Math.PI * 2; // use this alot so make it a constant
var xd = pA.x - pB.x;          // get x distance
var yd = pA.y - pB.y;          // get y distance
var dist = Math.hypot(xd,yd);  // get the distance between sprites
                               // Please note that legacy browsers will not 
                               // support hypot 
// now scale the max radius of each sprite and test if the distance is less 
// than the sum of both.
if (dist <= cA.maxRadius * pA.s + cB.maxRadius * pB.s){
     // passed first test sprites may be touching
     // now check the min radius scaled
     if (dist <= Math.min(cA.minRadius * pA.s, cB.minRadius * pB.s) * 2 ){
          // the sprites are closer than the smallest of the two's min
          // radius scaled so must be touching
          return true;  // all done return true
     }
Run Code Online (Sandbox Code Playgroud)

片段 3.

现在您需要进行极地测试。您需要获取从每个精灵到另一个精灵的方向,然后调整该方向以匹配精灵旋转,然后将方向标准化为碰撞数据中存储的极坐标数。

    // on from snippet 3.
    var dir = Math.atan2(yd, xd); // get the direction from A to B in radians
                                  // please note that y comes first in atan2

    // now subtract the rotation of each sprite from the directions
    var dirA = dir - pA.r;
    var dirB = dir + Math.PI - pB.r; // B's direction is opposite

    // now normalise the directions so they are in the range 0 - 1;
    dirA = (((dirA % TAU) + TAU) % TAU) / TAU;                  
    dirB = (((dirB % TAU) + TAU) % TAU) / TAU;         
Run Code Online (Sandbox Code Playgroud)

下一步将归一化相对方向转换为极坐标阵列中的正确索引需要考虑每个极坐标的角宽度。见图3。图中每个极坐标顶部的平位是角宽度。为此,我们Math.round在从 0-1 放大到坐标数时使用。由于接近 1 的值将四舍五入到错误的索引,因此您还必须使用模%来确保它不会超出范围。

    var indexA = Math.round(dirA * cA.coords.length) % cA.coords.length;
    var indexB = Math.round(dirB * cB.coords.length) % cB.coords.length;

    // now we can get the length of the coordinates.
    // also scale them at the same time
    var la = cA.coords[indexA] * pA.s;
    var lb = cB.coords[indexB] * pB.s;

    // now test if the distance between the sprites is less than the sum
    // of the two length
    if( dist <= la + lb ){
        // yes the two are touching
        return true;
    }
}
Run Code Online (Sandbox Code Playgroud)

注意事项

就是这样。与其他方法相比,它相对较快,但它不是像素完美的,因为它只考虑精灵的周长,如果精灵的周长不是凸的,则可能会出现算法返回不正确的命中的情况。

如果精灵非常粗糙,则可能存在碰撞无法检测到碰撞的情况,这同样适用于对坐标数量进行过采样。该图显示了精灵的 16 个坐标,接近 128 x 128,这似乎是一个不错的数字。较大的精灵需要更多、较小的精灵,但不要低于 8。

改进

在图 4 中,显示了一次未击中,但如果精灵的 B 坐标 15(从它们之间的方向顺时针旋转一个)稍长一点,就会出现未检测到的击中。为了改进算法,您可以根据距离测试任一侧的索引坐标,但您需要减小极坐标距离,以考虑到它们没有指向另一个精灵的中心这一事实。通过将极距乘以偏移角的余弦来实现此目的。

 // get the angle step for A and B
 var angleStepA = TAU / cA.coord.length;
 var angleStepB = TAU / cB.coord.length;

 // the number of coordinates to offset 
 var offCount = 1;

 // get next coord clockwise from A and scale it
 var lengOffA = cA.coord[(index + offCount) % cA.coord.length] * pA.s;

 // get next coordinate counter clockwise from B and scale it
 var lengOffB = cB.coord[(index + cB.coord.length - offCount) % cB.coord.length] * pB.s;

 // Now correct for the offest angle
 lengOffA *= Math.cos(offCount * angleStepA);
 lengOffB *= Math.cos(offCount * angleStepB);

 // Note that as you move away the length will end up being negative because
 // the coord will point away.

 if( dist < lengOffA + lengOffB ){
     // yes a hit
     return true;
 }
Run Code Online (Sandbox Code Playgroud)

这给算法增加了一点处理量,但并没有那么多,应该只相当于 A 与 B 的角度大小的一半。

您可能还希望使极坐标之间的区域成为线性斜率,并为第一个测试插入极距离。

更多的

进行测试的方法有很多,您使用的方法取决于需要。如果您只有几个精灵,则可以使用更复杂的算法来给出更好的结果,如果您有很多精灵,则可以使用更简单的测试。

不要煮过头了。

请记住,像素完美测试是程序员想要的,但玩家几乎无法辨别像素(现代显示器的像素小于人眼的径向分辨率)。如果两个几乎不可见的像素接触了 1/60 秒谁会认为这是一个命中???