ワンクリックで
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) 依赖参数 |