# 02: Button

Start with the 02-button-playground folder, it has the default scene & all the models and sound files for this room.

Resources:

# Adding a door with a button

Inside of room2.ts add the following code inside the CreateRoom2() function:

export function CreateRoom2(): void {
  const door = new Entity();
  engine.addEntity(door);
  door.addComponent(new GLTFShape("models/room2/Puzzle02_Door.glb"));
  door.addComponent(
    new Transform({
      position: new Vector3(24.1, 5.51634, 24.9)
    })
  );

  door.addComponent(new Animator());
  door
    .getComponent(Animator)
    .addClip(new AnimationState("Door_Open", { looping: false }));
  // Adding an additional animation for closing the door
  door
    .getComponent(Animator)
    .addClip(new AnimationState("Door_Close", { looping: false }));

  door.addComponent(new AudioSource(new AudioClip("sounds/door_squeak.mp3")));
}

This will add a door similar to the code in 01-door-complete now add the following code for the button:

  // Add the Button the we'll use to open the Door
  const button = new Entity();
  engine.addEntity(button);
  button.addComponent(new GLTFShape("models/room2/Square_Button.glb"));
  button.addComponent(
    new Transform({
      position: new Vector3(26.3714, 6.89, 26.8936)
    })
  );

  // Add the button animation
  button.addComponent(new Animator());
  button
    .getComponent(Animator)
    .addClip(new AnimationState("Button_Action", { looping: false }));

  // And a sound effect for when the button is pressed
  button.addComponent(new AudioSource(new AudioClip("sounds/button.mp3")));

To get the door to open when the button is pressed insert the following code:

  // When the player clicks the button
  button.addComponent(
    new OnClick((): void => {
	  // Animate the button press
	  button
      .getComponent(Animator)
      .getClip("Button_Action")
      .play();

	  // And play the button sound effect
      button.getComponent(AudioSource).playOnce();

	  // Open the door
	  door
      .getComponent(Animator)
      .getClip("Door_Open")
      .play();

	  // And play the sound effect
      door.getComponent(AudioSource).playOnce();
	})
  );

# Adding a timer display

Add a new entity and add a TextShape component:

  // The text for the timer is a separate entity
  const countdownText = new Entity();
  engine.addEntity(countdownText);

  countdownText.addComponent(
    new Transform({
      position: new Vector3(25.1272, 9.51119, 25.2116),
      rotation: Quaternion.Euler(20, 180, 0)
    })
  );

  // Use a `TextShape` and set the default value
  countdownText.addComponent(new TextShape("00:05"));

  // And style the text a bit
  countdownText.getComponent(TextShape).color = Color3.Red();
  countdownText.getComponent(TextShape).fontSize = 5;

Make a function to format the time, this should be places outside of the CreateRoom2() function:

// Function to convert the time left into a string like "00:05"
function formatTimeString(seconds: number): string {
  const mins = Math.floor(seconds / 60);
  const secs = Math.floor(seconds % 60);
  return (
    mins.toLocaleString(undefined, { minimumIntegerDigits: 2 }) +
    ":" +
    secs.toLocaleString(undefined, { minimumIntegerDigits: 2 })
  );
}

Update the TextShape value to use the new function:

  // Use a `TextShape` and set the default value
  countdownText.addComponent(new TextShape(formatTimeString(5)));

# Adding the timer

Add an Interval component inside of the button OnClick event:

let timeRemaining = 5;
counddownText.addComponent(
  new utils.Interval(1000, (): void =>{
    // 1 second has past (passed?)
	timeRemaining--;

	if(timeRemaining > 0){
		countdownText.getComponent(TextShape).value = formatTimeString(timeRemaining);
	} else {
	  // Timer has reached 0! Remove the interval to prevent a negative time
	  countdownText.removeComponent(utils.Interval);

	  // Close the door
	  door
      .getComponent(Animator)
      .getClip("Door_Close")
      .play();

      door.getComponent(AudioSource).playOnce();

	  countdownText.getComponent(TextShape).value = formatTimeString(5);
	}
  })
);

Prevent multiple intervals being created by adding an if condition in the button OnClick event and fix the animation bug by stopping animations:

