diff --git a/packages/core/src/Script.ts b/packages/core/src/Script.ts index 44d2fd2edf..6fa48ddd13 100644 --- a/packages/core/src/Script.ts +++ b/packages/core/src/Script.ts @@ -148,12 +148,29 @@ export class Script extends Component { onPointerExit(pointer: Pointer): void {} /** - * Called when the pointer is down while over the ColliderShape and is still holding down. + * This function will be called when the pointer is pressed on the collider. + * @param pointer + */ + onPointerStartDrag(pointer: Pointer): void {} + + /** + * When a drag collision occurs on the pointer, this function will be called every time it moves. * @param pointer - The pointer that triggered - * @remarks onPointerDrag is called every frame while the pointer is down. */ onPointerDrag(pointer: Pointer): void {} + /** + * This function will be called when the pointer is released while dragging. + * @param pointer - The pointer that triggered + */ + onPointerEndDrag(pointer: Pointer): void {} + + /** + * This function will be called when the pointer is lifted on the collider. + * @param pointer - The pointer that triggered + */ + onPointerDrop(pointer: Pointer): void {} + /** * Called when be disabled. */ diff --git a/packages/core/src/input/pointer/Pointer.ts b/packages/core/src/input/pointer/Pointer.ts index cc06fd2cf5..5622e68708 100644 --- a/packages/core/src/input/pointer/Pointer.ts +++ b/packages/core/src/input/pointer/Pointer.ts @@ -2,6 +2,7 @@ import { Vector2 } from "@galacean/engine-math"; import { DisorderedArray } from "../../DisorderedArray"; import { Entity } from "../../Entity"; import { Script } from "../../Script"; +import { HitResult } from "../../physics"; import { PointerButton } from "../enums/PointerButton"; import { PointerPhase } from "../enums/PointerPhase"; @@ -24,9 +25,13 @@ export class Pointer { position: Vector2 = new Vector2(); /** The change of the pointer. */ deltaPosition: Vector2 = new Vector2(); + /** The hit result of raycasting all scenes using pointer in this frame. */ + hitResult: HitResult = new HitResult(); /** @internal */ _events: PointerEvent[] = []; /** @internal */ + _eventsMap: number = PointerEventType.None; + /** @internal */ _uniqueID: number; /** @internal */ _upMap: number[] = []; @@ -37,16 +42,45 @@ export class Pointer { /** @internal */ _downList: DisorderedArray = new DisorderedArray(); - private _currentPressedEntity: Entity; - private _currentEnteredEntity: Entity; + private _pressedEntity: Entity; + private _enteredEntity: Entity; + private _draggedEntity: Entity; + + /** + * If this pointer is hold down, return the entity hit when pointer down. + */ + get pressedEntity(): Entity | null { + return this._pressedEntity; + } + + /** + * Returns the entity where the pointer is currently entered. + */ + get enteredEntity(): Entity | null { + return this._enteredEntity; + } + + /** + * Returns the entity currently dragged by the pointer. + */ + get draggedEntity(): Entity | null { + return this._draggedEntity; + } + + /** + * @internal + */ + constructor(id: number) { + this.id = id; + } /** * @internal */ _firePointerExitAndEnter(rayCastEntity: Entity): void { - if (this._currentEnteredEntity !== rayCastEntity) { - if (this._currentEnteredEntity) { - this._currentEnteredEntity._scripts.forEach( + if (this._enteredEntity !== rayCastEntity) { + if (this._enteredEntity) { + this._enteredEntity._scripts.forEach( (element: Script) => { element.onPointerExit(this); }, @@ -65,33 +99,53 @@ export class Pointer { } ); } - this._currentEnteredEntity = rayCastEntity; + this._enteredEntity = rayCastEntity; } } /** * @internal */ - _firePointerDown(rayCastEntity: Entity): void { + _firePointerDownAndStartDrag(rayCastEntity: Entity): void { + this._pressedEntity = this._draggedEntity = rayCastEntity; if (rayCastEntity) { rayCastEntity._scripts.forEach( (element: Script) => { element.onPointerDown(this); + element.onPointerStartDrag(this); + }, + (element: Script, index: number) => { + element._entityScriptsIndex = index; + } + ); + } + } + + /** + * @internal + */ + _firePointerUpAndClick(rayCastEntity: Entity): void { + if (rayCastEntity) { + const sameTarget = this._pressedEntity === rayCastEntity; + rayCastEntity._scripts.forEach( + (element: Script) => { + element.onPointerUp(this); + sameTarget && element.onPointerClick(this); }, (element: Script, index: number) => { element._entityScriptsIndex = index; } ); } - this._currentPressedEntity = rayCastEntity; + this._pressedEntity = null; } /** * @internal */ _firePointerDrag(): void { - if (this._currentPressedEntity) { - this._currentPressedEntity._scripts.forEach( + if (this._draggedEntity) { + this._draggedEntity._scripts.forEach( (element: Script) => { element.onPointerDrag(this); }, @@ -105,27 +159,36 @@ export class Pointer { /** * @internal */ - _firePointerUpAndClick(rayCastEntity: Entity): void { - const { _currentPressedEntity: pressedEntity } = this; - if (pressedEntity) { - const sameTarget = pressedEntity === rayCastEntity; - pressedEntity._scripts.forEach( + _firePointerEndDrag(receivingEntity: Entity): void { + const { _draggedEntity: draggedEntity } = this; + if (draggedEntity) { + draggedEntity._scripts.forEach( (element: Script) => { - sameTarget && element.onPointerClick(this); - element.onPointerUp(this); + element.onPointerEndDrag(this); + !!receivingEntity && element.onPointerDrop(this); }, (element: Script, index: number) => { element._entityScriptsIndex = index; } ); - this._currentPressedEntity = null; + this._draggedEntity = null; } } /** * @internal */ - constructor(id: number) { - this.id = id; + _dispose(): void { + const { hitResult } = this; + this._enteredEntity = this._pressedEntity = this._draggedEntity = hitResult.entity = hitResult.shape = null; } } + +export enum PointerEventType { + None = 0x0, + Down = 0x1, + Up = 0x2, + Leave = 0x4, + Move = 0x8, + Cancel = 0x10 +} diff --git a/packages/core/src/input/pointer/PointerManager.ts b/packages/core/src/input/pointer/PointerManager.ts index 17871481cf..3babb65ef3 100644 --- a/packages/core/src/input/pointer/PointerManager.ts +++ b/packages/core/src/input/pointer/PointerManager.ts @@ -9,7 +9,7 @@ import { HitResult } from "../../physics"; import { PointerButton, _pointerDec2BinMap } from "../enums/PointerButton"; import { PointerPhase } from "../enums/PointerPhase"; import { IInput } from "../interface/IInput"; -import { Pointer } from "./Pointer"; +import { Pointer, PointerEventType } from "./Pointer"; /** * Pointer Manager. @@ -18,7 +18,6 @@ import { Pointer } from "./Pointer"; export class PointerManager implements IInput { private static _tempRay: Ray = new Ray(); private static _tempPoint: Vector2 = new Vector2(); - private static _tempHitResult: HitResult = new HitResult(); /** @internal */ _pointers: Pointer[] = []; /** @internal */ @@ -74,6 +73,7 @@ export class PointerManager implements IInput { // Clean up the pointer released in the previous frame for (let i = pointers.length - 1; i >= 0; i--) { if (pointers[i].phase === PointerPhase.Leave) { + pointers[i]._dispose(); pointers.splice(i, 1); } } @@ -113,6 +113,7 @@ export class PointerManager implements IInput { for (let i = 0, n = pointers.length; i < n; i++) { const pointer = pointers[i]; pointer._upList.length = pointer._downList.length = 0; + pointer._eventsMap = PointerEventType.None; this._updatePointerInfo(frameCount, pointer, left, top, widthDPR, heightDPR); this._buttons |= pointer.pressedButtons; } @@ -126,26 +127,36 @@ export class PointerManager implements IInput { for (let i = 0, n = pointers.length; i < n; i++) { const pointer = pointers[i]; const { _events: events, position } = pointer; - pointer._firePointerDrag(); - const rayCastEntity = this._pointerRayCast(scenes, position.x / canvas.width, position.y / canvas.height); + const rayCastEntity = this._pointerRayCast( + scenes, + position.x / canvas.width, + position.y / canvas.height, + pointer.hitResult + ); pointer._firePointerExitAndEnter(rayCastEntity); const length = events.length; if (length > 0) { + if (pointer._eventsMap & PointerEventType.Move) { + pointer.phase = PointerPhase.Move; + pointer._firePointerDrag(); + } for (let i = 0; i < length; i++) { const event = events[i]; switch (event.type) { case "pointerdown": pointer.phase = PointerPhase.Down; - pointer._firePointerDown(rayCastEntity); + pointer._firePointerDownAndStartDrag(rayCastEntity); break; case "pointerup": pointer.phase = PointerPhase.Up; pointer._firePointerUpAndClick(rayCastEntity); + pointer._firePointerEndDrag(rayCastEntity); break; case "pointerleave": case "pointercancel": pointer.phase = PointerPhase.Leave; pointer._firePointerExitAndEnter(null); + pointer._firePointerEndDrag(null); break; } } @@ -208,6 +219,7 @@ export class PointerManager implements IInput { pointer._downList.add(button); pointer._downMap[button] = frameCount; pointer.phase = PointerPhase.Down; + pointer._eventsMap |= PointerEventType.Down; break; case "pointerup": _upList.add(button); @@ -215,13 +227,20 @@ export class PointerManager implements IInput { pointer._upList.add(button); pointer._upMap[button] = frameCount; pointer.phase = PointerPhase.Up; + pointer._eventsMap |= PointerEventType.Up; break; case "pointermove": pointer.phase = PointerPhase.Move; + pointer._eventsMap |= PointerEventType.Move; break; case "pointerleave": + pointer.phase = PointerPhase.Leave; + pointer._eventsMap |= PointerEventType.Leave; + break; case "pointercancel": pointer.phase = PointerPhase.Leave; + pointer._eventsMap |= PointerEventType.Cancel; + break; default: break; } @@ -233,8 +252,13 @@ export class PointerManager implements IInput { } } - private _pointerRayCast(scenes: readonly Scene[], normalizedX: number, normalizedY: number): Entity { - const { _tempPoint: point, _tempRay: ray, _tempHitResult: hitResult } = PointerManager; + private _pointerRayCast( + scenes: readonly Scene[], + normalizedX: number, + normalizedY: number, + hitResult: HitResult + ): Entity { + const { _tempPoint: point, _tempRay: ray } = PointerManager; for (let i = scenes.length - 1; i >= 0; i--) { const scene = scenes[i]; if (!scene.isActive || scene.destroyed) { diff --git a/tests/src/core/input/InputManager.test.ts b/tests/src/core/input/InputManager.test.ts index 32d349b44f..768fa1bba6 100644 --- a/tests/src/core/input/InputManager.test.ts +++ b/tests/src/core/input/InputManager.test.ts @@ -81,13 +81,15 @@ describe("InputManager", async () => { } const cameraEntity = root.createChild("camera"); - cameraEntity.transform.setPosition(0, 0, 1); - cameraEntity.addComponent(Camera); + cameraEntity.transform.setPosition(5, 5, 5); + const camera = cameraEntity.addComponent(Camera); + camera.orthographicSize = 5; const boxEntity = root.createChild("box"); + boxEntity.transform.setPosition(5, 5, 0); const collider = boxEntity.addComponent(StaticCollider); const boxShape = new BoxColliderShape(); - boxShape.size = new Vector3(10, 10, 10); + boxShape.size = new Vector3(2, 2, 2); collider.addShape(boxShape); class TestScript extends Script { @@ -103,16 +105,28 @@ describe("InputManager", async () => { console.log("onPointerDown"); } + onPointerUp(pointer: Pointer): void { + console.log("onPointerUp"); + } + onPointerClick(pointer: Pointer): void { console.log("onPointerClick"); } + onPointerStartDrag(pointer: Pointer): void { + console.log("onPointerStartDrag"); + } + onPointerDrag(pointer: Pointer): void { console.log("onPointerDrag"); } - onPointerUp(pointer: Pointer): void { - console.log("onPointerUp"); + onPointerEndDrag(pointer: Pointer): void { + console.log("onPointerEndDrag"); + } + + onPointerDrop(pointer: Pointer): void { + console.log("onPointerDrop"); } } TestScript.prototype.onPointerEnter = chai.spy(TestScript.prototype.onPointerEnter); @@ -121,10 +135,11 @@ describe("InputManager", async () => { TestScript.prototype.onPointerClick = chai.spy(TestScript.prototype.onPointerClick); TestScript.prototype.onPointerDrag = chai.spy(TestScript.prototype.onPointerDrag); TestScript.prototype.onPointerUp = chai.spy(TestScript.prototype.onPointerUp); + TestScript.prototype.onPointerDrop = chai.spy(TestScript.prototype.onPointerDrop); const script = boxEntity.addComponent(TestScript); const { left, top } = target.getBoundingClientRect(); - target.dispatchEvent(generatePointerEvent("pointerdown", 2, left + 2, top + 2)); + target.dispatchEvent(generatePointerEvent("pointerdown", 2, left + 2.5, top + 2.5)); engine.update(); expect(script.onPointerEnter).to.have.been.called.exactly(1); @@ -134,9 +149,9 @@ describe("InputManager", async () => { expect(script.onPointerDrag).to.have.been.called.exactly(0); expect(script.onPointerUp).to.have.been.called.exactly(0); - target.dispatchEvent(generatePointerEvent("pointermove", 2, left + 2, top + 2)); - target.dispatchEvent(generatePointerEvent("pointerup", 2, left + 2, top + 2, 0, 0)); - target.dispatchEvent(generatePointerEvent("pointerleave", 2, left + 2, top + 2, -1, 0)); + target.dispatchEvent(generatePointerEvent("pointermove", 2, left + 2.5, top + 2.5)); + target.dispatchEvent(generatePointerEvent("pointerup", 2, left + 2.5, top + 2.5, 0, 0)); + target.dispatchEvent(generatePointerEvent("pointerleave", 2, left + 2.5, top + 2.5, -1, 0)); engine.update(); expect(script.onPointerEnter).to.have.been.called.exactly(1); expect(script.onPointerExit).to.have.been.called.exactly(1); @@ -144,6 +159,7 @@ describe("InputManager", async () => { expect(script.onPointerClick).to.have.been.called.exactly(1); expect(script.onPointerDrag).to.have.been.called.exactly(1); expect(script.onPointerUp).to.have.been.called.exactly(1); + expect(script.onPointerDrop).to.have.been.called.exactly(1); target.dispatchEvent(generatePointerEvent("pointerdown", 3, left + 200, top + 200)); target.dispatchEvent(generatePointerEvent("pointerup", 3, left + 200, top + 200, 0, 0)); @@ -162,6 +178,87 @@ describe("InputManager", async () => { expect(deltaPosition).deep.equal(new Vector2(0, 0)); target.dispatchEvent(generatePointerEvent("pointerleave", 4, 0, 0)); engine.update(); + + // 新增测试 onPointerClick + // 1. 碰撞体上 down + // 2. move 出碰撞体 + // 3. move 回碰撞体 + // 4. up & leave + target.dispatchEvent(generatePointerEvent("pointerdown", 5, left + 2.5, top + 2.5)); + engine.update(); + expect(script.onPointerEnter).to.have.been.called.exactly(2); + expect(script.onPointerDown).to.have.been.called.exactly(2); + expect(script.onPointerExit).to.have.been.called.exactly(1); + expect(script.onPointerDrag).to.have.been.called.exactly(1); + target.dispatchEvent(generatePointerEvent("pointermove", 5, left + 200, top + 200)); + engine.update(); + expect(script.onPointerEnter).to.have.been.called.exactly(2); + expect(script.onPointerExit).to.have.been.called.exactly(2); + expect(script.onPointerDrag).to.have.been.called.exactly(2); + target.dispatchEvent(generatePointerEvent("pointermove", 5, left + 2.5, top + 2.5)); + engine.update(); + expect(script.onPointerEnter).to.have.been.called.exactly(3); + expect(script.onPointerExit).to.have.been.called.exactly(2); + expect(script.onPointerDown).to.have.been.called.exactly(2); + target.dispatchEvent(generatePointerEvent("pointerup", 5, left + 2.5, top + 2.5, 0, 0)); + target.dispatchEvent(generatePointerEvent("pointerleave", 5, left + 2.5, top + 2.5, -1, 0)); + engine.update(); + expect(script.onPointerEnter).to.have.been.called.exactly(3); + expect(script.onPointerExit).to.have.been.called.exactly(3); + expect(script.onPointerDown).to.have.been.called.exactly(2); + expect(script.onPointerUp).to.have.been.called.exactly(2); + expect(script.onPointerClick).to.have.been.called.exactly(2); + expect(script.onPointerDrop).to.have.been.called.exactly(2); + + // 1. 在碰撞体外 down + // 2. 在碰撞体上 up & leave + target.dispatchEvent(generatePointerEvent("pointerdown", 6, left + 200, top + 200)); + engine.update(); + expect(script.onPointerDown).to.have.been.called.exactly(2); + target.dispatchEvent(generatePointerEvent("pointerup", 6, left + 2.5, top + 2.5, 0, 0)); + target.dispatchEvent(generatePointerEvent("pointerleave", 6, left + 2.5, top + 2.5, -1, 0)); + engine.update(); + expect(script.onPointerUp).to.have.been.called.exactly(3); + expect(script.onPointerClick).to.have.been.called.exactly(2); + + // 1. 在碰撞体上 down + // 2. 在碰撞体外 up & leave + target.dispatchEvent(generatePointerEvent("pointerdown", 6, left + 2.5, top + 2.5)); + engine.update(); + expect(script.onPointerDown).to.have.been.called.exactly(3); + target.dispatchEvent(generatePointerEvent("pointerup", 6, left + 200, top + 200, 0, 0)); + target.dispatchEvent(generatePointerEvent("pointerleave", 6, left + 200, top + 200, -1, 0)); + engine.update(); + expect(script.onPointerUp).to.have.been.called.exactly(3); + expect(script.onPointerClick).to.have.been.called.exactly(2); + + // hit result + target.dispatchEvent(generatePointerEvent("pointerdown", 7, left + 2.5, top + 2.5)); + engine.update(); + const pointer = inputManager.pointers[0]; + const { hitResult } = pointer; + expect(hitResult.entity).to.have.been.eq(boxEntity); + const out = new Vector3(); + Vector3.subtract(hitResult.point, new Vector3(2.5 * 2, 2.5 * 2, 1), out); + expect(Math.abs(out.x) < 0.00001).to.deep.eq(true); + expect(Math.abs(out.y) < 0.00001).to.deep.eq(true); + expect(Math.abs(out.z) < 0.00001).to.deep.eq(true); + expect(pointer.draggedEntity).to.have.been.eq(boxEntity); + expect(pointer.enteredEntity).to.have.been.eq(boxEntity); + expect(pointer.pressedEntity).to.have.been.eq(boxEntity); + target.dispatchEvent(generatePointerEvent("pointerup", 7, left + 2.5, top + 2.5, 0, 0)); + engine.update(); + expect(hitResult.entity).to.have.been.eq(boxEntity); + expect(pointer.draggedEntity).to.have.been.eq(null); + expect(pointer.enteredEntity).to.have.been.eq(boxEntity); + expect(pointer.pressedEntity).to.have.been.eq(null); + target.dispatchEvent(generatePointerEvent("pointerleave", 7, left + 2.5, top + 2.5, -1, 0)); + engine.update(); + expect(hitResult.entity).to.have.been.eq(boxEntity); + expect(pointer.draggedEntity).to.have.been.eq(null); + expect(pointer.enteredEntity).to.have.been.eq(null); + expect(pointer.pressedEntity).to.have.been.eq(null); + expect(script.onPointerDrop).to.have.been.called.exactly(3); }); it("keyboard", () => {