使用 Canvas 进行像素完美的 2D 鼠标拾取

发布于 2024-12-22 15:19:46 字数 320 浏览 2 评论 0原文

我正在使用 Canvas 在 html5 中编写 2D 游戏,这需要检测鼠标单击和悬停事件。这有 3 个问题:检测必须是​​像素完美的、对象不是矩形的(房屋、奇怪形状的 UI 按钮......),并且需要快速且响应灵敏。 (显然暴力破解是不行的)

所以我想问的是如何找出鼠标位于哪个对象上,以及可能的优化是什么。

PS:我做了一些调查,发现一个人在这里使用了QuadTree。

I'm writing a 2D game in html5 using Canvas which requires mouse click and hover events to be detected. There are 3 problems with this: detections must be pixel-perfect, objects are not rectangular (houses, weird-shaped UI buttons...), and it is required to be fast and responsive. (Obviously brute force is not an option)

So what I want to ask is how do I find out which object the mouse is on, and what are the possible optimizations.

P.S: I did some investigation and found a guy who used QuadTree here.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

疧_╮線 2024-12-29 15:19:46

我有一个(已过时的)教程,解释了幽灵画布的概念,它非常适合像素完美的命中检测。该教程位于此处。忽略有关 较新教程,较新的教程没有使用幽灵画布概念。

这个想法是将相关图像绘制到内存画布上,然后使用 getImageData 获取鼠标单击的单个像素。然后您会看到该单个像素是否完全透明。

如果它不完全透明,那么你已经达到了目标。

如果它是完全透明的,则将下一个对象绘制到内存画布上并重复。

您只需要在最后清除内存中的画布即可。

getImageData 速度很慢,但如果您想要像素完美的命中检测并且不预先计算任何内容,那么它是您唯一的选择。

或者,您可以预先计算路径或具有偏移量的像素数组。这将需要大量工作,但可能会更快。例如,如果您有一个具有一定透明度的 40x20 图像,您将计算一个数组 [40][20],该数组的 true 或 false 对应于透明或不透明。然后,您将针对鼠标位置进行测试,并进行一些偏移,如果图像是在 (25, 55) 处绘制的,您需要从鼠标位置中减去该位置,然后测试当您查看时新位置是否为真数组[posx][posy]。

这就是我对你问题的回答。 我的建议?如果这是一款游戏,请忘记像素完美检测。

说真的。

相反,创建代表对象但不是像素完美的路径(不是在画布中,而是在纯 JavaScript 代码中),例如房子可能是一个顶部有一个三角形的正方形,它非常接近图像,但用于当谈到命中测试时,它是相反的。计算一个点是否位于路径内部比进行像素完美检测要相对极其快。多边形绕数规则检测中查找点。老实说,这是你最好的选择。

I have a (dated) tutorial that explains the concept of a ghost canvas which is decent for pixel-perfect hit detection. The tutorial is here. Ignore the warning about a newer tutorial, the newer one does not use the ghost canvas concept.

The idea is to draw the image in question to an in-memory canvas and then use getImageData to get the single pixel of the mouse click. Then you see if that single pixel is fully transparent or not.

If its not fully transparent, well, you've got your target.

If it is fully transparent, draw the next object to the in-memory canvas and repeat.

You only have to clear the in-memory canvas at the end.

getImageData is slow but it is your only option if you want pixel-perfect hit detection and aren't pre-computing anything.

Alternatively you could precompute a path or else an array of pixels with an offset. This would be a lot of work but might be faster. For instance if you have a 40x20 image with some transparency you'd compute an array[40][20] that would have true or false corresponding to transparent or not. Then you'd test that against the mouse position, with some offset, if the image is drawn at (25, 55) you'd want to subtract that from the mouse position and then test if the new position is true when you look at array[posx][posy].

That's my answer to your question. My Suggestion? Forget pixel-perfect detection if this is a game.

Seriously.

Instead make paths (not in canvas, in plain javascript code) that represent the objects but are not pixel perfect, for instance a house might be a square with a triangle on the top that is a very close approximation of the image but is used in its stead when it comes to hit testing. It is comparatively extremely fast to compute if a point is inside a path than it is to do pixel-perfect detection. Look up point in polygon winding number rule detection. That's your best bet, honestly.

一枫情书 2024-12-29 15:19:46

传统游戏开发中常见的解决方案是构建点击蒙版。您可以将所有内容以纯色重新渲染到单独的离屏画布上(渲染应该非常快)。当您想弄清楚单击的内容时,只需对屏幕外画布上的 x/y 坐标处的颜色进行采样即可。您最终会构建一个 color-->obj 哈希,类似于:

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

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

The common solution in traditional game development is to build a click mask. You can re-render everything onto a separate off-screen canvas in a solid color (the rendering should be very quick). When you want to figure out what was clicked on, you simply sample the color at the x/y co-ordinate on the off-screen canvas. You end up building a color-->obj hash, akin to:

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

You can also optimize the rendering to the secondary canvas to only happen when the user clicks on something. And using various techniques, you can further optimize it to only draw the part of the canvas that the user has clicked on (for example, you can split you canvas into an NxN grid, e.g. a grid of 20x20 pixel squares, and flag all of the objects in that square -- you'd then only need to re-draw a small number of objects)

时光无声 2024-12-29 15:19:46

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

http://www.kineticjs.com/

http://easeljs.com/

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

HTML5 Canvas is just a drawing plane, where you can set different transforms before calling each drawing API function. Objects cannot be created and there is no display list. So you have to build these features yourself or you can use different libraries available for this.

http://www.kineticjs.com/

http://easeljs.com/

A few months before I got interested in this and even wrote a library for this purpose. You can see it here : http://exsprite.com. Ended up facing a lot of performance issues, but because of lack of time I couldn't optimize it. It was really interesting, so waiting for some time to make it perfect.

五里雾 2024-12-29 15:19:46

我相信评论应该足够了。这就是我在 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 定位绝对和 x = -(width) 隐藏;

I believe the comments should suffice. This is how I determine user intention in my 2d isometric scroller, currently located at 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.

        }
    }
}

In conjunction with this:

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

Set the CSS positioning absolute and x = -(width) to hide;

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文