Temitope Olajide
Temitope's blog

Temitope's blog

Building an Endless Runner Game with Three.js, Mixamo, Vite, and Planetscale (Part one)

Building an Endless Runner Game with Three.js, Mixamo, Vite, and Planetscale (Part one)

Temitope Olajide's photo
Temitope Olajide
ยทJul 28, 2022ยท

15 min read

Table of contents

Introduction:

Hi everyone ๐Ÿ‘‹,

This is a series of tutorials on how to build a 3D endless runner game like Subway Surfers using Three.js, Mixamo, and Vite. I called it Cave Runner, you know, because it is based on a 3D character running endlessly in a wooden cave while dodging obstacles that are being spawned at it, and at the same time collecting coins that can be later used to unlock more characters. I know you're probably expecting a more captivating and inspiring game story, but that's all I got for now. If you can't wait to see what we are building, here is the link to play the game: http://cave-runner.netlify.app. You can find the final project of this tutorial here on GitHub: https://github.com/tope-olajide/cave-runne. You can always use it as a reference if you get lost while reading through this tutorial.

What are we Building?

We are building an endless runner game; the player can use different characters to run, slide and jump through obstacles. Additionally, the player can save game data online and compete against other players worldwide.

This series was divided into the following sections:

Prerequisite

This tutorial assumes basic knowledge of HTML, CSS, and JavaScript.

And since you are already familiar with JavaScript, you don't need to know TypeScript and threejs to follow this tutorial. In the first aspect of this tutorial, we will cover some aspects of threejs

Tools

We'll use the following tools to develop this game

Three.js

Three.js is a powerful JavaScript library and API used to create and render 3d applications in the browser. It makes use of WebGL to draw 3D graphics on your browser. Every modern browser should now support and enable WebGL by default, but to clear all doubt, you can visit this link to check if your browser supports it: get.webgl.org.

Mixamo

Mixamo is an excellent website that allows users to rig and animate 3D characters. To follow this tutorial, you don't need any prior rigging or 3D modeling knowledge because Mixamo is providing everything for us, absolutely FREE!

Towards the end of this article, I'm going to show you to download the characters and animations used in this game

PlanetScale

PlanetScale is an easy-to-use MySQL-compatible serverless database platform. We will leverage the power of Planetscale to save and retrieve our game data quickly and conveniently.

Netlify

Netlify is a cloud computing company that offers hosting and serverless backend services for web applications and static websites. We will host this game and the serverless functions that communicate with PlanetScale on it

Vite

Vite is an extremely fast JavaScript build tool for modern web projects. I'm using it for this project because loads very fast and it's easy to set up. To get started with Vite, you must have Nodejs installed; Vite requires Node.js 12.2.0 and above.

Scaffolding the project With Vite

In this section, we are going to set up threejs, build a scene, add a cube, then rotate it. After that, we will learn how to download the characters and animations used in this game.

The reference code for this section is available here: https://github.com/tope-olajide/cave-runner/tree/ft-animated-cube.

And you can preview the deployed website for this section here: https://deploy-preview-1--cave-runner.netlify.app/

Let's get started by launching your Command-line interface and navigating to your workspace directory.

Now run the following command: npm create vite@latest to bootstrap our project.

Vite will ask for the project name, you can use cave-runner.

Next, Vite will prompt you to select a framework, Select Vanilla.

Finally, Vite will prompt you to "Select a variant" Use the arrow key to select the vanilla ts

We'll wait for a few microseconds for Vite to scaffold our project.

Vite will create a new directory with our project name cave-runner

Screenshot_31.png

run

cd threejs-endless-runner

To navigate to the directory, then run:

npm install

To install all the packages that Vite generated for us.

Installing ThreeJs

Once all the packages have been installed, run the following command to install Threejs:

npm install three.

After that, we need to install its typing files by running the following command:

npm install @types/three --save-dev.

Setting up the project with Eslint and Airbnb style guide

To setup Eslint, we need to install it by running the following command:

npm install eslint --save-dev

Next, we need to set up a configuration file for Eslint by running the following command:

npm init @eslint/config

After a few seconds, the CLI will prompt the following questions:

How would you like to use ESLint?...

Select To check syntax, find problems, and enforce code style and press Enter.

What type of modules does your project use?

Select JavaScript modules (import/export) and press Enter.

Screenshot_3.png

Which framework does your project use?

Select None of these and press Enter

Screenshot_4.png

Does your project use TypeScript?

Select Yes and press Enter.

Screenshot_5.png

Where does your code run?

Select Browser and press Enter.

Screenshot_6.png

How would you like to define a style for your project?

Select Use a popular style guide and press Enter.

Screenshot_7.png