button.addComponent(
  new OnClick((): void => {
    // Checking if timer is running
    if (!countdownText.hasComponent(utils.Interval)) {
      // Animate the button press
      button
        .getComponent(Animator)
        .getClip("Button_Action")
        .stop(); // bug workaround
      button
        .getComponent(Animator)
        .getClip("Button_Action")
        .play();

      // And play the button sound effect
      button.getComponent(AudioSource).playOnce();

      // Open the door
      door
        .getComponent(Animator)
        .getClip("Door_Close")
        .stop(); // bug workaround
      door
        .getComponent(Animator)
        .getClip("Door_Open")
        .play();
      // And play the sound effect
      door.getComponent(AudioSource).playOnce();

      // And add an interval to update the text every 1 second and slam the door again when time runs out
      let timeRemaining = 5;
      countdownText.addComponent(
        new utils.Interval(1000, (): void => {
          // 1 second has past
          timeRemaining--;

          if (timeRemaining > 0) {
            // Update the display with the new timeRemaining
            countdownText.getComponent(TextShape).value = formatTimeString(
              timeRemaining
            );
          } else {
            // Timer has reached 0! Remove the interval so the display does not go negative
            countdownText.removeComponent(utils.Interval);

            // Close the door
            door
              .getComponent(Animator)
              .getClip("Door_Open")
              .stop(); // bug workaround
            door
              .getComponent(Animator)
              .getClip("Door_Close")
              .play();
            door.getComponent(AudioSource).playOnce();

            // Then reset the text
            countdownText.getComponent(TextShape).value = formatTimeString(5);
          }
        })
      );
    }
  })
);

# Refactor

Seperate the baseScene from game.ts, copy and paste them into a new file called baseScene.ts inside a folder called gameObjects:

export class BaseScene extends Entity {

  constructor() {
    super();
    engine.addEntity(this);
    this.addComponent(new GLTFShape("models/scene.glb"));
  }
}

Update the game.ts to create the BaseScene:

import { CreateRoom1 } from "./scenes/room1";
import { CreateRoom2 } from "./scenes/room2";

new BaseScene();

CreateRoom1();
CreateRoom2();

Out of room2.ts cut all of the door creation code into a new file door.ts inside the gameObjects folder and change the model, transform and sound to arguments:

export class Door extends Entity {

  constructor(
    model: GLTFShape,
	transform: TranformConstructorArgs,
	sound: AudioClip
  ) {
    super();
	engine.addEntity(this);

	this.addComponent(model);
	this.addComponent(new Transform(transform));

	this.addComponent(new Animator());
	this
	  .getComponent(Animator)
	  .addClip(new AnimationState("Door_Open", { looping: false }));
	this
	  .getComponent(Animator)
	  .addClip(new AnimationState("Door_Close", { looping: false }));

	this.addComponent(new AudioSource(sound));
  }
}

Update room1.ts & room2.ts to use the new Door entity:

Room1:

import { Door } from "../gameObjects/door";

