Introduction
In this tutorial we will be building a simple game where the player has to collect coins before the timer runs out.
We will go through the basics of the studio and scripting and we will build the game step by step.
You can play the final game here. (opens in a new tab)
Final code
main.js
import { Emitter, Events, World, Components, Player, UI, Camera } from '@oo/scripting'
import { config } from "./config"
import { Multiplayer } from "./Multiplayer";
import { GameControls } from "./Controls"
import { Display } from "./Display"
export default class Game {
coins = []
score = 0
time = 60
currentCoin = null
elapsedTime = 0
backgroundMusic = null
coinSound = null
constructor() {}
async onPreload() {
console.log("Game: preload")
if (config.multiplayer.enabled) {
await Multiplayer.connect();
}
}
onReady = async () => {
console.log("Game: ready")
this.backgroundMusic = Components.byName("ost")[0]
this.coinSound = Components.byName("collect")[0]
this.backgroundMusic.volume = 0.1
this.coinSound.volume = 0.1
this.coins = Components.byTag("coin")
this.coins.forEach((coin) => {
// hides the coin
coin.visible = false
// disables the collider
coin.rigidBody.enabled = false
})
Player.avatar.onCollisionEnter(({ other }) => {
if (other.tag === "coin") {
this.coinSound.play()
this.score += 1;
this.switchCoin();
}
})
}
onStart = async () => {
console.log("Game: start")
this.backgroundMusic.loop = true
this.backgroundMusic.play()
this.switchCoin()
Player.avatar.rigidBody.teleport(GameControls.startPosition);
Camera.controls.active = true
}
onUpdate = (dt: number) => {
this.elapsedTime += dt
if (this.elapsedTime > 5) {
this.switchCoin()
}
this.time -= dt
if (this.time < 0) {
World.stop({
score: this.score,
})
}
Display.root.render(
<div style={{
position: "fixed",
color: "#fff",
top: "150px",
left: "50%",
transform: "translateX(-50%)",
fontSize: "32px",
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column"
}}>
<UI.Prompt title={`${ Math.ceil(this.time) }s`} />
<UI.Heading
size="small"
background="#ffffff"
color="#79D263"
value={this.score}
/>
</div>
)
}
onPause = async () => {
console.log("Game: pause")
}
onResume = async () => {
console.log("Game: resume")
}
onEnd = async () => {
this.score = 0
this.time = 60
this.backgroundMusic.stop()
Player.avatar.animation = "dance"
Camera.controls.active = false
Camera.position.set(
Player.avatar.position.x,
Player.avatar.position.y + 1.5,
Player.avatar.position.z + 8
)
Camera.lookAt(Player.avatar.position)
}
onDispose = async () => {
console.log("Game: dispose")
}
switchCoin = () => {
if (this.currentCoin) {
this.currentCoin.visible = false
this.currentCoin.rigidBody.enabled = false
}
this.currentCoin = this.coins[Math.floor(Math.random() * this.coins.length)]
this.currentCoin.visible = true
this.currentCoin.rigidBody.enabled = true
this.elapsedTime = 0
}
}
Studio
First head out to https://oo.oncyber.io/ (opens in a new tab) and click on login button to create an account or sign in to your existing account.
Once you are signed in, click on the create button. you will be then redirected to the studio editor, where you can create a new game.
The studio editor is where you can design your game and add scripts to it. Here's the layout of the studio editor:
If you already have a game, you can create a new one by clicking on the create button in the navigation.
to follow this tutorial it is important to create a new space
Moving around the scene
The scene is where you can add objects to your game and design the environment. You can move around the scene by pressing w,a,s,d or the arrow keys on your keyboard. You can also move up and down by pressing space and b respectively. You can also drag the mouse to look around the scene.
Previewing the game
You can preview the game by clicking on the "preview" button on the top right corner of the screen. This will open a new tab with the game running in it.
Adding a 3D Asset to the scene
To add 3D Asset to the scene, click on the second button (Add assets) in the space editor, then select 3D library. This will open the list of our free 3d assets. You can add objects to the scene by dragging them from the list to the scene or by just clicking on them for them to be added in front of the you.
Tip you can use Ctrl + Z to undo and Ctrl + Y to redo
Moving the 3D Asset around
You can move the 3d asset around by clicking on it to select it. This will open the component editor menu. In the component editor menu, you can change the position, rotation, and scale of the 3d asset. You can also change the position and rotation of the 3d asset by dragging it around the scene or by using the gizmo.
Upload a custom glb model
Apart from the prebuilt 3d assets, you can also upload your own models to the scene. To upload a model, click on the forth button in the components list. This will open the upload menu. You can upload a model by dragging it from your file explorer to the upload menu or by clicking on the upload menu and selecting the model from your file explorer.
Once the model is uploaded, it'll show up in the list. You can add the model to the scene by dragging it from the list to the scene or by clicking on it for it to be added in front of you.
Let's upload a coin model and add it to the scene.
You can try to find your own coin model or you can just download this coin model (opens in a new tab)
It's important that custom glbs are well optimized and have a small file size, since that influences the loading time of the game and it's performance
Making the coin a collider and giving it a tag
Now that we have added the coin to the scene, let's make it a collider and give it a tag so we can interact with it in the script.
To make the coin a collider, click on the coin in the scene to select it. This will open the component editor menu. In the component editor menu, scroll down to the collider section and check the enabled checkbox. And give it the same settings as in the image below.
To give the coin a tag, open the scripting tab and in the Group identifier field type "coin", it'll automatically save.
Duplicating objects
You can duplicate objects by clicking on them to select them, then clicking on Ctrl + D or Cmd + D to duplicate. This will create a copy of the object with the same parameters as the original object.
Duplicate the coin a few times and place them around the scene.
World settings
Before getting into scripting, let's take a look at the world settings.
You can have fun with the world settings and change them however you like, try adding a background, water and fog to the scene.
Scripting
Now that we learned a little bit about the studio and set up our game environment, let's add some scripting to the game.
Clicking on World Items button in the space editor, will open a modal showing the different components added to the scene.
Click on the Script tab to open the scripts submodal.
In that submodal, you will find some boilerplate code. Simply click on any script to open a code editor for you to view or modify the scripts available.
A script is a piece of code that runs in the game and controls the behavior of the game. You can add, edit, and delete scripts in the script editor.
You'll note that the scripts are written in TypeScript, a superset of JavaScript that adds static typing and other features to the language. If you're not familiar with TypeScript, don't worry! You can still follow along with this tutorial without knowing TypeScript And don't hesitate to use ChatGPT to help you out OR reach out to our discord for any questions.
It is recommended to know the basics of Javascript but it's not required
Each new game has boilerplate scripts that comes with it.
In total there are 5 boilerplate scripts, mostly we will be working with in the main file. The other files are boilerplate for the game's configuration, controls, display, and multiplayer.
Let's get started by opening the main file and adding some functionality.
Hiding Coins in the Scene
After this wall of text, let's get our hands dirty and start coding our game.
let's start by hidding all the coins in the scene
-
from the
@oo/scripting
package let's import theComponents
objectimport { Emitter, Events, World, Components } from "@oo/scripting";
the components object contains methods to get all the components in the scene by type, tag, or name and to create, duplicate and delete components.
-
let's create a property in the
Game class
to store all the coins in the scenecoins = [];
-
let's get all the coins in the scene and store them in the coins property, we'll do this in the onReady method since we need the scene to be loaded before we can get the coins
onReady = () => { // Previous code... this.coins = Components.byTag("coin"); };
-
now that we have all the coins in the scene we can loop through them and hide them in the onStart method
onStart = () => { // Previous code... this.coins.forEach((coin) => { // hides the coin coin.visible = false; }); };
-
save the file with Ctrl + S or Cmd + S and then preview your changes by clicking on the preview button on the top right corner of the screen, you can now see that all the coins are hidden but you can still collide with them even if they are hidden, to fix this we need to disable the collider of the coins
onStart = () => { // Previous code... this.coins.forEach((coin) => { // hides the coin coin.visible = false; // disables the collider coin.rigidBody.enabled = false; }); };
coin.rigidBody is only available if the coin is a collider, check the Making the coin a collider and giving it a tag section to see how to make the coin a collider
- you can now preview your game and see that all the coins are hidden and you can't collide with them
Make sure to save the file and preview your changes often
Tip: you can use console.log and debugger and open the browser console to debug your game
Random Coin Visibility and Switching
Now that we know how to get all the coins in the scene, let's make the one random coin visible and enable its collider and switch to another random coin after a few seconds
-
let's create a property in the
Game class
to store the random coincurrentCoin = null;
-
let's get a random coin from the coins array and store it in the currentCoin property
onStart = () => { // Previous code... this.currentCoin = this.coins[Math.floor(Math.random() * this.coins.length)]; };
-
let's make the currentCoin visible and enable its collision
onStart = () => { // Previous code... this.currentCoin = this.coins[Math.floor(Math.random() * this.coins.length)]; this.currentCoin.visible = true; this.currentCoin.rigidBody.enabled = true; };
-
let's add a property in the
Game class
to track the elapsed time since the coin was switchedelapsedTime = 0;
-
let's add the elapsed time to the onUpdate method to track the elapsed time
onUpdate = (dt: number) => { this.elapsedTime += dt; };
-
let's check if the elapsed time is greater than 5 seconds and if it is we'll switch the current coin
onUpdate = (dt: number) => { this.elapsedTime += dt; if (this.elapsedTime > 5) { this.currentCoin.visible = false; this.currentCoin.rigidBody.enabled = false; this.currentCoin = this.coins[Math.floor(Math.random() * this.coins.length)]; this.currentCoin.visible = true; this.currentCoin.rigidBody.enabled = true; this.elapsedTime = 0; } };
-
since we select a random coin in the onStart method and in the onUpdate method we switch the current coin after 5 seconds, let's refactor the functionality of switching the current coin into a method
switchCoin = () => { if (this.currentCoin) { this.currentCoin.visible = false; this.currentCoin.rigidBody.enabled = false; } this.currentCoin = this.coins[Math.floor(Math.random() * this.coins.length)]; this.currentCoin.visible = true; this.currentCoin.rigidBody.enabled = true; this.elapsedTime = 0; };
-
let's call the switchCoin method in the onStart method
onStart = () => { // Previous code... this.switchCoin(); };
-
let's call the switchCoin method in the onUpdate method
onUpdate = (dt: number) => { this.elapsedTime += dt; if (this.elapsedTime > 5) { this.switchCoin(); } };
-
save the file and preview your changes
Checking Player collisions and Adding a Score System
Now that we know how to switch the current coin, let's add a score system to the game and increase the score when the player collides with the current coin
-
let's create a property in the
Game class
to store the scorescore = 0;
-
import the game controls from the Controls file
import { GameControls } from "./Controls";
-
in the
onReady
method let's define a collision listener for the player and check if the player is colliding with the current coin and if it is we'll increase the score and switch the coinonReady = (dt: number) => { // Previous code... Player.avatar.onCollisionEnter(({ other }) => { if (other.tag === "coin") { this.score += 1; this.switchCoin(); } }); };
GameControls.controls.collidesWith is an array of all the objects the player is colliding with, we are checking if the array contains an object with the name "coin.glb" which is the name of the coin object in the scene
-
let's add some UI to display the score
import { Display } from "./Display";
in the onUpdate method let's add the score to the UI
onUpdate = (dt: number) => { // Previous code... Display.root.render( <p style={{ position: "fixed", color: "#fff", top: "150px", left: "50%", transform: "translateX(-50%)", fontSize: "32px", }}> {this.score} </p> ) }
Display.root.render is a method that renders a react component to the UI, in this case we are rendering a paragraph with the score in it
-
let's add a better looking UI, you can import the UI object from the @oo/scripting package. the UI object contains prebuilt UI components that you can use in your game
import { Emitter, Events, World, Components, UI } from "@oo/scripting";
in the onUpdate method let's add the score to the UI
onUpdate = (dt: number) => { // Previous code... Display.root.render( <div style={{ position: "fixed", color: "#fff", top: "150px", left: "50%", transform: "translateX(-50%)", fontSize: "32px", display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column" }}> <UI.Heading size="small" background="#ffffff" color="#79D263" value={this.score} /> </div> ) }
you can find the list of all the prebuilt UI components in the UI documentation (opens in a new tab)
-
save the file and preview your changes
Adding a Timer and Ending the Game
Now that we know how to switch the current coin and increase the score, let's add a timer to the game and end the game when the timer reaches 0
-
let's create a property in the
Game class
to store the timetime = 60;
-
let's add the time to the UI
onUpdate = (dt: number) => { // Previous code... Display.root.render( <div style={{ position: "fixed", color: "#fff", top: "150px", left: "50%", transform: "translateX(-50%)", fontSize: "32px", display: "flex", alignItems: "center", justifyContent: "center", flexDirection: "column" }}> <UI.Prompt title={`${ Math.ceil(this.time) }s`} /> <UI.Heading size="small" background="#ffffff" color="#79D263" value={this.score} /> </div> ) }
-
let's decrease the time in the onUpdate method
onUpdate = (dt: number) => { // Previous code... // UI... this.time -= dt; };
-
let's check if the time is less than 0 and if it is we'll end the game
onUpdate = (dt: number) => { // Previous code... // UI... this.time -= dt; if (this.time < 0) { World.stop({ score: this.score, }); } };
Calling
World.stop()
will end the game and show the end screen, it takes an object as a parameter that contains the score and any other data you want to send to the end screenthe score will be displayed on the end screen by default and saved to the database
-
let's do a reset when the game ends
onEnd = () => { this.score = 0; this.time = 60; };
-
save the file and preview your changes
Adding Background Music and Sound Effects
Let's add some background music and sound effects to the game
You can download the below audio files or use your own
-
Upload the background music and sound effects that you would like to use
follow the same steps as uploading the coin model, after uploading the mp3 files it'll show up in the list
-
Add them to the scene
-
Change the name of the audio components to "ost" for the background music and "collect" for the coin sound by clicking on the name of the audio component in the list, this will just make it easier to find them later in the list when you have a lot of uploads
-
once the title is set, we can then use the
Components
object to get the audio componentsbackgroundMusic = null; coinSound = null;
onReady = () => { // Previous code... this.backgroundMusic = Components.byName("ost")[0]; this.coinSound = Components.byName("collect")[0]; this.backgroundMusic.volume = 0.1; this.coinSound.volume = 0.1; };
-
in the onStart method let's play the background music
onStart = () => { // Previous code... this.backgroundMusic.loop = true; this.backgroundMusic.play(); };
-
in the collision listener in the
onReady
method let's play the coin sound when the player collides with the current coinLet's first import the
Player
in the@oo/scripting
packageimport { Emitter, Events, World, Components, UI, Player, } from "@oo/scripting";
now we can add the collision listener on the player
onReady = (dt: number) => { // Previous code... Player.avatar.onCollisionEnter(({ other }) => { if (other.tag === "coin") { this.coinSound.play(); this.score += 1; this.switchCoin(); } }); };
-
in the onEnd method let's stop the background music
onEnd = () => { // Previous code... this.backgroundMusic.stop(); };
-
save the file and preview your changes
Adding a Custom Animation to the User
Let's add a custom animation to the user
-
Go to mixamo (opens in a new tab) to find an animation that you like, we will use this animation to make the user dance when the game ends
-
Download the animation as a fbx file
-
Upload the animation in the studio and give it a name (in this case we'll call it "dance"), we can now use it in the script
To upload an animation in studio, open the world settings tab and click on the VRM animations component
This will open the VRM animations component editor, click on the upload fbx for Custom-1. This will open the file explorer where you can select the animation file to upload.
After the animation selected, it'll open a modal where you can enter the name of the animation (in this case, name it "dance") and the animation will start uploading.
-
In the onEnd method let's play the animation when the game ends
onEnd = () => { // Previous code... Player.avatar.animation = "dance"; };
-
save the file and preview your changes
Resetting User Position When the Game Starts
Let's reset the position of the user when the game starts
-
in the onStart method let's reset the position of the user
onStart = () => { // Previous code... Player.avatar.rigidBody.teleport(GameControls.startPosition); };
-
save the file and preview your changes
Adding Custom Camera Behavior
Let's control the camera
-
let's first import the
Camera
object from the@oo/scripting
packageimport { Emitter, Events, World, Components, UI, Player, Camera, } from "@oo/scripting";
-
in onEnd method let's deactivate the current behavior of the camera and set the camera position centered around the player so that we can see him have his victory dance closer
onEnd = () => { // Previous code... Camera.controls.active = false; };
now that the camera is deactivated we can control it however we want manually
let's set the camera position close in from of the avatar
onEnd = () => { // Previous code... Camera.controls.active = false; Camera.position.set( Player.avatar.position.x, Player.avatar.position.y + 1.5, Player.avatar.position.z + 8 ); Camera.lookAt(Player.avatar.position); };
-
in the onStart method let's bring back the previous behavior of the camera
onStart = () => { // Previous code... Camera.controls.active = true; };
-
save the file and preview your changes
Changing the player speed and jump
In the config file, you can change the speed and jump of the player, try to change the values and see how it affects the game
We also have support for two camera modes, firstperson
and thirdperson
export const config = {
// Previous code...
controls: {
type: "platformer",
params: {
// Previous code...
walkSpeed: 10,
runSpeed: 25,
jumpHeight: 10,
jumpDuration: 3,
},
camera: {
mode: "thirdperson",
},
},
};
Publishing the game
Now that we have finished our game, let's publish it so other people can play it.
To publish the game, click on the publish button on the top right corner of the screen. This will create a production build of the game and publish it to the https://oo.oncyber.io (opens in a new tab) website.
You can keep changing your game without affecting the published version, to update the published version, you'll need to publish it again.