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

时间:2016-03-15 13:22:29

标签: javascript jquery canvas

所以基本上我要这么做,当屏幕上的两个角色碰到而有人按下按钮时,它会带走他们的健康。我唯一不知道该怎么做就是如何检测他们何时触摸。

$(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;

  }
};

3 个答案:

答案 0 :(得分:3)

径向周长测试

通过用一组极性协调定义每个精灵的形状来实现快速几乎像素完美的碰撞。每个坐标描述距中心的距离(中心是任意的,但必须在精灵内),并且沿着该方向从中心到最远像素的中心的方向。坐标数(n)由最外侧像素的周长确定。我不知道之前是否已经描述过,因为我现在只是想到了它,所以它的实际稳健性需要测试。

概念

下图显示了基本概念。

Polar collision

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

要获取您需要访问像素信息的坐标,可以在制作期间完成此操作并添加为代码或在游戏设置期间。它将导致类似

的数据结构
var sprite = {
    ...
    collisionData : {
        maxRadius : 128,
        minRadius : 20,
        coords : [128,30,50, ... ],
    }
}

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

摘录2。

碰撞测试

对于pApB引用sprite positionData(代码段2)和cA以及cB引用每个sprite的collisionData(代码段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
     }

摘录3。

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

    // 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;         

将归一化相对方向转换为极坐标阵列中的正确索引的下一步需要考虑每个极坐标的角宽度。参见图中的图3.每个极坐标顶部的平坦位是角宽度。为此,我们在从0-1放大到坐标数时使用Math.round。当接近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;
    }
}

警告

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

如果精灵非常粗糙,可能会发生碰撞无法检测到碰撞的情况,这同样适用于过度采样坐标的数量。该图显示了精灵的16个坐标,其接近128乘128,这似乎是一个很好的数字。更大的精灵需要更少的更小,但不要低于8。

<强>改进

在图中,图4显示了一个未命中,但是如果精灵的B坐标15(从它们之间的方向顺时针方向)有点长,则会有未检测到的命中。为了改进算法,你可以测试两侧的索引坐标与距离,但你需要减少极坐标距离,以说明它们没有指向另一个精灵的中心。通过将极距乘以偏移角的cos来做到这一点。

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

这为算法增加了一点处理,但不是那么多,只应该是A的角度大小的一半,而不是B.

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

更多

有许多方法可以进行测试,您使用的方法取决于需求。如果您只有几个精灵,那么可以使用更复杂的算法来提供更好的结果,如果您有许多精灵然后使用更简单的测试。

不要过度烹饪。

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

答案 1 :(得分:0)

你可以分两步完成。

  1. 检查重叠的边界圆圈。

  2. 如果圆圈重叠,请检查重叠的(更精确的)边界矩形。

  3. 如果您有两张图片,那么每张图片的中心等于(x + w/2), (y + h/2)。并且每个半径等于sqrt((w/2)^2 + (h/2)^2)

    当两个圆圈之间的距离(sqrt((x1-x2)^2 + (y1-y2)^2))小于max(radius1, radius2)时,两个圆圈重叠。

    边界矩形碰撞检测是直观的,但计算上可能更难(可能会影响大量对象)。

答案 2 :(得分:0)

我知道这是一个很老的问题,但是我遇到了同样的问题,并且找到了一个更简单的解决方案。

首先,我们需要编写一个长的if语句来检测两个图像是否实际接触。对于半透明图像,即使非透明部分实际上没有接触也是如此。

 if (/*horizontal*/ (sprite.x + sprite.width >= sprite2.x && sprite.x <= sprite2.x + sprite2.width && sprite2.hidden == false) && /*vertical*/ (sprite.y + sprite.height >= sprite2.y && sprite.y <= sprite2.y + sprite2.height && sprite.hidden == false)) {
}

由于图像是半透明的,因此仅此一项是行不通的。我的下一步是创建两个新的透明画布,其宽度和高度等于原始画布。

// draws sprite onto canvas
var spriteCanvas = document.createElement("CANVAS");
var spriteCtx = spriteCanvas.getContext("2d");
spriteCanvas.width = canvas.width;
spriteCanvas.height = canvas.height;

var spriteImage = new Image();
spriteImage.src = sprite.base64Src;
spriteCtx.drawImage(spriteImage, sprite.x, sprite.y, sprite.width, sprite.height);

// draws sprite2 onto canvas
var sprite2Canvas = document.createElement("CANVAS");
var sprite2Ctx = sprite2Canvas.getContext("2d");
sprite2Canvas.width = canvas.width;
sprite2Canvas.height = canvas.height;

var sprite2Image = new Image();
sprite2Image.src = sprite2.base64Src;
sprite2Ctx.drawImage(sprite2Image, sprite2.x, sprite2.y, sprite2.width, sprite2.height);

现在,我们有两个孤立的图像。我们可以遍历画布上的每个像素,检查该像素是否不透明,然后检查另一个画布上的同一像素是否不透明,但这效率非常低。相反,我们找到了两个图像的重叠。 我们可以使用context.getImageData()获得每个像素的RGBA(红色,绿色,蓝色,透明)颜色。我们可以检查每个A(透明度)值是否大于0,以确定像素是否透明。如果您觉得这个值太低,请随时增加它。

// gets the overlap of the two
var spriteOverlap;
var sprite2Overlap;
        
var cropX = (sprite.x > sprite2.x) ? [sprite.x, (sprite2.x + sprite2.width) - sprite.x + 1] : [sprite2.x, (sprite.x + sprite.width) - sprite2.x + 1];
        var cropY = (sprite.y + sprite.height > sprite2.y + sprite2.height) ? [sprite.y, (sprite2.y + sprite2.height) - sprite.y + 1] : [sprite2.y, (sprite.y + sprite.height) - sprite2.y + 1];
        
spriteOverlap = spriteCtx.getImageData(cropX[0], cropY[0], cropX[1], cropY[1]).data;
sprite2Overlap = sprite2Ctx.getImageData(cropX[0], cropY[0], cropX[1], cropY[1]).data;
            

现在我们只有重叠的部分,我们可以检查第一张图像上的每个像素是否不透明,然后再检查另一张图像上的相同像素。

pixelOverlap = false;

// loops through every overlaping pixel in sprite2
for(var i = 0; i < (sprite2Overlap.length / 4); i++){
    // checks if the current pixel has an opacity greater than one on both sprite or sprite2
    if(sprite2Overlap[i * 3] > 0 && spriteOverlap[i * 3] > 0){
        pixelOverlap = true;
    }
}

// if a pixel overlap was already found, sprite2 makes the function run faster
if(!pixelOverlap){
    // loops through every overlaping pixel in sprite
    for(var i = 0; i < (spriteOverlap.length / 4); i++){
        // checks if the current pixel has an opacity greater than one on both sprite or sprite2
        if(sprite2Overlap[i * 3] > 0 && spriteOverlap[i * 3] > 0){
            pixelOverlap = true;
        }
    }
}

最后,如果这是一个函数,请不要忘记返回pixelOverlap

return pixelOverlap;