@@ -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.
0 commit comments