بنقرة واحدة
galacean-interaction
此 Skill 包含了在 Galacean Engine 中通过 Script 脚本处理物体交互(如点击)的核心模式,基于 onPointerClick 实现
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
此 Skill 包含了在 Galacean Engine 中通过 Script 脚本处理物体交互(如点击)的核心模式,基于 onPointerClick 实现
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
此 Skill 约定了 Galacean Engine 中自定义着色器的编写方式,包含 Shader.create、BaseMaterial 参数绑定、动画驱动、透明混合与常见排错。
此 Skill 包含了使用 Galacean Engine 物理引擎的注意事项、距离检测替代方案以及常见问题解决方案
此 Skill 包含了在 Galacean Engine 中创建物理碰撞体的核心模式,涵盖动态碰撞体 (DynamicCollider) 和 静态碰撞体 (StaticCollider) 的实现
使用 Galacean Engine 开发2D游戏的核心模式,包括真2D(Sprite)和伪2D(3D正交投影)实现
此 Skill 包含了 Galacean Engine 相机的配置方法,包括透视/正交投影设置、俯视视角配置、窗口适配等
Galacean Engine 移动端游戏开发指南,包含适配、优化和最佳实践
| name | galacean-interaction |
| description | 此 Skill 包含了在 Galacean Engine 中通过 Script 脚本处理物体交互(如点击)的核心模式,基于 onPointerClick 实现 |
在 Galacean Engine 中,通过编写继承自 Script 的脚本组件并实现 onPointerClick 等生命周期函数,可以轻松实现物体的点击交互。
需引入 Script 类以及可能需要操作的碰撞体组件:
import { Script, Entity, DynamicCollider, StaticCollider } from '@galacean/engine'
适用场景:物体被鼠标或触摸点击时触发逻辑(如消除方块、触发跳跃)。
核心机制:
Script 并实现 onPointerClick()。DynamicCollider 或 StaticCollider,引擎的输入系统会通过物理射线检测来触发点击事件。代码模式:
class InteractionScript extends Script {
// 可选:缓存 Collider 组件以便在点击时修改其状态
private _collider?: DynamicCollider;
onAwake() {
// 获取同级挂载的 Collider 组件
this._collider = this.entity.getComponent(DynamicCollider)!;
}
/**
* 当指针(鼠标/触摸)点击该物体时触发
*/
onPointerClick() {
// 1. 执行交互逻辑,例如修改游戏状态
console.log("Clicked:", this.entity.name);
// 2. 操作 Collider (例如禁用,防止再次被点击)
if (this._collider) {
this._collider.enabled = false;
}
// 3. 触发其他行为 (如动画、移动)
// this.doSomething();
}
}
结合实际应用场景,点击交互常包含以下步骤:
// 示例:点击方块逻辑
onPointerClick() {
// Check game state
if (!canInteract) return;
// Disable physics interaction
if (this._collider) {
this._collider.enabled = false;
}
// Visual feedback
this.entity.transform.setScale(0.8, 0.8, 0.8);
// Trigger movement or logic
this.startMoveAnimation();
}
适用场景:需要监听整个屏幕的点击事件(如点击屏幕任意位置发射子弹、点击屏幕继续游戏),而不需要判定点击了哪个具体 3D 物体。
实现方式:直接对 Canvas 元素添加事件监听器。
代码模式:
建议在专门的控制脚本(如 GameControlScript)中管理全局事件:
export class GameControlScript extends Script {
onAwake() {
// 1. 获取 Canvas 元素 (假设 ID 为 'canvas')
const canvas = document.getElementById('canvas');
// 2. 绑定原生事件
if (canvas) {
canvas.addEventListener('pointerdown', this.handlePointerDown);
}
}
// 使用箭头函数以保留 this 上下文
handlePointerDown = (e: PointerEvent) => {
// 3. 处理点击逻辑
console.log("Global Click:", e.clientX, e.clientY);
// 示例:根据点击位置发射子弹或处理 UI
// this.shoot(e.clientX, e.clientY);
}
onDestroy() {
// 4. 清理事件监听,防止内存泄漏
const canvas = document.getElementById('canvas');
if (canvas) {
canvas.removeEventListener('pointerdown', this.handlePointerDown);
}
}
}
适用场景:移动设备上的虚拟摇杆控制,需要全屏响应触摸事件。
touchend 和 touchcancel,避免意外情况下摇杆卡住getBoundingClientRect() 获取准确的触摸位置export class JoystickScript extends Script {
private joystickActive: boolean = false;
private joystickCenter: { x: number, y: number } = { x: 0, y: 0 };
private joystickRadius: number = 60;
onAwake() {
const canvas = document.getElementById('canvas');
if (!canvas) return;
// 触摸开始 - 全屏任意位置都可触发
canvas.addEventListener('touchstart', (e: TouchEvent) => {
const touch = e.touches[0];
const rect = canvas.getBoundingClientRect();
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
// 不要在 here 限制区域,让全屏都可以触发摇杆
this.joystickActive = true;
this.joystickCenter = { x: touch.clientX, y: touch.clientY };
// 显示摇杆UI在触摸位置
this.showJoystickAt(x, y);
});
// 触摸移动
canvas.addEventListener('touchmove', (e: TouchEvent) => {
if (!this.joystickActive) return;
e.preventDefault(); // 防止页面滚动
const touch = e.touches[0];
this.updateJoystick(touch.clientX, touch.clientY);
});
// 触摸结束
canvas.addEventListener('touchend', () => {
this.resetJoystick();
});
// 触摸取消(如来电打断)
canvas.addEventListener('touchcancel', () => {
this.resetJoystick();
});
}
private updateJoystick(touchX: number, touchY: number): void {
const dx = touchX - this.joystickCenter.x;
const dy = touchY - this.joystickCenter.y;
const distance = Math.sqrt(dx * dx + dy * dy);
// 限制摇杆移动范围
let normalizedX = dx;
let normalizedY = dy;
if (distance > this.joystickRadius) {
normalizedX = (dx / distance) * this.joystickRadius;
normalizedY = (dy / distance) * this.joystickRadius;
}
// 计算归一化方向 (-1 到 1)
const dirX = normalizedX / this.joystickRadius;
const dirY = normalizedY / this.joystickRadius;
// 应用移动逻辑
this.applyMovement(dirX, dirY);
}
private resetJoystick(): void {
this.joystickActive = false;
this.hideJoystick();
this.applyMovement(0, 0); // 停止移动
}
// 抽象方法:由子类实现
showJoystickAt(x: number, y: number): void {}
hideJoystick(): void {}
applyMovement(x: number, y: number): void {}
}
在使用 Galacean 的 inputManager.pointers 处理拖拽交互时,移动端触摸松开(touchend)会遇到一个关键问题:
问题:当手指从屏幕上松开时,pointers 数组会立即变为空,导致 PointerPhase.Up 事件无法被捕获。
症状:
// ❌ 这种方式在移动端会失败
onUpdate() {
const pointers = this.engine.inputManager.pointers;
if (pointers && pointers.length > 0) {
const pointer = pointers[0];
if (pointer.phase === PointerPhase.Down) {
this.startDrag();
}
else if (pointer.phase === PointerPhase.Move && this.isDragging) {
this.updateDrag();
}
else if (pointer.phase === PointerPhase.Up && this.isDragging) {
// ❌ 在移动端,手指松开后 pointers 数组为空
// 这段代码永远不会执行!
this.endDrag();
}
}
}
问题分析:
pointers 数组,phase = Downphase = Movepointers.length = 0,导致整个 if 块不执行核心思路:当 pointers 为空但 isDragging 仍为 true 时,说明手指刚刚松开。
// ✅ 兼容 PC 和移动端的正确实现
export class DragScript extends Script {
private isDragging: boolean = false;
private currentDragPos: Vector3 = new Vector3();
onUpdate() {
const inputManager = this.engine.inputManager;
const pointers = inputManager.pointers;
if (pointers && pointers.length > 0) {
const pointer = pointers[0];
// 开始拖拽(鼠标按下 / 触摸开始)
if (pointer.phase === PointerPhase.Down) {
this.handlePointerDown(pointer);
}
// 拖拽中(鼠标移动 / 触摸移动)
else if (pointer.phase === PointerPhase.Move && this.isDragging) {
this.handlePointerMove(pointer);
}
// 结束拖拽(鼠标松开 - 仅 PC 端会触发)
else if (pointer.phase === PointerPhase.Up && this.isDragging) {
this.handlePointerUp();
}
}
// ✅ 关键:处理移动端触摸松开
// 当 pointers 为空但还在拖拽状态时,说明手指刚松开
else if (this.isDragging) {
this.handlePointerUp();
}
}
private handlePointerDown(pointer: any) {
// 检查是否点击在目标区域
const worldPos = this.screenToWorld(pointer.position.x, pointer.position.y);
if (this.isInTargetArea(worldPos)) {
this.isDragging = true;
}
}
private handlePointerMove(pointer: any) {
const worldPos = this.screenToWorld(pointer.position.x, pointer.position.y);
if (worldPos) {
this.currentDragPos.copyFrom(worldPos);
this.updateDragPosition(worldPos);
}
}
private handlePointerUp() {
if (!this.isDragging) return;
this.isDragging = false;
// 使用 currentDragPos 计算松手时的位置/速度
// 注意:此时无法从 pointer 获取位置,必须使用之前保存的值
this.performAction(this.currentDragPos);
}
private performAction(finalPos: Vector3) {
// 执行松手后的逻辑(如发射、释放等)
console.log('Released at:', finalPos);
}
}
保存拖拽状态:
isDragging 标志跟踪拖拽状态currentDragPos 保存最后的拖拽位置双重松开检测:
// PC 端:pointer.phase === PointerPhase.Up
if (pointer.phase === PointerPhase.Up && this.isDragging) {
this.handlePointerUp();
}
// 移动端:pointers 为空 + isDragging 为 true
else if (this.isDragging) {
this.handlePointerUp();
}
不依赖 pointer 参数:
handlePointerUp() 不应依赖 pointer 参数currentDragPos)状态重置:
handlePointerUp() 中立即设置 isDragging = false弹弓发射(Angry Birds 风格):
export class SlingshotScript extends Script {
private isDragging: boolean = false;
private dragStartPos: Vector3 = new Vector3();
private currentDragPos: Vector3 = new Vector3();
onUpdate() {
const pointers = this.engine.inputManager.pointers;
if (pointers && pointers.length > 0) {
const pointer = pointers[0];
if (pointer.phase === PointerPhase.Down) {
const worldPos = this.screenToWorld(pointer.position);
if (this.isNearBird(worldPos)) {
this.isDragging = true;
this.dragStartPos.copyFrom(worldPos);
}
}
else if (pointer.phase === PointerPhase.Move && this.isDragging) {
const worldPos = this.screenToWorld(pointer.position);
this.currentDragPos.copyFrom(worldPos);
this.updateBirdPosition(worldPos);
}
else if (pointer.phase === PointerPhase.Up && this.isDragging) {
this.launchBird();
}
}
// ✅ 移动端松手处理
else if (this.isDragging) {
this.launchBird();
}
}
private launchBird() {
this.isDragging = false;
// 使用保存的位置计算发射速度
const velocity = new Vector3();
Vector3.subtract(this.dragStartPos, this.currentDragPos, velocity);
velocity.scale(5);
this.bird.launch(velocity);
}
}
如果遇到触摸事件问题,可以添加日志:
onUpdate() {
const pointers = this.engine.inputManager.pointers;
console.log('Pointers:', pointers?.length, 'Dragging:', this.isDragging);
if (pointers && pointers.length > 0) {
console.log('Pointer phase:', pointers[0].phase);
}
// ... 其他逻辑
}
预期输出:
Pointers: 1 Dragging: false → Pointers: 1 Dragging: truePointers: 1 Dragging: truePointers: 0 Dragging: true → Pointers: 0 Dragging: false| 场景 | 推荐做法 | 避免做法 |
|---|---|---|
| 检测松手 | 检查 pointers.length === 0 && isDragging | 只检查 pointer.phase === Up |
| 保存位置 | 在 Move 阶段持续保存 currentDragPos | 在 Up 时才从 pointer 读取 |
| 状态管理 | 使用 isDragging 布尔标志 | 依赖 pointers 数组判断状态 |
| 参数依赖 | handlePointerUp() 使用保存的数据 | handlePointerUp(pointer) 依赖参数 |