UI Controller and State Transition Issue: UI Displays All at Once and Spacebar Interaction Not Working

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}

I’d be happy to take a look. Can you share your project with the support workspace?