Making a 3D web runner game #1: Creating a basic 3D scene with Three.js

Let’s discover Three.js and see how to make a simple 3D scene in the browser!

This article is also available on Medium.

Last week, I talked about my latest video game: Hyperspeed! This game is a 3D infinite runner made entirely with web tech (HTML, CSS and JavaScript). And as promised, today, I start a series of tutorial to share how I made this game and how you too can create a full-web 3D runner game 🙂

In this first episode, we’ll recap how the game works and see the related programming concepts; then, we’ll setup our dev environment; finally, we’ll create a basic 3D scene with Three.js.


This tutorial is available either in video format or in text format – see below 🙂

A quick recap on Hyperspeed

So – Hyperspeed is a runner. In this game, you control a spaceship that flies across an infinite grid and you can only change its trajectory by having it translate to the left or to the right. Your goal is to collect bonuses along the way (those are the little coloured orbs) while avoiding obstacles (those are the white cubes).

Whenever you hit a bonus, it increases your score. Whenever you collide with an obstacle, you lose a bit of hull integrity. If it reaches zero, your ship gets destroyed and it’s game over!

Infinite runners are direct descendants of arcade games: the whole point is that you are fighting a never ending and that you’re essentially playing against yourself.

Image from: https://www.timeout.com/usa/news/play-atomic-boy-indiana-jones-and-more-old-school-arcade-games-for-free-online-now-050820

From a programming standpoint, this means that we have to continuously fill the level with new bonuses and obstacles so that the player can keep on playing.

So in this series, we will discuss:

  • procedural generation: we’ll need to setup rules to automatically produce those objects at a regular rate
  • 3D for the web: throughout these tutorials, we’ll explore the Three.js package and use it make a simple scene for our game
  • event handling: of course, we will need to react to user events to make the ship move!
  • audio manager: because a bit of music and SFX is always nice 🙂
  • and much more…!

Setting up a simple web project

Before we actually dive into Hyperspeed or even 3D, let’s create a brand new project and set up some basic web architecture. What I usually like to do in terms of files tree when working on web projects is to have my index.html (or optionally index.php) stay at the root level, but then create styles/ and scripts/ subfolders for my CSS and JS files.

Let’s start with some super simple files:

  • for now, our index.css will just change our doc styling to use a sans-serif font:
  • our main.js script will create a little alert when the page loads:
  • finally, our index.html will import these styles and scripts, and then it will just contain a “hello world” div:

To run this script, something really handy is to have some local dev server for your code. You can run basic dev servers using either Python (via the http.server built-in module) or PHP, for example.

But if you work in the Visual Studio Code IDE, like me, then you can add a little package from the online repository called “Live Server”:

Live Server’s animated demo, from: https://marketplace.visualstudio.com/items?itemName=ritwickdey.LiveServer

This tool is nice because it creates a dev server with hot reload, meaning that whenever you save your files, the matching page in your browser automatically refreshes. This avoids running obsolete code: you’re always up-to-date with your latest changes 😉

Note: if you don’t want to use a dev server, then you can actually get away with just opening the index.html directly in your browser for now. But later on in the series, when we start loading assets (for our music and sounds), you’ll need to have to a local dev server – with or without hot reload!

Once you’ve started your local server, you can go to the matching address (in my case localhost:5500) and you’ll see our super simple HTML page:

Ok – now that we have setup our web project, let’s talk a bit about Three.js and see how we can install it!

Three.js basics

What is Three.js?

Three.js is a JavaScript library that allows you to render 3D in the browser. As you can see in the image below (a showcase of some projects from the community), you can do very various 3D scenes. It’s not too hard to use and it relies on the WebGL renderer which makes it really fast and efficient.

Note: we won’t be diving into this for this series, but if you want to make more photorealistic renders, you can even benefit from some PBR (physical-based rendering) materials with specular, roughness, etc.

To learn more about Three.js, you can check either their documentation (with the full API) or their showcase examples.

