This is a tutorial on creating a basic **AI in Decentraland**. We'll add a dog which follows you around and listens to some of your commands.

Full source code is available below and on GitHub.

If you are new to Decentraland development, you may want to start with our beginner tutorial, creating a Jukebox.

This tutorial was sponsored by Decentraland.

## Setting Up the Environment

One time setup:

```
npm install -g decentraland
```

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.

Now 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 models we've created or use your own of course.

## Add the Models to the Scene

Modify `scene.tsx`

, removing everything between the `scene`

tags and then adding gltf models:

```
<scene>
<gltf-model
id="Dog"
src="art/BlockDog.gltf"
/>
<gltf-model
id="Bowl"
src="art/BlockDogBowl.gltf"
/>
</scene>
```

**Test**: You should see both the dog and his bowl.

`gltf-model`

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

## Add Babylonjs

We will be using Babylonjs for their Vector3 and Quaternion support.

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

```
npm install babylonjs
```

Then add the following to the top of `scene.tsx`

:

```
import {Vector3, Quaternion} from "babylonjs";
```

Note: The Vector3Component that Decentraland uses by default is just data. To ease coding, we'll use Babylon's instead which includes functions such as `.subtract`

and `.normalize`

. You could use another library for this or code the math yourself.

## Add `state`

for position and rotation

Above the `SampleScene`

class, add an interface for state:

```
export interface IState
{
characterPosition: Vector3,
bowlPosition: Vector3,
dogPosition: Vector3,
dogRotation: Quaternion,
}
```

Add `IState`

to the `SampleScene`

class definition:

```
export default class SampleScene extends DCL.ScriptableScene<any, IState>
```

Then inside the class, add the state object itself including their default values:

```
state = {
characterPosition: new Vector3(0, 0, 0),
bowlPosition: new Vector3(1, 0, 1),
dogPosition: new Vector3(9, 0, 9),
dogRotation: new Quaternion(0, 0, 0, 1),
};
```

And finally leverage this state in the `render`

function:

```
<gltf-model...
position={this.state.dogPosition}
rotation={this.state.dogRotation.toEulerAngles().scale(180 / Math.PI)}
transition={{
rotation: {
duration: 300
}
}}
/>
<gltf-model...
position={this.state.bowlPosition}
/>
```

Note: we'll be leveraging the `characterPosition`

later in the tutorial.

**Test**: The dog is in one corner, the bowl in the other.

`export interface IState`

This is declaring the type of information that will be stored by the state object. This declaration is optional, but a best practice and will allow your IDE to display better hints.

`Vector3`

For more information see Babylonjs's doc on Vector3.

`Quaternion`

For more information see Babylonjs's doc on Quaternion.

Quaternion is the preferred format used by games for storing and modifying rotation. For more information on the Quaternion format, see our tutorial on Quaternions.

`<any, IState>`

This format is for Generics in Typescript. Generics allow you to write more general purpose code and then have specific types slotted in after. In this example we are allowing our base class, `DCL.ScriptableScene`

, to know exactly what state will be stored.

`.toEulerAngles().scale(180 / Math.PI)`

Decentraland is expecting rotations in Eulers, using degrees. This is converting our Quaternion to that desired format.

