Second Decentraland Coding Challenge
Second Decentraland Coding Challenge
This second stage of the coding challenge series builds upon the first stage.
In this second stage, you must write a small proof-of-concept that renders a scene from instructions provided by a sandboxed Javascript environment.
You must push your solution to a GitHub repository and send an email to challenge@decentraland.org including the URL.
Requirements
Ensure that your submission is in line with these requirements:
- Game Engine: it must use either Bevy (for Rust) or Unity (for C#).
- Compliance: it must produce the behavior detailed below.
- Code quality: the code must be readable, well-organized and well-architectured.
- Running script: it must run with a specific script (see below).
- License: it must include an Apache License 2.0 file, as the Decentraland source.
- Functionality:
- Create a game environment with ECS-like functionality (adding entities, attaching components, and updating data).
- Load the test file in the sandboxed environment (see below).
- Establish a bidirectional channel between the game and sanboxed environments.
- Correctly render and animate a cube according to the script’s instructions.
- Provide keyboard input from the game engine to the script.
- Detect pressed keys inside the script.
Global Objects
The sandboxed environment for this challenge is similar to the one provided in the [[first stage]], with some additional complexity.
The most important change is to the engine.sendMessage
method. Now, it is expected to send an array of requests and receive an array of responses. All messages are in serialized JSON form.
Unlike in the previous challenge, output written to stdout
will be ignored. You are free to use it for your own purposes.
Here’s the environment the sandboxed script will expect, in pseudocode resembling TypeScript:
// EntityId uniquely references an Entity shared between the host and the script.
type EntityId = number
// EngineRequest is a message sent from the script to the host. Each type of message has a unique name and specific information.
type EngineRequest =
{ method: "entity_add", data: EngineEntityAdd } |
{ method: "entity_transform_set", data: EngineEntityTransformSet }
type EngineEntityAdd = {
id: EntityId
}
type EngineEntityTransformSet = {
entityId: EntityId,
transform: Transform
}
type Transform = {
position: [number, number, number], // XYZ position
rotation: [number, number, number, number] // XYZW quaternion
scale: [number, number, number] // XYZ size
}
// EngineResponse is a message sent from the host to the script. Each type of message has a unique name and specific information.
type EngineResponse = |
{ method: "key_up", data: EngineKeyPressed } |
{ method: "key_down", data: EngineKeyPressed }
type EngineKeyPressed = {
key: "space" // only the space key will be used in this proof of concept
}
// EngineModule can be imported by the script via `require` to exchange JSON messages.
type EngineModule = {
async sendMessage(messages: Array<string>): Array<string>
}
declare function require(moduleName: "~engine"): EngineModule
declare function require(moduleName: string): any // throw an error
Sandboxed Code
This is the script that will be fed to the sandboxed execution environment:
const engine = require("~engine")
// Our simple serialization/deserialization functions for messages:
const encode = (obj) => JSON.stringify(obj)
const decode = (obj) => JSON.parse(obj)
// Queues for incoming and outgoing messages:
const incoming = []
const outgoing = []
async function sendReceive() {
// Exchange serialized messages:
const newIncoming = await engine.sendMessages(outgoing.map(encode)).map(decode)
// Place incoming messages in the queue, and clear the outgoing messages we just sent:
incoming = incoming.concat(newIncoming)
outgoing = []
}
// The known ID of the only Entity in this example:
const cubeId = 1
let rotationX = 0
let scaleY = 0
let isSpaceBarPressed = 0
module.exports.onStart = async function() {
outgoing.push({
method: "entity_add",
data: { id: cubeId }
})
outgoing.push({
method: "entity_transform_update",
data: {
entityId: cubeId,
transform: {
position: [0, 0, 0],
rotation: [0, 0, 0, 0],
scale: [1, 1, 1]
}
}
})
await sendReceive()
}
module.exports.onUpdate = async function(dt) {
// Process incoming messages:
for (msg of incoming) {
if (msg.method === "key_down" && msg.data.key === "space") {
isSpaceBarPressed = true
}
if (msg.method === "key_up" && msg.data.key === "space") {
isSpaceBarPressed = false
}
}
// Clear queue
incoming = [];
/**
* Pressing the space bar makes the cube grow bigger.
* If it's released, it shrinks back to its original size.
*/
if (isSpaceBarPressed) {
scaleY += dt
} else {
scaleY = Math.max(1.0, scaleY - dt)
}
/**
* The cube rotates on the X axis with time
*/
rotationX += dt
// Queue outgoing messages:
outgoing.push({
method: "entity_transform_update",
data: {
entityId: cubeId,
transform: {
position: [0, 0, 0],
rotation: [rotationX, 0, 0, 0],
scale: [1, scaleY, 1]
}
}
})
// Make the exchange:
await sendReceive()
}
Expected Execution and Result
Your submission must run as follows, from the root of your project:
$ ./bin/run
Ensure that the run
script takes care of any preparations you need.
Rust projects will run in a Unix shell environment with cargo
installed, and C# projects will run in a Windows 10 shell environment with dotnet
and .NET 6
installed.
Remember your code will be evaluated as well, to examine general quality and whether there’s a solid, extensible implementation.
Final word
Thank you for participating in this challenge. We’re looking forward to see your submission!