Creating a button that triggers an animation in Studio

Hi there :wave:

In Studio I’m trying to create a custom component that triggers an animation on a click/touch event

I have 2 scripts that work individually. A script that console logs when a Plane is clicked, and another script that Animates an entity position.

I want to combine or create a script that triggers the animation based on a click, instead of the animation starting immediately as is the case now.

Below is the code for those seperate scripts

Is there someone who could help guide me in the right direction?

Click script

import * as ecs from '@8thwall/ecs'

// Register the UIController component
ecs.registerComponent({
  name: 'LogClickController',
  schema: {
    planeEntity: ecs.eid,  // Entity ID for the plane in the scene
  },

  stateMachine: ({world, eid, schemaAttribute}) => {
    // Function to log a message when the plane is clicked
    const logMessage = () => {
      console.log('Plane clicked! Logging a message.')  // Log message
    }

    // Define the state and handle click on the plane
    ecs.defineState('start').initial()
      .onEvent(ecs.input.SCREEN_TOUCH_START, 'logMessage', {
        target: schemaAttribute.cursor(eid).planeEntity,
      })
      .onEnter(() => {
        console.log('Event listener added to plane.')  // Log message when event listener is set
        world.events.addListener(schemaAttribute.cursor(eid).planeEntity,
          ecs.input.SCREEN_TOUCH_START, logMessage)
      })
      .onExit(() => {
        // Remove event listener when exiting the state
        console.log('Event listener removed from plane.')  // Log message when event listener is removed
        world.events.removeListener(schemaAttribute.cursor(eid).planeEntity,
          ecs.input.SCREEN_TOUCH_START, logMessage)
      })
  },
})

Animation script

import * as ecs from '@8thwall/ecs'

ecs.registerComponent({
  name: 'PositionAnimation',
  schema: {
    targetX: ecs.f32,   // x-position target for animation
    targetY: ecs.f32,   // y-position target for animation
    targetZ: ecs.f32,   // z-position target for animation
    duration: ecs.f32,  // Duration of the animation
  },
  data: {
    startX: ecs.f32,
    startY: ecs.f32,
    startZ: ecs.f32,
    elapsedTime: ecs.f32,
    isAnimating: ecs.boolean,
  },

  // Function to start the animation
  add: (world, component) => {
    const {data, schema} = component
    data.startX = ecs.Position.get(world, component.eid).x
    data.startY = ecs.Position.get(world, component.eid).y
    data.startZ = ecs.Position.get(world, component.eid).z
    data.elapsedTime = 0
    data.isAnimating = true
  },

  tick: (world, component) => {
    const {data, schema} = component

    if (data.isAnimating) {
      data.elapsedTime += world.time.delta  // Increase elapsed time by delta time

      const progress = Math.min(data.elapsedTime / schema.duration, 1)  // Calculate progress (0 to 1)

      const currentX = data.startX + (schema.targetX - data.startX) * progress
      const currentY = data.startY + (schema.targetY - data.startY) * progress
      const currentZ = data.startZ + (schema.targetZ - data.startZ) * progress

      // Update position based on the animation progress
      ecs.Position.set(world, component.eid, {x: currentX, y: currentY, z: currentZ})

      // If animation is complete, stop it
      if (progress === 1) {
        data.isAnimating = false
      }
    }
  },
})

Welcome to the forums! :partying_face:

I’d recommend creating a global event, allowing you to listen for it from anywhere and take action when it’s triggered.

world.events.addListener(target: eid, name: string, listener: function) -> void

world.events.dispatch(target: eid, name: string, data: object /* (optional) */) -> void

You would set the target to be world.events.globalId

Awesome, thank you GeorgeButler that works! :raised_hands:

Follow up now I have that working, I can’t get the click to register immediately. I’ve been testing for a while and the click to animation never works on the first click.

I have to keep tapping my screen on mobile or clicking in the editor preview for a number of times (about 5 to 10 times) and then at some point the click registers and the animation triggers.

Edit
I’ve discovered that if I wait a while before attempting the click it does register on the first try. So it seems like stuff is still loading in the background even though the scene and entities are visible. What is the recommended way to handle this? Is there a way to detect if the full scene is loaded and can be interacted with and show a loading state for example?

I’ve put all code in AnimationController.ts

import * as ecs from '@8thwall/ecs'

// Component that dispatches a global event on click
ecs.registerComponent({
  name: 'LogClickController',
  schema: {
    planeEntity: ecs.eid,  // Entity ID for the plane in the scene
  },

  stateMachine: ({world, eid, schemaAttribute}) => {
    const triggerAnimationEvent = () => {
      console.log('Plane clicked! Dispatching animation event.')  // Log message
      // Dispatch the global event to trigger the animation
      world.events.dispatch(world.events.globalId, 'StartPositionAnimation')
    }

    ecs.defineState('start').initial()
      .onEvent(ecs.input.SCREEN_TOUCH_START, 'triggerAnimationEvent', {
        target: schemaAttribute.cursor(eid).planeEntity,
      })
      .onEnter(() => {
        console.log('Event listener added to plane.')  // Log message
        world.events.addListener(schemaAttribute.cursor(eid).planeEntity,
          ecs.input.SCREEN_TOUCH_START, triggerAnimationEvent)
      })
      .onExit(() => {
        console.log('Event listener removed from plane.')  // Log message
        world.events.removeListener(schemaAttribute.cursor(eid).planeEntity,
          ecs.input.SCREEN_TOUCH_START, triggerAnimationEvent)
      })
  },
})

// Component that plays the position animation in response to the global event
ecs.registerComponent({
  name: 'PositionAnimation',
  schema: {
    targetX: ecs.f32,   // Target x-position for animation
    targetY: ecs.f32,   // Target y-position for animation
    targetZ: ecs.f32,   // Target z-position for animation
    duration: ecs.f32,  // Duration of the animation
  },
  data: {
    startX: ecs.f32,
    startY: ecs.f32,
    startZ: ecs.f32,
    elapsedTime: ecs.f32,
    isAnimating: ecs.boolean,
  },

  add: (world, component) => {
    const {data} = component
    data.isAnimating = false  // Start with animation off

    // Listen for the global event to start the animation
    world.events.addListener(world.events.globalId, 'StartPositionAnimation', () => {
      console.log('Received StartPositionAnimation event! Starting animation.')
      const position = ecs.Position.get(world, component.eid)
      data.startX = position.x
      data.startY = position.y
      data.startZ = position.z
      data.elapsedTime = 0
      data.isAnimating = true
    })
  },

  tick: (world, component) => {
    const {data, schema} = component

    if (data.isAnimating) {
      data.elapsedTime += world.time.delta

      const progress = Math.min(data.elapsedTime / schema.duration, 1)

      const currentX = data.startX + (schema.targetX - data.startX) * progress
      const currentY = data.startY + (schema.targetY - data.startY) * progress
      const currentZ = data.startZ + (schema.targetZ - data.startZ) * progress

      ecs.Position.set(world, component.eid, {x: currentX, y: currentY, z: currentZ})

      if (progress === 1) {
        data.isAnimating = false  // Stop animation when complete
      }
    }
  },
})