`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.

## Animate the Dog

There should always be an animation playing on the dog. For this tutorial, we'll be supporting an idle animation, walking, and sitting. Weights will be used in order to transition between them.

To begin, add the animations to the dog's `gltf-model`

and hardcode the weight for each:

```
const animationWeights = {idle: 1, walk: 0, sit: 0};
return (
<scene>
<gltf-model...
skeletalAnimation={[
{
clip: "Idle",
weight: animationWeights.idle,
},
{
clip: "Walking",
weight: animationWeights.walk,
},
{
clip: "Sitting",
weight: animationWeights.sit,
},
]}
/>
```

**Test**: The dog should be wagging his tail. Change the hard coded weights to view the other animations, e.g. `{idle: 0, walk: 1, sit: 0}`

## Define Goals for the Dog

We'll be implementing the dog's AI as a state machine. Define each of the possible states in an `enum`

:

```
enum Goal
{
Idle,
Sit,
Follow,
GoDrink,
Drinking,
}
```

Then track the current goal, previous goal, and a weight to transition between them:

```
export interface IState
{
...
dogGoal: Goal,
dogPreviousGoal: Goal,
dogAnimationWeight: number,
}
...
state = {
...
dogGoal: Goal.Idle,
dogPreviousGoal: Goal.Idle,
dogAnimationWeight: 1,
};
```

Now implement a function to calculate each animation's weight based on the dog's goals.

```
getAnimationRates() : {idle: number, sit: number, walk: number}
{
const weight = Math.min(Math.max(this.state.dogAnimationWeight, 0), 1);
const inverse = 1 - weight;
let sit = 0;
let walk = 0;
switch(this.state.dogPreviousGoal)
{
case Goal.Sit:
sit = inverse;
break;
case Goal.Follow:
case Goal.GoDrink:
walk = inverse;
break;
}
switch(this.state.dogGoal)
{
case Goal.Sit:
sit = weight;
break;
case Goal.Follow:
case Goal.GoDrink:
walk = weight;
break;
}
return {idle: 1 - (sit + walk), sit, walk};
}
```

Then update `animationWeights`

in `render`

to use this function:

```
const animationWeights = this.getAnimationRates();
```

**Test**: The dog should be wagging his tail exactly as he did before. Try changing the `dogGoal`

to change the animation. If the `dogAnimationWeight`

is less than 1, you should see the animation selected by `dogPreviousGoal`

blending in.

`Math.min(Math.max(x, 0), 1)`

This clamps the value of x to be between 0 and 1.

`idle: 1 - (sit + walk)`

The total weight across all animations should equal 1. By setting the idle weight to be the remainder, we are ensuring the dog is always moving a little.

## Sit on Command

Change the dog's goal to `Sit`

when you click on him. If he's already sitting, return to `Idle`

.

Add an interval which fires each frame to increase the `dogAnimationWeight`

smoothly.

```
sceneDidMount()
{
this.eventSubscriber.on("Dog_click", () =>
{
this.setDogGoal(this.state.dogGoal == Goal.Sit ? Goal.Idle : Goal.Sit);
});
setInterval(() =>
{
const weight = Math.min(Math.max(this.state.dogAnimationWeight, 0), 1);
this.setState({dogAnimationWeight: weight + .01});
}, 1000/60);
}
setDogGoal(goal: Goal)
{
this.setState({
dogGoal: goal,
dogAnimationWeight: 1 - this.state.dogAnimationWeight,
dogPreviousGoal: this.state.dogGoal
});
}
```

**Test**: Clicking the dog will transition between sitting and standing.

`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(...`

The `eventSubscriber`

allows you to respond to object specific events, in this example when the user clicks on the object.

`this.setState`

Use `setState`

to modify any of the variables stored by `state`

. When using this function, render is called automatically. See Decentraland's Scene State.

## Go Drink Some Water

When you click on the bowl, the dog should walk over and have a drink.

Update the `sceneDidMount`

function to add click event for the bowl. In the existing interval, add logic to `walkTowards`

the target.

```
this.eventSubscriber.on("Bowl_click", () =>
{
this.setDogGoal(Goal.GoDrink);
});
setInterval(() =>
{
...
switch(this.state.dogGoal)
{
case Goal.Follow:
case Goal.GoDrink:
const targetLocation = this.state.dogGoal == Goal.Follow ? this.state.characterPosition : this.state.bowlPosition;
const delta = targetLocation.subtract(this.state.dogPosition);
if(delta.lengthSquared() < 2)
{
this.setDogGoal(this.state.dogGoal == Goal.Follow ? Goal.Sit : Goal.Drinking);
}
else
{
this.walkTowards(targetLocation);
}
}
}, 1000/60);
```

Create a function for `walkTowards`

which smoothly updates the dog's position and set's his rotation to face the target.

```
walkTowards(position: Vector3)
{
let delta = position.subtract(this.state.dogPosition);
delta = delta.normalize().scale(.015); // .015 is the walk speed
delta.y = 0;
const newPosition = this.state.dogPosition.add(delta);
if(!isInBounds(newPosition))
{
this.setDogGoal(Goal.Idle);
}
else
{
this.setState({
dogPosition: newPosition,
dogRotation: lookAt(this.state.dogPosition, position, -Math.PI / 2)
});
}
}
```

Add the following functions to the bottom of the `scene.tsx`

file:

```
function isInBounds(position: Vector3): boolean
{
return position.x > .5 && position.x < 9.5
&& position.z > .5 && position.z < 9.5;
}
```

Note: If you own multiple parcels of land, you may want to update the bounds used here.

Unfortunately there is not a `lookAt`

implementation easily accessible. So we copied the following from Babylon's TransformNode:

```
// Math from Babylon TransformNode
function lookAt(
pos: Vector3,
targetPoint: Vector3,
yawCor: number = 0,
pitchCor: number = 0,
rollCor: number = 0): Quaternion
{
const dv = targetPoint.subtract(pos);
const yaw = -Math.atan2(dv.z, dv.x) - Math.PI / 2;
const len = Math.sqrt(dv.x * dv.x + dv.z * dv.z);
const pitch = Math.atan2(dv.y, len);
return Quaternion.RotationYawPitchRoll(yaw + yawCor, pitch + pitchCor, rollCor);
}
```

**Test**: Click on the water bowl and the dog should walk over to it and then stop and stand in front of it.

`.lengthSquared()`

This checks the distance remaining to the target. When calculating distance, the lengthSquared is first calculated and then a square root is taken to get the length. Game developers often use lengthSquared where possible as a best practice anytime we do not need the specific length.

`normalize().scale(.015)`

By normalizing the delta position we get a direction. Then the direction is scaled, providing the correct offset for a single frame of movement.

`-Math.PI / 2`

This is defining a rotation offset for the model. Without this, in our example the dog would walk sideways.

## Follow the Character Around

The dog should randomly decide to follow the character from time to time. When he catches up to the character, the dog sits (to ask for a treat).

Add the following to the `sceneDidMount`

function to make the player's position available in `state`

and to `considerGoals`

periodically:

```
this.subscribeTo("positionChanged", (e) =>
{
this.setState({characterPosition: new Vector3(e.position.x, e.position.y, e.position.z)});
});
setInterval(() =>
{
if(this.state.dogAnimationWeight < 1)
{
return;
}
switch(this.state.dogGoal)
{
case Goal.Idle:
this.considerGoals([
{goal: Goal.Sit, odds: .1},
{goal: Goal.Follow, odds: .9},
]);
case Goal.Drinking:
this.considerGoals([
{goal: Goal.Sit, odds: .1},
]);
case Goal.Follow:
this.considerGoals([
{goal: Goal.Idle, odds: .1},
]);
case Goal.GoDrink:
case Goal.Sit:
this.considerGoals([
{goal: Goal.Idle, odds: .1},
]);
}
}, 1500);
```

Note: We are using a second, slower interval here so the dog does not change his mind too quickly.

Then implement the `considerGoals`

function:

```
considerGoals(goals: {goal: Goal, odds: number}[])
{
for(let i = 0; i < goals.length; i++)
{
if(Math.random() < goals[i].odds)
{
switch(goals[i].goal)
{
case Goal.Follow:
if(!isInBounds(this.state.characterPosition))
{
continue;
}
}
this.setDogGoal(goals[i].goal);
return;
}
}
}
```

**Test**: When you are in bounds, the dog may randomly start following you.

`subscribeTo("positionChanged"...`

Decentraland communicates information by way of events whereever possible. See Decentraland's Event Handling.

`new Vector3`

Here we are creating a Babylonjs Vector3 from Decentraland's Vector3Component.

That’s it! Hope this was helpful and let us know if you have questions.

Next steps:

- The model has an animation called 'Drinking'. Add this animation to the code above.
- Use slerp to adjust the rotation in code and then the dot product to scale the dog's movement speed.
- When the animation states change rapidly, it does not transition smoothly. Can you fix this? Maybe better consideration of previous states or by leveraging animation transitions defined by your modeler.
- Add more puppies!

# Source Code

** scene.tsx**:

```
import * as DCL from 'metaverse-api'
import {Vector3, Quaternion} from "babylonjs";
export interface IState
{
characterPosition: Vector3,
bowlPosition: Vector3,
dogPosition: Vector3,
dogRotation: Quaternion,
dogGoal: Goal,
dogPreviousGoal: Goal,
dogAnimationWeight: number,
}
enum Goal
{
Idle,
Sit,
Follow,
GoDrink,
Drinking,
}
export default class SampleScene extends DCL.ScriptableScene<any, IState>
{
state = {
characterPosition: new Vector3(0, 0, 0),
bowlPosition: new Vector3(1, 0, 1),
dogPosition: new Vector3(9, 0, 9),
dogRotation: new Quaternion(0, 0, 0, 1),
dogGoal: Goal.Idle,
dogPreviousGoal: Goal.Idle,
dogAnimationWeight: 1,
};
getAnimationRates() : {idle: number, sit: number, walk: number}
{
const weight = Math.min(Math.max(this.state.dogAnimationWeight, 0), 1);
const inverse = 1 - weight;
let sit = 0;
let walk = 0;
switch(this.state.dogPreviousGoal)
{
case Goal.Sit:
sit = inverse;
break;
case Goal.Follow:
case Goal.GoDrink:
walk = inverse;
break;
}
switch(this.state.dogGoal)
{
case Goal.Sit:
sit = weight;
break;
case Goal.Follow:
case Goal.GoDrink:
walk = weight;
break;
}
return {idle: 1 - (sit + walk), sit, walk};
}
sceneDidMount()
{
this.eventSubscriber.on("Dog_click", () =>
{
this.setDogGoal(this.state.dogGoal == Goal.Sit ? Goal.Idle : Goal.Sit);
});
this.eventSubscriber.on("Bowl_click", () =>
{
this.setDogGoal(Goal.GoDrink);
});
setInterval(() =>
{
const weight = Math.min(Math.max(this.state.dogAnimationWeight, 0), 1);
this.setState({dogAnimationWeight: weight + .01});
switch(this.state.dogGoal)
{
case Goal.Follow:
case Goal.GoDrink:
const targetLocation = this.state.dogGoal == Goal.Follow ? this.state.characterPosition : this.state.bowlPosition;
const delta = targetLocation.subtract(this.state.dogPosition);
if(delta.lengthSquared() < 2)
{
this.setDogGoal(this.state.dogGoal == Goal.Follow ? Goal.Sit : Goal.Drinking);
}
else
{
this.walkTowards(targetLocation);
}
}
}, 1000/60);
this.subscribeTo("positionChanged", (e) =>
{
this.setState({characterPosition: new Vector3(e.position.x, e.position.y, e.position.z)});
});
setInterval(() =>
{
if(this.state.dogAnimationWeight < 1)
{
return;
}
switch(this.state.dogGoal)
{
case Goal.Idle:
this.considerGoals([
{goal: Goal.Sit, odds: .1},
{goal: Goal.Follow, odds: .9},
]);
case Goal.Drinking:
this.considerGoals([
{goal: Goal.Sit, odds: .1},
]);
case Goal.Follow:
this.considerGoals([
{goal: Goal.Idle, odds: .1},
]);
case Goal.GoDrink:
case Goal.Sit:
this.considerGoals([
{goal: Goal.Idle, odds: .1},
]);
}
}, 1500);
}
considerGoals(goals: {goal: Goal, odds: number}[])
{
for(let i = 0; i < goals.length; i++)
{
if(Math.random() < goals[i].odds)
{
switch(goals[i].goal)
{
case Goal.Follow:
if(!isInBounds(this.state.characterPosition))
{
continue;
}
}
this.setDogGoal(goals[i].goal);
return;
}
}
}
setDogGoal(goal: Goal)
{
this.setState({
dogGoal: goal,
dogAnimationWeight: 1 - this.state.dogAnimationWeight,
dogPreviousGoal: this.state.dogGoal
});
}
walkTowards(position: Vector3)
{
let delta = position.subtract(this.state.dogPosition);
delta = delta.normalize().scale(.015); // .015 is the walk speed
delta.y = 0;
const newPosition = this.state.dogPosition.add(delta);
if(!isInBounds(newPosition))
{
this.setDogGoal(Goal.Idle);
}
else
{
this.setState({
dogPosition: newPosition,
dogRotation: lookAt(this.state.dogPosition, position, -Math.PI / 2)
});
}
}
async render()
{
const animationWeights = this.getAnimationRates();
return (
<scene>
<gltf-model
id="Dog"
src="art/BlockDog.gltf"
position={this.state.dogPosition}
rotation={this.state.dogRotation.toEulerAngles().scale(180 / Math.PI)}
transition={{
rotation: {
duration: 300
}
}}
skeletalAnimation={[
{
clip: "Idle",
weight: animationWeights.idle,
},
{
clip: "Walking",
weight: animationWeights.walk,
},
{
clip: "Sitting",
weight: animationWeights.sit,
},
]}
/>
<gltf-model
id="Bowl"
src="art/BlockDogBowl.gltf"
position={this.state.bowlPosition}
/>
</scene>
)
}
}
function isInBounds(position: Vector3): boolean
{
return position.x > .5 && position.x < 9.5
&& position.z > .5 && position.z < 9.5;
}
// Math from Babylon TransformNode
function lookAt(
pos: Vector3,
targetPoint: Vector3,
yawCor: number = 0,
pitchCor: number = 0,
rollCor: number = 0): Quaternion
{
const dv = targetPoint.subtract(pos);
const yaw = -Math.atan2(dv.z, dv.x) - Math.PI / 2;
const len = Math.sqrt(dv.x * dv.x + dv.z * dv.z);
const pitch = Math.atan2(dv.y, len);
return Quaternion.RotationYawPitchRoll(yaw + yawCor, pitch + pitchCor, rollCor);
}
```