Let’s keep working on our web runner, model a spaceship and finish decoupling our systems!
This article is also available on Medium.
Last time, we worked on our game logic and we prepared the skeleton of our Game class. In this episode, we’re going to increase the decoupling between the game code and the main routine even more, and we’ll see how to use Three.js geometry utilities to model our little spaceship!
This tutorial is available either in video format or in text format — see below 🙂
Finish up decoupling our systems
A quick overview
If we take back our
main.js script, we see that for now, it contains three parts:
- first, we create the basic Three.js objects (i.e. the scene, the camera and the renderer) and we add the auto-generated HTML canvas to our DOM: at that point, we have a valid but empty 3D scene
- then, we actually initialise our 3D scene: for now, we’re creating our green cube from a geometry and a material – then we add it to the scene and we place our camera
- and finally, we define our animate loop function and we call it once to start the auto-looping
As we said in the previous tutorial, what we want to do is make the main routine agnostic of all the details about the 3D scene so that we can update our game logic as much as we want without having to worry about the main script.
This means that we’ll want to extract the middle chunk of code from this logic, the one that creates the contents of the 3D scene. This is the part that will vary from game to game and that we’ll need to move to the
Game class. Remember how last time we created our constructor and the
_initializeScene() method? These are the ones that we’re going to populate gradually to move this logic over inside the class.
Doing the same… but better!
Before we actually work on our spaceship and talk about more complex geometries, let’s start by reproducing the exact same scene: our rotating green cube. But, of course, we’ll have it all handled by the game instance so that this scene initialisation logic is properly decoupled from the main routine.
If we look at our code, we see that there are 2 objects that we need to pass to our game instance so that it can recreate the scene: the scene and the camera. We will send them to our instance via its constructor, as simple input parameters:
And now, we can pass them directly to the
_initializeScene() method, and we can also copy back the bit of code in the middle of our
Instead of this scene initialisation logic, our
main.js script will have just a single line where we create our game instance and we pass it the scene and the camera objects:
The problem is that, now, the
cube variable doesn’t exist anymore, so the animate loop will crash when it tries to update its rotation. What we want to do is also make the game instance take care of rotating the cube (because, once again, it should not now that there is a cube in the scene… it should only know that the scene has to be updated!).
Let’s take those lines and move them to the
update() method of our
Note that I have replaced the
cube variable by a
this.cube variable so that it’s an instance variable and we can access it from every method in our class. This means we have to change it in our
_initializeScene() method, too:
Finally, in the
main.js script, instead of updating the
cube directly, we’ll just call the
update() method of our game instance:
Now, if you save this, you’ll get exactly the same 3D scene as before but everything is done inside of the
Game class, which is way better! 🙂
Time for some modelling!
With that said, it’s finally time to actually start modeling our spaceship 😉
First things first: what is it we want to model, exactly? Three.js provides us with a gallery of geometry utilities, but we have to know what shape we want our ship to have before diving into the docs!
Here, I will describe the process to make the ship I use in Hyperspeed but, of course, feel free to find your own spaceship design…
Studying the ship’s design
So – as you can see on this image, my spaceship model is composed of 7 parts: you have the ship body that is a tetrahedron (that’s like a pyramid) and then 3 reactors that each have a “socket” and a “light”/”energy” cylinders:
We will instantiate this ship in our
game.js script, in the
_initializeScene() method: we’ll simply replace the green cube with our spaceship.
Making the body (and creating a specific sub-hierarchy)
Let’s start with the ship body. To create this mesh, we’ll want to use Three.js’s tetrahedron geometry.
There is a little optimisation trick we can use, and that’s to create buffer geometries instead of geometries. Basically, buffer geometries are a specific type of geometry that contains info about vertex positions, face indices, normals and so on, so that all of this data can be passed to the GPU much quicker. It is much more efficient than the basic geometry equivalent.
When you work with custom geometry and you build your shape by hand, buffer geometries can be a bit harder to use as a developer, but here we’re just using basic primitives so they’re clearly a better choice 😉
So instead of using tetrahedron geometry, let’s actually use a tetrahedron buffer geometry – and pass in our radius parameter that sets the size of the object. We’ll also define a basic material with a light gray colour to get our ship body mesh:
Now, you might be thinking that we have to add this object to the scene, like we did with our cube before. And, yes we do…
But the thing is that, rather than adding this ship body directly into the scene, we are actually going to create a virtual anchor for the 7 parts of our ship. So the ship itself will not really be a 3D object but a 3D group (a THREE.Group), it will be a collection of several children that are all packed together in this specific sub-hierarchy in our scene.
This will allow us to transform the 7 objects as if they were one, so we’ll be able to easily translate, rotate or scale the entire ship all at once!
Note: again, I’m making our ship group an instance variable because we’ll want to access it inside other methods of our instance 🙂
This group is a new level in our hierarchy and we can now add our ship body to it to fill our sub-hierarchy using
.add() as usual. But of course, in the end, we have to remember to add the group itself to the scene for this sub-hierarchy to be actually parented to the scene:
We’ll leave the camera as is for the moment, which gives us the following code for our
_initializeScene() method so far:
Before we save however, there is something that we need to change, and that’s our
update() method. Remember that we were modifying the rotation of our cube in this function, but since the cube doesn’t exist anymore, it will cause some null references.
So let’s just get rid of that
this.cube variable and don’t do anything in our
update() method for now:
At that point, we can save and we see that we have a little object in our scene: that’s our ship body! But, let’s be honest: it doesn’t look like much yet…
Fixing some translations and rotations
The problem is that this ship body is not properly rotated: we want to see it from the rear. This means we have to rotate it 45 degrees around the X and Y axis, which can be done via the
However, we need to be careful because the angles we give here have to be converted to radians – so we’ll just multiply by pi and divide by a 180 to get the equivalent angles in radians:
Now we see that our ship is properly rotated:
The next thing we want to do is move and rotate the camera a bit so that it is closer to the object and we see the terrain and the ship from above. Once again, we’ll use the
rotateX() method on our camera and we’ll set the camera position with a little offset on the Y axis:
Ok! We now have a nice rear view of our ship body 🙂
Note that, in Hyperspeed, I’ve added a little additional part in wireframe mode to mark the edges of the mesh – we’ll work on that in one of the last episode of this series but you can already try it out if you want!
Putting on some reactors!
Another important thing about geometries is that if several objects in your scene have the exact same shape, then you can actually re-use the same geometry variable when you create each mesh. This will improve memory management and avoid redundancy.
We can do the same for materials if all objects have the same colour and same visual properties.
In our case, this is particularly useful because all reactor sockets and all react lights share the same geometries and materials:
- we have “large” cylinders all 3 reactor sockets, and “smaller” cylinders for all 3 reactor lights
- and we have a medium gray colour for the sockets, and a light blueish colour for the lights
Let’s start with the sockets and create 2 variables: the
reactorSocketGeometry and the
reactorSocketMaterial to share between our 3 objects. The geometry will use the cylinder buffer geometry with some custom parameters and the material will be a basic material with a medium gray colour:
Now, we can use those two variables to create 3 meshes for the reactor sockets of our ship, and we can add them to the ship group we created before:
But if you save this, you won’t see anything – because for now the sockets are actually inside the ship body, at the origin point!
So, again, let’s set their position and rotation:
Obviously, those values depend on the shape of your ship and you may have to tweak those depending on your own design…
If we save, we see that we have the 3 reactor sockets behind our ship! 🙂
The lights are very similar: we simply need to create another cylinder geometry that is a bit smaller and a basic light blue material:
Then we can create another set of objects, add them to the ship and set their positions and rotations:
Save this and you’ll get our little spaceship in your 3D scene, ready to go and fly on a grid! 😉
Today’s episode was a bit shorter but we took care of several things: we’ve managed to transfer all of our 3D scene initialisation logic into the
Game class and we’ve explored Three.js geometry and buffer geometry tools to create our spaceship model.
Next time, we’ll see how to add an infinite grid to our scene and animate it so that it feels like we’re moving forward indefinitely. We’ll talk a bit about shaders and how they can help you optimise lots of visual effects in your game…