orillusion入门系列六 | 系统交互02
-
在前一期系统交互01中学习了最基本的键盘和鼠标输入,挂载键盘输入系统后能够获得键盘的输入按键,挂载了触控事件,可以在场景中监控到鼠标或触控的操作。今天继续了解引擎提供的系统交互功能,再根据一些项目实践,丰富应用实例。
物体拾取
欲与物体交互首先需要识别出目标对象,在一个3D场景中最方便的识别出目标物体的方式无疑是通过鼠标直接选择。引擎提供的物体拾取功能可以轻松的完成这个目标。
基本用法
Orillusion的拾取功能使用起来非常简单,只需要做一个初始化的配置和挂载事件,需要注意的是拾取模式有像素和包围盒两种,一般推荐像素。具体的用法总结一下:
- 初始化
// 开启拾取功能 Engine3D.setting.pick.enable = true; // 设置拾取模式,pixel 像素, bound 包围盒 Engine3D.setting.pick.mode = `pixel`; // 初始化引擎 await Engine3D.init(); ......
- 挂载事件
// 创建一个物理对象 let boxObj = new Object3D(); // 挂载碰撞体组件 boxObj.addComponent(ColliderComponent); // 开启拾取事件 Engine3D.pickFire.addEventListener(PointerEvent3D.PICK_CLICK, onPick, this);
这里挂载了 PointerEvent3D.PICK_CLICK 点击事件,其它还有更多事件可以参考官方文档。
- 响应事件
对拾取功能的使用关键在响应事件的处理,例如 onPick的响应类型是 PointerEvent3D 类型的变量
onPick(e: PointerEvent3D) { console.log('onPick', 'pickInfo:',e.data.pickInfo) console.log('onPick', 'target:',e.target) }
在以上代码可以看到,e.target是一个被点击的 Object3D类型的物理对象,e.data.pickInfo是点击的具体信息,例如世界坐标和屏幕坐标。
完整示例
写一个完整的示例,在该示例中创建一个简单的场景,添加两个立方体,其中一个添加了碰撞体组件,在挂载事件中响应事件,将该物理对象颜色设置成一个随机色,所在能够达到点击第一个物体后变色,而第二个立方体因为没有挂载碰撞体所以不会有效果。
import { Camera3D, Collider, ColliderComponent, Engine3D, ForwardRenderJob, LitMaterial, HoverCameraController, MeshRenderer, Object3D, PointerEvent3D, Scene3D, webGPUContext, BoxGeometry, } from '@orillusion/core'; export default class PixelPick { cameraObj: Object3D; camera: Camera3D; scene: Scene3D; controller: HoverCameraController; async run() { await this.init(); await this.setup(); await this.start(); } /*** * 配置并初始化引擎 */ private async init() { Engine3D.setting.pick.enable = true; Engine3D.setting.pick.mode = `pixel`; // 初始化引擎 await Engine3D.init(); // 创建一个场景 this.scene = new Scene3D(); // // 创建一个相机 this.cameraObj = new Object3D(); this.camera = this.cameraObj.addComponent(Camera3D); // // 设置相机类型 this.camera.perspective(60, webGPUContext.aspect, 1, 5000.0); // // 设置相机控制器 this.controller = this.cameraObj.addComponent(HoverCameraController); this.controller.setCamera(20, -20, 25); // 添加相机至场景 this.scene.addChild(this.cameraObj); } /** * 引擎功能代码 */ private async setup() { { let boxObj = new Object3D(); // 添加碰撞体检测 boxObj.addComponent(ColliderComponent); let mr: MeshRenderer = boxObj.addComponent(MeshRenderer); mr.geometry = new BoxGeometry(3, 3, 3); // 设置材质 mr.material = new LitMaterial(); // 添加对象至场景 this.scene.addChild(boxObj); } { let boxObj = new Object3D(); boxObj.x = 10; let mr: MeshRenderer = boxObj.addComponent(MeshRenderer); mr.geometry = new BoxGeometry(3, 3, 3); // 设置材质 mr.material = new LitMaterial(); // 添加对象至场景 this.scene.addChild(boxObj); } } /** * 启动渲染 */ private async start() { let view = new View3D(); // 指定渲染的场景 view.scene = this.scene; // 指定使用的相机 view.camera = this.camera; // 开始渲染 Engine3D.startRenderView(view); // 开启拾取事件 view.pickFire.addEventListener(PointerEvent3D.PICK_CLICK, this.onPick, this); } private onPick(e: PointerEvent3D) { console.log('onPick', 'pickInfo:', e.data.pickInfo) console.log('onPick', 'target:', e.target) let obj = e.target as Object3D; let mr = obj.getComponent(MeshRenderer); mr.material.baseColor = Color.random() } }
需要注意开启拾取事件必须在开始渲染之后,否则会有一个异常。
该示例运行效果如下:点击第一个物体会变色,第二个不会有响应
包围盒拾取
使用包围盒拾取不够精确,简单介绍一下使用方法,和像素拾取做一个对比说明。
- 设置包围盒类型为bound,即: Engine3D.setting.pick.mode =
bound
; - 创建包围盒
// 设置包围盒尺寸 let size: number = 2; // 创建包围盒形状 let shape: BoxColliderShape = new BoxColliderShape().setFromCenterAndSize(new Vector3(0, 0, 0), new Vector3(size, size, size)); //加一个碰撞盒子。 let collider = boxObj.addComponent(ColliderComponent); // 设置碰撞盒子形状 collider.shape = shape;
其它方面用法与像素用法无异。
自定义事件
在ECS架构下不同功能模块做到了方便的解耦和组合,但是组件间的通信的通信是不可缺少的,引擎提供了自定义事件主要用于不同组件间的通信。
自定义事件的和系统类事件(键盘、触控、拾取)类似。- 挂载事件
// 与其它类似事件类似,直接将一个定定义名称作为事件名挂载 let name = "customerEvent"; Engine3D.inputSystem.addEventListener(name, OnCustomerEvent, this); // 定义响应函数 OnCustomerEvent(e: CEvent) { console.log(e.type, e.data); let params = e.data; console.log(params); }
- 发送事件
// 定义事件参数 let params = { }; // 与响应事件定义相同的事件名称 let name = "customerEvent"; // 定义一个事件 let e = new CEvent(name, params) // 触发消息 Engine3D.inputSystem.dispatchEvent(e);
- 删除事件
// 挂载的事件类型在不需要时可以删除该事件挂载 let name = "customerEvent"; // 删除事件 Engine3D.inputSystem.removeEventListener(name, OnCustomerEvent, this);
以上定义了一个名称为customerEvent的事件,响应函数为OnCustomerEvent,在需要发送事件时可以通过Engine3D.inputSystem.dispatchEvent触发,响应函数会自动得到调用。
综合示例
引擎提供了灵活的底层支持功能,但是实际项目中的需求是多变的,这里总结一下常用的经典用法。
单击与双击
许多情况下需要处理鼠标的双击或单击事件,无论是通过 Engine3D.inputSystem.addEventListener 挂载的全场景事件还是通过 Engine3D.pickFire.addEventListener挂载的拾取事件,要实现区分单击与双击的思路都是相同的。
只需要在击事件中判断间隔时间与是否同一目标一致,当点击的时间在一定间隔(300ms)内只有一次时可断定为单击,当在间隔时间内有第二次点击,则清理掉定时器并判定为单击。// 记录操作时间 let timer; // 记录目标 let target; Engine3D.pickFire.addEventListener(PointerEvent3D.PICK_CLICK, (e: PointerEvent3D) => { if (e.target === target && timer) { clearTimeout(timer); // 双击 timer = undefined; } else { target = e.target timer = setTimeout(() => { timer = undefined; // 单击 }, 300); } }, this);
监控对象
在某些情况下,物体的变换参数需要实时的监控,当监控到变化时触发某些业务场景。场景没有提供直接的功能,但是我们可以通过脚本来实现。
具体思路是创建一个用于监控的脚本,在该脚本中记录参数的变化,在每一帧中进行判断。再将该脚本挂载到需要监控的物理对象中。下面我们定义一个这样的脚本,本脚本仅以 position为例,需要其它参数可以类似增加。class ActionAttachScript extends ComponentBase { _position: Vector3 = new Vector3(); start() { console.log('start ActionAttachScript') this._position = this.object3D.localPosition.clone(); } onNotify(){ // 处理变化业务 } update() { let position = this.transform.localPosition; if (!this._position.equals(position)) { this.onNotify(); this._position.set(position.x, position.y, position.z); } } destroy(): void { console.log('destroy ActionAttachScript') } }
小结
本节学习了系统交互的两个重要的功能:拾取和事件。在普通的业务系统中一般不需要直接处理拾取功能,已经由业务框架自动处理了,但是无论在2D还是3D系统中都需要面对拾取的问题,特别是在3D系统中,不同的对象形状不同的位置,如果物体正处于运动中会更加复杂,所以引擎提供了基础的功能后,还需要手动的处理拾取事件。自定义事件更好的封装业务,配合自定义组件,对于开发复杂大规模的系统提供了比较完备的支持。
作为3D新手,后续会不断的记录学习过程,期待与你一起学习一起飞!