ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Godot 异形按钮:重写 _has_point 实现多边形精准点击

Godot 异形按钮:重写 _has_point 实现多边形精准点击 1. 用 Godot 做异形按钮默认 Button 到底卡在哪游戏 UI 里的按钮十有八九是矩形的直到美术丢来一个六边形技能格、一个斜切角的主菜单项或者一个星形收集徽章——你才会发现 Godot 的 Button 和 TextureButton 都是按矩形框算点击的那个看不见的矩形会一直悬在多边形外面。玩家手指点在图形之外的空白角落按钮照样亮起来这种“幽灵热区”在移动端尤其明显因为手指本来就没鼠标精确。这篇东西想解决的就是这个问题在 Godot 里做一个多边形、任意形状的按钮让点击判定严格贴着图形轮廓走。适合已经写过一两个 Godot 项目、会用 GDScript、也对 Control 节点体系有点感觉的人如果你刚接触 Godot也能照着抄因为我把每一行代码的来龙去脉都拆开了。先把结论摆出来最靠谱的路线是写一个继承 Control 的自定义节点重写_has_point()虚函数绘制交给draw_colored_polygon()。不用 TextureButton 的比特位掩码不用 Area2D 物理碰撞也不用去改引擎源码。1.1 默认 Button 的矩形命中是天生的不是配置问题很多人第一反应是去翻 Button 的文档想找个hit_shape或者collision_polygon之类的属性。找不到因为 Control 这一套本来就是为矩形布局设计的。Control内部维护的rect只有一个位置和一个尺寸鼠标进出的判断就是point.x 0 point.x size.x point.y 0 point.y size.y这么朴素。Engine 其实留了口子。Control有一个虚函数_has_point(point: Vector2) - bool官方文档的原话大意是返回该 Control 是否包含给定点用于判断鼠标是否在控件上方、GUI 事件是否应该路由到它。默认实现就是那个矩形判断你重写它矩形就变成了任意形状。这个口子从 Godot 3 就有Godot 4 依然在。而且它不只是在点击时被调用——鼠标进入、离开、悬停检测全都走这个函数。也就是说你重写它之后mouse_entered/mouse_exited信号也会跟着变精确高亮效果自然就贴着轮廓走了。1.2 三条可选路线我为什么最后选了 Control 子类真要在 Godot 里实现异形点击能走的路不止一条。我把试过的、看别人用过的都列一下顺便说说各自的天花板在哪。路线一TextureButton texture_click_mask。TextureButton 有个属性叫texture_click_mask接收一个BitMap白色区域算可点击黑色算穿透。这确实能实现任意形状但你得先准备一张和按钮等大的黑白图在编辑器的导入面板里从纹理生成 BitMap。问题在于Bitmap 是按像素存的按钮尺寸一大比如 1024×1024这张掩码图的内存就下不来分辨率一改掩码就得重新生成形状稍微调一下就得回到美术那边重新导图。做原型还行做需要动态变化的 UI 就很别扭。路线二Area2D CollisionPolygon2D。这条路在 2D 游戏层面是通的碰撞形状天然支持多边形鼠标位置转成世界坐标用get_overlapping_bodies或者input_event信号就能做。但它脱离了 Control 体系不能塞进 VBoxContainer 自动排版锚点系统用不了主题Theme用不了焦点Focus导航也用不了。你要自己管缩放、管层级、管屏幕适配等于把 Control 已经帮你做的活儿重新做一遍。路线三自定义 Control 子类重写_has_point()。这就是我最终选的。它保留了 Control 的全部生态能进容器、能设锚点、能吃 Theme、能参与焦点导航、能被 Tween 直接动size和position。你只是把“矩形”这个默认答案换成了“多边形”其他一切照旧。代价是要自己写绘制逻辑和事件处理大概一百多行一次写好可以反复用。提示如果你只是想要一个圆角矩形按钮别走这条路线。StyleBoxFlat 的corner_radius就够了视觉上圆角但命中区还是矩形——这通常可以接受因为手指本来就有宽度。真正的异形按钮是给菱形、六边形、星形、箭头这类“留白很大”的形状准备的。2. 核心原理把点击判定从矩形里“抠”出来动手之前得先搞清楚 Godot 的鼠标事件是怎么一步步走到你的按钮上的。搞清楚了后面调 bug 就不用靠猜。2.1 Control 的鼠标命中流程_has_point()卡在哪一环Godot 的视口Viewport拿到一个鼠标事件后会从场景树的顶层往下遍历。遍历过程中对每个 Control 都问一遍这个点在你身上吗问的就是has_point()而has_point()内部会调用你重写的_has_point()。命中之后还有第二道关卡mouse_filter。这个属性有三个值MOUSE_FILTER_STOP默认吃掉事件不再往下面的节点传。MOUSE_FILTER_PASS自己处理同时也往下传。MOUSE_FILTER_IGNORE完全不参与事件直接穿透过去。做自定义按钮mouse_filter保持STOP就行。这一点看着简单但坑很多——如果你不小心把父节点的mouse_filter也设成了STOP而父节点恰好处在按钮上面比如一个半透明的背景板那事件会被父节点先抢走按钮永远收不到_gui_input。这个后面第 5 章还会细说。2.2 点在多边形内的判定用一个“射线穿过几次”的直觉理解判断一个点是否落在多边形内部计算机图形学里最经典的算法叫射线法Ray Casting / Even-Odd Rule。思路很朴素从这个点往右随便射一条水平射线数它和多边形的边相交了几次。奇数次说明点在内部偶数次说明在外部。你可以这么想——多边形是一条闭合的围墙你从墙内往外走必然穿过奇数道墙从墙外走到墙外穿过的墙一定是偶数道或者零道。Godot 的Geometry2D单例直接提供了这个函数Geometry2D.is_point_in_polygon(point, polygon)参数顺序是点在前、多边形在后返回bool。它就是你整个方案的核心判定依据。注意传进去的polygon必须是简单多边形也就是边不能自相交。如果顶点顺序排错了搞出一个“蝴蝶结”形状判定结果会非常诡异——有的区域明明在图形里却判成外部。这个后面在问题排查里会展开。如果你出于某种原因不想依赖引擎函数比如想自己加边界容差手写射线法也就十来行static func point_in_polygon(point: Vector2, poly: PackedVector2Array) - bool: var inside : false var n : poly.size() var j : n - 1 for i in n: var a : poly[i] var b : poly[j] if (a.y point.y) ! (b.y point.y): var x_cross : (b.x - a.x) * (point.y - a.y) / (b.y - a.y) a.x if point.x x_cross: inside not inside j i return inside这段代码每次判定是 O(n)n 是顶点数。对按钮这种量级4 到 20 个顶点来说开销基本可以忽略。2.3 绘制交给draw_colored_polygon()它自己会做三角剖分命中判定解决了视觉上还得把多边形画出来。Godot 的 CanvasItem 提供了draw_colored_polygon(points, color)传入一圈顶点和一个颜色引擎内部会先做三角剖分再交给渲染器。这里有个细节值得说凸多边形的三角剖分是平凡的任何一个顶点连到其他所有不相邻顶点就行凹多边形就麻烦一些需要用到耳切Ear Clipping之类的算法。Godot 4 的draw_colored_polygon()内部处理了这个绝大多数简单凹多边形都能正确渲染。还有个更底层的组合Geometry2D.triangulate_polygon(polygon)会返回一个PackedInt32Array每三个索引构成一个三角形。你可以拿它配合draw_primitive()手工画控制力更强代价是要自己组装数组。绝大多数情况下直接用draw_colored_polygon()就够了。实操心得draw_colored_polygon()传的顶点顺序不影响填充结果无论顺逆时针都能画但会影响边线描边。用draw_polyline()画轮廓时顶点必须按一个方向首尾相接还要手动把第一个点补到数组末尾来闭合。3. 完整实现一个能直接用的 PolygonButton原理讲完上代码。我把它写成一个tool脚本这样在编辑器里放进去就能看到实际形状不用运行游戏。3.1 坐标空间的设计为什么用 [-1, 1] 归一化第一个要做的决定是多边形顶点用什么坐标系存三种选择像素坐标、[0, 1] 归一化、[-1, 1] 归一化。选最后一种。原因有三。第一(0, 0)是中心符号本身就是方向提示写六边形的时候一眼能看出哪个顶点在上第二缩放是自然的顶点乘上半个尺寸就是实际位置不管控件拉成 200×100 还是 600×300 都成立第三方便和别的坐标系互换比如要贴纹理时算 UV(p 1) / 2就变成 [0, 1] 了。局部坐标的换算公式是local_x (p.x * 0.5 0.5) * size.x local_y (p.y * 0.5 0.5) * size.y验证一下p (-1, -1)得到(0, 0)正好是 Control 的左上角p (1, 1)得到(size.x, size.y)右下角p (0, 0)得中心。逻辑闭合。3.2 缓存别在每次鼠标移动时重算顶点_has_point()的调用频率比你想象的高。鼠标每移动一个像素视口就要问一遍所有相关 Control鼠标在按钮上画个圈这个函数可能被调几百次。如果你的按钮有几十个顶点每次都在里面重新计算像素坐标纯属浪费。用一个脏标记dirty flag缓存结果就好顶点改了、尺寸变了就标记为脏下次要用的时候再重算一次。关键是要在正确的时机标脏set_polygon()被调用时收到NOTIFICATION_RESIZED通知时控件尺寸变了显式设置scale时不过 Control 的 scale 会影响绘制但_has_point拿到的已经是局部坐标所以不用管3.3 事件处理按下、抬起、拖出去再松手按钮的语义比想象中细腻光有mouse_button按下抬起不够。标准按钮的行为是这样必须在按钮内部按下、并在按钮内部抬起才算一次完整的点击。如果在按钮里按下、拖到外面松开应该取消这次点击不发出pressed信号。Godot 的 GUI 系统在_gui_input里给你的是鼠标焦点事件——一旦你在按下时accept_event()后续的移动和抬起事件即使在控件外面也还是会送到你这里。所以你不能假设抬起时鼠标还在按钮上必须自己做一次判定if _has_point(get_local_mouse_position()): pressed.emit()这一行如果不写就会出现“拖出去松开也触发”的诡异行为。还有个容易忽略的点拖拽过程中鼠标离开了多边形高亮应该回到常态色而不是继续显示按下色。所以状态色判定里得同时看_held和_hovered。3.4 完整代码下面这份可以直接存成polygon_button.gd用。tool class_name PolygonButton extends Control ## 任意形状多边形按钮。 ## 命中判定重写 Control._has_point()绘制走 draw_colored_polygon()。 signal pressed signal button_down signal button_up export_group(Shape) ## 顶点坐标取值区间 [-1, 1]。 ## (0, 0) 是控件矩形中心(-1, -1) 是左上角(1, 1) 是右下角。 ## 顶点需按同一方向依次排列不可自交叉。 export var polygon: PackedVector2Array PackedVector2Array([ Vector2(0.0, -1.0), Vector2(1.0, 0.0), Vector2(0.0, 1.0), Vector2(-1.0, 0.0), ]) : set set_polygon export_group(Colors) export var normal_color: Color Color(0.24, 0.52, 0.90) export var hover_color: Color Color(0.34, 0.62, 1.00) export var pressed_color: Color Color(0.16, 0.40, 0.75) export var disabled_color: Color Color(0.35, 0.35, 0.38) export var border_color: Color Color(0.08, 0.12, 0.20, 0.75) export_range(0.0, 16.0, 0.5) var border_width: float 2.0 export_group(Behavior) export var disabled: bool false : set set_disabled var _hovered: bool false var _held: bool false var _local_points: PackedVector2Array PackedVector2Array() var _points_dirty: bool true func _ready() - void: mouse_filter Control.MOUSE_FILTER_STOP focus_mode Control.FOCUS_ALL if not mouse_entered.is_connected(_on_mouse_entered): mouse_entered.connect(_on_mouse_entered) if not mouse_exited.is_connected(_on_mouse_exited): mouse_exited.connect(_on_mouse_exited) if not focus_entered.is_connected(queue_redraw): focus_entered.connect(queue_redraw) if not focus_exited.is_connected(queue_redraw): focus_exited.connect(queue_redraw) _refresh_cursor() func _notification(what: int) - void: if what NOTIFICATION_RESIZED: _points_dirty true queue_redraw() # ---------- 属性 setter ---------- func set_polygon(value: PackedVector2Array) - void: polygon value _points_dirty true queue_redraw() func set_disabled(value: bool) - void: disabled value if disabled: _held false _refresh_cursor() queue_redraw() func _refresh_cursor() - void: mouse_default_cursor_shape ( Control.CURSOR_ARROW if disabled else Control.CURSOR_POINTING_HAND ) func _on_mouse_entered() - void: _hovered true queue_redraw() func _on_mouse_exited() - void: _hovered false queue_redraw() # ---------- 几何计算 ---------- func _rebuild_points() - void: _local_points PackedVector2Array() _local_points.resize(polygon.size()) for i in polygon.size(): var p : polygon[i] _local_points[i] Vector2( (p.x * 0.5 0.5) * size.x, (p.y * 0.5 0.5) * size.y ) _points_dirty false func get_local_points() - PackedVector2Array: if _points_dirty: _rebuild_points() return _local_points # ---------- 命中判定 ---------- func _has_point(point: Vector2) - bool: if polygon.size() 3: return false return Geometry2D.is_point_in_polygon(point, get_local_points()) # ---------- 绘制 ---------- func _draw() - void: if polygon.size() 3: return var pts : get_local_points() draw_colored_polygon(pts, _current_color()) if border_width 0.0: var loop : pts.duplicate() loop.append(pts[0]) draw_polyline(loop, border_color, border_width, true) if has_focus(): var center : size * 0.5 var inner : PackedVector2Array() for p in pts: inner.append(center (p - center) * 0.85) inner.append(inner[0]) draw_polyline(inner, Color(1, 1, 1, 0.65), 1.0, true) if Engine.is_editor_hint(): for p in pts: draw_circle(p, 3.0, Color(1.0, 0.45, 0.1, 0.9)) func _current_color() - Color: if disabled: return disabled_color if _held and _hovered: return pressed_color if _hovered: return hover_color return normal_color # ---------- 输入 ---------- func _gui_input(event: InputEvent) - void: if disabled: return if event is InputEventMouseButton and event.button_index MOUSE_BUTTON_LEFT: if event.pressed: _held true button_down.emit() elif _held: _held false button_up.emit() if _has_point(get_local_mouse_position()): pressed.emit() accept_event() queue_redraw() return if event.is_action_pressed(ui_accept) and has_focus(): button_down.emit() pressed.emit() button_up.emit() accept_event()代码里_draw()的最后一段只在编辑器里跑会在每个顶点画一个橙色小圆点方便你在检查器里改数值的时候看形状对不对。发布版本不会执行因为Engine.is_editor_hint()在运行时返回 false。有个小细节值得单独说has_focus()走的是整块矩形区域的焦点提示。这是_has_point管不到的部分——焦点框是引擎自己算的。如果你在意这个可以干脆把focus_mode设成FOCUS_NONE纯触摸设备上其实也无所谓。4. 落地到实际场景几种形状的调参与适配代码写完只是开始真正让它在项目里站住脚还得处理形状设计、布局适配和不同输入方式的差异。4.1 几种常见形状的顶点配置顶点都写在 [-1, 1] 区间里下面是几个可以直接抄的例子。形状顶点数组说明菱形(0,-1) (1,0) (0,1) (-1,0)默认值技能格常用正三角(0,-1) (0.87,0.5) (-0.87,0.5)宽度按 √3/2 折算视觉正三角正六边形每 60° 取一点半径 1见下方代码左箭头(-1,-1) (0,-1) (1,0) (0,1) (-1,1)五个顶点播放/下一步用圆角八边形在正方形基础上各角切 0.3比矩形柔和比圆形省顶点正六边形的顶点生成方式var hex : PackedVector2Array() for i in 6: var a : deg_to_rad(60.0 * i - 90.0) hex.append(Vector2(cos(a), sin(a)))减 90 度是为了让第一个顶点在正上方视觉上更符合直觉。实操心得顶点数不要贪多。20 个顶点以内命中判定和三角剖分的开销都可以忽略上百个顶点的“伪圆形”没必要直接用draw_circle()配合半径判定反而又快又准。多边形按钮真正的价值在“留白大、形状有辨识度”的场景。4.2 在容器与不同分辨率下怎么不出事这个节点本质上是 Control所以容器和锚点两套布局系统都能用。丢进HBoxContainer的时候容器只会分配一个矩形区域你的多边形在这个矩形里按比例缩放。想让它在横向上撑满设size_flags_horizontal SIZE_EXPAND_FILL想让形状保持宽高比就把size_flags_vertical和水平方向设成一致的然后自己算一个固定的custom_minimum_size。分辨率适配这块要注意一点归一化坐标意味着形状会跟着矩形一起被拉伸。如果按钮在 1920×1080 下是 200×200 的菱形切到 21:9 的屏幕后被拉成 240×200菱形就成了扁菱形。绝大多数情况下这可以接受甚至正是你想要的——UI 元素随屏幕等比缩放本来就自然。但如果你的美术对形状比例有硬要求就得在_rebuild_points()里做等比修正取min(size.x, size.y)作为基准边长再把点居中偏移。func _rebuild_points_keep_aspect() - void: var side : minf(size.x, size.y) var offset : (size - Vector2(side, side)) * 0.5 _local_points PackedVector2Array() for p in polygon: _local_points.append(offset Vector2( (p.x * 0.5 0.5) * side, (p.y * 0.5 0.5) * side )) _points_dirty false代价是形状只占矩形的一部分剩下的区域是纯空白——不过_has_point()用的也是同一套点所以空白区域不会误触发逻辑上是自洽的。4.3 触摸屏上的额外考量移动端的手指接触面积大概有 40 到 60 像素比鼠标精确度差远了。异形按钮在触摸屏上会面临一个真实问题形状边角处的点击很难命中。我的做法是做一个“膨胀系数”。判定的时候不直接用原多边形而是以形状中心为原点把每个顶点向外扩个 8% 到 12% 再判func _has_point(point: Vector2) - bool: if polygon.size() 3: return false var pts : get_local_points() if touch_tolerance 0.0: return Geometry2D.is_point_in_polygon(point, pts) var center : size * 0.5 var inflated : PackedVector2Array() for p in pts: inflated.append(center (p - center) * (1.0 touch_tolerance)) return Geometry2D.is_point_in_polygon(point, inflated)把这个touch_tolerance做成export桌面端设 0移动端设 0.1。膨胀后的判定区域会稍微溢出到图形外面视觉上有轻微的不一致但换来的是触摸命中率的大幅提升——这个取舍在真实项目里非常值。注意不要简单地给size加内边距来做膨胀那样形状本身也跟着变大了视觉会走样。用中心缩放的方式形状不动只有判定区变大。5. 常见问题与排查实录这一章是我踩过的坑的合集。异形按钮的调试难点在于“看不见”——判定区在屏幕上是不可见的出问题的时候只能靠推理。5.1 点击完全没反应按这个顺序排查基本能定位。第一看mouse_filter。如果你的 PolygonButton 放在一个 Panel 或者 ColorRect 里而那个父节点的mouse_filter是STOP默认值父节点会先把事件吃掉。解决办法是把父节点的mouse_filter改成PASS或IGNORE。第二看_has_point有没有真的被调用。加个print最直接。如果没有输出要么是mouse_filter的问题要么是控件size是(0, 0)——刚创建出来的 Control 尺寸默认是零多边形一个顶点都算不出来判定自然永远返回 false。手动设个size或者custom_minimum_size再看。第三检查方法签名。必须是func _has_point(point: Vector2) - bool:参数名可以换但类型和返回值不能错。写成_has_point(point)不带类型标注也能跑但如果拼写成了_has_point_position之类的就完全无效而且 Godot 不会报错——它只当你写了个普通方法。5.2 悬停高亮闪烁或者错位闪烁通常意味着多边形自交叉。想象一个蝴蝶结形状(0,-1) (1,1) (0,1) (-1,1)。第 2 和第 3 个顶点靠得太近第 3 到第 4 条边折回来穿过了第 1 到第 2 条边。射线法在这种形状上会给出反直觉的结果——鼠标在图形中间晃判定结果可能在 true 和 false 之间跳。诊断方法很土但有效把顶点顺序理一遍画在纸上看看边是不是两两不相交。或者临时把_draw()里的顶点圆点放大到 8 像素用draw_string()把索引标上去一眼就看出顺序对不对。5.3 凹多边形渲染或判定出问题draw_colored_polygon()对凹多边形的支持在绝大多数情况下是可靠的但极少数情况下顶点极多、非常扭曲会出现填充错误。稳妥的替代方案是自己三角剖分var indices : Geometry2D.triangulate_polygon(pts) if indices.size() 0: var colors : PackedColorArray() for i in pts.size(): colors.append(_current_color()) draw_polygon(pts, colors, PackedVector2Array(), null, indices)triangulate_polygon()返回的索引数组每三个一组直接喂给draw_polygon()的第五个参数。如果它返回空数组说明多边形非法自交叉或者退化成线这时候你就该回头检查顶点顺序了。5.4 性能与缓存前面提过脏标记缓存这里说下为什么值得做。我做过一个粗略的测试一个 24 顶点的多边形按钮鼠标在它上面快速画圈时_has_point()的调用频率能到每秒上千次。每次重算 24 个点的坐标是 48 次乘加运算再加一次数组分配——单看微不足道但一个界面放 20 个这样的按钮累积起来就是每秒几十万次运算而且还会产生大量临时数组给 GC 增加压力。加上缓存之后这些开销全部消失。另一件事_draw()里的draw_polyline()如果开了抗锯齿第五个参数传true代价比不开高不少。如果一个界面有几十个异形按钮可以考虑关掉抗锯齿或者只在边框比较显眼的按钮上开。5.5 问题速查表现象可能原因处理方式点击完全没反应mouse_filter被父节点吃掉父节点改PASS或IGNORE点击完全没反应控件size是 (0,0)设custom_minimum_size或进容器点击完全没反应方法名拼错确认是_has_point(point: Vector2) - bool空白角落也能点中_has_point没被重写成功用print确认被调用悬停高亮乱闪多边形自交叉重排顶点顺序画图验证拖出去松手也触发没做抬起位置判定加_has_point(get_local_mouse_position())缩放后命中偏移缓存没刷新检查NOTIFICATION_RESIZED是否触发凹多边形填充错乱三角剖分失败改用triangulate_polygon()draw_polygon()移动端边角点不中手指接触面积大加touch_tolerance膨胀判定区编辑器里看不到形状脚本没加tool首行加tool并重新加载场景实操心得调试异形按钮的时候临时把_has_point()改一行让它只在判定成功时queue_redraw()并在_draw()里画一个红色蒙版覆盖判定区。肉眼看着红块走一圈命中范围对不对立刻就见分晓比任何日志都直观。调完记得删掉。6. 还能怎么往下扩展基础版本跑通之后有几个方向可以继续做深。6.1 给多边形贴纹理纯色多边形按钮在很多游戏里够用了但如果美术给了一张六边形的金属边框贴图你就得把纹理映射上去。用draw_polygon()的 UV 版本func _draw_textured() - void: var pts : get_local_points() var uvs : PackedVector2Array() for p in polygon: uvs.append(Vector2(p.x * 0.5 0.5, p.y * 0.5 0.5)) var colors : PackedColorArray() var tint : _current_color() for i in pts.size(): colors.append(tint) var indices : Geometry2D.triangulate_polygon(pts) draw_polygon(pts, colors, uvs, texture, indices)UV 的算法就是把归一化坐标从 [-1, 1] 映射到 [0, 1]纹理会被完整拉伸到控件的边界矩形上然后由多边形裁剪掉多余部分。colors数组用来做整体色调调制——比如按下时把颜色压暗高亮时提亮只要把tint换成对应颜色就行纹理本身不用变。6.2 状态过渡用 Tween 而不是硬切现在的代码里颜色是从normal_color直接跳到hover_color的硬切。做精致的 UI 时用 Tween 补个 0.08 到 0.12 秒的过渡会舒服很多。做法是维护一个_current_color变量变更时杀掉旧 Tween、创建新 Tweenvar _display_color: Color var _color_tween: Tween func _animate_to(target: Color) - void: if _color_tween and _color_tween.is_valid(): _color_tween.kill() _color_tween create_tween() _color_tween.tween_property(self, _display_color, target, 0.1) _color_tween.step_finished.connect(func(_s): queue_redraw())然后_draw()用_display_color而不是_current_color()。还有一个视觉技巧按下时把整个形状缩小一点点。因为_has_point()用的是get_local_points()而它依赖size所以你最好别直接动size会触发缓存重建、还会影响布局。更干净的做法是在_draw()里对点做一次缩放不动实际尺寸var shrink : 0.94 if (_held and _hovered) else 1.0 var center : size * 0.5 var draw_pts : PackedVector2Array() for p in pts: draw_pts.append(center (p - center) * shrink) draw_colored_polygon(draw_pts, _display_color)这样视觉上有按压反馈但命中判定区始终稳定不会因为动画产生抖动。6.3 编辑器里可视化拖顶点我自己的做法比较土但很省事在_draw()里用tool把顶点画出来前面代码里已经有了然后在检查器的polygon数组里直接改数值。改一个数视口里的橙色小点立刻移动形状实时更新。如果你想要更爽的拖拽体验可以写一个 EditorPlugin在forward_canvas_gui_input()里捕获鼠标拖动把位置换算回归一化坐标写回polygon。这属于锦上添花项目里有一两个异形按钮的话手改数值完全够用。6.4 组合成更复杂的交互多边形按钮做好之后你会发现很多之前觉得麻烦的东西变简单了。比如一个环形的技能冷却条外圈是环形进度内圈就是你的多边形按钮比如地图上的不规则区域点击每个区域的轮廓直接拿来当多边形顶点再比如手绘风格的对话框选项让美术画好形状轮廓你只取顶点数据。最后分享一个我用了很久的小技巧建一个形状库脚本把常用形状预定义成常量。class_name PolygonShapes const DIAMOND : [Vector2(0,-1), Vector2(1,0), Vector2(0,1), Vector2(-1,0)] const HEX_FLAT : [...] const HEX_POINTY : [...]在检查器里复制粘贴数组比每次重新敲坐标快得多而且能保证整个项目的形状风格统一。踩过几次“每个按钮顶点都差一点点、整体看起来歪歪扭扭”的坑之后我现在所有项目都会先建这个库。
返回列表