How do you install Three.js in your project?

As you can see in their docs, there are basically 2 ways you can get Three.js into your project – either through the NPM repository if you’re working in a Node environment, or from a CDN:

Three.js docs on how to install it (either through NPM or a CDN) – From: https://threejs.org/docs/index.html#manual/en/introduction/Installation

The thing is: I had a big constraint on my production server – namely, it cannot run Node. It’s just old plain PHP.

So this leaves me with no choice: I’m going to make a static web project that uses the CDN version of Three.js. This project will work with vanilla HTML/CSS/JS and usual <script> tags for integrating external sources. But there is one little trick that I like to do: instead of relying on the CDN itself, I find it better to go and download the script, and then add it locally to my project. This way, I’m sure it’s always available and I don’t have network latency when I load it for my game.

To get the CDN version of the lib, you can go to cdnjs for example and get the minified production-ready script (three.min.js):

Websites like cdnjs contain thousands of JS lib, for example Three.js 🙂

What I usually do is create a little vendors/ folder in my project’s directory and simply paste the contents of the JS file from the CDN inside it:

To avoid depending on an external CDN, it’s better to download the CDN minified script locally!

Then I can just reference it in my index.html, with <script> tags, and point to a local path instead of an external HTTP address:

Making a simple 3D scene

Now that we’ve imported it in our project, it’s time to actually use this lib and see how to create a basic 3D scene! In this first episode, we will simply go through Three.js Getting started tutorial together and see the different steps in making a Three.js scene.

In the end, we will get a simple web page with a little green cube rotating on itself, like this:

Creating the base Three.js objects

A Three.js 3D scene relies on 3 basic components:

  • a Scene instance that will hold of objects to render and all of the context settings; it’s basically like a workspace and, in particular, it will contain a hierarchy of objects to populate it
  • a camera (they are several available but in this project, we’ll use a PerspectiveCamera): just like a real camera for photo or cinema shoots, this object is able to take in a 3D space and convert this information to a 2D space, as an image
  • a renderer (here, a WebGLRenderer): that tool is what uses the camera to get a snapshot of the 3D scene’s current state and display it on our screen

Let’s copy the code from the Three.js’s tutorial in our window.onload() function, so that it looks like this:

The camera object takes in a bunch of parameters: the field of view, the aspect ratio, the far and near clip planes. You can just go with the given values if you’re not too used to those variables.

Note: we’ll discuss the aspect ratio a bit more in the last episode of this series, when we talk about resizing the browser window.

The renderer also needs to be initialised: we have to give it the size of the screen to render on. This can be computed automatically using the window.innerWidth and window.innerHeight built-in JS variables that simply return the current size of the browser window.

Once we’ve prepared our 3D scene, we need to actually link this to our web page. What we want to do is get the result from the renderer and display it in our HTML. Three.js makes it easy: all we have to do is add the DOM element that the renderer created to our page and it will directly show the 3D scene!

This DOM element is an HTML canvas: the renderer will simply repaint it with the updated image whenever it’s asked to refresh the view.

For now, if we save our file, this gives us a big black block:

We can remove our “hello world” div and follow Three.js’s advice, that is to remove the margin of the body so that the canvas is neatly stuck to the edge of the screen:

Adding a cube

Now that we have setup the stage, let’s add some actors! Namely: a green cube 🙂

To create a new 3D object in our scene, we’ll follow a classic 3D workflow:

  1. create some geometry, a shape to show on screen
  2. create a material to define the visual properties of the object (here: it’s colour)
  3. bundle all of this into a 3D instance

Note: I won’t be explaining 3D basics in this series of tutorials – if you’re not too familiar with it, you should definitely go browse the Internet for some info so you’re not lost with the semantics! 😉

In Three.js, you have lots of geometry utilities to create basic primitives, like the BoxGeometry() for the cube. Those define a set of vertices and faces that make for a given 3D shape.