Which style guide do you want to follow?

Select Airbnb: https://github.com/airbnb/javascript and press Enter.

What format do you want your config file to be in?

Select JSON and press Enter.

Would you like to install them now?

Select Yes and press Enter.

Screenshot_9.png

Which package manager do you want to use?

Select NPM and press Enter.

After that, Eslint will install all the necessary dependencies based on the configuration options we selected.

To make ESLint work in our VS Code editor, we need to install VS Code ESLint extension. This will help us to catch and fix errors inside our editor.

Let's run:

npm run dev

on the CLI to start the server, and visit http://localhost:3000/ on our browser, if everything works well we should see a Vite welcome screen like this one:

Screenshot_10.png

Setting Up a Scene

Since diving into the world of 3D graphics and animation, I noticed that the "Hello World" in the 3D world is rendering a scene containing some lights, a cube, and a camera, precisely what we will build right now.

Open up the index.html file and add a canvas element to it:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="favicon.svg" />
    <link rel="stylesheet" href="./src/style.css" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Threejs Endless Runner</title>
  </head>
  <body>
    <canvas id="app"> </canvas>
    <script type="module" src="/src/main.ts"></script>
  </body>
</html>

Now open up style.css and let's update it with some basic styles:

html,
body {
  overflow: hidden;
  margin: 0px;
  height: 100%;
}

#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  width: 100%;
  height: 100%;
  display: block;
}

Now, let's open up the main.js file in the scr folder, delete everything inside it and add the following code:


import { WebGLRenderer, PerspectiveCamera, Scene } from 'three';

const width = window.innerWidth;
const height = window.innerHeight;

const renderer = new WebGLRenderer({
  canvas: document.getElementById('app') as HTMLCanvasElement,
  antialias: true,
  precision: 'mediump',
});
const mainCamera = new PerspectiveCamera(60, width / height, 0.1, 1000);
function onWindowResize() {
  mainCamera.aspect = window.innerWidth / window.innerHeight;
  mainCamera.updateProjectionMatrix();

  renderer.setSize(window.innerWidth, window.innerHeight);
}

window.addEventListener('resize', onWindowResize);

renderer.setSize(width, height);

What's happening here

EswZVekXYAYYqwQ.jpg

At first, we imported the Scene, PerspectiveCamera, and the WebGLRenderer objects from three.js. That's because before we can display anything with three.js, we will need all those three. Before we go any further, here's a little explanation of what they are:

The Scene

A typical threejs scene comprises a Camera, Light, and 3D objects. The scene is where we set up all objects that need to be rendered. Objects like our 3D models, Light, Cameras, etc., are positioned here.

The PerspectiveCamera

Basically, only the objects that are shown by the camera will be rendered to the screen. There are different types of cameras in three.js, the Perspective Camera and the Orthographic Camera. According to three.js, the Perspective Camera mimics how the human eye sees. For example, when you view objects at a long distance, they appear smaller and get bigger when moving closer. That's the way the perspective camera works. I will be using the perspective camera throughout the development of this project.

The perspective camera takes in four arguments which are:

1. Field of View: FoV is the maximum area that the camera can see at a given time. The default value is 50, but I'll use 60 for this example. There is really no perfect Field of View value for video games. Just keep adjusting the number till you get the perfect value for the game you are building.

2. Aspect Ratio: the aspect ratio is the ratio of width and height of the screen. The default value is 1, but I used the width of the user's device divided by the height (width/height).

3. Near Plane: This specifies how close the objects in a scene are to the camera. The default value is 0.1. Anything closer than 0.1 to the camera will not be rendered.

4. Far Plane: This specifies how far the objects in a scene are from the camera. The default value is 2000. I will be using 1000 in this example. Anything objects farther than 1000 from the camera will not be captured by the camera.

WebGLRenderer

Three.js uses WebGLRenderer to draw everything the camera sees to the screen using WebGL on supported devices.

const renderer = new WebGLRenderer({
  canvas: document.getElementById('app') as HTMLCanvasElement,
  antialias:true,
  precision:'mediump'
});

In the code above, we specified the canvas where WebGLRenderer will draw everything it sees. The I enabled the antialias option, to smoothen the jagged edges in our game and the precision:'mediump' option to make sure the game can play smoothly on lower-end devices. There are also lowp and highp precision setting options.

renderer.setSize(width, height); set the size of the output canvas to the width and the height provided. and const scene = new Scene(); creates a new scene.

The onWindowResize function scales the content of the canvas to fit the width and height of the device's screen any time the player resizes or rotates the screen.

Creating a Cube

Let's update our main.ts file with the following code:

import {
  WebGLRenderer, PerspectiveCamera, Scene, BoxGeometry, MeshPhongMaterial, Mesh,
} from 'three';

