Decentraland Tutorial: Creating a Music Jukebox

in tutorial •  5 months ago

This is a tutorial on how to create a simple Jukebox in Decentraland.

Full source code is available below and on GitHub.

This tutorial was sponsored by Decentraland.


Setting Up the Environment

Install Node.js and Python if you don't already have them installed.

Then run:

npm install -g decentraland

npm install -g

This will install a Node.js package. Using -g installs it once for the machine (vs the default of installing packages per-project).

Create a "Basic" Project

With a cmd prompt in the project's directory, run:

dcl init

Start with a basic scene for this tutorial. This will populate the directory with a few files for us to start from. We require scene.tsx for this tutorial which only comes with the basic and interactive templates.

dcl

dcl is the command for the Decentraland SDK that we installed above.

init

dcl init will ask a series of questions to configure a project template that you can then build from.

Start the Game

dcl start

This should open a new tab automatically to http://localhost:8000

Add Assets

Add the art and sounds for our app to the project's directory.

Download the model and music we've selected or use your own of course.

Add the Jukebox to the Scene

Modify scene.tsx, removing everything between the scene tags and then adding a gltf model:

<scene>
  <gltf-model
    src="art/Jukebox.gltf">
  </gltf-model>
</scene>

gltf-model

gltf is the model format Decentraland uses. It's supported by Blender, see Decentralands Scene Content Guide for more information.

Position and Scale Jukebox

Adjust the position and scale until it looks good.

<gltf-model ...
  position={{x: 5, y: 0, z: 9.5}}
  scale={.6}>
</gltf-model>

scale

The scale can either be a single number as shown here or a Vector3 (similiar to the position). Use a Vector3 to scale the model unevenly (e.g. in order to stretch the height).

position

The position entered is relative to the tile's origin.

Define the Songs to be Included

In the SampleScene class, add songs:

export default class SampleScene extends DCL.ScriptableScene 
{
  songs: {src: string, name: string}[] = 
  [
    {src: "sounds/Concerto a' 4 Violini No 2 - Telemann.mp3", name: "Telemann"},
    {src: "sounds/Double Violin Concerto 1st Movement - J.S. Bach.mp3", name: "Bach"},
    {src: "sounds/Rhapsody No. 2 in G Minor – Brahms.mp3", name: "Brahms"},
    {src: "sounds/Scherzo No1_ Chopin.mp3", name: "Chopin"},
  ];

{src: string, name: string}[]

This is Typescript syntax for defining the type of information the songs variable will include. Here we have an array of JSON data. The JSON data includes a src, which is the path to the song's file, and a name which will appear in Decentraland.

Add Buttons

Add a model for a button on the Jukebox for each song. We'll make these interactive soon.

First add {this.renderButtons()} as a child of the model:

