Skip to content

Commit 1bcaa4a

Browse files
authored
feat: trigger area components (#1210)
* TriggerArea and TriggerAreaResult components * triggerAreaEventsSystem helper * TriggerArea extension helper
1 parent b983b3b commit 1bcaa4a

29 files changed

Lines changed: 676 additions & 81 deletions

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,9 @@ test-cli:
6262
@mkdir -p tmp/scene
6363
cd tmp/scene; $(PWD)/packages/@dcl/sdk-commands/dist/index.js init
6464

65+
test-file:
66+
node_modules/.bin/jest --detectOpenHandles --colors --runTestsByPath $(FILE)
67+
6568
init-test-scene:
6669
git clone https://github.com/decentraland/sdk7-scene-template test-scene
6770
cd test-scene && npm i ./../packages/@dcl/sdk && npm i ./../packages/@dcl/sdk-commands && npm i ./../packages/@dcl/js-runtime

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"bugs": "https://github.com/decentraland/js-sdk-toolchain/issues",
77
"dependencies": {
88
"@actions/core": "^1.10.0",
9-
"@dcl/protocol": "1.0.0-16800835851.commit-2ec49fe",
9+
"@dcl/protocol": "1.0.0-18103449748.commit-258fabc",
1010
"@dcl/quickjs-emscripten": "^0.21.0-3680274614.commit-1808aa1",
1111
"@dcl/ts-proto": "1.153.0",
1212
"@types/fs-extra": "^9.0.12",
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Entity, IEngine, LastWriteWinElementSetComponentDefinition } from '../../engine'
2+
import { ColliderLayer, PBTriggerArea, TriggerArea, TriggerAreaMeshType } from '../generated/index.gen'
3+
4+
/**
5+
* @public
6+
*/
7+
export interface TriggerAreaComponentDefinitionExtended
8+
extends LastWriteWinElementSetComponentDefinition<PBTriggerArea> {
9+
/**
10+
* @public
11+
* Set a box in the MeshCollider component
12+
* @param entity - entity to create or replace the TriggerArea component
13+
* @param collisionMask - the collision layers mask for the trigger to react, default: Player
14+
*/
15+
setBox(entity: Entity, collisionMask?: ColliderLayer | ColliderLayer[]): void
16+
17+
/**
18+
* @public
19+
* Set a sphere in the MeshCollider component
20+
* @param entity - entity to create or replace the TriggerArea component
21+
* @param collisionMask - the collision layers mask for the trigger to react, default: Player
22+
*/
23+
setSphere(entity: Entity, collisionMask?: ColliderLayer | ColliderLayer[]): void
24+
}
25+
26+
export function defineTriggerAreaComponent(
27+
engine: Pick<IEngine, 'defineComponentFromSchema'>
28+
): TriggerAreaComponentDefinitionExtended {
29+
const theComponent = TriggerArea(engine)
30+
31+
function getCollisionMask(layers?: ColliderLayer | ColliderLayer[]) {
32+
if (Array.isArray(layers)) {
33+
return layers.map((item) => item as number).reduce((prev, item) => prev | item, 0)
34+
} else if (layers) {
35+
return layers
36+
}
37+
}
38+
39+
return {
40+
...theComponent,
41+
setBox(entity: Entity, collisionMask?: ColliderLayer | ColliderLayer[]): void {
42+
theComponent.createOrReplace(entity, {
43+
mesh: TriggerAreaMeshType.TAMT_BOX,
44+
collisionMask: getCollisionMask(collisionMask)
45+
})
46+
},
47+
setSphere(entity: Entity, collisionMask?: ColliderLayer | ColliderLayer[]): void {
48+
theComponent.createOrReplace(entity, {
49+
mesh: TriggerAreaMeshType.TAMT_SPHERE,
50+
collisionMask: getCollisionMask(collisionMask)
51+
})
52+
}
53+
}
54+
}

packages/@dcl/ecs/src/components/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { MediaState } from './generated/pb/decentraland/sdk/components/common/me
1717
import { defineVirtualCameraComponent, VirtualCameraComponentDefinitionExtended } from './extended/VirtualCamera'
1818
import { defineInputModifierComponent, InputModifierComponentDefinitionExtended } from './extended/InputModifier'
1919
import { defineLightSourceComponent, LightSourceComponentDefinitionExtended } from './extended/LightSource'
20+
import { defineTriggerAreaComponent, TriggerAreaComponentDefinitionExtended } from './extended/TriggerArea'
2021
import defineTagsComponent, { TagsComponentDefinitionExtended } from './manual/Tags'
2122

2223
export * from './generated/index.gen'
@@ -71,6 +72,10 @@ export const InputModifier: LwwComponentGetter<InputModifierComponentDefinitionE
7172
export const LightSource: LwwComponentGetter<LightSourceComponentDefinitionExtended> = (engine) =>
7273
defineLightSourceComponent(engine)
7374

75+
/* @__PURE__ */
76+
export const TriggerArea: LwwComponentGetter<TriggerAreaComponentDefinitionExtended> = (engine) =>
77+
defineTriggerAreaComponent(engine)
78+
7479
/**
7580
* @alpha
7681
*/

packages/@dcl/ecs/src/components/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ export type { INetowrkEntity, INetowrkEntityType } from './manual/NetworkEntity'
1414
export type { INetowrkParent, INetowrkParentType } from './manual/NetworkParent'
1515
export type { InputModifierHelper, InputModifierComponentDefinitionExtended } from './extended/InputModifier'
1616
export type { LightSourceHelper, LightSourceComponentDefinitionExtended } from './extended/LightSource'
17+
export type { TriggerAreaComponentDefinitionExtended } from './extended/TriggerArea'

packages/@dcl/ecs/src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export * from './systems/raycast'
1111
export * from './systems/videoEvents'
1212
export * from './systems/async-task'
1313
export * from './systems/tween'
14+
export * from './systems/triggerArea'
1415
export * from './engine/entity'
1516
export * from './components/types'
1617

@@ -34,7 +35,8 @@ import {
3435
INetowrkParent,
3536
VirtualCameraComponentDefinitionExtended,
3637
InputModifierComponentDefinitionExtended,
37-
LightSourceComponentDefinitionExtended
38+
LightSourceComponentDefinitionExtended,
39+
TriggerAreaComponentDefinitionExtended
3840
} from './components/types'
3941
import { NameComponent } from './components/manual/Name'
4042
import { TagsComponentDefinitionExtended } from './components/manual/Tags'
@@ -53,6 +55,7 @@ export const Tween: TweenComponentDefinitionExtended = /* @__PURE__*/ components
5355
export const VirtualCamera: VirtualCameraComponentDefinitionExtended = /* @__PURE__*/ components.VirtualCamera(engine)
5456
export const InputModifier: InputModifierComponentDefinitionExtended = /* @__PURE__*/ components.InputModifier(engine)
5557
export const LightSource: LightSourceComponentDefinitionExtended = /* @__PURE__*/ components.LightSource(engine)
58+
export const TriggerArea: TriggerAreaComponentDefinitionExtended = /* @__PURE__*/ components.TriggerArea(engine)
5659

5760
/**
5861
* @alpha

packages/@dcl/ecs/src/runtime/initialization/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { createRaycastSystem, RaycastSystem } from '../../systems/raycast'
1111
import { createVideoEventsSystem, VideoEventsSystem } from '../../systems/videoEvents'
1212
import { TweenSystem, createTweenSystem } from '../../systems/tween'
1313
import { pointerEventColliderChecker } from '../../systems/pointer-event-collider-checker'
14+
import { createTriggerAreaEventsSystem, TriggerAreaEventsSystem } from '../../systems/triggerArea'
1415

1516
/**
1617
* @public
@@ -65,6 +66,13 @@ export { VideoEventsSystem }
6566
export const tweenSystem: TweenSystem = createTweenSystem(engine)
6667
export { TweenSystem }
6768

69+
/**
70+
* @public
71+
* Register callback functions for trigger area results.
72+
*/
73+
export const triggerAreaEventsSystem: TriggerAreaEventsSystem = /* @__PURE__ */ createTriggerAreaEventsSystem(engine)
74+
export { TriggerAreaEventsSystem }
75+
6876
/**
6977
* Adds pointer event collider system only in DEV env
7078
*/
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
import * as components from '../components'
2+
import { DeepReadonlyObject, Entity, IEngine } from '../engine'
3+
import { EntityState } from '../engine/entity'
4+
import { PBTriggerAreaResult, TriggerAreaEventType } from '../components'
5+
6+
/**
7+
* @public
8+
*/
9+
export type TriggerAreaEventSystemCallback = (result: DeepReadonlyObject<PBTriggerAreaResult>) => void
10+
11+
/**
12+
* @public
13+
*/
14+
export interface TriggerAreaEventsSystem {
15+
/**
16+
* @public
17+
* Execute callback when an entity enters the Trigger Area
18+
* @param entity - The entity that already has the TriggerArea component
19+
* @param cb - Function to execute the 'Enter' type of result is detected
20+
*/
21+
onTriggerEnter(entity: Entity, cb: TriggerAreaEventSystemCallback): void
22+
23+
/**
24+
* @public
25+
* Remove the callback for Trigger Area 'Enter' type of result
26+
* @param entity - Entity where the Trigger Area was attached
27+
*/
28+
removeOnTriggerEnter(entity: Entity): void
29+
30+
/**
31+
* @public
32+
* Execute callback when an entity stays in the Trigger Area
33+
* @param entity - The entity that already has the TriggerArea component
34+
* @param cb - Function to execute the 'Stay' type of result is detected
35+
*/
36+
onTriggerStay(entity: Entity, cb: TriggerAreaEventSystemCallback): void
37+
38+
/**
39+
* @public
40+
* Remove the callback for Trigger Area 'Stay' type of result
41+
* @param entity - Entity where the Trigger Area was attached
42+
*/
43+
removeOnTriggerStay(entity: Entity): void
44+
45+
/**
46+
* @public
47+
* Execute callback when an entity exits the Trigger Area
48+
* @param entity - The entity that already has the TriggerArea component
49+
* @param cb - Function to execute the 'Exit' type of result is detected
50+
*/
51+
onTriggerExit(entity: Entity, cb: TriggerAreaEventSystemCallback): void
52+
53+
/**
54+
* @public
55+
* Remove the callback for Trigger Area 'Exit' type of result
56+
* @param entity - Entity where the Trigger Area was attached
57+
*/
58+
removeOnTriggerExit(entity: Entity): void
59+
}
60+
61+
/**
62+
* @internal
63+
*/
64+
export function createTriggerAreaEventsSystem(engine: IEngine): TriggerAreaEventsSystem {
65+
const triggerAreaResultComponent = components.TriggerAreaResult(engine)
66+
const entitiesMap = new Map<
67+
Entity,
68+
{
69+
triggerCallbackMap: Map<TriggerAreaEventType, TriggerAreaEventSystemCallback>
70+
lastConsumedTimestamp: number
71+
}
72+
>()
73+
74+
function hasCallbacksMap(entity: Entity): boolean {
75+
return entitiesMap.has(entity) && entitiesMap.get(entity) !== undefined
76+
}
77+
78+
function addEntityCallback(
79+
entity: Entity,
80+
triggerType: TriggerAreaEventType,
81+
callback: TriggerAreaEventSystemCallback
82+
) {
83+
if (hasCallbacksMap(entity)) {
84+
entitiesMap.get(entity)!.triggerCallbackMap.set(triggerType, callback)
85+
} else {
86+
entitiesMap.set(entity, {
87+
triggerCallbackMap: new Map<TriggerAreaEventType, TriggerAreaEventSystemCallback>([[triggerType, callback]]),
88+
lastConsumedTimestamp: -1
89+
})
90+
}
91+
}
92+
93+
function removeEntityCallback(entity: Entity, triggerType: TriggerAreaEventType) {
94+
if (!entitiesMap.has(entity) || !entitiesMap.get(entity)!.triggerCallbackMap.has(triggerType)) return
95+
const triggerCallbackMap = entitiesMap.get(entity)!.triggerCallbackMap
96+
triggerCallbackMap.delete(triggerType)
97+
98+
// Remove entity if no more trigger callbacks are registered
99+
if (triggerCallbackMap.size === 0) entitiesMap.delete(entity)
100+
}
101+
102+
function onTriggerEnter(entity: Entity, cb: TriggerAreaEventSystemCallback) {
103+
addEntityCallback(entity, TriggerAreaEventType.TAET_ENTER, cb)
104+
}
105+
function removeOnTriggerEnter(entity: Entity) {
106+
removeEntityCallback(entity, TriggerAreaEventType.TAET_ENTER)
107+
}
108+
109+
function onTriggerStay(entity: Entity, cb: TriggerAreaEventSystemCallback) {
110+
addEntityCallback(entity, TriggerAreaEventType.TAET_STAY, cb)
111+
}
112+
function removeOnTriggerStay(entity: Entity) {
113+
removeEntityCallback(entity, TriggerAreaEventType.TAET_STAY)
114+
}
115+
116+
function onTriggerExit(entity: Entity, cb: TriggerAreaEventSystemCallback) {
117+
addEntityCallback(entity, TriggerAreaEventType.TAET_EXIT, cb)
118+
}
119+
function removeOnTriggerExit(entity: Entity) {
120+
removeEntityCallback(entity, TriggerAreaEventType.TAET_EXIT)
121+
}
122+
123+
engine.addSystem(function TriggerAreaResultSystem() {
124+
const garbageEntries = []
125+
for (const [entity, data] of entitiesMap) {
126+
if (engine.getEntityState(entity) === EntityState.Removed) {
127+
garbageEntries.push(entity)
128+
continue
129+
}
130+
131+
const result = triggerAreaResultComponent.get(entity)
132+
133+
// The Explorer may be taking time before the result component is put
134+
if (result.size === 0) continue
135+
136+
const values = Array.from(result.values())
137+
138+
// determine starting index for new values (more than one could be added between System updates)
139+
// search backwards to find the anchor at lastConsumedTimestamp
140+
let startIndex = 0
141+
if (data.lastConsumedTimestamp >= 0) {
142+
const newestTimestamp = values[values.length - 1].timestamp
143+
144+
// if nothing new, skip processing
145+
if (newestTimestamp <= data.lastConsumedTimestamp) {
146+
continue
147+
}
148+
149+
// Find index of value with the lastConsumedTimestamp
150+
let i = values.length - 2
151+
while (i >= 0 && values[i].timestamp > data.lastConsumedTimestamp) i--
152+
153+
// Mark the following value index as the starting point to trigger all the new value callbacks
154+
startIndex = i + 1
155+
}
156+
157+
if (startIndex >= values.length) continue
158+
159+
// Trigger callbacks for all the new values
160+
for (let i = startIndex; i < values.length; i++) {
161+
switch (values[i].eventType) {
162+
case TriggerAreaEventType.TAET_ENTER:
163+
if (!data.triggerCallbackMap.has(TriggerAreaEventType.TAET_ENTER)) continue
164+
data.triggerCallbackMap.get(TriggerAreaEventType.TAET_ENTER)!(values[i])
165+
break
166+
case TriggerAreaEventType.TAET_STAY:
167+
if (!data.triggerCallbackMap.has(TriggerAreaEventType.TAET_STAY)) continue
168+
data.triggerCallbackMap.get(TriggerAreaEventType.TAET_STAY)!(values[i])
169+
break
170+
case TriggerAreaEventType.TAET_EXIT:
171+
if (!data.triggerCallbackMap.has(TriggerAreaEventType.TAET_EXIT)) continue
172+
data.triggerCallbackMap.get(TriggerAreaEventType.TAET_EXIT)!(values[i])
173+
break
174+
}
175+
}
176+
177+
data.lastConsumedTimestamp = values[values.length - 1].timestamp
178+
}
179+
180+
// Clean up garbage entries
181+
garbageEntries.forEach((garbageEntity) => entitiesMap.delete(garbageEntity))
182+
})
183+
184+
return {
185+
onTriggerEnter,
186+
removeOnTriggerEnter,
187+
onTriggerStay,
188+
removeOnTriggerStay,
189+
onTriggerExit,
190+
removeOnTriggerExit
191+
}
192+
}

0 commit comments

Comments
 (0)