Hello! Please choose your
desired language:
Dismiss

The brand new Genesis Plaza is not just gorgeous to look at, but it also packs a lot of very cool interactive content. We wanted to share some tips and tricks that went into its creation as much of it could surely help a lot of creators out there.

We have made the code for the Plaza open source so anyone can explore and learn from what’s there. Take a look!

https://github.com/decentraland-scenes/Genesis-Plaza

To keep you from getting overwhelmed we’ve put together a few smaller repos that focus on specific mechanics that are out there in the Plaza. These are easier to digest with less clutter to sort through.

Part 2 of the post will focus on some of the new features that were introduced together with the Plaza. In this first post we’ll discuss some general recommendations and best practices and take a look at often-overlooked but essential tools that any Decentraland dev should have in their toolbox. We’ll also explore how these tools helped make the Plaza what it is and how they made our lives so much easier while we were working on it.

Tip 1: Position objects quickly

Setting an entity’s position is fairly easy, but building the Plaza involved setting coordinates for a LOT of entities, including wearables on display, teleports and so much more. If we tried to place each of those by just guessing and adjusting, we’d still be working on it. This little hack was a HUGE time saver for us.

Before we start it’s important to note that in many cases it’s a better alternative to use the Builder to set an item’s positions and then export the scene to keep working on the project with the SDK. In our case, however, that was not an option because the Plaza is a 10x10 scene and the Builder can’t deal with such big scenes. Also, this little trick allows you to have a much more flexible workflow, since you don’t need to export a fresh scene from the Builder for every model you add or move.

Instead, we just added these few lines of code into game.ts.

Input.instance.subscribe('BUTTON_DOWN', ActionButton.PRIMARY, false, (e) => {
   log(`pos: `, Camera.instance.position)
   log(`rot: `, Camera.instance.rotation)
})

With this, you get the player’s position and rotation logged to the browser’s console (view> Developers> JavaScript Console) every time you press the E key . You can then copy these coordinates and rotation values into the Transform component of any entity you want to position.

It helps to use the third person camera to find the right spot more accurately. Note that in third person you will still be logging the avatar’s position, not that of the camera. So feel free to swirl the camera around the player, it won’t affect the position values, only the rotation values.

The rotation here is expressed as a Quaternion, as we can use this to directly copy into a Transform, but if you prefer you could express this rotation as euler angles by logging Camera.instance.rotation.eulerAngles instead.

To learn more about how to set an entity’s position and rotation, see this page in the docs: https://docs.decentraland.org/development-guide/entity-positioning/

Note: Remember to comment out this bit of code when you’re done creating the scene, as logging those messages has an effect on the scene’s performance!

Tip 2: The utils library

If you haven’t yet discovered the decentraland-ecs-utils library, it’s immensely helpful. It includes a lot of handy functions that greatly simplify common tasks, like delaying an action, moving an entity smoothly over time, etc.

https://www.npmjs.com/package/decentraland-ecs-utils

One thing from this library that we used extensively in Genesis Plaza were trigger boxes. For example, all elevators here are triggered by trigger boxes, so that the elevator is activated as soon as the player steps in it.

// entity to use as trigger 
const moonTowerTriggerEntity = new Entity()
moonTowerTriggerEntity.addComponent(new Transform(triggerPos))
   
// dimensions and relative position of the trigger box
let moonTowerTriggerBox = new utils.TriggerBoxShape(triggerScale, Vector3.Zero())
 
// add trigger component
moonTowerTriggerEntity.addComponent(
  new utils.TriggerComponent(
    moonTowerTriggerBox, //shape
    0, 0, null, null, //ignored parameters
    //onCameraEnter function:
    () => {
      log('triggered platform')
         moonTower_Elevator.activate()
     }
   )
)
   
// add trigger entity to engine
engine.addEntity(moonTowerTriggerEntity)

We also used similar trigger boxes to activate different things that were heavy to load or didn’t want to keep playing continuously. For example, the large video screen on the North-East corner is surrounded by a large trigger box and the video streaming is only turned on when the player walks near it.

Tip 3: P2P MessageBus

Genesis Plaza needed to be an experience that you share with others around you, it was imperative to have some multiplayer mechanics in the Plaza. When a player changes something, other players there need to see it as well.

There are different ways to make this possible, but the simplest is using the scene’s Message Bus.

The elevator code shared above is a simplification of what we really did. The elevators weren’t activated directly by the trigger areas, instead they were sending a message via the Message Bus and then we were listening for that message and reacting to it.

In a single-player scenario, everything works just the same, since the player receives their own messages and reacts to them. But if multiple players are in the scene, then they will all receive each other’s messages too, making sure they all see the elevator doing the same!

// add trigger component
moonTowerTriggerEntity.addComponent(
  new utils.TriggerComponent(
    moonTowerTriggerBox, //shape
    0, 0, null, null, //ignored parameters
    //onCameraEnter function:
    () => {
      log('triggered platform')
      // send a message
      sceneMessageBus.emit('moonElevatorActivated', {})
    }
  )
)
   