  <gltf-model ...>
    {this.renderButtons()}
  </gltf-model>

Then create the renderButtons() method:

renderButtons() 
{
  return this.songs.map((song, index) =>
  {
    let x = index % 2 == 0 ? -.4 : .1;
    let y = Math.floor(index / 2) == 0 ? 1.9 : 1.77;
    return (
      <entity
          position={{x, y, z: -.7}}>
        <cylinder
          id={"song" + index}
          rotation={{x: 90, y: 0, z: 0}}
          scale={{x: .05, y: .2, z: .05}}
          color="#990000" />
      </entity>
    )
  });
}

this.songs.map((song, index) =>

.map will call the following method for each item in the array.

x and y

We are positioning the buttons automatically based on their index. You may need to make adjustments if you have a different jukebox model or if you have more/less songs. Alternatively you could position each explicitly by defining the button's position in the songs JSON data.

entity

The entity here acts as a parent object to assist with positioning. We will be adding a text label for the name of the song below. This approach allows the button and the text to be positioned together, and still allow the button to move when pressed without also moving the text.

rotation

Rotation here is in Euler form, defined in degrees.

Add Text for Each Song

Inside the entity for each song, add a text label displaying the song's name.

  <cylinder ... />
  <text 
    hAlign="left"
    value={this.songs[index].name} 
    position={{x: .26, y: 0, z: -.1}}
    scale={.4} />
</entity>

Add State for the Buttons

In the SampleScene class, add state:

songs: ...;

state: {buttonState: boolean[], lastSelectedButton: number} = {
  buttonState: Array(this.songs.length).fill(false),
  lastSelectedButton: 0,
};

Array(this.songs.length).fill(false)

This creates an array to track state for each song. .fill(false) sets the default state for each button to be unpressed, without this the default would be undefined.

Position Buttons when Pressed

Set the position for the button's cylinder when pressed, and use a transition to animate it.

let buttonZ = 0;
if(this.state.buttonState[index]) 
{
  buttonZ = .1;
}
return (
  <entity ...>
    <cylinder ...
      position={{x: 0, y: 0, z: buttonZ}} 
      transition={{position: {duration: 100}}} />
      <text ...

Note you could change the buttonState manually at this point to test.

transition

Transitions can be used in Decentraland to animate from one state to another. This works for position, rotation, scale, and color. See Scene State in the Decentraland docs for more information.

Push buttons

Subscribe to the click event for each button. When clicked, update the button state. The previously clicked button (if any) should automatically unclick when another is selected.

sceneDidMount() 
{
  for(let i = 0; i < this.songs.length; i++)
  {
    this.eventSubscriber.on(`song${i}_click`, () => 
    {
        let newButtonState = this.state.buttonState.slice();
        if(i != this.state.lastSelectedButton)
        {
          newButtonState[this.state.lastSelectedButton] = false;
        }
        newButtonState[i] = !this.state.buttonState[i];
        this.setState({
          buttonState: newButtonState,
          lastSelectedButton: i
        });
    });
  }
}

sceneDidMount

sceneDidMount is an event which is called after the scene has loaded. We use this for any initialization required, such as subscribing to object events.

this.eventSubscriber.on('song${i}_click', () =>

The eventSubscriber allows you to respond to object specific events, in this example when the user clicks on the object. song${i} is the id of the object and click is the event type.

.slice()

We start by copying the current buttonState array. This allows us to make changes without potentially causing unintended consequences.

this.setState

Use setState to modify any of the variables stored by the state variable. When using this method, render is called automatically, without any need to call forceUpdate (which would be less efficient than only rendering changes). See Scene State in the Decentraland docs for more information.

Play the Selected Song

Add sound to the entity for each song to play when that button is selected.

<entity ...
  sound={{ 
    src:song.src, 
    playing:this.state.buttonState[index]}}>


That’s it! You now know the basics and with a bit of time should be able to create some interesting interactive experiences in Decentraland. Hope this was helpful and let us know if you have questions.

Some possible next steps:

  • Maybe charge for playing songs, see the pay to open sample.
  • Maybe add multiplayer support, so people may listen to music together. See the multiplayer sample.
  • Maybe add a dance floor, thumping speakers, and a bar.



Source Code

scene.tsx:

import * as DCL from 'metaverse-api'

export default class SampleScene extends DCL.ScriptableScene 
{
  songs: {src: string, name: string}[] = 
  [
    {src: "sounds/Concerto a' 4 Violini No 2 - Telemann.mp3", name: "Telemann"},
    {src: "sounds/Double Violin Concerto 1st Movement - J.S. Bach.mp3", name: "Bach"},
    {src: "sounds/Rhapsody No. 2 in G Minor – Brahms.mp3", name: "Brahms"},
    {src: "sounds/Scherzo No1_ Chopin.mp3", name: "Chopin"},
  ];

  state: {buttonState: boolean[], lastSelectedButton: number} = { 
    buttonState: Array(this.songs.length).fill(false),
    lastSelectedButton: 0,
  };

  sceneDidMount() 
  {
    for(let i = 0; i < this.songs.length; i++)
    {
      this.eventSubscriber.on(`song${i}_click`, () => 
      {
        let newButtonState = this.state.buttonState.slice();
        if(i != this.state.lastSelectedButton)
        {
          newButtonState[this.state.lastSelectedButton] = false;
        }
        newButtonState[i] = !this.state.buttonState[i];
        this.setState({
          buttonState: newButtonState,
          lastSelectedButton: i
        });
      });
    }
  }

  renderButtons() 
  {
    return this.songs.map((song, index) =>
    {
      let x = index % 2 == 0 ? -.4 : .1;
      let y = Math.floor(index / 2) == 0 ? 1.9 : 1.77;
      let buttonZ = 0;
      if(this.state.buttonState[index]) 
      {
        buttonZ = .1;
      }
      return (
        <entity
            position={{x, y, z: -.7}}
            sound={{ 
              src:song.src, 
              playing:this.state.buttonState[index]}}>
          <cylinder
            id={"song" + index}
            position={{x: 0, y: 0, z: buttonZ}} 
            transition={{position: {duration: 100}}} 
            rotation={{x: 90, y: 0, z: 0}}
            scale={{x: .05, y: .2, z: .05}}
            color="#990000" />
            <text 
              hAlign="left"
              value={this.songs[index].name} 
              position={{x: .26, y: 0, z: -.1}}
              scale={.4}/>
        </entity>
      )
    });
  }

  async render() 
  {
    return (
      <scene>
        <gltf-model
          src="art/Jukebox.gltf"
          scale={.6}
          position={{x: 5, y: 0, z: 9.5}}>
          {this.renderButtons()}
        </gltf-model>
      </scene>
    )
  }
}
Authors get paid when people like you upvote their post.
If you enjoyed what you read here, create your account today and start earning FREE STEEM!
Sort Order:  

Great tutorial ! Congratz !

It's nice too see that you finally got something polished and completed the tutorial. I've watched most of the creation of this jukebox on stream, but I will give it a go and follow the tutorial as well. Also big + for that multiplayer idea, hope will get a reality and fulfill that scenario that you presented (people hanging out in a virtual scene). It kind of reminds me of "second life", where people can build stuff and sell it...nice idea though with the "Bar/Dance floor".

this.eventSubscriber.on('song${i}_click', () => ....

didn't work for me, i had to change it to:

this.eventSubscriber.on('song' + i + '_click', () => ....

·

It's hard to see, but it's using a different kind of quote. For the former, you must use the 'grave key' (it's a single quote with an angle) ` vs '

hth