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>
)
}
}
Great tutorial ! Congratz !
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit
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".
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit
this.eventSubscriber.on('song${i}_click', () => ....
didn't work for me, i had to change it to:
this.eventSubscriber.on('song' + i + '_click', () => ....
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit
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
Downvoting a post can decrease pending rewards and make it less visible. Common reasons:
Submit