业务上会遇到这么一种情况,不规则图片需要添加触摸事件,这个时候点击图片的空白区域也会有触摸事件进来,这个时候如果我想只在有像素的地方点击有事件进来,那么要怎么处理呢?比如下图:

找了一下资料有2种方法:
方案一:射线法(Ray Casting Algorithm)- 通用性最强
这是最经典且适用于任意多边形(凸多边形和凹多边形)的算法。其原理是从触摸点向任意方向(通常向右)发射一条射线,统计射线与多边形边相交的次数。如果相交次数为奇数 ,则点在多边形内;如果是偶数,则在多边形外。
方案二:像素级检测(Alpha 检测)- 适合复杂异形图片
如果形状极其不规则且难以用多边形描述,可以通过读取图片像素的 Alpha 值来判断。如果触摸点对应的像素 Alpha > 0,则视为击中。
注意 :此方法性能开销较大,不建议在每帧或大量对象中使用,通常用于静态按钮。需要修改 C++ 底层或使用 RenderTexture 获取像素数据,纯 Lua 实现较困难且效率低。一般建议优先使用多边形近似。
那么这里只讲一下方案一的实现逻辑
1.先用DrawNode画多边形,前提需要把顶点坐标找到。
2.用射线法检测触摸点是否在多边形内。
可以看一下我画的图:

顶点数越多一般就和多边形越贴合,我本来3个顶点就够了,只是为了试试而已,不止如此我还试了凹多边形,虽然drawNode不支持渲染凹多边形,只支持凸多边形,但不影响射线法的判断。

至于这个射线法我也去研究了一下
一、核心原理
射线法的核心逻辑是交点数奇偶性判定 :从待判断点向任意方向(通常选水平向右)引一条无限延伸的射线,统计该射线与多边形所有边的交点总数。若交点数为奇数,说明点在多边形内部;若为偶数,则点在多边形外部。
背后的拓扑逻辑是:射线每穿越一次多边形边界,点的内外状态就会翻转一次,从初始的外部(0次,偶数)经过奇数次翻转后,最终会停留在内部区域。
二、关键前提
多边形的顶点必须按顺时针或逆时针的顺序环绕排列,且首尾顶点闭合,否则算法会直接失效。该方法天然支持凹多边形,也可适配带孔多边形(外环用射线法判定为内部后,再对每个内环单独判定,点落在任意内环内则最终结果为外部)。
三、特殊情况处理
- 射线刚好穿过多边形顶点:通过规则过滤,仅统计边的纵坐标较大的上端点作为有效交点,避免重复计数导致结果错误。
- 射线与多边形某条边完全重合:直接跳过该水平边,避免无效计算和除零错误。
- 点落在多边形的边上:可在主逻辑外单独增加点在线段上的判断,根据业务需求将其判定为内部或外部。
下面贴一下测试代码:
Lua
local test = class("test", function()
return display.newNode()
end)
function test:ctor(param)
self:setTouchEnabled(true)
self:setNodeEventEnabled(true)
self:setTouchSwallowEnabled(true)
self:initUI()
end
function test:initUI()
self:setContentSize(cc.size(display.width,display.height))
local colorLayer = cc.LayerColor:create(cc.c4b(255,255,255,255))
:addTo(self)
:align(display.CENTER,0,0)
:size(display.width*1.5,display.height*1.5)
self.sprite = display.newSprite("#xh_role_02_lock.png")
:addTo(self)
:align(display.CENTER,display.cx,display.cy)
self.verts = {
cc.p(-230, -170), -- 顶点 A
cc.p(350, 180), -- 顶点 B
cc.p(-385, 180), -- 顶点 C
cc.p(-250, 50), -- 顶点 D
}
local drawNode = cc.DrawNode:create()
self:addChild(drawNode) -- 假设 self 是当前 Layer 或 Scene
drawNode:setPosition(self.sprite:getPositionX(),self.sprite:getPositionY())
self.drawNode = drawNode
--self.drawNode:hide()
-- 2. 定义顶点 (顺时针或逆时针均可,建议保持一致)
local verts = self.verts
-- 3. 定义颜色
local fillColor = cc.c4f(1, 0, 0, 1) -- 半透明红色填充 cc.c4f(1, 0, 0, 0.5)
local borderColor = cc.c4f(0, 0, 0, 1) -- 黑色边框 cc.c4f(0, 0, 0, 1)
local borderWidth = 2 -- 边框宽度
-- 4. 绘制多边形
drawNode:drawPolygon(self.verts,{fillColor = fillColor, borderWidth = borderWidth, borderColor = borderColor})
self:setTouchEnabled(true)
self:setTouchSwallowEnabled(true)
self:addNodeEventListener(cc.NODE_TOUCH_EVENT, handler(self, self.onTouch))
end
function test:onTouch(event)
local name,x,y = event.name, event.x, event.y
if name == "began" then
return true
elseif name == "moved" then
elseif name == "ended" then
local location = cc.p(x,y)
local localPos = self.drawNode:convertToNodeSpace(location)
if self:isPointInPolygon(localPos, self.verts) then
print(">>> 触摸点在多边形内部!")
else
print(">>> 触摸点在多边形外部。")
end
end
end
function test:isPointInPolygon(pos, points)
local nCross = 0 -- 交点计数器
local count = #points
for i = 1, count do
local p1 = points[i]
local p2 = points[(i % count) + 1] -- 下一个点,最后一个点连回第一个点
-- 求解 y=p.y 与线段 p1p2 的交点
-- 1. 判断线段是否跨越水平线 p.y
-- 如果两点都在水平线同侧,则不相交
if (p1.y > pos.y and p2.y <= pos.y) or (p1.y <= pos.y and p2.y > pos.y) then
-- 2. 计算交点的 x 坐标
-- 直线方程两点式: (x - x1)/(x2 - x1) = (y - y1)/(y2 - y1)
-- 推导 x: x = x1 + (y - y1) * (x2 - x1) / (y2 - y1)
local xIntersect = p1.x + (pos.y - p1.y) * (p2.x - p1.x) / (p2.y - p1.y)
-- 3. 判断交点是否在测试点的右侧
-- 如果交点在右侧,说明射线穿过了一条边
if xIntersect > pos.x then
nCross = nCross + 1
end
end
end
-- 如果交点数为奇数,则在多边形内;偶数则在外部
return (nCross % 2 == 1)
end
return test
判断交点的时候判断的是右侧,其实左侧也是一样的,拿上纸笔试试看,还真是那么回事,但这个方法也是有弊端的:
- 特殊边界判定复杂
当射线恰好经过多边形顶点、与多边形边重合时,常规奇偶计数逻辑会失效,必须额外编写代码处理这类临界情况,否则会直接出现检测错误。 - 性能开销随顶点数上升
算法需要遍历多边形的所有边做相交判断,若多边形顶点数量多,单次检测的计算量会明显增加,大量对象同时检测时容易出现性能卡顿。 - 无法直接处理非简单多边形
对于存在自相交的复杂多边形,射线法的奇偶计数规则会完全失效,不能直接得到正确的点内外判定结果。 - 点在边上的判定冗余
射线法本身无法直接识别点落在多边形边上的情况,必须额外单独编写逻辑做前置判断,增加了代码的复杂度。