// listen for message
sceneMessageBus.on('moonElevatorActivated', (e) => {
  moonTower_Elevator.activate()
  log('moon tower elevator')
})

The balloon and the flying train use the same mechanics as the elevators, with one added consideration: we include a wait period so that other players can’t activate the trigger area again while it’s in flight.

Here’s the code that listens for messages of the balloon being activated. Note that we first check the value of a boolean flag. If it’s true we do nothing, otherwise we set it to true, and then use the utils library to set it back to false after 150 seconds (which is how long the balloon’s animation lasts).

 
sceneMessageBus.on('balloonActivated', (e) => {
  if (ballonIsFlying) {
    log('balloon was already in flight')
     return
  }
  ballonIsFlying = true
  balloon.activate()
  balloon.addComponentOrReplace(
    new utils.Delay(150 * 1000, () => {
      ballonIsFlying = false
    })
  )
})

Then there’s the floor piano. It’s perhaps the ultimate example of combining the Message Bus and trigger areas. This piano wasn’t part of the first release of Genesis Plaza, so you might have not seen it, but you can find it by going straight up towards the north from the center of the Plaza, behind the whale building.

The piano simply consists of a set of trigger areas, one for each key. When activated, each trigger area sends a message via the Message Bus telling you and others to play the corresponding sound file for that piano note.

You can find a cleaner version of the piano in its own separate repo here: https://github.com/decentraland-scenes/piano-floor-example-scene

To learn more about using the Message Bus, check out this page in the docs: https://docs.decentraland.org/development-guide/remote-scene-considerations/#p2p-messaging

We’ll cover more ways to achieve multiplayer mechanics in a coming article.

Tip 4: Use custom classes

A lot of functionality in the scene was repeated across several entities. For example, we had 50 wearables on display, 16 paintings in the hallway and 5 elevators. When you know in advance that you’re going to have to repeat a part of your code often, it pays off to abstract it away into something you can reuse. Instead of repeatedly spelling out the whole set of components for each one of these entities, it pays to define your own class that has much of its functionality implicitly built in.

If you sort through the Genesis Plaza repo, you’ll find we did quite a bit of that. The code would have otherwise been so much longer and harder to maintain.

This idea was very well covered in the Escape Room tutorial we did together with Nick from HardlyDifficult last year:

Here’s the most relevant video to this tip: https://www.youtube.com/watch?v=_kksSC91DKE&list=PLAcRraQmr_GOhimaVZSlJrkzCvo8crIBQ&index=10

And here’s the full playlist: https://www.youtube.com/watch?v=j7XbiTZ9GN0&list=PLAcRraQmr_GOhimaVZSlJrkzCvo8crIBQ

Tip 5: Optimizing Performance

It’s important to us that players have a smooth first experience with Decentraland when they visit Genesis Plaza. This was a challenge since the Plaza is large and has a lot of content that is loaded into memory at the same time. As mentioned, we used some trigger areas to keep some functionality off while the player wasn’t near enough to appreciate it, but we also put much care into making everything in the Plaza as lightweight as we could.

The main bottleneck in a scene’s performance is usually the sending of messages that happens between the scene’s code and the engine. If you want to optimize how smoothly a scene runs, the single most important thing to pay attention to is how many messages are sent from the SDK to the engine.

How can you know if this stream of messages is running into a bottleneck? Here’s a nice little trick: When you run the scene in preview mode, note that on the top-right corner it says “P = Toggle Panel”. There’s a good chance you never noticed it was there, but it’s been there for a long time. Hit P and that opens a panel with some useful information that gets updated in real time as you interact.

As you interact with things that involve messages between the SDK and the engine, you’ll notice the ‘Processed Messages’ number will grow. That’s alright as long as the ‘Pending on Queue’ number remains in 0 or close to 0. If that number starts to grow, then you’ve entered the danger zone and you know you need to do more optimizations.

Note: Don’t keep the panel open while you’re not using it, since it has an impact on performance.

Whenever you have something that moves in your scene, there’s a tradeoff to consider: should it move based on an animation or should it be moved by code? There are good reasons to use one method or the other. It all comes down to what you’re trying to do. For the case of the Plaza, all of the moving objects do so via animation, that includes all of the elevators, the balloon and the flying train. Playing an animation is a lot more efficient in terms of resources than moving an object frame by frame.

If we move an elevator by playing an animation, we just need to send one message to the engine that tells it to play the animation and the engine then handles the rest from there. If we move the elevator via code, on the other hand, we need to send the engine a new message with the new position on every single frame. If we have a few things moving like that at the same time in our scene, it all adds up.

One of the various downsides of using animations rather than code, is that we then can’t fetch the position of the moving entity at a given time, as the entity’s position stored in the Transform component doesn’t really change as the animation plays. If we needed to fetch the position of the balloon at any given time, we would have to move it by code. In our case, we opted for the lightest approach, as that was our priority.


So, quite a few useful ideas shared in this first installment. Take some time to digest everything, explore the repo, try things out on your own. And stay tuned for more articles covering other aspects of how we built Genesis Plaza.