Similarly, we have various material types; we’ll pick the most basic one for now, the MeshBasicMaterial() (a simple uniform colour) and we’ll assign it a green colour.

When working with this lib, you’ll have to write your colours in six-digit hexadecimal form with the 0x prefix, so something like 0xffaa9d for example. In our case, to have a green colour, we’ll pass in 0x00ff00 (because remember that the first pair of characters represents the red value, the second the green value and the third the blue value).

Finally, with Three.js, your 3D instances are “meshes“, and they’re created by passing the geometry and the material that you’ve created to the Mesh() creator.

All in all, this gives us the following code:

To really instantiate the cube in our scene, though, we have to remember to add this newly created object to the scene hierarchy. So here is our updated main.js file:

But this actually places the object at the world origin point with the (x, y, z) coordinates (0, 0, 0) – which is the exact same location as the camera! So if we keep this as is, we won’t be able to see the object from our camera. To fix this, let’s just move the camera a bit back on the Z axis:

Fixing the render!

Now, if you save your scene and let the page auto-reload you will see… nothing! Still a black screen!

So – what’s going on?

Well, the problem is that, for now, we’ve created stuff in our scene but haven’t actually rendered it. We have various 3D objects that are in the field of our camera, but we haven’t told the renderer to use the camera to transform this 3D info into a 2D image and show it on our screen…

To do that, we just need to call:

renderer.render(scene, camera);

But the issue here is that, when we do that, we just render once. Therefore, if we were to move the camera or an object in the scene, the image would stay exactly the same: this takes a one-shot picture of the initial state of our game, and then it doesn’t refresh the screen anymore.

The solution is to instead implement an animate loop, or render loop. This works as follows:

  • you create a function that contains all the logic to “update the scene”: we’ll call it animate() and, for now, it will just re-render our scene
  • then, you have this function call itself recursively, using the built-in requestAnimationFrame() JS method
  • finally, you call the function one first time to initialise the loop (then from that point on, it will handle re-calling itself so you don’t have to worry about it anymore)

The requestAnimationFrame() function is a nice way of having your web page perform some action regularly, depending on the framerate of your browser, and interrupting the calls whenever you switch to another tab (which saves a lot of computing power and battery for your device). It’s basically like setInterval() but on steroids and much more efficient 🙂

So – here is our full updated code for the main.js file so far:

If you save the file, after the page has reloaded, you’ll see that we’ve successfully rendered our cube in the middle of the 3D scene!

That’s cool but… that doesn’t really make the most of our 3D setup – let’s be honest, we could have rendered the same scene with a simple green div. So we have to step it up a little and add some animation to this cube, so we actually see it’s an object inside a 3D space 😉

Animating the scene

This is, in truth, pretty straight-forward. We just have to add some instructions into our update loop so that, in addition to re-rendering the scene, it also changes some properties of our cube object.

More precisely, we want to edit its rotation along the X and the Y axes, so that the cube rotates on itself around the origin point.

As shown in Three.js’s tutorial, we simply need to access the rotation property of the cube, and increase it by a little amount every time our animate() function is called:

If you use a higher increment value, then the changes will be more brutal and it will feel like the cube “teleports” to another rotation rather than moving smoothly. So, best to keep it small if you want to simulate a continuous time flow.

Save your file, have the web page reload and watch the magic happen! 🙂

Conclusion

In this tutorial, we’ve introduced our project, we’ve discussed the basics of Three.js and we’ve made a simple 3D scene using this lib. We’ve seen that to initialise a scene, you have to create some variables: the scene, the camera and the renderer. Then, to create more 3D objects, you have to create a Geometry and a Material, and package them as a Mesh. Finally, we’ve added a simple update loop to our scene using the built-in requestAnimationFrame() JS method and we’ve made our cube rotate smoothly.

Next time, we’ll work on our game logic. We will design our main Game class and get a brief overview of the different stages our game will run through.

Leave a Reply

Your email address will not be published. Required fields are marked *