const width = window.innerWidth;
const height = window.innerHeight;

const renderer = new WebGLRenderer({
  canvas: document.getElementById('app') as HTMLCanvasElement,
  antialias: true,
  precision: 'mediump',
});
const mainCamera = new PerspectiveCamera(60, width / height, 0.1, 1000);
function onWindowResize() {
  mainCamera.aspect = window.innerWidth / window.innerHeight;
  mainCamera.updateProjectionMatrix();

  renderer.setSize(window.innerWidth, window.innerHeight);
}

window.addEventListener('resize', onWindowResize);

renderer.setSize(width, height);

const geometry = new BoxGeometry();
const material = new MeshPhongMaterial({ color: 0x0000ff });
const cube = new Mesh(geometry, material);
cube.position.z = -5;
const scene = new Scene();

scene.add(cube);
renderer.render(scene, mainCamera);

First, we added BoxGeometry, MeshPhongMaterial, and Mesh to the list of imported objects from three.js.

The BoxGeometry class is used for creating a six-faced solid shape with a given 'width', 'height', and 'depth'.

MeshPhongMaterial is used to add color and other properties like emissive, specular, shininess, and other properties to the surface of our 3D objects.

The Mesh object takes in the geometry we created and applies the material to it. Then we set the cube position on the z-axis to -5, in front of the camera.

The default position of the camera is (0,0,0), Meaning 0 on the X axis, 0 on the Y axis, and 0 on the Z axis. Three.js uses cartesian coordinates to set the positions of objects in a scene.

scene.add(cube); adds the cube object to the scene.

Finally, the renderer.render(scene, mainCamera); method takes in the scene and the camera and draws everything the camera sees onto the screen.

If you refresh your browser now, everything should be black, don't panic... yet, that's because we haven't added the light yet.

Let there be light

let's update our main.ts file


import {
  WebGLRenderer, PerspectiveCamera, Scene, BoxGeometry, MeshPhongMaterial, Mesh, DirectionalLight,
} from 'three';

const width = window.innerWidth;
const height = window.innerHeight;

const renderer = new WebGLRenderer({
  canvas: document.getElementById('app') as HTMLCanvasElement,
  antialias: true,
  precision: 'mediump',
});

const mainCamera = new PerspectiveCamera(60, width / height, 0.1, 1000);
function onWindowResize() {
  mainCamera.aspect = window.innerWidth / window.innerHeight;
  mainCamera.updateProjectionMatrix();

  renderer.setSize(window.innerWidth, window.innerHeight);
}

window.addEventListener('resize', onWindowResize);

renderer.setSize(width, height);

const geometry = new BoxGeometry();
const material = new MeshPhongMaterial({ color: 0x0000ff });
const cube = new Mesh(geometry, material);
cube.position.z = -5;
const scene = new Scene();

scene.add(cube);
const light = new DirectionalLight(0xFFFFFF, 1);
light.position.z = 2;
scene.add(light);

renderer.render(scene, mainCamera);

We imported the DirectionalLight from three.js. Then, we set the color to white (0xFFFFFF) and the intensity to 1. Right after that, we set the position of the light on the z-axis to 2 so that it stays between the camera and the cube, then we added it to the scene using scene.add(light);.

If you refresh your browser now, we should see the cube we added to the scene:

Screenshot_11.png

Rotating the Cube

We're going to add a little life to the cube by rotating it. Let's update the main.js file with the following code:

import './style.css'

import { WebGLRenderer, PerspectiveCamera, Scene, BoxGeometry, MeshPhongMaterial, Mesh, DirectionalLight } from 'three';

const width = window.innerWidth;
const height = window.innerHeight;

const renderer = new WebGLRenderer({
  canvas: document.getElementById('app') as HTMLCanvasElement
});
renderer.setSize(width, height);

const mainCamera = new PerspectiveCamera(60, width / height, 0.1, 1000);

const scene = new Scene();

const geometry = new BoxGeometry();
const material = new MeshPhongMaterial({ color: 0x0000ff });
const cube = new Mesh(geometry, material);
cube.position.set(0, 0, -5);

scene.add(cube);

const rotateCube = () => {
  cube.rotation.y -= 0.03;
  cube.rotation.z -= 0.01;
}
const light = new DirectionalLight(0xFFFFFF, 1);
light.position.set(0, 0, 2);
scene.add(light);

const render = () => {
  rotateCube();
  renderer.render(scene, mainCamera);
  requestAnimationFrame(render);
}
render();

