外观
Stage03|Runtime Objects & Assets
Lesson028|触摸命中、事件遮挡与坐标转换
Tags: #Creator2.x #Stage03 #Input #HitTest #CoordinateDifficulty: ⭐⭐⭐⭐☆
一、本课从“看得见,却点不到”开始
项目弹出购买确认框:
text
Canvas
├── GameLayer
│ └── Map
└── ModalLayer
├── Mask
└── Dialog
└── ConfirmButton测试同时报告三种问题:
- ConfirmButton 显示正常,点击却没有反应;
- 点击半透明 Mask,下面的地图也移动了;
- 触摸点调试标记比手指偏了一段距离。
三种问题分别落在输入流程的不同位置:
text
坐标换算
→ 节点命中
→ 上层遮挡
→ 事件传播
→ 业务输入状态上一课讲了命中之后的传播。本课回到前半段:目标如何被选中,以及如何证明问题出在哪一层。
二、本课目标
完成本课后,你应该能够:
- 区分屏幕、世界、父节点局部和目标节点局部坐标。
- 使用
convertToNodeSpaceAR与convertToWorldSpaceAR。 - 解释可见像素、
contentSize和命中区域的差异。 - 排查层级、透明遮挡、Button 状态和
BlockInputEvents。 - 设计可靠的全屏弹窗输入边界。
- 用 Creator 2.4.x 实验测量锚点、缩放和父节点变换的影响。
三、一次点击的完整流水线
text
平台触摸点
↓
View / Canvas / Camera 换算
↓
按层级和渲染顺序寻找候选
↓
检查 active、尺寸和交互组件
↓
确定 target
↓
捕获 / 目标 / 冒泡
↓
业务输入模式决定是否执行“按钮点不到”可能是坐标转换错、上层透明节点被命中、Button 禁用、尺寸为 0、监听未注册、传播被停止,或者业务状态禁止输入。
所以排查不能从“再注册一次 click”开始。
四、四种常见坐标
1. 屏幕坐标
ts
const screenPoint = event.getLocation();它表示输入在视图中的位置,不等于任意节点的 x/y。
2. 世界坐标
节点经过父级平移、缩放、旋转后,在场景世界空间中的位置。
3. 父节点局部坐标
node.x、node.y 在 Creator 2.x 中描述节点相对父节点的位置。
4. 目标节点局部坐标
把世界点转换到目标节点自己的坐标空间:
ts
const localPoint = this.node.convertToNodeSpaceAR(worldPoint);四种坐标的数字可能碰巧一样,但语义仍不同。
五、为什么触摸 x 不能和 node.x 直接比较
错误代码:
ts
if (event.getLocationX() > this.node.x) {
this.selectRightSide();
}左边通常是视图输入坐标,右边是父空间局部位置。父节点如果移动或缩放:
text
Parent.position = (300, 100)
Parent.scale = 1.5
Child.x = 50Child 的世界位置显然不再等于 50。
几何比较之前,先把参与比较的量转换到同一个坐标空间。
六、Creator 2D UI 中的常用转换
把节点锚点位置转到世界空间:
ts
const world = this.node.convertToWorldSpaceAR(cc.v2(0, 0));把世界点转到节点局部空间:
ts
const local = this.node.convertToNodeSpaceAR(world);在常见单 Canvas 2D UI 中,触摸位置常被这样使用:
ts
private onTouch(event: cc.Event.EventTouch) {
const local = this.node.convertToNodeSpaceAR(event.getLocation());
cc.log('local touch:', local.x, local.y);
}但不要把这段经验无条件扩展到自定义 Camera、多 Canvas、RenderTexture 或复杂世界场景。输入点若与节点不在同一世界映射中,应先通过负责渲染该对象的 Camera 做屏幕/世界转换,再进入节点局部空间。
七、AR 与锚点
AR 可以理解为 Anchor Relative,局部原点与锚点相关。节点尺寸 200×100 时:
text
anchor = (0.5, 0.5)
x 约为 -100 到 100
y 约为 -50 到 50
anchor = (0, 0)
x 约为 0 到 200
y 约为 0 到 100同一可见位置,转换后的局部数字会随锚点变化。这不是转换失败。
锚点感知的矩形判断:
ts
private containsLocalPoint(local: cc.Vec2): boolean {
const size = this.node.getContentSize();
const minX = -size.width * this.node.anchorX;
const minY = -size.height * this.node.anchorY;
return local.x >= minX
&& local.x <= minX + size.width
&& local.y >= minY
&& local.y <= minY + size.height;
}这是教学实现。普通按钮应优先用引擎交互组件,不要重复手写命中系统。
八、可见像素、节点尺寸和命中区域不是一回事
一张星形 PNG 可能是:
text
PNG 外接矩形:200×200
星形可见像素:只占中间
节点 contentSize:200×200用户点击透明角落时,节点仍可能落在矩形命中范围内。需要区分:
- 源图片画布;
- Texture/SpriteFrame 导入信息;
- 节点
contentSize; - 世界变换后的包围范围;
- Button 或自定义交互区域;
- Collider 所表达的物理区域。
Collider 不等于 UI Button 的默认点击区域,Sprite 的透明像素也不等于自动点击穿透。
九、按钮能否响应的证据链
text
activeInHierarchy 是否为 true
→ Button.enabled 是否为 true
→ interactable 是否为 true
→ contentSize 是否合理
→ 节点是否位于触摸位置
→ 是否被上层节点拦截
→ Click Events / node.on 是否绑定
→ 是否被 ScrollView 转为取消ts
cc.log(
'active=', this.node.activeInHierarchy,
'size=', this.node.width, this.node.height,
'enabled=', this.button.enabled,
'interactable=', this.button.interactable
);视觉正常只证明显示链路,不证明交互链路和业务回调正确。
十、透明节点为什么能挡住点击
透明只描述渲染结果,不描述输入资格。以下对象视觉透明时仍可能影响输入:
- 注册了触摸监听的全屏节点;
- 挂有
cc.BlockInputEvents的节点; - 透明 Button 或交互容器;
- 未关闭的弹窗遮罩;
- 调试层留下的大尺寸节点。
不要用 opacity 关闭输入:
ts
this.mask.opacity = 0; // 不保证取消输入如果对象整体不应工作,应按设计使用 active = false 或显式禁用交互组件。视觉状态与输入状态要分别管理。
十一、BlockInputEvents 能做什么
Creator 2.4.x 的 cc.BlockInputEvents 用于阻止触摸/鼠标事件继续影响后面的节点,常用于弹窗遮罩。
text
ModalRoot(覆盖 Canvas)
├── Mask(全屏,BlockInputEvents)
└── Dialog
└── Buttons需要同时满足:
- Mask 尺寸覆盖目标区域;
- 节点处于正确层级;
- 组件启用;
- 弹窗打开时节点 active;
- 底层若使用独立全局输入器,还要切换输入模式。
BlockInputEvents 不是暂停所有输入的魔法。地图若直接监听全局输入,仍要由 InputContext 禁用地图命令。
十二、同级顺序为什么重要
错误结构:
text
Canvas
├── Dialog
└── Mask(位于 Dialog 之上并覆盖全屏)Mask 可能连 Dialog 自己也挡住。更清楚的结构:
text
Canvas
└── ModalRoot
├── Mask
└── DialogCreator 2.x UI 的渲染和命中顺序受层级、兄弟顺序、Camera 和组件影响。不要只凭名字猜。可以临时给 Mask 着色,并给候选节点输出 event.target。
十三、父级变换为什么不该手算
Dialog 若旋转 30 度、缩放 0.8,触摸屏幕点不能和未变换宽高直接比较。正确链路:
text
触摸点
→ 转为与 Dialog 一致的世界空间
→ Dialog.convertToNodeSpaceAR
→ 得到局部点
→ 在局部矩形中判断错误手算:
ts
const localX = (touchX - node.x) / node.scaleX;它忽略父节点链、锚点、旋转和 Camera。
十四、多分辨率为什么会放大问题
设计分辨率、Fit Width/Fit Height、设备宽高比和 Canvas 适配,会让设计坐标与物理像素产生映射。
ts
const centerX = 375; // 只在某些假设下成立更稳妥的方式:
- 使用节点与 Canvas 的实际尺寸;
- 用布局组件表达约束;
- 从事件和 Camera 获取正确映射;
- 在长屏、平板和旋转后验证;
- 不把设计稿像素直接当运行时屏幕坐标。
十五、贯穿案例的正确分层
显示层
Mask 提供半透明背景,Dialog 提供内容。opacity 只负责视觉。
节点输入层
Mask 覆盖 Canvas 并挂 BlockInputEvents,Dialog 位于 Mask 之上,按钮有明确监听。
传播层
ConfirmButton 执行确认命令;点击 Mask 关闭时,不让同一节点事件触发下面 UI。
业务输入层
弹窗打开时,InputContext 进入 modal 状态,地图移动和战斗快捷键暂停;关闭后恢复。
text
视觉遮罩
≠ 节点事件遮挡
≠ 全局业务输入锁十六、Creator 2.4.x 编辑器实验
所有节点、组件和属性都通过 Creator 编辑器创建与调整。不要手工编辑
.scene、.prefab或.meta。
实验结构
text
Canvas
├── BottomButton(300×100)
├── CoordinatePanel(300×200)
└── TransparentCover(覆盖 BottomButton)TransparentCover 初始 opacity 设为 80,实验后再设为 0。
CoordinateProbe.ts
ts
const { ccclass } = cc._decorator;
@ccclass
export default class CoordinateProbe extends cc.Component {
onEnable() {
this.node.on(
cc.Node.EventType.TOUCH_END,
this.onTouchEnd,
this
);
}
onDisable() {
this.node.off(
cc.Node.EventType.TOUCH_END,
this.onTouchEnd,
this
);
}
private onTouchEnd(event: cc.Event.EventTouch) {
const input = event.getLocation();
const local = this.node.convertToNodeSpaceAR(input);
const anchorWorld = this.node.convertToWorldSpaceAR(cc.v2());
cc.log(
'[CoordinateProbe]',
'input=', input.x, input.y,
'local=', local.x, local.y,
'anchorWorld=', anchorWorld.x, anchorWorld.y,
'inside=', this.contains(local)
);
}
private contains(local: cc.Vec2): boolean {
const size = this.node.getContentSize();
const minX = -size.width * this.node.anchorX;
const minY = -size.height * this.node.anchorY;
return local.x >= minX
&& local.x <= minX + size.width
&& local.y >= minY
&& local.y <= minY + size.height;
}
}实验 A:中心锚点
给 CoordinatePanel 挂脚本,Anchor 设为 (0.5, 0.5),尺寸 300×200。点击中心和四周。
预期:
text
中心附近 local ≈ (0, 0)
左边 local.x 为负
下边 local.y 为负
矩形内 inside = true实验 B:改变锚点
Anchor 改为 (0, 0),再次点击相同视觉位置。预期局部范围大致变为 x=0~300、y=0~200。触摸位置没变,局部原点变了。
实验 C:父级变换
增加父节点,将父节点移动、缩放并旋转。预期屏幕输入数字变化,但局部点仍稳定描述相对面板的位置,inside 仍正确。
实验 D:透明遮挡
- BottomButton 的 Click Events 输出
Bottom clicked。 - TransparentCover 覆盖按钮并挂
BlockInputEvents。 - Cover opacity 设为 0。
- 点击视觉上的 BottomButton。
预期:Cover 不可见,底层按钮仍被阻挡。禁用组件或关闭 Cover 后,按钮恢复响应。
十七、实验失败时怎么查
local 明显偏移
text
输入点是否与节点处于同一世界映射
→ 是否有自定义 Camera
→ 是否多个 Canvas
→ View 是否缩放
→ 是否混用物理像素和设计坐标先在单 Canvas UI 验证,再逐层加入复杂条件。
inside 与视觉不一致
检查 contentSize、锚点、Sprite Size Mode、父级变换,以及图片是否有透明留白。inside 判断节点矩形,不是非透明像素。
Cover 挡不住底层
text
Cover 是否 activeInHierarchy
→ 尺寸是否覆盖
→ BlockInputEvents 是否 enabled
→ 同级顺序是否正确
→ 底层是否使用独立全局输入Button 有 TOUCH_START,却没有点击回调
触摸可能移动出范围、被 ScrollView 接管、收到 CANCEL,或 Button 过程中变为不可交互。要记录完整触摸序列。
十八、真实项目故障:只在部分安卓机偏移
现象
技能指示器在编辑器正确,在全面屏设备整体向上偏移。代码把原生触摸位置直接减去固定状态栏高度,再赋给 Canvas 子节点。
错误代码
ts
indicator.x = rawTouchX;
indicator.y = rawTouchY - 44;它混合了平台物理像素、经验值和节点父空间坐标。
修复链
text
确认触摸 API 坐标语义
→ 确认渲染目标 Camera
→ 转为世界坐标
→ 转为 indicator.parent 局部坐标
→ 赋给 indicator
→ 多宽高比和旋转回归真正的修复不是换一个更准确的常数,而是消除跨坐标系直接赋值。
十九、常见误区
误区一:看得见就一定点得到
显示成功不证明交互组件、尺寸、层级和业务状态正确。
误区二:透明节点不会挡点击
透明是视觉属性;节点仍可能有尺寸、监听器或 BlockInputEvents。
误区三:所有 x/y 都是屏幕坐标
node.x/y 通常是父节点局部坐标。
误区四:手算转换更快
父级旋转、多层缩放、锚点或 Camera 会让手算失效。
误区五:Collider 决定 UI 点击
物理碰撞和 UI 命中是不同系统,除非项目显式连接。
误区六:BlockInputEvents 暂停所有游戏输入
独立全局输入仍需业务模式控制。
二十、版本边界
本课以 Creator 2.4.x 的 cc.Vec2、convertToNodeSpaceAR、convertToWorldSpaceAR、cc.Button 和 cc.BlockInputEvents 为主。
Creator 3.x 使用不同的模块类型和 UITransform/Camera 接口,不能直接照搬 2.4.x 代码。Creator 2.4.x 的具体命中细节也会受事件类型、组件、Camera 和小版本影响;复杂项目应制作最小实验并记录 target。
官方验证入口:
二十一、练习
A. 概念题
- 为什么
event.getLocationX()不能直接和node.x比较? - 锚点从中心改到左下后,局部坐标怎样变化?
- opacity 为 0 的节点为什么仍可能挡按钮?
- 为什么 Collider 不能直接代表 Button 范围?
B. 应用题
实现“点击血条左半边选择自己,右半边选择敌人”。说明在哪个空间比较,怎样处理锚点。
C. 诊断题
弹窗按钮显示正常但点不到。给出从几何、层级、组件、传播到业务状态的排查流程。
D. 反例分析
项目在所有设备上使用 touchY - 80 修正安全区偏移。解释为何不可靠,并给出正确边界。
二十二、练习参考答案
A. 概念题答案
- 前者通常是视图输入坐标,后者是父节点局部坐标;不同空间不能直接比较。
- 局部原点从中心移到左下。尺寸 200×100 时,范围大致从 [-100,100]×[-50,50] 变为 [0,200]×[0,100]。
- opacity 只影响渲染,节点仍可能有尺寸、监听器和
BlockInputEvents。 - Collider 属于物理系统,Button 属于 UI 交互系统,两者没有默认等价契约。
B. 应用题答案
先把触摸点转入血条局部空间。用锚点算 minX = -width * anchorX,中心线是 minX + width / 2;local.x 小于中心线为左,否则为右。不能比较触摸屏幕 x 和血条 node.x。
C. 诊断题答案
text
1. 输出 activeInHierarchy、尺寸和世界位置。
2. 输出 Button enabled/interactable。
3. 记录 TOUCH_START,确认是否命中。
4. 给可疑透明节点临时着色。
5. 检查 Mask 是否位于 Dialog 之上。
6. 检查父节点激活、缩放和裁剪。
7. 检查 ScrollView 是否取消触摸。
8. 检查传播是否提前停止。
9. 检查 Click Events 和脚本绑定。
10. 检查业务输入模式。D. 反例答案
80 是某种设备和分辨率下的经验像素,无法表达设计分辨率适配、横竖屏、自定义 Camera 和真实安全区。安全区只应影响布局边界;输入点通过 View、Camera 和 Node 转换链进入目标父空间,不在业务按钮里叠加固定补偿。
二十三、本课总结
text
输入先经过坐标映射和命中,之后才传播。
几何比较前必须统一坐标空间。
AR 转换以节点锚点为局部原点。
可见像素、contentSize、Button 区域和 Collider 不同。
透明不代表不参与输入。
BlockInputEvents 管节点遮挡,不等于暂停所有业务输入。二十四、下一课预告
text
Lesson029|编辑器导入系统与运行时资源系统的区别下一课跟随一张 PNG 从 assets 进入 AssetDB、参与构建,再到运行时被 cc.resources.load 取得,彻底区分“文件存在”“导入成功”和“运行时可加载”。
