Skip to content

Commit 77493c3

Browse files
authored
feat: ui renderer default virtual screen (#1444)
1 parent 8caf648 commit 77493c3

5 files changed

Lines changed: 388 additions & 10 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* Platform detection hook for the UI scale system.
3+
*
4+
* react-ecs stays independent of the scene runtime (`~system/*` modules), so
5+
* it cannot ask the explorer which platform it runs on. Instead, the host SDK
6+
* injects the check at module-load time (see `@dcl/sdk/react-ecs`). Until a
7+
* provider is injected — or when none ever is — the platform is assumed to be
8+
* non-mobile.
9+
*/
10+
let isMobileProvider: () => boolean = () => false
11+
12+
/**
13+
* Injects the function used to detect whether the scene runs on a mobile
14+
* platform. Called by `@dcl/sdk` with its `isMobile()` platform helper.
15+
*/
16+
export function setIsMobileProvider(provider: () => boolean): void {
17+
isMobileProvider = provider
18+
}
19+
20+
/**
21+
* Whether the scene is running on a mobile platform, according to the
22+
* injected provider. Platform detection is asynchronous on the SDK side, so
23+
* this may return false during the first ticks and flip to true once the
24+
* explorer information arrives.
25+
*/
26+
export function isMobile(): boolean {
27+
return isMobileProvider()
28+
}

packages/@dcl/react-ecs/src/system.ts

Lines changed: 82 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ import {
1212
setScreenInsetArea,
1313
setUiScaleFactor
1414
} from './components/utils'
15+
import { isMobile } from './platform'
16+
17+
// react-ecs compiles with `types: []` (no runtime typings), so the console
18+
// global provided by the scene runtime is declared here.
19+
declare const console: { log(message: string): void }
1520

1621
/**
1722
* @public
@@ -26,6 +31,26 @@ export type UiRendererOptions = {
2631
virtualHeight: number
2732
}
2833

34+
/**
35+
* Default virtual screen size used on mobile platforms, and the size 16:9
36+
* virtual screens are overridden to on mobile (phone screens are much wider
37+
* than 16:9, so a 16:9 virtual canvas would letterbox the UI).
38+
*/
39+
const DEFAULT_MOBILE_VIRTUAL_SIZE: UiRendererOptions = { virtualWidth: 1600, virtualHeight: 720 }
40+
41+
/**
42+
* Default virtual screen size used on non-mobile platforms.
43+
*/
44+
const DEFAULT_VIRTUAL_SIZE: UiRendererOptions = { virtualWidth: 1920, virtualHeight: 1080 }
45+
46+
function isValidVirtualSize(options: UiRendererOptions | undefined): options is UiRendererOptions {
47+
return !!options && options.virtualWidth > 0 && options.virtualHeight > 0
48+
}
49+
50+
function is16by9(options: UiRendererOptions): boolean {
51+
return options.virtualWidth * 9 === options.virtualHeight * 16
52+
}
53+
2954
/**
3055
* @public
3156
*/
@@ -36,6 +61,11 @@ export interface ReactBasedUiSystem {
3661
destroy(): void
3762
/**
3863
* Set the main UI renderer. Optional virtual size defines the global UI scale factor.
64+
*
65+
* When no virtual size is provided, a platform default is used: 1600x720 on
66+
* mobile, 1920x1080 otherwise. Providing an invalid size (values \<= 0)
67+
* disables the virtual screen (no UI scaling). On mobile, a provided 16:9
68+
* virtual size is overridden to 1600x720 to fit phone screens.
3969
*/
4070
setUiRenderer(ui: UiComponent, options?: UiRendererOptions): void
4171
/**
@@ -50,7 +80,8 @@ export interface ReactBasedUiSystem {
5080
* @param entity - The entity to associate with this UI renderer. When the entity is removed,
5181
* the UI renderer is automatically cleaned up.
5282
* @param ui - The UI component to render
53-
* @param options - Optional virtual size used for UI scale factor when main UI has none
83+
* @param options - Optional virtual size used for UI scale factor when main UI has none.
84+
* Defaults and the mobile 16:9 override behave as in {@link ReactBasedUiSystem.setUiRenderer}.
5485
*/
5586
addUiRenderer(entity: Entity, ui: UiComponent, options?: UiRendererOptions): void
5687
/**
@@ -78,6 +109,12 @@ export function createReactBasedUiSystem(engine: IEngine, pointerSystem: Pointer
78109
// Unique owner for the interactable area module variable.
79110
const interactableAreaOwner = Symbol('react-ecs-interactable-area')
80111

112+
// Last 16:9 size we already logged the mobile override for, so the log
113+
// fires once per provided size instead of every tick. Tracked as raw numbers
114+
// to avoid allocating a comparison string every tick.
115+
let loggedMobileOverrideW = 0
116+
let loggedMobileOverrideH = 0
117+
81118
function getActiveVirtualSize(): UiRendererOptions | undefined {
82119
// Main renderer options win; otherwise use the first additional renderer option.
83120
if (virtualSize) return virtualSize
@@ -87,6 +124,40 @@ export function createReactBasedUiSystem(engine: IEngine, pointerSystem: Pointer
87124
return undefined
88125
}
89126

127+
/**
128+
* Resolves the virtual screen to scale the UI against, or `undefined` when
129+
* the virtual screen is disabled.
130+
*/
131+
function resolveVirtualSize(): UiRendererOptions | undefined {
132+
const provided = getActiveVirtualSize()
133+
const mobile = isMobile()
134+
135+
// No creator-provided size: fall back to the platform default.
136+
if (!provided) {
137+
return mobile ? DEFAULT_MOBILE_VIRTUAL_SIZE : DEFAULT_VIRTUAL_SIZE
138+
}
139+
140+
// An explicitly provided but invalid size (values <= 0) disables the
141+
// virtual screen — no UI scaling at all.
142+
if (!isValidVirtualSize(provided)) {
143+
return undefined
144+
}
145+
146+
// On mobile, 16:9 virtual screens don't fit phone aspect ratios — override them.
147+
if (mobile && is16by9(provided)) {
148+
if (loggedMobileOverrideW !== provided.virtualWidth || loggedMobileOverrideH !== provided.virtualHeight) {
149+
loggedMobileOverrideW = provided.virtualWidth
150+
loggedMobileOverrideH = provided.virtualHeight
151+
console.log(
152+
`Mobile platform detected: overriding 16:9 virtual screen size ${provided.virtualWidth}x${provided.virtualHeight} with ${DEFAULT_MOBILE_VIRTUAL_SIZE.virtualWidth}x${DEFAULT_MOBILE_VIRTUAL_SIZE.virtualHeight}`
153+
)
154+
}
155+
return DEFAULT_MOBILE_VIRTUAL_SIZE
156+
}
157+
158+
return provided
159+
}
160+
90161
function ReactBasedUiSystem() {
91162
const components: React.ReactNode[] = []
92163

@@ -133,18 +204,25 @@ export function createReactBasedUiSystem(engine: IEngine, pointerSystem: Pointer
133204
setInteractableArea(canvasInfo.interactableArea, interactableAreaOwner)
134205
}
135206

136-
const activeVirtualSize = getActiveVirtualSize()
137-
if (!activeVirtualSize) {
207+
// The virtual screen (provided or defaulted) only applies while some
208+
// renderer is registered; with no UI at all the scale factor is released.
209+
if (uiComponent === undefined && additionalRenderers.size === 0) {
138210
// Reset only if this system owns the scale factor.
139211
resetUiScaleFactor(uiScaleFactorOwner)
140212
return
141213
}
142214

215+
const activeVirtualSize = resolveVirtualSize()
216+
if (!activeVirtualSize) {
217+
// Virtual screen explicitly disabled by an invalid provided size.
218+
resetUiScaleFactor(uiScaleFactorOwner)
219+
return
220+
}
221+
143222
if (!canvasInfo) return
144223

145224
const { width, height, devicePixelRatio } = canvasInfo
146225
const { virtualWidth, virtualHeight } = activeVirtualSize
147-
if (!virtualWidth || !virtualHeight) return
148226

149227
// Normalize by devicePixelRatio so virtual px map to logical px (matching the
150228
// vw/vh path); without it the scale was inflated on high-dpr mobile screens.

packages/@dcl/sdk/src/react-ecs.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,12 @@
1818
*/
1919

2020
import ReactEcs from '@dcl/react-ecs'
21+
import { setIsMobileProvider } from '@dcl/react-ecs/dist/platform'
22+
import { isMobile } from './platform'
23+
24+
// react-ecs cannot reach ~system/Runtime itself, so the platform check used
25+
// for virtual screen size defaults is injected here.
26+
setIsMobileProvider(isMobile)
27+
2128
export * from '@dcl/react-ecs'
2229
export default ReactEcs

0 commit comments

Comments
 (0)