Getting Started Course

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.

sign in button

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.

create button

The studio editor is where you can design your game and add scripts to it. Here's the layout of the studio editor:

Studio

If you already have a game, you can create a new one by clicking on the create button in the navigation.

create button

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.

Preview

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.

3d library

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.

component_editor

guizmo

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.

Upload

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.

collider

To give the coin a tag, open the scripting tab and in the Group identifier field type "coin", it'll automatically save.

Script tag

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.

World settings

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.

World items

Click on the Script tab to open the scripts submodal.

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.

Script editor

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

  1. from the @oo/scripting package let's import the Components object

    import { 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.

  2. let's create a property in the Game class to store all the coins in the scene

    coins = [];
  3. 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");
    };
  4. 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;
        });
    };
  5. 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

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

  1. let's create a property in the Game class to store the random coin

    currentCoin = null;
  2. 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)];
    };
  3. 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;
    };
  4. let's add a property in the Game class to track the elapsed time since the coin was switched

    elapsedTime = 0;
  5. let's add the elapsed time to the onUpdate method to track the elapsed time

    onUpdate = (dt: number) => {
        this.elapsedTime += dt;
    };
  6. 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;
        }
    };
  7. 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;
    };
  8. let's call the switchCoin method in the onStart method

    onStart = () => {
        // Previous code...
     
        this.switchCoin();
    };
  9. let's call the switchCoin method in the onUpdate method

    onUpdate = (dt: number) => {
        this.elapsedTime += dt;
     
        if (this.elapsedTime > 5) {
            this.switchCoin();
        }
    };
  10. 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

  1. let's create a property in the Game class to store the score

    score = 0;
  2. import the game controls from the Controls file

    import { GameControls } from "./Controls";
  3. 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 coin

    onReady = (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

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

  5. 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)

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

  1. let's create a property in the Game class to store the time

    time = 60;
  2. 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>
        )
    }
  3. let's decrease the time in the onUpdate method

    onUpdate = (dt: number) => {
        // Previous code...
     
        // UI...
     
        this.time -= dt;
    };
  4. 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 screen

    the score will be displayed on the end screen by default and saved to the database

  5. let's do a reset when the game ends

    onEnd = () => {
        this.score = 0;
        this.time = 60;
    };
  6. 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

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

  2. Add them to the scene

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

    audio

  4. once the title is set, we can then use the Components object to get the audio components

    backgroundMusic = 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;
    };
  5. in the onStart method let's play the background music

    onStart = () => {
        // Previous code...
     
        this.backgroundMusic.loop = true;
     
        this.backgroundMusic.play();
    };
  6. in the collision listener in the onReady method let's play the coin sound when the player collides with the current coin

    Let's first import the Player in the @oo/scripting package

    import {
        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();
            }
        });
    };
  7. in the onEnd method let's stop the background music

    onEnd = () => {
        // Previous code...
     
        this.backgroundMusic.stop();
    };
  8. save the file and preview your changes

Adding a Custom Animation to the User

Let's add a custom animation to the user

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

  2. Download the animation as a fbx file

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

    Animations

    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.

    VRM animation component editor

    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.

    dance

  4. In the onEnd method let's play the animation when the game ends

    onEnd = () => {
        // Previous code...
     
        Player.avatar.animation = "dance";
    };
  5. 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

  1. in the onStart method let's reset the position of the user

    onStart = () => {
        // Previous code...
        Player.avatar.rigidBody.teleport(GameControls.startPosition);
    };
  2. save the file and preview your changes

Adding Custom Camera Behavior

Let's control the camera

  1. let's first import the Camera object from the @oo/scripting package

    import {
        Emitter,
        Events,
        World,
        Components,
        UI,
        Player,
        Camera,
    } from "@oo/scripting";
  2. 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);
    };
  3. in the onStart method let's bring back the previous behavior of the camera

    onStart = () => {
        // Previous code...
     
        Camera.controls.active = true;
    };
  4. 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.