I’m using the 8th Wall Niantic Studio web editor, and I want to disable the default permission popups and replace them with my own custom popups. However, the project doesn’t include any head.html
or body.html
files that I can edit. Even if I create these files, they aren’t automatically injected into the final HTML output. Is there any way to achieve this?
The easiest way would be to inject CSS that overrides the classnames used in the prompts.
https://www.8thwall.com/docs/legacy/guides/advanced-topics/load-screen/
This is how I create HTML in studio:
// onboardingScreen.js
// Builds a simple modal-style onboarding card prompting for gyroscope permission.
// ✅ Returns a detached DOM node; you decide where to insert it
// ✅ Keeps your existing IDs so other code keeps working
// ✅ Adds basic accessibility (role="dialog", aria-modal, labels)
// 🔧 Usage:
// const ui = CreateOnboardingScreen()
// document.body.appendChild(ui)
// // Add your click handler elsewhere: document.getElementById('onboardingButton')?.addEventListener(...)
function CreateOnboardingScreen() {
const container = document.createElement('div')
container.id = 'onboardingScreen'
container.setAttribute('role', 'dialog')
container.setAttribute('aria-modal', 'true')
container.setAttribute('aria-labelledby', 'onboardingTitle')
container.setAttribute('aria-describedby', 'onboardingDesc')
container.innerHTML = `
<div id="onboardingPopup">
<p id="onboardingTitle">This Game Uses Gyro!</p>
<p id="onboardingDesc">Please accept the permissions to continue.</p>
<button id="onboardingButton" type="button">Continue</button>
</div>
`
return container
}
export { CreateOnboardingScreen }
and then in another script i add the html to the scene and create event listeners
// ui-controller.js
// Centralized UI flow: loading → menu → prep → game → gameOver with gyro permission gate.
// ✅ Build/attach onboarding UI and request gyroscope permission on user gesture
// ✅ Loading bar blends asset progress (0–90%) + extra delay (10%) then auto-To Menu
// ✅ Smooth fade-to-black transition into game; toggles UI visibility and parenting
// ✅ Mute button syncs with audioManager via 'Mute Pressed'
// 🔊 Listens: 'Score Updated', 'Desired Camera', UI clicks on play/restart/mute
// 📦 Depends on: @8thwall/ecs (Ui/Hidden/Disabled/Position, events), requestGyroscopePermission, CreateOnboardingScreen
import * as ecs from '@8thwall/ecs'
import { requestGyroscopePermission } from './gyro-permissions'
import { CreateOnboardingScreen } from './createUI'
import './index.css'
ecs.registerComponent({
name: 'uiController',
// ───────────────────────────────── Schema
schema: {
gyroContinueButton: ecs.eid,
gyroContinueDiv: ecs.eid,
scoreText: ecs.eid,
loadingBarFill: ecs.eid,
loadingDelay: ecs.f32,
loadingUi: ecs.eid,
menuUi: ecs.eid,
playButton: ecs.eid,
blackScreen: ecs.eid,
platforms: ecs.eid,
scoreUI: ecs.eid,
player: ecs.eid,
zOffset: ecs.eid,
gameOverUI: ecs.eid,
gameOverScore: ecs.eid,
gameOverHighScore: ecs.eid,
restartButton: ecs.eid,
muteButton: ecs.eid,
// @asset
muteImage: ecs.string,
// @asset
unmuteImage: ecs.string,
},
schemaDefaults: {
loadingDelay: 3, // seconds
},
// ─────────────────────────────── Runtime data
data: {
muted: ecs.boolean,
},
// ─────────────────────────────── Add (init once per entity)
add: (world, component) => {
component.data.muted = false
},
// ─────────────────────────────── State Machine
stateMachine: ({ world, eid, schemaAttribute, dataAttribute }) => {
const sRef = () => schemaAttribute.get(eid)
const dRef = () => dataAttribute.get(eid)
const toMenu = ecs.defineTrigger()
const toGame = ecs.defineTrigger()
let currentScore = 0
let timePassedMs = 0
let fadeOffTime = 0
let fadeOnTime = 0
let blackScreenTime = 0
let delayLoadPercent = 0
let firstAssetsLoadedEvent = true
let blacked = false
let totalLoad = 0
const extraLoadTimeMs = sRef().loadingDelay * 1000
// Detached container for onboarding; only shown on Play
const uiContainer = document.createElement('div')
uiContainer.style.display = 'none'
const gyroCard = CreateOnboardingScreen() // contains #onboardingButton
uiContainer.appendChild(gyroCard)
document.body.appendChild(uiContainer)
const fadeOpacity = (alpha) => {
ecs.Ui.mutate(world, sRef().blackScreen, (c) => {
c.backgroundOpacity = alpha
})
}
const handleClick = async () => {
// Must be invoked by direct user gesture
const ans = await requestGyroscopePermission()
world.events.dispatch(world.events.globalId, 'Play Music') // kick BGM
uiContainer.style.display = 'none'
const tgt = world.events.globalId
if (ans === 'Granted') world.events.dispatch(tgt, 'Permission Accepted')
else if (ans === 'Denied') world.events.dispatch(tgt, 'Permission Denied')
else if (ans === 'Not Supported') world.events.dispatch(tgt, 'Permission Not Supported')
else world.events.dispatch(tgt, 'Permission Error')
const btn = document.getElementById('onboardingButton')
if (btn) btn.removeEventListener('click', handleClick)
}
const setGameOverUi = (payload) => {
ecs.Position.mutate(world, sRef().gameOverUI, (c) => {
c.y = payload.data.position
})
}
const setScore = (payload) => {
const score = Number(payload?.data?.score ?? payload?.score ?? 0)
ecs.Ui.mutate(world, sRef().scoreText, (cursor) => {
cursor.text = String(score)
})
currentScore = score
}
const toggleMute = () => {
const nowMuted = !dRef().muted
dataAttribute.cursor(eid).muted = nowMuted
ecs.Ui.mutate(world, sRef().muteButton, (cursor) => {
cursor.image = nowMuted ? sRef().muteImage : sRef().unmuteImage
})
world.events.dispatch(world.events.globalId, 'Mute Pressed')
}
// ───────────────────────────── States
ecs.defineState('loading').initial()
.onTick(() => {
const { pending, complete } = ecs.assets.getStatistics()
const progress = complete / (pending + complete || 1)
// 0–90% for assets, last 10% for "extra load" feel / UI prep
let assetLoadPercent = Math.min(progress * 90, 90)
if (progress >= 1) {
if (firstAssetsLoadedEvent) {
world.events.dispatch(world.events.globalId, 'Assets Loaded')
firstAssetsLoadedEvent = false
}
assetLoadPercent = 90
timePassedMs += world.time.delta
delayLoadPercent = Math.min((timePassedMs / extraLoadTimeMs) * 10, 10)
if (timePassedMs > extraLoadTimeMs + 100) {
world.events.dispatch(world.events.globalId, 'To Menu')
toMenu.trigger()
}
}
totalLoad = assetLoadPercent + delayLoadPercent
ecs.Ui.mutate(world, sRef().loadingBarFill, (cursor) => {
cursor.width = `${totalLoad}%`
})
})
.onTrigger(toMenu, 'Menu')
ecs.defineState('Menu')
.onEnter(() => {
ecs.Hidden.set(world, sRef().loadingUi)
ecs.Hidden.remove(world, sRef().menuUi)
const btn = document.getElementById('onboardingButton')
if (btn) btn.addEventListener('click', handleClick, { once: true })
})
.listen(sRef().playButton, ecs.input.UI_CLICK, () => {
uiContainer.style.display = 'block'
world.events.dispatch(world.events.globalId, 'Play Pressed')
})
.onEvent('Permission Accepted', 'prepGame', { target: world.events.globalId })
ecs.defineState('prepGame')
.onEnter(() => {
fadeOffTime = 0
fadeOnTime = 0
blackScreenTime = 0
blacked = false
})
.onTick(() => {
const dt = world.time.delta
if (fadeOnTime < 300) {
fadeOpacity(fadeOnTime / 300)
fadeOnTime += dt
return
}
if (blackScreenTime < 500) {
if (!blacked) {
blacked = true
fadeOpacity(1)
ecs.Hidden.remove(world, sRef().platforms)
ecs.Hidden.remove(world, sRef().scoreUI)
world.setParent(sRef().player, sRef().zOffset)
world.events.dispatch(world.events.globalId, 'Prep Game')
ecs.Hidden.set(world, sRef().menuUi)
ecs.Disabled.set(world, sRef().menuUi)
ecs.Hidden.set(world, sRef().gameOverUI)
}
blackScreenTime += dt
return
}
if (fadeOffTime < 300) {
fadeOpacity(1 - fadeOffTime / 300)
fadeOffTime += dt
return
}
fadeOpacity(0)
toGame.trigger()
})
.onTrigger(toGame, 'game')
ecs.defineState('game')
.onEnter(() => {
world.events.dispatch(world.events.globalId, 'Start Game')
})
.listen(world.events.globalId, 'Score Updated', setScore)
.onEvent('Game Over', 'gameOver', { target: world.events.globalId })
ecs.defineState('gameOver')
.onEnter(() => {
ecs.Hidden.remove(world, sRef().gameOverUI)
const KEY = 'botBounceHighScore'
const finalScore = Number.isFinite(currentScore) ? currentScore : 0
let highScore = 0
try {
const stored = localStorage.getItem(KEY)
if (stored != null) {
const parsed = parseInt(stored, 10)
if (Number.isFinite(parsed)) highScore = parsed
}
} catch (e) {
console.warn('Failed to load score history:', e)
}
if (finalScore > highScore) {
highScore = finalScore
try {
localStorage.setItem(KEY, String(highScore))
} catch (e) {
console.warn('Failed to save score history:', e)
}
}
ecs.Ui.mutate(world, sRef().gameOverHighScore, (c) => {
c.text = String(highScore)
})
ecs.Ui.mutate(world, sRef().gameOverScore, (c) => {
c.text = String(finalScore)
})
})
.listen(world.events.globalId, 'Desired Camera', setGameOverUi)
.onEvent(ecs.input.UI_CLICK, 'prepGame', { target: sRef().restartButton })
// Global listeners active across states
ecs.defineStateGroup(['game', 'gameOver', 'prepGame'])
.listen(sRef().muteButton, ecs.input.UI_CLICK, toggleMute)
},
})
these are specifically the lines that create the UI
const uiContainer = document.createElement(‘div’)
uiContainer.style.display = ‘none’
const gyroCard = CreateOnboardingScreen() // contains #onboardingButton
uiContainer.appendChild(gyroCard)
document.body.appendChild(uiContainer)
This topic was automatically closed 4 days after the last reply. New replies are no longer allowed.