输入
元素通过应用的 ElementInput 响应鼠标、触摸和 XR 控制器。它监听浏览器输入,确定指针下方的元素,并在组件上触发事件。本页介绍全部交互元素共用的事件。按钮在其上增加悬停和按下状态,滚动视图则通过这些事件实现拖动。
启用界面输入
元素接收输入需要两个条件:应用具有 ElementInput,详见配置;元素自身启用了输入。未启用输入的元素不会被命中,因此仅对交互元素开启,装饰和按钮标签等应关闭,使标签上的点击到达下方按钮。
- 引擎
- 编辑器
- React
- Web Components
card.element.useInput = true;
也可以向 addComponent('element', ...) 传入 useInput: true。应用没有 ElementInput 时,调试版引擎会对启用输入的元素记录警告。
勾选 Element 组件的 Use Input。
<Element type="image" useInput />
<pc-element type="image" use-input></pc-element>
各开发方式创建的输入设备不同,这会影响界面与游戏输入的配合:
| 开发方式 | 输入设备 | 元素输入 |
|---|---|---|
| 引擎 | 传给 AppOptions 的设备 | 需要自行创建,且先于鼠标和触摸设备 |
| 编辑器 | INPUT 中启用的鼠标、触摸、键盘和手柄 | 始终创建,先于其他设备 |
| React | app.mouse 和 app.touch,没有键盘设备 | 始终创建,先于其他设备 |
| Web Components | app.mouse 和 app.keyboard,没有触摸设备,但元素输入自行处理触摸 | 始终创建,先于其他设备 |
输入事件
以下事件在元素组件上触发:
| 事件 | 触发时机 |
|---|---|
mouseenter | 指针进入元素 |
mouseleave | 指针离开元素 |
mousemove | 指针在元素上移动;若已在元素上按下,则直到释放前的全部移动都交给它 |
mousedown | 在元素上按下鼠标键 |
mouseup | 在元素上释放鼠标键,或在元素上按下后于任意位置释放 |
mousewheel | 指针位于元素上时滚动鼠标滚轮 |
click | 鼠标或触摸在同一元素上按下并释放 |
touchstart | 触摸在元素上开始 |
touchmove | 从元素开始的触摸发生移动,无论移到何处 |
touchleave | 从元素开始的触摸移出,每次触摸只触发一次 |
touchend | 从元素开始的触摸结束,无论结束位置 |
touchcancel | 浏览器取消从元素开始的触摸 |
selectstart, selectend, selectmove, selectenter, selectleave | XR 控制器或手指向并选择元素,详见 XR 界面 |
监听事件
监听元素组件上的事件,不同开发方式的监听代码位置如下:
- 引擎
- 编辑器
- React
- Web Components
card.element.on('mouseenter', () => {
card.element.opacity = 1;
});
card.element.on('mouseleave', () => {
card.element.opacity = 0.6;
});
on 返回 EventHandle,调用其 off() 停止监听。
为元素实体添加 Script 组件,在脚本 initialize 中监听:
import { Script } from 'playcanvas';
export class HoverOpacity extends Script {
static scriptName = 'hoverOpacity';
initialize() {
const element = this.entity.element;
const enter = element.on('mouseenter', () => {
element.opacity = 1;
});
const leave = element.on('mouseleave', () => {
element.opacity = 0.6;
});
// Stop listening if the script is destroyed before its entity
this.once('destroy', () => {
enter.off();
leave.off();
});
}
}
useElementEvent 监听所在实体的元素事件,ElementEvent 将其封装为组件。将 <ElementEvent> 放到 <Element> 后,确保开始监听时元素已存在:
import { useEffect, useRef, useState } from 'react';
import { Entity } from '@playcanvas/react';
import { Element } from '@playcanvas/react/components';
import { useParent } from '@playcanvas/react/hooks';
export function useElementEvent(name, handler) {
const entity = useParent();
const handlerRef = useRef(handler);
handlerRef.current = handler;
useEffect(() => {
const handle = entity.element?.on(name, event => handlerRef.current(event));
return () => handle?.off();
}, [entity, name]);
}
export function ElementEvent({ name, handler }) {
useElementEvent(name, handler);
return null;
}
function Card() {
const [hovered, setHovered] = useState(false);
return (
<Entity name="card">
<Element type="image" anchor={[0.5, 0.5, 0.5, 0.5]} pivot={[0.5, 0.5]}
width={200} height={120} useInput opacity={hovered ? 1 : 0.6} />
<ElementEvent name="mouseenter" handler={() => setHovered(true)} />
<ElementEvent name="mouseleave" handler={() => setHovered(false)} />
</Entity>
);
}
像示例一样通过状态驱动 props,不要在处理函数中直接修改引擎组件。React 每次渲染都会重新应用 <Element> 的 props,从而覆盖直接修改。
<Entity> 的 onClick 和 onPointer* 是另一套基于 GPU 拾取的指针事件。即使未启用 useInput,也能作用于图像和文本,适合简单可点击图像。但它绕过界面输入系统,不提供按钮状态、命中扩展或滚动拖动。
通过 whenReady 等待元素就绪,再在 component 上监听:
import { whenReady } from '@playcanvas/web-components';
const card = await whenReady('pc-entity[name="card"] > pc-element');
card.component.on('mouseenter', () => card.setAttribute('opacity', '1'));
card.component.on('mouseleave', () => card.setAttribute('opacity', '0.6'));
<pc-entity> 的 onclick 和 onpointer* 同样是通过 GPU 拾取的另一套指针事件,虽然可用于图像和文本,但会绕过界面输入,不提供按钮状态、命中扩展或滚动拖动。
每个处理函数接收事件对象。event.element 始终是最初触发事件的元素,即使当前处理函数属于其祖先;event.event 是原始浏览器事件:
| 事件对象 | 适用事件 | 属性 |
|---|---|---|
ElementMouseEvent | mouse* 及鼠标 click | x、y 为相对画布左上角的 CSS 像素位置;dx、dy 为上次事件后的移动量;button;wheelDelta 为 -1、0 或 1;以及 ctrlKey、altKey、shiftKey、metaKey |
ElementTouchEvent | touch* 及触摸 click | 本次触摸的 x、y;浏览器触摸对象 touch;以及与浏览器事件相同的 touches、changedTouches |
ElementSelectEvent | select* 及 XR 选择产生的 click | inputSource,即控制器或手 |
三者还都包含 element、camera(通过哪个摄像机命中元素)及 event。
事件冒泡
事件先在被命中元素上触发,再沿父元素向上,直到遇到没有 Element 的实体。祖先无论是否启用输入都会收到,因此菜单上一个监听器就能处理所有子项点击:
menu.element.on('click', (event) => {
console.log(`${event.element.entity.name} was clicked`);
});
调用 event.stopPropagation() 可阻止继续向上传播。
防止界面输入传到游戏
直接读取鼠标或触摸设备的游戏代码,例如点击射击,也会看到落在界面上的点击。stopPropagation() 不仅停止冒泡,还阻止底层浏览器事件继续执行后续监听器。鼠标和触摸设备监听同一浏览器事件,因此在它们之前创建 ElementInput,即可阻止 HUD 点击进入 app.mouse,包括 app.mouse.wasPressed():
// Presses on any input-enabled element in the HUD never reach app.mouse or app.touch
hud.element.on('mousedown', event => event.stopPropagation());
hud.element.on('touchstart', event => event.stopPropagation());
HUD 自身的组元素应保持输入关闭,子项事件仍会冒泡到它。若开启输入,整个组矩形都会阻止点击进入游戏。
编辑器、React 和 Web Components 已先创建 ElementInput。直接使用引擎时,也应按配置先创建它,再创建鼠标与触摸设备。
哪个元素接收事件
元素重叠时,只有一个接收事件。ElementInput 按以下顺序检测,首次命中即停止:
- 从上到下检查摄像机。 从最后绘制到最先绘制,确保上层界面优先。只有渲染元素所属图层的摄像机才参与检测。
- 从上到下检查图层。 后绘制的图层优先,仅在界面使用多图层时有影响。
- 先屏幕空间元素,再世界空间屏幕元素,最后没有屏幕的元素。
- 先检查最上方元素。 各组内
drawOrder最大者优先,通常是层级中更靠后的元素,详见绘制顺序与性能。
屏幕上的元素一旦命中,即使比其他命中点更远,也会结束搜索。世界空间屏幕重叠时,优先级较高者优先。只有没有屏幕的元素才按距离比较,以最近者为准。
检测区域是元素矩形,而非图像可见像素或字形形状。命中扩展可扩大它,遮罩可裁切它。禁用实体不参与检测。
点击与拖动
鼠标释放或触摸结束时,若仍处于最初按下的同一元素上,则触发 click。释放前移出再移回仍算点击。
在元素上按下后,无论指针移到何处,该元素都会接收全部 mousemove 和最终 mouseup。触摸同理,touchmove 和 touchend 始终交给起始元素,因此指针离开滑块或滚动视图后仍能继续拖动。
触摸屏
触摸点击后,浏览器会模拟鼠标事件以兼容只处理鼠标的页面。ElementInput 忽略同一元素上的重复模拟 click,但其他事件仍会到达,包括 mouseenter、mousedown 和 mouseup。若点击后隐藏元素,例如按钮关闭对话框,模拟点击就会落到后方对象。取消画布的 touchend 默认行为可阻止模拟,适用于所有开发方式:
app.graphicsDevice.canvas.addEventListener('touchend', (event) => {
event.preventDefault();
});
ElementInput 还会取消画布上的所有 touchmove,因此从画布开始的触摸不会滚动页面。画布嵌在长网页中时需要考虑这一点。
指针锁定
第一人称游戏等锁定指针时,ElementInput 会忽略鼠标按下。显示菜单前调用 app.mouse.disablePointerLock(),关闭后再锁定。
禁用界面输入
- 关闭
useInput可停止单个元素接收输入,禁用实体也不接收输入。 - 设置按钮
active为false可保留显示但不响应,详见按钮。 - 设置
app.elementInput.enabled为false可暂停全部界面输入,例如等待菜单退出动画。此属性尚未收录于 API 参考。
另请参阅
- 按钮:悬停、按下与按钮专有事件。
- XR 界面:用控制器和手指向元素。
- 触屏摇杆:基于触摸事件构建屏幕摇杆。
- Element 组件、
<pc-element>和 ElementComponent:包含useInput的全部属性参考。 - ElementInput:负责向元素传递输入的对象参考。