Create your first world tutorial part 2
Important
Join the Meta Horizon Creator Program! As a member, you gain: - Access to monetization opportunities including monthly bonuses, in-world purchases and competition cash prizes.
- Helpful resources including educational content, technical support and a collaborative creator community.
Important
The desktop editor is in early access and we need your feedback! To report bugs, go to the main menu and select Report a problem. To give us feedback, select Help us improve from the main menu. In this tutorial, you’ll continue to learn how to create a simple Meta Horizon Worlds game.
Part 1 of the tutorial shows you how to place an asset in your newly created world from the
Public Asset Library, manipulate the asset using the editor’s UI, preview your world, and test the world on a mobile device.
Part 2 takes a step further, where you’ll import
custom models to your world and
write your first script to create entity behavior. The tutorial ends with testing the simple game in virtual reality.
The key learning objectives for part 2 are the following:
- Import custom models to your world
- Add entities
- Script entity behavior
- Try your world in virtual reality
Part 2: Import custom models and write your first script
In this part of the tutorial, you will import
custom models, which are complex 3D models that are not available in the public asset library. You will then write your first script, attach it to an entity to create behavior.
Section 1: Add a pedestal and a rifle In this section, you’ll add a pedestal and a rifle to the graveyard scene that you created in
part 1.
Download the
Demo Assets
. This file is a zip archive that contains pre-made world objects that you’ll add to your game (like the rifle).
Extract the zip archive into a folder on your local hard disk.
Add the pedestal to the scene. Open the Assets panel by clicking the Assets tab on the bottom of the screen.
Add the new asset by clicking Add New, and then clicking 3D Model.
In the Import Models dialog box, enable Preserve offset pivots.
Select the asset files to import by clicking Choose files on your device.
In the file picker window that appears, select the 3D model file (SingleBlock.fbx) and its associated texture file (StoneBlockKit_BR.png), and then click Open.
The selected files appear in the Import Models dialog box.
In the Import Models dialog box, click Import. Wait for the asset files to be imported into the desktop editor.
After the asset files have been imported, drag the SingleBlock asset into the scene.
The SingleBlock asset appears in the Hierarchy as an object named “SingleBlock”.
In the Hierarchy, rename the “SingleBlock” object to “Pedestal”.
Enable Snap to surfaces by clicking the Snap to Surfaces button. This allows you to easily position the base of the object along the ground.
With the Pedestal object selected in the Hierarchy, position it by dragging it by the orange dot. You can place the Pedestal anywhere on the ground.
Note: If this behavior isn’t working, then you might have forgotten to enable Preserve Offset Pivots when you imported the asset. You can enable this behavior by removing the asset from the desktop editor, and then re-importing it—this time ensuring that you enabled Preserve Offset Pivots.
Add the rifle asset to the scene.
Open the Assets panel by clicking the Assets tab.
Click Add New, and then click 3D Model.
In the Import Models dialog box, disable Preserve offset pivots because the 3D model for the rifle uses more than one material for the mesh in the FBX file.
Select the asset files to import by clicking Choose files on your device.
In the file picker window that appears, select the 3D model file (ACWpnBattleRifle.fbx) and its associated texture files (WpnBattleRifleA_BR.png, WpnBattleRifleA_MEO.png, WpnIndictator_BR.png, and WpnIndictator_MEO.png), and then click Open.
In the Import Models dialog box, click Import. Wait for the asset files to be imported into the desktop editor.
Drag the rifle asset into the scene, and place it upon the Pedestal.
Note: If you’re having difficulty positioning the rifle, remember that you can always use the orange surface snapping manipulator to move objects anywhere along the ground. You can activate it by pressing the “W” key. Optionally, using “Ctrl+G” lets you group the pedestal and rifle as one object, so you can move them together.
In the Hierarchy, rename the “ACWpnBattleRifle” object to “Rifle”.
Your Hierarchy should now look like this.
Section 2: Make the rifle grabbable In this section, you’ll learn how to make the rifle grabbable by the avatar.
Select the Rifle object from the Hierarchy.
In the Property panel, set the following two property values:
Motion = “Interactive” and Interaction = “Grabbable”
Note: The Interaction property appears only after you set the Motion value to “Interactive”.
Section 3: Try-out your new world In this section, you’ll try-out your new world to see what it’s like to pick up the rifle.
Click the Play button on the menu bar to enter preview mode.
Note: Ensure you’ve configured world simulation to start automatically whenever you start the preview mode. Click the the ellipsis icon to open Preview Configuration and toggle on Auto-start simulation on Preview entry and Auto-stop simulation on Preview entry.
Maneuver the avatar over to the rifle using the arrow keys, and then pick it up by pressing the “E” key.
The way the avatar holds the rifle looks somewhat awkward, but don’t worry, you’ll soon fix that.
Exit preview mode by pressing Escape twice.
Fix the way the avatar holds onto the rifle. With the Rifle object selected in the Hierarchy, in the Property panel, scroll down to the More section, and enable both Use VR Grab Anchor and Use HWXS Grab Anchor.
Set a pose to use when the avatar holds onto the rifle. With Rifle selected in the Hierarchy, scroll down through the property list until you see Avatar Pose. Click the drop-down menu beside it, and then select Rifle.
Now your avatar can hold onto the rifle properly.
But the rifle still doesn’t do anything yet.
Section 4: Add a hello world script You now have a rifle that you can pick up and move around with, but it still doesn’t do anything. In this section, you’ll make something happen when you fire the rifle. You’ll write code that prints “Hello World” in the Console whenever you fire the rifle.
Open the Scripts Panel by clicking the Script Panel drop-down. The Scripts dialog appears.
Create a new script by clicking the plus button.
Name your script “Shoot”.
Note: It takes a few seconds for the script to appear after you’ve inputted the name.
Open the script in VS Code. Click the menu icon next to the script name, and then select Open in External Editor.
Note: Currently, scripts can only be opened in the aforementioned way, from the top dropdown menu.
The new script opens in VS Code in a file called Shoot.ts
. It contains boilerplate code.
Note: The start()
function is called whenever the entity that the script component is attached to is created. At this point though, you haven’t attached this script component to an entity.
Add the following debug statement to the start()
function. When the entity that this script component is attached to is created, this statement prints “Hello World!” to the Console.
start() {
console.log("Hello, World!");
}
Save the script file. You can press “Ctrl+S”.
In the desktop editor, attach your script component to the rifle entity.
Select the Rifle object from the Hierarchy.
In the Properties pane, scroll down to the Scripts section.
Attach the script component by selecting “Shoot:Shoot” from the Attached Script drop-down selection list.
Preview your world by clicking the Play button on the menu bar.
As soon as the Rifle entity is created, the script prints “Hello, World!” to the Console.
End the preview by pressing Escape twice.
You can see the debug message by clicking the Console tab at the bottom of the page.
You’ve made your world interactive! The script outputs the message “Hello, World!” to the Console.
Section 5: Refine your script But you really want the interaction to occur when you pull the trigger, not simply when the rifle is created. In this section, you’ll revise your script to print a message when you pull the trigger. When the rifle is created, an event is also created that fires each time you pull the trigger.
Replace the code in the start()
function with the following code:
start() {
// React to an event when the user pulls the trigger.
this.connectCodeBlockEvent(this.entity, hz.CodeBlockEvents.OnIndexTriggerDown, (player: hz.Player) => {
console.log("boom!");
});
}
Save your script.
When editing your script, errors might appear in the Console. When this happens, you can clear the error messages from the Console by clicking Clear.
Note: Normally, you shouldn’t see any error messages in the Console window. If you do though, then try copying and pasting the code instead of typing it yourself.
Preview your world by clicking the Play button.
Walk over to the rifle and grab it.
Fire the rifle several times by clicking the A button on the screen. As you fire the rifle, notice that a “boom!” message appears in the Console window along with the number of times that the message appeared.
End the simulation by pressing Escape.
Section 6: Add a projectile launcher to the rifle You now have a rifle that you can pick up and carry around, but it doesn’t actually do anything except print debug messages. In this section, you’ll make the rifle launch projectiles.
Select the Rifle from the Hierarchy.
Focus on the Rifle in the scene by pressing the “F” key.
Click the Build button.
From the drop-down menu that appears, select Gizmos.
Select Projectile Launcher.
The Projectile Launcher gizmo appears in the scene, and in the Hierarchy. Pressing the “F” key while the object is selected brings the object to focus.
Close the Build Gizmos panel.
Attach the ProjectileLauncher to the Rifle by making it a child of the Rifle. In the Hierarchy, drag the projectile launcher, and drop it onto the Rifle.
The ProjectileLauncher should appear indented in the hierarchy since it’s now a child object of the Rifle object. You can expand the hierarchy by clicking the triangle.
With the ProjectileLauncher selected in the Hierarchy, position it relative to the Rifle.
Adjust the Position values of the projectile so it aligns with the aim of the rifle.
These adjustments in settings ensure that the projectile launcher appears at the front of the rifle, and that projectiles fire in the forward direction.
Additionally, to make the projectiles easier to see, adjust Scale and Trail Length Scale based on your preference.
Everything is now hooked up! Next, you’ll edit the code to make the rifle interactive.
Section 7: Hook up the projectile launcher Earlier in this tutorial, you got a debug message to appear when you pulled the trigger on the rifle. In this section, you’ll update your script to use the projectile launcher whenever you pull the trigger.
To use the projectile launcher, you need to reference it in your script. Update the Shoot class’s propsDefinition with the following statement:
class Shoot extends hz.Component<typeof Shoot> {
static propsDefinition = {
launcher: {type: hz.PropTypes.Entity}
};
Add a statement to the start()
function that creates a reference to the projectile launcher gizmo.
start() {
// Store a reference to the projectile gizmo in the launcherGizmo variable.
let launcherGizmo = this.props.launcher?.as(hz.ProjectileLauncherGizmo);
With a reference to the launcherGizmo
, you can call a function on it (launchProjectile()
) to launch a projectile whenever you pull the trigger.
Add a statement just before the start()
function that adds a property for holding the launcher options.
// The options to use when launching the projectile.
launcherOptions: hz.LaunchProjectileOptions = {speed: 50};
Add a statement to the OnIndexTriggerDown event for launching a projectile.
start() {
// Store a reference to the projectile gizmo in the launcherGizmo variable.
let launcherGizmo = this.props.launcher?.as(hz.ProjectileLauncherGizmo);
// Handle the OnIndexTriggerDown event when the user pulls the trigger.
this.connectCodeBlockEvent(this.entity, hz.CodeBlockEvents.OnIndexTriggerDown, (player: hz.Player) => {
console.log("boom!");
launcherGizmo?.launch(this.launcherOptions);
});
}
This change made it so that when the Rifle is created, you hook it up to the trigger, but now this event asks the projectile launcher gizmo to launch a projectile instead of just printing “boom”.
Save your script.
In the desktop editor, select the Rifle object from the Hierarchy.
In the Property pane, scroll down to the Scripts section. Notice that there is now a launcher
property that you can set. This property appears because you added it to the propsDefinition
in your script.
Note: You might have to deselect and then reselect the Rifle object if this new property doesn’t appear.
Set this launcher property to the ProjectileLauncher object. Click on the field beside launcher, and then select ProjectileLauncher from the list that appears.
Preview your world by clicking the Play button.
Walk over to the rifle, grab it, and then click the mouse button. Notice what happens, a shot appears to come out of the rifle when you pull the trigger. Next, you’ll update your script to accumulate points whenever the player hits the target.
Exit Preview mode by pressing Escape.
Section 8: Count points whenever you hit the target In this section, you’ll update the script so that you score a point each time you hit the target.
In the desktop editor, select the SpawnPoint in the Hierarchy.
Open the Public Asset Library by clicking the Asset Library drop-down menu.
From the list that appears, select “Interactive”.
From the menu that appears, select “skeletoncrayta”.
A skeleton object named “[UnityAssetBundleGizmo]” object is added to your Hierarchy, and appears in your scene.
Rename the skeleton object from “[UnityAssetBundleGizmo]” to “Target”.
Position the target anywhere in the scene.
Close the Interactive panel.
Update your script so that whenever a projectile hits an object, a point is added to your score. You’ll need to add a variable to track the current point value, and to initialize its value to zero. Add the following statement near the top of your class, just above the start()
function.
// Keep track of the user's score.
points: number = 0;
Add another event listener inside the start()
function that fires whenever a projectile hits an object. Copy the following statements to the end of the start()
function.
if (launcherGizmo) {
this.connectCodeBlockEvent(
launcherGizmo,
hz.CodeBlockEvents.OnProjectileHitObject,
(objectHit: hz.Entity, position: hz.Vec3, normal: hz.Vec3) => {
this.points = this.points + 1;
console.log("You're up to " + this.points + ' points!');
},
);
}
Your complete Shoot script should now look like this.
import * as hz from 'horizon/core';
class Shoot extends hz.Component<typeof Shoot> {
static propsDefinition = {
launcher: {type: hz.PropTypes.Entity},
};
// The options to use when launching the projectile.
launcherOptions: hz.LaunchProjectileOptions = {speed: 50};
// Keep track of the user's score.
points: number = 0;
start() {
// Store a reference to the projectile gizmo in the launcherGizmo variable.
let launcherGizmo = this.props.launcher?.as(hz.ProjectileLauncherGizmo);
// Handle the OnIndexTriggerDown event when the user pulls the trigger.
this.connectCodeBlockEvent(
this.entity,
hz.CodeBlockEvents.OnIndexTriggerDown,
(player: hz.Player) => {
console.log('boom!');
launcherGizmo?.launch(this.launcherOptions);
},
);
if (launcherGizmo) {
this.connectCodeBlockEvent(
launcherGizmo,
hz.CodeBlockEvents.OnProjectileHitObject,
(objectHit: hz.Entity, position: hz.Vec3, normal: hz.Vec3) => {
this.points = this.points + 1;
console.log("You're up to " + this.points + ' points!');
},
);
}
}
}
hz.Component.register(Shoot);
Save your script.
Test your world.
In the desktop editor, select the Console tab at the bottom of the screen.
Click the Play button to enter preview mode. Your avatar spawns into your world, ready to go and get the rifle.
Walk over to the rifle, pick it up, and then fire several shots at the skeleton.
Every time you hit the skeleton, a message prints to the Console that tells you how many points you’ve scored. If a shot doesn’t hit the skeleton, then the console message simply doesn’t appear.
Section 9: Display the score In this section, you’ll revise your script so that the score appears in the game.
Add a Text gizmo to your scene.
Click the Build drop-down.
Select Gizmos.
Select Text.
A Text gizmo is added to the scene.
Position the Text gizmo within the scene so that you can read the score while you’re shooting at the target.
You’ll need to rotate the Text gizmo so that the text displays in the correct orientation.
Update your script to use the Text gizmo. Remember, if you want to reference an entity property within a script, then you need to add that property to the propsDefinition
. Add a scoreView
property to propsDefinition
.
static propsDefinition = {
launcher: {type: hz.PropTypes.Entity},
scoreView: {type: hz.PropTypes.Entity}
};
To be able to use the scoreView
property, you need a reference to scoreView
as its specific type: TextGizmo
. Store a reference to the scoreGizmo
object in your start()
function.
// Store a reference to scoreView as its specific type: TextGizmo.
let scoreGizmo = this.props.scoreView?.as(hz.TextGizmo);
Update the scoreboard text by adding a single statement beneath the console.log
statement that calls the scoreGizmo.text.set()
function.
this.connectCodeBlockEvent(
launcherGizmo,
hz.CodeBlockEvents.OnProjectileHitObject,
(objectHit: hz.Entity, position: hz.Vec3, normal: hz.Vec3) => {
this.points = this.points + 1;
console.log("You're up to " + this.points + ' points!');
scoreGizmo?.text.set(this.points + ' points');
},
);
Save your script.
Your complete script should now look like this.
import * as hz from 'horizon/core';
class Shoot extends hz.Component<typeof Shoot> {
static propsDefinition = {
launcher: {type: hz.PropTypes.Entity},
scoreView: {type: hz.PropTypes.Entity},
};
// The options to use when launching the projectile.
launcherOptions: hz.LaunchProjectileOptions = {speed: 50};
// Keep track of the user's score.
points: number = 0;
start() {
// Store a reference to the projectile gizmo in the launcherGizmo variable.
let launcherGizmo = this.props.launcher?.as(hz.ProjectileLauncherGizmo);
// Store a reference to scoreView as its specific type: TextGizmo.
let scoreGizmo = this.props.scoreView?.as(hz.TextGizmo);
// Handle the OnIndexTriggerDown event when the user pulls the trigger.
this.connectCodeBlockEvent(
this.entity,
hz.CodeBlockEvents.OnIndexTriggerDown,
(player: hz.Player) => {
console.log('boom!');
launcherGizmo?.launch(this.launcherOptions);
},
);
if (launcherGizmo) {
this.connectCodeBlockEvent(
launcherGizmo,
hz.CodeBlockEvents.OnProjectileHitObject,
(objectHit: hz.Entity, position: hz.Vec3, normal: hz.Vec3) => {
this.points = this.points + 1;
console.log("You're up to " + this.points + ' points!');
scoreGizmo?.text.set(this.points + ' points');
},
);
}
}
}
hz.Component.register(Shoot);
In the desktop editor, select the Rifle object from the Hierarchy.
Scroll to the Scripts section of the Property panel. Since you added scoreView
to propDefinitions
in your script, this property now appears in the Scripts section of the Property panel.
Note: You might have to deselect the Rifle, and then reselect it if this new property doesn’t appear.
Set the value of scoreView
to the Text gizmo that you added to the scene. Click the drop-down selector beside the label scoreView, and select “Text”.
Test your world.
In the desktop editor, select the Console tab at the bottom of the screen.
Press the Play button to enter preview mode. Your avatar spawns into your world, ready to go and get the rifle.
Walk over to the rifle and grab it, and use it to take several shots at the skeleton.
You should see your score floating in space, and it should increment each time you shoot the skeleton.
Section 10: Create a second rifle In this section, you’ll convert your Rifle/ProjectileLauncher combination into a Template Asset, and then you’ll use that asset to create another rifle.
From the Hierarchy, select the Rifle. This object has the script attached to it, and it’s also the parent of the ProjectileLauncher.
Right-click the Rifle object in the Hierarchy, and from the menu that appears, select Create Asset.
The Create New Asset dialog appears.
Type a name for the asset, and type a description of the asset, and then click Create.
Open the Assets tab, and then select the My Assets folder. You’ll see your second rifle asset there.
Click and drag the new AutoRifle asset, and drop it anywhere in the scene.
Test your world. Press the Play button to enter preview mode. Your avatar spawns into your world, ready to go and get the second rifle.
Notice that you can use either rifle to shoot the skeleton and generate a score. Each rifle has its own score, which means the text box swaps between the scores as you swap rifles.
You’re done! You’ve completed building a game in Meta Horizon Worlds! In the next section (which is optional), you’ll try running your game in 3D on a Meta Quest headset.
Section 11: Adding analytics to your world Now that we have built out a core world and user experience, we want to build some analytics so that we can measure how users interact with it and if they are making it through the experience as we expect. In this section we will:
- Use Area analytics to measure how many users go into the immediate target area
- Use Discovery analytics to measure what percentage of world users actually end up firing the weapon
1. Enable horizon/analytics API You can enable the horizon/analytics
API in your Script settings in the desktop editor. This enables the TypeScript APIs to be called from the scripts.
- Open the script folder and click the settings cog

- Choose API in the left hand menu

- Toggle on
horizon/analytics

2. Add Tutorial Turbo Analytics With Area from the Asset Library The Tutorial Turbo Analytics With Area asset is an extension of Turbo Analytics created specifically for this tutorial. Turbo Analytics consists of a preconfigured set of Scripts and Entities that can be added to your world to accelerate implementation.

- Open the Asset Library
- Navigate to the Interactive category
- Find Tutorial Turbo Analytics With Area and drag it into your world
- Right click Tutorial Turbo Analytics With Area in the hierarchy and select Ungroup
3. Set up Area analytics around the target We want to know how many users enter the specific target area, so we’ll add a simple trigger that represents that area, so we know when players enter the area and when they leave it.
- Select the green Turbo_Area1 Area Trigger and change its scale from
3
to 2
in all dimensions. - Place the green area trigger box so that the Target is in its center
- Rename the area parameter by changing it from
1
to target
Now, when players enter or leave that trigger, Analytics events will be sent!
4. Add Discovery analytics It’s possible to send Analytics events through TypeScript. The Discovery Analytics events indicate that a player has made a discovery in the world. For this step, we’ll send a Discovery Analytics event when the player pulls the trigger on their controller.
- Open the Shoot script in your editor
- Import the Analytics module and some helper objects to this script under the other import
import {
Action,
DiscoveryMadePayload,
Turbo,
TurboDataService,
} from 'horizon/analytics';
import {Analytics} from 'TurboAnalytics';
- Add code for the analytics logging of the Discovery event and place it after the launch logic:
const turboData: DiscoveryMadePayload = {
player: player,
discoveryItemKey: 'trigger_pulled',
discoveryIsImplied: false,
discoveryNumTimes: 1,
discoveryAmount: 1,
};
Analytics()?.sendDiscoveryMade(turboData);
Your entire shoot script should now look like this:
import \* as hz from 'horizon/core';
import { Action, DiscoveryMadePayload, Turbo, TurboDataService } from 'horizon/analytics';
import { Analytics } from 'TurboAnalytics';
class Shoot extends hz.Component<typeof Shoot> {
static propsDefinition = {
launcher: {type: hz.PropTypes.Entity},
scoreView: {type: hz.PropTypes.Entity}
};
// The options to use when launching the projectile.
launcherOptions: hz.LaunchProjectileOptions = {speed: 50};
// Keep track of the user's score.
points: number = 0;
start() {
// Store a reference to the projectile gizmo in the launcherGizmo variable.
let launcherGizmo = this.props.launcher?.as(hz.ProjectileLauncherGizmo);
// Store a reference to scoreView as its specific type: TextGizmo.
let scoreGizmo = this.props.scoreView?.as(hz.TextGizmo);
// Handle the OnIndexTriggerDown event when the user pulls the trigger.
this.connectCodeBlockEvent(this.entity, hz.CodeBlockEvents.OnIndexTriggerDown, (player: hz.Player) => {
console.log("boom!");
launcherGizmo?.launch(this.launcherOptions);
const turboData: DiscoveryMadePayload = {
player: player,
discoveryItemKey: 'trigger_pulled',
discoveryIsImplied: false,
discoveryNumTimes: 1,
discoveryAmount: 1
};
Analytics()?.sendDiscoveryMade(turboData);
});
if (launcherGizmo) {
let player = hz.Player;
this.connectCodeBlockEvent(
launcherGizmo,
hz.CodeBlockEvents.OnProjectileHitObject,
(objectHit: hz.Entity, position: hz.Vec3, normal: hz.Vec3) => {
this.points = this.points + 1;
console.log("You're up to " + this.points + ' points!');
scoreGizmo?.text.set(this.points + ' points');
},
);
}
}
}
hz.Component.register(Shoot);
Validate your Analytics logging. You won’t be able to see full analytics for your world until it is published and has at least 10 daily visitors. But we can go ahead and check that events are being sent from edit mode.
- Go to the Horizon Creator Portal and navigate to the insights page for your world.

- Click the ‘Turbo Validation’ tab.

- Confirm that you see both Area and Discovery events being fired (note these can take up to 1-2 hours to show up).
Section 12: Play your game in virtual reality The steps in this section are optional. In this section, you’ll see what it’s like to play your game in 3D in Meta Horizon Worlds on your Meta Quest VR headset.
Grab your Quest headset, strap it on, and then launch Meta Horizon Worlds.
Navigate to the Create page. You can get there by clicking the fourth icon from the left on the menu bar at the bottom of the page.
Find the world that you just created, and click it. As soon as your world launches on your Quest VR headset, the desktop editing session will end.
Walk over to one of the rifles, grab it, and then fire a few shots at the skeleton. Hold both the front trigger and grip button simultaneously to shoot the target. Orient your hand while holding the grip button to aim the rifle. Watch your score as you score points.
You’ve completed this playtest - Congratulations!
Note: To exit/reset the world, click the Menu button on the left controller, and click the Home icon (next to your profile) with the front trigger.
To learn more about Meta Horizon Worlds, try the following:
- Try the Batting cage tutorial now that you’ve created your first world.
- Learn about the desktop editor with the Introduction to the desktop editor.
- Learn about the other tools available by reading our Tools overview.
- Join the Meta Horizon Creator Program to learn about our program benefits.