使用Canvas实现完美的2D鼠标拾取

4
我正在使用Canvas编写一个2D游戏,需要检测鼠标点击和悬停事件。这里有三个问题:检测必须精准到像素级别、物体不是矩形(例如房屋、奇形怪状的UI按钮...)且需要快速响应。(显然采用暴力算法不是一个好选择)
因此,我想问的是如何找出鼠标所在的物体,并进行可能的优化。
P.S:我进行了一些调查,发现有个人使用QuadTree 这里
4个回答

5
我有一份(过时的)教程,解释了幽灵画布概念,该概念对于像素完美的点击检测很好。教程在这里。请忽略关于更新教程的警告,新教程不使用幽灵画布概念。
想法是将相关图像绘制到内存画布中,然后使用 getImageData 来获取鼠标单击的单个像素。然后看看该单个像素是否完全透明。
如果不是完全透明的,则找到了目标。
如果完全透明,则将下一个对象绘制到内存画布上并重复此过程。
您只需在最后清除内存画布即可。 getImageData 很慢,但如果要实现像素完美的点击检测并且没有预计算任何东西,它是您唯一的选择。
或者,您可以预先计算路径或者带有偏移量的像素数组。这将需要大量工作,但可能会更快。例如,如果您有一个40x20的图像带有一些透明度,则会计算一个数组[40][20],其中true或false对应于透明或不透明。然后,将其与鼠标位置进行测试,并添加一些偏移量,如果图像在(25,55)处绘制,则需要将其从鼠标位置中减去,然后在查看数组[posx][posy]时测试新位置是否为true。
这是我的答案。我的建议?如果这是一个游戏,请忘记像素完美的检测。
说真的。
相反,制作路径(不在画布中,在普通JavaScript代码中),表示对象但不是像素完美的,例如,房子可能是一个带有三角形的正方形构成的图像,用于击中测试。如果要在多边形内检测点,它比进行像素完美的检测要极快得多。查找点的多边形缠绕数规则检测。老实说,这是您最好的选择。

你能详细介绍一下预计算边界多边形吗?可以做一个教程吗? :) - Ethan
我可以做一个教程,但需要1-2天的时间。 - Simon Sarris
还有,你能给我指一些关于使用JavaScript检查凸多边形的好资源吗? - Ethan
希望周五到周六我能完成所有的代码。不过由于假期的事情可能会太忙了。 - Simon Sarris
你的教程至今仍是一颗宝石,@SimonSarris。我希望它能够更新,但我仍从中学到了很多东西。 - Giampaolo Ferradini
1
谢谢@GiampaoloFerradini,我会尽快制作更新版本。 - Simon Sarris

3

传统游戏开发中的常见解决方案是构建一个点击掩码。您可以在单独的屏幕外画布上重新渲染所有内容,以纯色显示(渲染应该非常快)。当您想要弄清楚点击了什么时,只需在屏幕外画布上的x/y坐标处取样颜色即可。最终您会构建一个颜色-对象哈希表,类似于:

var map = {
      '#000000' : obj1
    , '#000001' : obj2
    , ...
};

您也可以将次要画布的渲染方式优化为仅在用户单击某些内容时才进行。并且使用各种技术,您可以进一步优化它以仅绘制用户点击的画布部分(例如,您可以将画布分割成一个NxN网格,例如20x20像素的正方形网格,并标记该网格中的所有对象-然后您只需要重新绘制少量对象)


如何将带有透明区域的图像渲染为纯色对象? - Ethan
1
使用纯色填充新画布,然后使用“destination-in”绘制图像,您将得到交集,然后可以将其绘制到新画布上。 - user578895

1

HTML5 Canvas只是一个绘图平面,在调用每个绘图API函数之前,您可以设置不同的变换。无法创建对象,也没有显示列表。因此,您必须自己构建这些功能,或者可以使用可用的不同库。

http://www.kineticjs.com/

http://easeljs.com/

几个月前,我对此产生了兴趣,甚至为此编写了一个库。你可以在这里看到它:http://exsprite.com。最终面临了许多性能问题,但因为时间不够,我无法进行优化。这真的很有趣,所以我在等待一些时间来完善它。

0

我相信注释应该足够了。这是我在我的2D等距滚动游戏中确定用户意图的方法,目前位于http://untitled.servegame.com

var lastUp = 0;
function mouseUp(){
    mousedown = false; //one of my program globals.
    var timeNow = new Date().getTime();
    if(mouseX == xmouse && mouseY == ymouse && timeNow > lastUp + 100){//if it was a centralized click. (mouseX = click down point, xmouse = mouse's most recent x) and is at least 1/10th of a second after the previous click.
        lastUp = new Date().getTime();
        var elem = document.elementFromPoint(mouseX, mouseY); //get the element under the mouse.
        var url = extractUrl($(elem).css('background-image')); // function I found here: http://webdevel.blogspot.com/2009/07/jquery-quick-tip-extract-css-background.html

        imgW = $("#hiddenCanvas").width(); //EVERY art file is 88px wide. thus my canvas element is set to 88px wide. 
        imgH = $(elem).css('height').split('p')[0]; //But they vary in height. (currently up to 200);

        hiddenCanvas.clearRect(0, 0, imgW, imgH); //so only clear what is necessary.

        var img = new Image();
        img.src = url;                          
        img.onload = function(){
            //draw this elements image to the canvas at 0,0
            hiddenCanvas.drawImage(img,0,0);

            ///This computes where the mouse is clicking the element.
            var left = $(elem).css('left').split('p')[0]; //get this element's css absolute left.
            var top = $(elem).css('top').split('p')[0];
            offX = left - offsetLeft; //left minus the game rendering element's absolute left. gives us the element's position relative of document 0,0
            offY = top - offsetTop;
            offX = mouseX - offX; //apply the difference of the click point's x and y
            offY = mouseY - offY;


            var imgPixel = hiddenCanvas.getImageData(offX, offY, 1, 1); //Grab that pixel. Start at it's relative X and it's relative Y and only grab one pixel.
            var opacity = imgPixel.data[3]; //get the opacity value of this pixel.

            if(opacity == 0){//if that pixel is fully transparent
                $(elem).hide();
                var temp = document.elementFromPoint(mouseX, mouseY); //set the element right under this one
                $(elem).show();
                elem = temp;
            }

            //draw a circle on our hiddenCanvas so when it's not hidden we can see it working!
            hiddenCanvas.beginPath();
            hiddenCanvas.arc(offX, offY, 10, 0, Math.PI*2, true); 
            hiddenCanvas.closePath();
            hiddenCanvas.fill();


            $(elem).css("top", "+=1"); //apply something to the final element.

        }
    }
}

关于此:

<canvas id="hiddenCanvas" width="88" height="200"></canvas>

将CSS的定位设置为absolute,并将x = -(width)以隐藏;


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