@dcl-regenesislabs/web-editor 0.3.20 → 0.3.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/editor.js +2276 -452
- package/package.json +1 -1
- package/prefabs/camera-rig/ai.md +81 -0
- package/prefabs/camera-rig/composite.json +52 -0
- package/prefabs/camera-rig/data.json +22 -0
- package/prefabs/camera-rig/scripts/camera-rig.ts +199 -0
- package/prefabs/camera-rig/thumbnail.png +0 -0
- package/prefabs/catalog.json +1 -1
- package/prefabs/pickup-item/ai.md +94 -0
- package/prefabs/pickup-item/composite.json +44 -0
- package/prefabs/pickup-item/data.json +26 -0
- package/prefabs/pickup-item/scripts/pickup-item.ts +43 -0
- package/prefabs/pickup-item/thumbnail.png +0 -0
- package/prefabs/work-station/ai.md +91 -0
- package/prefabs/work-station/composite.json +52 -0
- package/prefabs/work-station/data.json +26 -0
- package/prefabs/work-station/scripts/work-station.ts +46 -0
- package/prefabs/work-station/thumbnail.png +0 -0
- package/scene/about +1 -1
- package/scene/{bafkreidloe2ozo4x2bmsjzt7svco43amj6pmj5zo7sisjnubj5w4et4sjm → bafkreihd3gtmg3p2azy5yfmzwihyk2pwunwuqkhrjybqzdp6vavsosna4y} +1 -1
- package/scene/{bafybeid4ndp7yylj3crls72kctre6gtc4hggnrxf7qqutw5giym2rr4qba → bafybeifiaotfpb4kis6f46xdw6a2rm6c534wewr55bq7zwcidmthvd4y7m} +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dcl-regenesislabs/web-editor",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.21",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "The Decentraland scene editor for the web explorer: editor.js plus the files it loads beside it. Served by version, not installed.",
|
|
6
6
|
"repository": {
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
---
|
|
2
|
+
prefab: camera-rig
|
|
3
|
+
claims-globals: __dclCameraRig_v1
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Camera Rig — AI guide
|
|
7
|
+
|
|
8
|
+
A fixed game camera: while the player stands inside the rig's area (or all the
|
|
9
|
+
time), their view switches to a camera looking down at the area from a fixed
|
|
10
|
+
angle, far enough back that the whole area fits their screen, a phone's wide
|
|
11
|
+
canvas included. Leaving the area gives the default camera back.
|
|
12
|
+
|
|
13
|
+
The project copy is normally custom/camera_rig/ — check what is on disk, a
|
|
14
|
+
second copy is custom/camera_rig_2/.
|
|
15
|
+
|
|
16
|
+
## When to use
|
|
17
|
+
|
|
18
|
+
Any game played on a board, arena or kitchen the player should see whole:
|
|
19
|
+
Overcooked-style co-op, party games, top-down shooters, puzzle rooms, a
|
|
20
|
+
mini-golf hole. The default third-person camera trails the avatar and hides
|
|
21
|
+
most of such a play area; place one Camera Rig over it instead of writing
|
|
22
|
+
VirtualCamera code.
|
|
23
|
+
|
|
24
|
+
## API
|
|
25
|
+
|
|
26
|
+
The rig's own Transform is the setup — no code, no names to wire:
|
|
27
|
+
- position: the point the camera looks at. Place it at the centre of the play
|
|
28
|
+
area, at floor height.
|
|
29
|
+
- scale: the area in metres (default 12 x 4 x 12). It is both where the camera
|
|
30
|
+
takes over and what it frames. Set it with the placePrefab `scale` to the
|
|
31
|
+
play area's size, leaving a metre of floor around the edges.
|
|
32
|
+
- rotation: which way is up on screen (the rig's forward, +Z). The assistant
|
|
33
|
+
cannot rotate it; the default faces north (+Z). If the user wants another
|
|
34
|
+
side, tell them to turn the rig with the rotate gizmo.
|
|
35
|
+
|
|
36
|
+
Params, set in the placePrefab request:
|
|
37
|
+
- mode: 'isometric' (default; angled, the Overcooked look) | 'top-down'
|
|
38
|
+
(straight down, a board-game view) | 'follow' (the same fixed angle, but
|
|
39
|
+
the camera tracks the player, for areas bigger than one screen).
|
|
40
|
+
- activeWhen: 'inside this area' (default) | 'always' (the whole scene, as
|
|
41
|
+
long as the player is in it).
|
|
42
|
+
- angle: degrees the camera looks down from the horizon for isometric and
|
|
43
|
+
follow, 10 to 88 (default 55). Top-down ignores it.
|
|
44
|
+
- distance: metres from the camera to the look point. 0 (default) computes it
|
|
45
|
+
so the whole area fits on screen; set it only to force a closer shot.
|
|
46
|
+
- fitSize: metres of ground the shot must show across and deep. 0 (default)
|
|
47
|
+
uses the rig's scale; in follow mode 0 means 10 m around the player.
|
|
48
|
+
- transitionSeconds: how long the view glides into the rig's camera (default
|
|
49
|
+
0.6, 0 cuts).
|
|
50
|
+
|
|
51
|
+
Framing uses the player's real canvas aspect (a 1600x720 phone landscape is
|
|
52
|
+
handled) plus a margin so the phone's touch buttons do not cover the edges.
|
|
53
|
+
The framing math is in the module header of
|
|
54
|
+
src/scripts/runtime/pure/cameraRig.ts.
|
|
55
|
+
|
|
56
|
+
## Movement and players
|
|
57
|
+
|
|
58
|
+
Movement keys keep working; the rig never freezes the avatar. Whether "forward"
|
|
59
|
+
walks toward the top of the screen under a fixed camera depends on the
|
|
60
|
+
explorer and is NOT verified yet: if the user reports inverted or sideways
|
|
61
|
+
controls, say so plainly instead of patching it with InputModifier. Each player
|
|
62
|
+
gets their own view: the rig only reacts to the player at that screen.
|
|
63
|
+
|
|
64
|
+
## Do / Don't
|
|
65
|
+
|
|
66
|
+
- DO place exactly one rig per play area. Overlapping rigs are allowed: the
|
|
67
|
+
one entered last wins, and leaving it falls back to the one still around the
|
|
68
|
+
player.
|
|
69
|
+
- DON'T also set MainCamera or create a VirtualCamera in a script for the same
|
|
70
|
+
area; the rig owns the view while the player is inside. A script's own
|
|
71
|
+
cutscene camera is left alone when the rig lets go.
|
|
72
|
+
- DON'T parent the rig to a scaled object: its scale is the area.
|
|
73
|
+
- DON'T use 'follow' for a room that fits one screen; 'isometric' reads better.
|
|
74
|
+
|
|
75
|
+
## Example
|
|
76
|
+
|
|
77
|
+
"A kitchen game seen from above": the kitchen floor is 14 x 10 m centred at
|
|
78
|
+
(16, 0, 16). placePrefab camera-rig named "Kitchen Camera" at position
|
|
79
|
+
(16, 0, 16) with scale (16, 4, 12), params mode: 'isometric', angle: 55,
|
|
80
|
+
activeWhen: 'inside this area'. Every cook who walks into the kitchen sees all
|
|
81
|
+
the stations at once; walking out restores the normal camera.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 1,
|
|
3
|
+
"components": [
|
|
4
|
+
{
|
|
5
|
+
"name": "core-schema::Name",
|
|
6
|
+
"data": {
|
|
7
|
+
"0": {
|
|
8
|
+
"json": {
|
|
9
|
+
"value": "Camera Rig"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "core::Transform",
|
|
16
|
+
"data": {
|
|
17
|
+
"0": {
|
|
18
|
+
"json": {
|
|
19
|
+
"scale": { "x": 12, "y": 4, "z": 12 }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "core::TriggerArea",
|
|
26
|
+
"data": {
|
|
27
|
+
"0": {
|
|
28
|
+
"json": {
|
|
29
|
+
"mesh": 0,
|
|
30
|
+
"collisionMask": 8
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"name": "asset-packs::Script",
|
|
37
|
+
"data": {
|
|
38
|
+
"0": {
|
|
39
|
+
"json": {
|
|
40
|
+
"value": [
|
|
41
|
+
{
|
|
42
|
+
"path": "{assetPath}/scripts/camera-rig.ts",
|
|
43
|
+
"priority": 0,
|
|
44
|
+
"layout": "{\"params\":{},\"actions\":[]}"
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
]
|
|
52
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "7ea892f3-168f-40fb-8338-d9c91c23708e",
|
|
3
|
+
"name": "Camera Rig",
|
|
4
|
+
"description": "A fixed game camera — top-down, isometric or following the player — that takes over while a player is inside its area and frames the whole area on any screen, phones included.",
|
|
5
|
+
"category": "custom",
|
|
6
|
+
"tags": [
|
|
7
|
+
"camera",
|
|
8
|
+
"top-down",
|
|
9
|
+
"isometric",
|
|
10
|
+
"view"
|
|
11
|
+
],
|
|
12
|
+
"version": "1.0.0",
|
|
13
|
+
"changelog": [
|
|
14
|
+
{
|
|
15
|
+
"version": "1.0.0",
|
|
16
|
+
"notes": "Initial release: a top-down, isometric or follow camera that takes over while a player is inside its area (or all the time), fits the whole area on desktop and phone screens, and hands the default camera back on the way out."
|
|
17
|
+
}
|
|
18
|
+
],
|
|
19
|
+
"origin": {
|
|
20
|
+
"source": "builtin"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
// A fixed game camera. While this player stands inside the rig's area (or all
|
|
2
|
+
// the time, with 'always'), their view switches to a camera that looks at the
|
|
3
|
+
// rig from above along the rig's heading, far enough back that the whole area
|
|
4
|
+
// fits on their screen — a phone's wide canvas included. Leaving the area hands
|
|
5
|
+
// the default camera back.
|
|
6
|
+
//
|
|
7
|
+
// The rig's own Transform is the whole authoring surface: its position is where
|
|
8
|
+
// the camera looks, its rotation is which way is up on screen, and its scale is
|
|
9
|
+
// the area in metres — the volume the editor draws while building, and the
|
|
10
|
+
// TriggerArea that decides "inside" while playing.
|
|
11
|
+
//
|
|
12
|
+
// The camera itself is a separate root entity: a VirtualCamera inherits its
|
|
13
|
+
// entity's scale, and the rig's scale is the area, not something to film with.
|
|
14
|
+
import {
|
|
15
|
+
MainCamera,
|
|
16
|
+
Transform,
|
|
17
|
+
TriggerArea,
|
|
18
|
+
UiCanvasInformation,
|
|
19
|
+
VirtualCamera,
|
|
20
|
+
engine,
|
|
21
|
+
triggerAreaEventsSystem,
|
|
22
|
+
type Entity
|
|
23
|
+
} from '@dcl/sdk/ecs'
|
|
24
|
+
import { isServer } from '@dcl/sdk/network'
|
|
25
|
+
import { isMobile } from '@dcl/sdk/platform'
|
|
26
|
+
import { localPlayerPosition } from '~runtime/playerPositions'
|
|
27
|
+
import { entityWorld } from '~runtime/sceneNames'
|
|
28
|
+
import { Membership } from '~runtime/pure/membership'
|
|
29
|
+
import {
|
|
30
|
+
CAMERA_FOV_DEG,
|
|
31
|
+
aspectFor,
|
|
32
|
+
cameraOnTop,
|
|
33
|
+
cameraPose,
|
|
34
|
+
claimCamera,
|
|
35
|
+
framingDistance,
|
|
36
|
+
pitchFor,
|
|
37
|
+
releaseCamera,
|
|
38
|
+
yawOf,
|
|
39
|
+
type CameraPose,
|
|
40
|
+
type RigClaims
|
|
41
|
+
} from '~runtime/pure/cameraRig'
|
|
42
|
+
import type { Vec3 } from '~runtime/pure/worldTransform'
|
|
43
|
+
|
|
44
|
+
// Collision layers by value: the SDK renamed CL_RESERVED2 to CL_MAIN_PLAYER, and
|
|
45
|
+
// this script compiles against whatever pin the creator's project carries. The
|
|
46
|
+
// local avatar alone carries 8, so the area reacts to the player at this screen.
|
|
47
|
+
const CL_MAIN_PLAYER = 8
|
|
48
|
+
const SPHERE_MESH = 1
|
|
49
|
+
// A player on the boundary crosses it every few frames; without this the view
|
|
50
|
+
// would snap back and forth.
|
|
51
|
+
const EXIT_GRACE_S = 0.3
|
|
52
|
+
const FOLLOW_EYE_M = 1
|
|
53
|
+
const MOVED_EPSILON = 1e-3
|
|
54
|
+
// Several rigs in one scene agree on who owns the screen through this key.
|
|
55
|
+
const CLAIMS_KEY = '__dclCameraRig_v1'
|
|
56
|
+
|
|
57
|
+
export class CameraRig {
|
|
58
|
+
private camera: Entity | null = null
|
|
59
|
+
private members = new Membership<Entity>()
|
|
60
|
+
private pose: CameraPose | null = null
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
public src: string,
|
|
64
|
+
public entity: Entity,
|
|
65
|
+
/** How the camera looks at the area */
|
|
66
|
+
public mode: 'top-down' | 'isometric' | 'follow' = 'isometric',
|
|
67
|
+
/** When the camera takes over */
|
|
68
|
+
public activeWhen: 'inside this area' | 'always' = 'inside this area',
|
|
69
|
+
/** Degrees the camera looks down from the horizon, for isometric and follow. Top-down looks straight down. */
|
|
70
|
+
public angle: number = 55,
|
|
71
|
+
/** Metres from the camera to what it looks at. 0 fits the whole area on screen. */
|
|
72
|
+
public distance: number = 0,
|
|
73
|
+
/** Metres of ground the shot must show across and deep. 0 uses this rig's own area (its scale); follow uses 10. */
|
|
74
|
+
public fitSize: number = 0,
|
|
75
|
+
/** Seconds the view takes to glide into this camera. 0 cuts straight to it. */
|
|
76
|
+
public transitionSeconds: number = 0.6
|
|
77
|
+
) {}
|
|
78
|
+
|
|
79
|
+
start(): void {
|
|
80
|
+
// The camera is one player's screen; a Multiplayer Server has none.
|
|
81
|
+
if (isServer()) return
|
|
82
|
+
const camera = engine.addEntity()
|
|
83
|
+
Transform.create(camera, { position: Transform.getOrNull(this.entity)?.position ?? { x: 0, y: 0, z: 0 } })
|
|
84
|
+
// Built as a value, not a literal, so an older SDK pin without `fov` still
|
|
85
|
+
// type-checks; its encoder drops the field and the explorer's default is 60.
|
|
86
|
+
const settings = {
|
|
87
|
+
defaultTransition: {
|
|
88
|
+
transitionMode: { $case: 'time' as const, time: Math.max(0, this.transitionSeconds) }
|
|
89
|
+
},
|
|
90
|
+
fov: CAMERA_FOV_DEG
|
|
91
|
+
}
|
|
92
|
+
VirtualCamera.create(camera, settings)
|
|
93
|
+
this.camera = camera
|
|
94
|
+
|
|
95
|
+
if (this.activeWhen === 'always') {
|
|
96
|
+
TriggerArea.deleteFrom(this.entity)
|
|
97
|
+
return
|
|
98
|
+
}
|
|
99
|
+
if (TriggerArea.getOrNull(this.entity)?.mesh === SPHERE_MESH) {
|
|
100
|
+
TriggerArea.setSphere(this.entity, CL_MAIN_PLAYER)
|
|
101
|
+
} else {
|
|
102
|
+
TriggerArea.setBox(this.entity, CL_MAIN_PLAYER)
|
|
103
|
+
}
|
|
104
|
+
// Stay as well as enter: a player who spawns inside the area never crosses
|
|
105
|
+
// into it, and the engine's enter fires only on the crossing.
|
|
106
|
+
triggerAreaEventsSystem.onTriggerEnter(this.entity, (result) => this.seen(result.trigger?.entity))
|
|
107
|
+
triggerAreaEventsSystem.onTriggerStay(this.entity, (result) => this.seen(result.trigger?.entity))
|
|
108
|
+
triggerAreaEventsSystem.onTriggerExit(this.entity, (result) => this.gone(result.trigger?.entity))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
update(dt: number): void {
|
|
112
|
+
const camera = this.camera
|
|
113
|
+
if (camera === null) return
|
|
114
|
+
this.members.tick(dt, EXIT_GRACE_S)
|
|
115
|
+
const active = this.activeWhen === 'always' || this.members.size > 0
|
|
116
|
+
if (active) this.aim(camera)
|
|
117
|
+
const claims = rigClaims()
|
|
118
|
+
if (active) claimCamera(claims, camera)
|
|
119
|
+
else releaseCamera(claims, camera)
|
|
120
|
+
applyClaims(claims)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private aim(camera: Entity): void {
|
|
124
|
+
const rig = entityWorld(this.entity)
|
|
125
|
+
if (rig === null) return
|
|
126
|
+
let target: Vec3 = rig.position
|
|
127
|
+
if (this.mode === 'follow') {
|
|
128
|
+
const feet = localPlayerPosition()
|
|
129
|
+
if (feet === null && this.pose !== null) return
|
|
130
|
+
if (feet !== null) target = { x: feet.x, y: feet.y + FOLLOW_EYE_M, z: feet.z }
|
|
131
|
+
}
|
|
132
|
+
const mobile = isMobile()
|
|
133
|
+
const canvas = UiCanvasInformation.getOrNull(engine.RootEntity)
|
|
134
|
+
const pitchDeg = pitchFor(this.mode, this.angle)
|
|
135
|
+
const distance = framingDistance({
|
|
136
|
+
mode: this.mode,
|
|
137
|
+
distance: this.distance,
|
|
138
|
+
fitSize: this.fitSize,
|
|
139
|
+
area: rig.scale,
|
|
140
|
+
pitchDeg,
|
|
141
|
+
aspect: aspectFor(canvas, mobile),
|
|
142
|
+
mobile
|
|
143
|
+
})
|
|
144
|
+
const pose = cameraPose(target, yawOf(rig.rotation), pitchDeg, distance)
|
|
145
|
+
if (this.pose !== null && samePose(this.pose, pose)) return
|
|
146
|
+
this.pose = pose
|
|
147
|
+
Transform.createOrReplace(camera, { position: pose.position, rotation: pose.rotation })
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// result.trigger.entity is the avatar that moved; the area is triggeredEntity.
|
|
151
|
+
private seen(raw?: number): void {
|
|
152
|
+
const who = toEntity(raw)
|
|
153
|
+
if (who !== undefined) this.members.touch(who)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private gone(raw?: number): void {
|
|
157
|
+
const who = toEntity(raw)
|
|
158
|
+
if (who !== undefined) this.members.drop(who)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function toEntity(raw?: number): Entity | undefined {
|
|
163
|
+
return raw === undefined || raw === 0 ? undefined : (raw as Entity)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function samePose(a: CameraPose, b: CameraPose): boolean {
|
|
167
|
+
const p = Math.abs(a.position.x - b.position.x) + Math.abs(a.position.y - b.position.y) + Math.abs(a.position.z - b.position.z)
|
|
168
|
+
const r =
|
|
169
|
+
Math.abs(a.rotation.x - b.rotation.x) +
|
|
170
|
+
Math.abs(a.rotation.y - b.rotation.y) +
|
|
171
|
+
Math.abs(a.rotation.z - b.rotation.z) +
|
|
172
|
+
Math.abs(a.rotation.w - b.rotation.w)
|
|
173
|
+
return p < MOVED_EPSILON && r < MOVED_EPSILON
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function isClaims(value: unknown): value is RigClaims<Entity> {
|
|
177
|
+
return typeof value === 'object' && value !== null && Array.isArray((value as { order?: unknown }).order)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function rigClaims(): RigClaims<Entity> {
|
|
181
|
+
const globals = globalThis as unknown as Record<string, unknown>
|
|
182
|
+
const current = globals[CLAIMS_KEY]
|
|
183
|
+
if (isClaims(current)) return current
|
|
184
|
+
const created: RigClaims<Entity> = { order: [], applied: null }
|
|
185
|
+
globals[CLAIMS_KEY] = created
|
|
186
|
+
return created
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function applyClaims(claims: RigClaims<Entity>): void {
|
|
190
|
+
const wanted = cameraOnTop(claims)
|
|
191
|
+
if (wanted === claims.applied) return
|
|
192
|
+
if (wanted !== null) {
|
|
193
|
+
MainCamera.createOrReplace(engine.CameraEntity, { virtualCameraEntity: wanted })
|
|
194
|
+
} else if (MainCamera.getOrNull(engine.CameraEntity)?.virtualCameraEntity === claims.applied) {
|
|
195
|
+
// Only undo our own write: a cutscene camera a script set meanwhile stays.
|
|
196
|
+
MainCamera.createOrReplace(engine.CameraEntity, {})
|
|
197
|
+
}
|
|
198
|
+
claims.applied = wanted
|
|
199
|
+
}
|
|
Binary file
|