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.
Install Node.js and Python if you don't already have them installed.
Then run:
npm install -g decentraland
npm install -gThis will install a Node.js package. Using -g installs it once for the machine (vs the default of installing packages per-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.
dcldcl is the command for the Decentraland SDK that we installed above.
initdcl init will ask a series of questions to configure a project template that you can then build from.
dcl start
This should open a new tab automatically to http://localhost:8000
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.
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-modelgltf is the model format Decentraland uses. It's supported by Blender, see Decentralands Scene Content Guide for more information.
Adjust the position and scale until it looks good.
<gltf-model ...
position={{x: 5, y: 0, z: 9.5}}
scale={.6}>
</gltf-model>
scaleThe 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).
positionThe position entered is relative to the tile's origin.
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 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 yWe 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.
entityThe 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.
rotationRotation here is in Euler form, defined in degrees.
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>
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.
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.
transitionTransitions 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.
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
});
});
}
}
sceneDidMountsceneDidMount 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.setStateUse 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.
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:
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>
)
}
}