export function CreateRoom1(): void {
  const door = new Door(
    new GLTFShape("models/room1/Puzzle01_Door.glb"),
	{
		position: new Vector3(21.18, 10.9, 24.5)
	},
	new AudioClip("sounds/door_squeak.mp3")
  );

  let isDoorOpen = false;
  ...

Room2:

import { Door } from "../gameObjects/door";

export function CreateRoom2(): void {
  const door = new Door(
  	  new GLTFShape("models/room2/Puzzle02_Door.glb"),
	  {
	  	  position: new Vector3(24.1, 5.51634, 24.9)
	  },
	  new AudioClip("sounds.door_squeak.mp3")
  );

  const button = new Entity();
  ...

Update door.ts to have open, close and toggle logic:

export class Door extends Entity {
  public isOpen: boolean;

  // Allow each room to specify a unique look and feel
  constructor(
    model: GLTFShape,
    transform: TranformConstructorArgs,
    sound: AudioClip
  ) {
    super();
    engine.addEntity(this);

    this.addComponent(model);
    this.addComponent(new Transform(transform));

    this.addComponent(new Animator());
    this.getComponent(Animator).addClip(
      new AnimationState("Door_Open", { looping: false })
    );
    this.getComponent(Animator).addClip(
      new AnimationState("Door_Close", { looping: false })
    );

    this.addComponent(new AudioSource(sound));
  }

  public openDoor(playAudio = true): void {
    if (!this.isOpen) {
      this.isOpen = true;

      this.getComponent(Animator)
        .getClip("Door_Close")
        .stop(); // bug workaround
      this.getComponent(Animator)
        .getClip("Door_Open")
        .play();

      if (playAudio) {
        this.getComponent(AudioSource).playOnce();
      }
    }
  }

  public closeDoor(playAudio = true): void {
    if (this.isOpen) {
      this.isOpen = false;

      this.getComponent(Animator)
        .getClip("Door_Open")
        .stop(); // bug workaround
      this.getComponent(Animator)
        .getClip("Door_Close")
        .play();

      if (playAudio) {
        this.getComponent(AudioSource).playOnce();
      }
    }
  }

  public toggleDoor(playAudio = true): void {
    if (this.isOpen) {
      this.closeDoor(playAudio);
    } else {
      this.openDoor(playAudio);
    }
  }
}

Change room1.ts & room2.ts to use the new function:

Room1:

  door.addComponent(
    new OnClick((): void => {
      // In this room, the door opens when clicked
      door.openDoor();
    })
  );

Room2:

button.addComponent(
  new OnClick((): void => {
    // Checking if timer is running
    if (!countdownText.hasComponent(utils.Interval)) {
      // Animate the button press
      button
        .getComponent(Animator)
        .getClip("Button_Action")
        .stop(); // bug workaround
      button
        .getComponent(Animator)
        .getClip("Button_Action")
        .play();

      // And play the button sound effect
      button.getComponent(AudioSource).playOnce();

      // Open the door
      door.openDoor();

      // And add an interval to update the text every 1 second and slam the door again when time runs out
      let timeRemaining = 5;
      countdownText.addComponent(
        new utils.Interval(1000, (): void => {
          // 1 second has past
          timeRemaining--;

          if (timeRemaining > 0) {
            // Update the display with the new timeRemaining
            countdownText.getComponent(TextShape).value = formatTimeString(
              timeRemaining
            );
          } else {
            // Timer has reached 0! Remove the interval so the display does not go negative
            countdownText.removeComponent(utils.Interval);

            // Close the door
			door.closeDoor();

            // Then reset the text
            countdownText.getComponent(TextShape).value = formatTimeString(5);
          }
        })
      );
    }
  })
);

Create button.ts inside the gameObject folder and update the logic similar to the door:

export class Button extends Entity {
  // The shape and position may differ
  constructor(model: GLTFShape, 
  transform: TranformConstructorArgs) {
    super();
    engine.addEntity(this);

    this.addComponent(model);
    this.addComponent(new Transform(transform));

    this.addComponent(new AudioSource(new AudioClip("sounds/button.mp3")));

    this.addComponent(new Animator());
    this.getComponent(Animator).addClip(
      new AnimationState("Button_Action", { looping: false })
    );
  }

  public press(): void {
    this.getComponent(Animator)
      .getClip("Button_Action")
      .stop(); // bug workaround
    this.getComponent(Animator)
      .getClip("Button_Action")
      .play();
    this.getComponent(AudioSource).playOnce();
  }
}

Create timer.ts inside the gameObject folder and update the logic:

export class Timer extends Entity {
  // Store the text entity for use in the method below

  constructor(transform: TranformConstructorArgs) {
    super();
    engine.addEntity(this);

    this.addComponent(new Transform(transform));

    // The value to display will be controlled by the scene itself
    this.addComponent(new TextShape());
    this.getComponent(TextShape).color = Color3.Red();
    this.getComponent(TextShape).fontSize = 5;
  }

  private formatTimeString(seconds: number): string {
    const mins = Math.floor(seconds / 60);
    const secs = Math.floor(seconds % 60);
    return (
      mins.toLocaleString(undefined, { minimumIntegerDigits: 2 }) +
      ":" +
      secs.toLocaleString(undefined, { minimumIntegerDigits: 2 })
    );
  }

  // This method can be called anytime to change the number of seconds on the clock
  public updateTimeString(seconds: number): void {
    this.getComponent(TextShape).value = this.formatTimeString(seconds);
  }
}

Replace room2.ts code to use the new Button and Timer entity:

// Create the timer on the wall
  const countdownClock = new Timer({
    position: new Vector3(25.1272, 9.51119, 25.2116),
    rotation: Quaternion.Euler(20, 180, 0)
  });
  // and set the default value
  countdownClock.updateTimeString(5);

  // Create a button
  const button = new Button(new GLTFShape("models/room2/Square_Button.glb"), {
    position: new Vector3(26.3714, 6.89, 26.8936)
  });

  button.addComponent(
    new OnClick((): void => {
      if (!countdownClock.hasComponent(utils.Interval)) {
        // Play the press effect
        button.press();
        // Open the door
        door.openDoor();

        let timeRemaining = 5;
        countdownClock.addComponent(
          new utils.Interval(1000, (): void => {
            timeRemaining--;

            if (timeRemaining > 0) {
              // Use the updateTime helper
              countdownClock.updateTimeString(timeRemaining);
            } else {
              countdownClock.removeComponent(utils.Interval);

              // Close the door
              door.closeDoor();

              // Then reset the text
              countdownClock.updateTimeString(5);
            }
          })
        );
      }
    })
  );