We created a function that rotates the cube on the y-axis by -=0.03 and on the z-axis by -= 0.01, and called it rotateCube. I also created a render() function. I called the rotateCube() function inside the render() function and we move the renderer.render(scene, mainCamera) to the render() function.`

Inside the render function, I created the requestAnimationFrame() function, which takes in the render function as an argument.

The Window.requestAnimationFrame() method creates a loop that calls everything inside therender() function over and over again, usually up to 60 times per second.

If Everything goes well, we should see a screen that looks like this:

rotating-cube.gif

The 3D Assets used in this tutorial

The Wooden cave, Coins, and the obstacles (Spike, wooden box, and barrel) used in this tutorial are modeled by me with Blender, and they are all free to be used for any purpose. The 3D characters and animations used in this tutorial are downloaded from Mixamo. Mixamo is free to use for personal and commercial purposes, although they have some restrictions on their website

  • Mixamo is not available for Enterprise and Federated IDs.
  • Mixamo is not available for users who have a country code from China. The current release of Creative Cloud in China does not include web services.

You can click on this link for more information. The 3D characters used in this project might look slightly different from the original one downloaded from Mixamo, that's because I have optimized the models by reducing the resolution of their materials and I also reduced the polygon count on some models. I did all this to reduce the models' size so that the game can download faster, no problem at all if you want to stick with the original file.

How To Download Characters and Animations from Mixamo

Thepublic/assets folder of this project's files already contains all the models and animations used in this tutorial. If you do not intend to add additional characters and animations, you may skip this section and return to it later.

Xbot, Jolleen, and Ty are the three characters featured in this game (you can add more if you want). Each of the characters will have its own animations: running, jumping, sliding, stumbling, and dancing.

Downloading a Character

First, you need an Adobe account to download from Mixamo. Visit: mixamo.com on your computer and click on Signup or Log in if you already have an Adobe account.

Once you are logged in, you will see two tabs at the top left of the page Characters and Animations. Click on the Characters tab and search for Xbot

Screenshot_3.png

Select X bot and click on the download button. A Download Settings dialog will pop up, under the Format, select FBX Binary(.fbx) and under Pose Select T-Pose, then click on the Download button.

Screenshot_4.png

Downloading its Animations

Downloading The Running Animation

Switch to the Animations tab, and make sure that Xbot is the selected character (the selected character will appear on the right-hand side, beside the animations). It is very important because the animations we are about to download can only work for the selected character.

Search for Running, Mixamo has many running animations, some of which are named Running, so you will have to choose the best running style that suits your game. Also, remember to check the in place option. This will prevent the animation from moving out of place while it's playing in the game.

Screenshot_5.png Next, click on the download button. A Download Settings dialog will pop up. Under the Skin option, select without skin. We selected that option because we don't want to download the animation with the model; we already have it. Leave the remaining options as it was, then download. When saving, save it as xbot@running. It is very important to save it like that so we can uniquely identify the animation that belongs to each character.

Screenshot_6.png

Screenshot_7.png

Downloading The Jumping Animation

Switch to the Animations tab if you are not there already, and make sure that Xbot is still the selected character. Search for Jump, just like the running animations above, Mixamo has many jumping animations, some of which are named Jump. I've added a screenshot of the one I chose below, it also has the in place option. Also, remember to save it as xbot@jumping, and remember to download without skin

Screenshot_8.png'

Screenshot_6.png

Screenshot_9.png

Downloading The Sliding Animation

On the Animations tab, search for Running slide. Again a lot of results, but select only the one named Running Slide among the results (see the attached screenshot below). Unfortunately, in place option is not available for this specific animation, so we will use threejs later in this tutorial to force the animation to remain in the same position while playing. Also, remember to save it as xbot@sliding, and remember to download without skin.

Screenshot_10.png

Screenshot_6.png

Screenshot_11.png

Downloading The Stumbling Animation

On the Animations tab, search for stumble backwards (See the attached screenshot). Select download without skin, and save it as xbot@stumbling.

Screenshot_12.png

Screenshot_6.png

Screenshot_13.png

Downloading The Dancing Animation

Go back to the Animations tab and search for salsa, then select the first one. Download without skin, and save it as xbot@dancing.

Screenshot_15.png

Screenshot_6.png

Screenshot_14.png

That's all! You can repeat the same process Jolleen and Peasant Girl:

Screenshot_16.png

Screenshot_17.png

Summary and next steps

In this article, I explained how to set up a threejs app with Vite, and then proceeded to build a scene that contained a light camera and animated cube. I also explained how to download the characters used in this game with their animations.

The next part of the tutorial is probably going to be the longest part of this series, we will learn how to:

  • import 3D models into a scene
  • animate the character by making it run, jump, slide and stumble.
  • detect collisions with obstacles and coins
  • to save the coins and scores, and many more
ย 
Share this