Hello, I am currently encountering an issue where my UI controller is supposed to display specific UI screens based on game states and allow interaction using the Spacebar. However, when the game starts, all the UI screens are displayed at once instead of being shown during their respective states. Additionally, the Spacebar does not work to transition between UI screens or game states. I think the issue could be with the UI controller itself, the game manager not listening to the events dispatched by the UI controller, or the character controller not responding to these events properly.
ui-controller.js
import * as ecs from ā@8thwall/ecsā
ecs.registerComponent({
name: āUIControllerā,
schema: {
welcomeContainer: ecs.eid,
instructionsContainer: ecs.eid,
moveInstructionsContainer: ecs.eid,
gameOverContainer: ecs.eid,
finalScoreContainer: ecs.eid,
perfectScoreContainer: ecs.eid,
pointTitle: ecs.eid,
pointValue: ecs.eid,
},
data: {
currentScreen: ecs.string,
},
stateMachine: ({world, eid, schemaAttribute, dataAttribute}) => {
const resetUI = () => {
[āwelcomeContainerā, āinstructionsContainerā, āmoveInstructionsContainerā, āgameOverContainerā, āfinalScoreContainerā, āperfectScoreContainerā]
.forEach(container => ecs.Position.set(world, schemaAttribute.get(eid)[container], {y: -70}))
}
ecs.defineState('welcome').initial()
.onEvent('ShowInstructionsScreen', 'instructions')
.onEnter(() => {
resetUI()
dataAttribute.cursor(eid).currentScreen = 'welcome'
ecs.Position.set(world, schemaAttribute.get(eid).welcomeContainer, {y: 0})
})
ecs.defineState('instructions')
.onEvent('ShowMovementScreen', 'movement')
.onEnter(() => {
resetUI()
dataAttribute.cursor(eid).currentScreen = 'instructions'
ecs.Position.set(world, schemaAttribute.get(eid).instructionsContainer, {y: 0})
})
ecs.defineState('movement')
.onEvent('StartGame', 'inGame')
.onEnter(() => {
resetUI()
dataAttribute.cursor(eid).currentScreen = 'movement'
ecs.Position.set(world, schemaAttribute.get(eid).moveInstructionsContainer, {y: 0})
})
ecs.defineState('inGame')
.onEvent('gameOver', 'gameOver')
.onEvent('finalScore', 'finalScore')
.onEvent('perfectScore', 'perfectScore')
.onEnter(() => {
resetUI()
ecs.Position.set(world, schemaAttribute.get(eid).pointTitle, {y: 0})
ecs.Position.set(world, schemaAttribute.get(eid).pointValue, {y: 0})
})
ecs.defineState('gameOver')
.onEvent('restart', 'welcome')
.onEnter(() => {
resetUI()
ecs.Position.set(world, schemaAttribute.get(eid).gameOverContainer, {y: 0})
})
ecs.defineState('finalScore')
.onEvent('restart', 'welcome')
.onEnter(({score}) => {
resetUI()
ecs.Position.set(world, schemaAttribute.get(eid).finalScoreContainer, {y: 0})
world.events.dispatch(eid, 'UpdateScoreUI', {score})
})
ecs.defineState('perfectScore')
.onEvent('restart', 'welcome')
.onEnter(() => {
resetUI()
ecs.Position.set(world, schemaAttribute.get(eid).perfectScoreContainer, {y: 0})
})
},
tick: (world, component) => {
const {eid} = component
const {currentScreen} = component.dataAttribute.get(eid)
if (world.input.getKeyDown('Space')) {
if (currentScreen === 'welcome') {
world.events.dispatch(eid, 'ShowInstructionsScreen')
} else if (currentScreen === 'instructions') {
world.events.dispatch(eid, 'ShowMovementScreen')
} else if (currentScreen === 'movement') {
world.events.dispatch(eid, 'StartGame')
} else if (currentScreen === 'gameOver') {
world.events.dispatch(eid, 'restart')
}
}
},
add: (world, component) => {
const data = component.dataAttribute.get(component.eid)
data.currentScreen = āwelcomeā
},
})
game-manager.js
import * as ecs from ā@8thwall/ecsā
import * as balloonhit from ā./assets/Audio/balloonhit.mp3ā
ecs.registerComponent({
name: āGameManagerā,
schema: {
pointsPerBalloon: ecs.f32,
},
schemaDefaults: {
pointsPerBalloon: 1,
},
data: {
score: ecs.f32,
currentState: ecs.string,
},
stateMachine: ({world, eid, dataAttribute}) => {
const handleBalloonCollision = () => {
dataAttribute.cursor(eid).score += dataAttribute.cursor(eid).pointsPerBalloon
world.events.dispatch(eid, āScoreUpdatedā, {score: dataAttribute.cursor(eid).score})
// Play sound when balloon is collected
ecs.Audio.set(world, eid, {
url: balloonhit,
})
}
const handleGameOverCollision = () => {
world.events.dispatch(eid, 'gameOver')
}
const handleFinaleCollision = () => {
const {score} = dataAttribute.cursor(eid)
if (score === 8) {
world.events.dispatch(eid, 'perfectScore')
} else {
world.events.dispatch(eid, 'finalScore', {score})
}
}
ecs.defineState('preGame').initial()
.onEvent('StartGame', 'start')
.onEnter(() => {
dataAttribute.cursor(eid).currentState = 'preGame'
dataAttribute.cursor(eid).score = 0
world.events.dispatch(eid, 'ShowWelcomeScreen')
})
ecs.defineState('start')
.onEvent('interact', 'inGame')
.onEnter(() => {
world.events.dispatch(eid, 'ShowMovementScreen')
})
ecs.defineState('inGame')
.onEvent('gameOver', 'dead')
.onEvent('finale', 'final')
.onEnter(() => {
world.events.addListener(world.events.globalId, 'BalloonCollected', handleBalloonCollision)
world.events.addListener(world.events.globalId, 'GameOverCollision', handleGameOverCollision)
world.events.addListener(world.events.globalId, 'FinaleCollision', handleFinaleCollision)
})
.onExit(() => {
world.events.removeListener(world.events.globalId, 'BalloonCollected', handleBalloonCollision)
world.events.removeListener(world.events.globalId, 'GameOverCollision', handleGameOverCollision)
world.events.removeListener(world.events.globalId, 'FinaleCollision', handleFinaleCollision)
})
ecs.defineState('dead')
.onEvent('restart', 'preGame')
.onEnter(() => {
world.events.dispatch(eid, 'ShowGameOverScreen')
})
ecs.defineState('final')
.onEvent('restart', 'preGame')
.onEnter(({score}) => {
world.events.dispatch(eid, score === 8 ? 'ShowPerfectScoreScreen' : 'ShowFinalScoreScreen', {score})
})
},
tick: (world, component) => {
const {eid} = component
const data = component.dataAttribute.cursor(eid)
if (world.input.getKeyDown('Space')) {
if (data.currentState === 'preGame') {
world.events.dispatch(eid, 'StartGame')
} else if (data.currentState === 'dead' || data.currentState === 'final') {
world.events.dispatch(eid, 'restart')
}
}
},
add: (world, component) => {
const data = component.dataAttribute.cursor(component.eid)
data.currentState = āpreGameā
data.score = 0
},
})
character-controller.js
import * as ecs from ā@8thwall/ecsā
import {Balloon} from ā./balloonā
import * as rubberBootsForest from ā./assets/Audio/rubberBootsForest.mp3ā
// Character Controller Component
const characterController = ecs.registerComponent({
name: ācharacterControllerā,
schema: {
speed: ecs.f32, // Movement speed
character: ecs.eid, // Entity ID of the character model
},
schemaDefaults: {
speed: 80.0,
},
data: {
currentAngle: ecs.f32,
currentState: ecs.string, // Current state of the character
isAudioPlaying: ecs.i32, // Flag to indicate if audio is playing (1 = true, 0 = false)
},
stateMachine: ({world, eid, dataAttribute, schemaAttribute}) => {
const schema = schemaAttribute.get(eid)
const {character} = schema
// Balloon collision handler defined in a broader scope
const handleBalloonCollision = (event) => {
const otherEntity = event.data.other
if (Balloon.has(world, otherEntity)) {
console.log('Balloon collected!')
world.deleteEntity(otherEntity) // Remove the balloon entity
world.events.dispatch(world.events.globalId, 'BalloonCollected') // Dispatch global event
}
}
// Define 'idle' state
ecs.defineState('idle').initial()
.onEvent('start_moving', 'moving', {target: world.events.globalId})
.onEnter(() => {
dataAttribute.set(eid, {currentState: 'idle'})
console.log('Character state changed to: idle')
// Stop audio if playing
if (dataAttribute.get(eid).isAudioPlaying === 1) {
ecs.Audio.set(world, eid, {volume: 0})
dataAttribute.set(eid, {isAudioPlaying: 0})
}
// Set idle animation
ecs.GltfModel.set(world, character, {
animationClip: 'idleAction',
loop: true,
timeScale: 0.5,
})
})
// Define 'moving' state
ecs.defineState('moving')
.onEvent('stop_moving', 'idle')
.onEnter(() => {
dataAttribute.set(eid, {currentState: 'moving'})
console.log('Character state changed to: moving')
// Set walking animation
ecs.GltfModel.set(world, character, {
animationClip: 'walkAction',
loop: true,
timeScale: 2,
})
// Play walking audio
if (dataAttribute.get(eid).isAudioPlaying === 0) {
ecs.Audio.set(world, eid, {
url: rubberBootsForest,
loop: true,
volume: 1,
})
dataAttribute.set(eid, {isAudioPlaying: 1})
}
})
// Attach collision listeners for balloon detection
ecs.defineState('active')
.onEnter(() => {
world.events.addListener(eid, ecs.physics.COLLISION_START_EVENT, handleBalloonCollision)
})
.onExit(() => {
world.events.removeListener(eid, ecs.physics.COLLISION_START_EVENT, handleBalloonCollision)
})
},
tick: (world, component) => {
const {eid} = component
const schema = characterController.get(world, eid)
const delta = world.time.delta / 1000
let appliedForce = 0
let facingAngle = null
// Check for diagonal and single-direction movement
if (world.input.getAction('forward') && world.input.getAction('left')) {
appliedForce = schema.speed
facingAngle = Math.PI + Math.PI / 4 // Forward + Left (Z- and X+ diagonal)
world.events.dispatch(eid, 'start_moving')
} else if (world.input.getAction('forward') && world.input.getAction('right')) {
appliedForce = schema.speed
facingAngle = Math.PI - Math.PI / 4 // Forward + Right (Z- and X- diagonal)
world.events.dispatch(eid, 'start_moving')
} else if (world.input.getAction('backward') && world.input.getAction('left')) {
appliedForce = schema.speed
facingAngle = -Math.PI / 4 // Backward + Left (Z+ and X+ diagonal)
world.events.dispatch(eid, 'start_moving')
} else if (world.input.getAction('backward') && world.input.getAction('right')) {
appliedForce = schema.speed
facingAngle = Math.PI / 4 // Backward + Right (Z+ and X- diagonal)
world.events.dispatch(eid, 'start_moving')
} else if (world.input.getAction('forward')) {
appliedForce = schema.speed
facingAngle = Math.PI // Forward (Z- direction)
world.events.dispatch(eid, 'start_moving')
} else if (world.input.getAction('backward')) {
appliedForce = schema.speed
facingAngle = 0 // Backward (Z+ direction)
world.events.dispatch(eid, 'start_moving')
} else if (world.input.getAction('left')) {
appliedForce = schema.speed
facingAngle = -Math.PI / 2 // Left (X+ direction)
world.events.dispatch(eid, 'start_moving')
} else if (world.input.getAction('right')) {
appliedForce = schema.speed
facingAngle = Math.PI / 2 // Right (X- direction)
world.events.dispatch(eid, 'start_moving')
} else {
world.events.dispatch(eid, 'stop_moving')
}
// Update the character's facing direction
if (facingAngle !== null) {
const halfAngle = facingAngle / 2
const s = Math.sin(halfAngle)
const c = Math.cos(halfAngle)
world.setQuaternion(eid, 0, s, 0, c)
}
// Apply movement in the facing direction
if (appliedForce !== 0 && facingAngle !== null) {
const forwardX = Math.sin(facingAngle) * appliedForce
const forwardZ = Math.cos(facingAngle) * appliedForce
ecs.physics.applyForce(world, eid, forwardX, 0, forwardZ)
}
},
})
export {characterController}