Creating a 3D browser game with physics in a few steps.
Today I will tell you about making new levels, creating a level selecting menu and showing player stats (How good is he as street basketball player).
First, note that we added 2 new files to app.js: levelData.js and utils/textures.js.
utils/textures returns functions for generating textures from inputed data. It will be used to print stats and level items in level menu.
Second thing is that we add a raycaster variable. This thing is used to check if 3D vector generated from 2D mouse position intersects with other scene objects.
And in last 21 lines of init() function we added checking for app loading status. I won’t describe this part cause it is not related to main application and you will probably skip or rewrite this part in your app. Just keep in mind that we use event listeners here to do various things that will make impact on app preloader.
Three sections. What will be
By the way, let’s define three game sections:
- Main game. Player throws the ball until he score a goal.
- Goal details. Player scored a goal and now he see it’s time, attempts and accuracy.
- Level selecting menu. Player wants to choose another level or get back to current one.
Let’s make a nice transition for camera, goal stats and game headline.
At first, we need to define a variable (ratio in code below) that will store relation between window width and window height. THREE.PerspectiveCamera already has two methods that will give us width and height values ->.getFilmWidth() and .getFilmHeight()
Note that APP.camera is a whitestorm.js wrapper for camera, to get it’s Three.js camera use APP.camera.getNative()
Let’s add a headline of this game. For that we use WHS.Text. We need to generate a .js font file, I used typeface.js generator to do this. I named my file 1.js, you can name it as you want. Note that in font parameter we type font url, not a font name like in three.js
For material i applied a texture using handy WHS.texture function. “repeat” parameter there automatically applies THREE.RepeatWrapping to wrapS and wrapT of the texture. For more information check example in three.js docs.
Due to performance reasons this text will be available only for desktops.
To make this text be center-aligned we need to calculate it’s width, divide it by 2 and subtract this value from text’s X position (If we want to center it by X axis like now). Text mesh’s width we can find simply by finding subtraction of bounding box’s X max and X min values.
On image below you can see how we do this. Blue line is a center of the screen.
To show goal details we need to make a 2D text. The easiest way is to create a plane and make a text as it’s texture. To make it we need to create a 2D canvas 2000 x 1000 (this values will be only used as dimension, the only thing you always need to keep the same is ratio. You can make it 1000 x 500 or etc.)
Then we create an img element and apply base64 exported from canvas element to img’s src.
Last step is to make a THREE.Texture from this image. It can be simply done by pasing image as a parameter: new THREE.Texture(image)
This file is utils/textures.js:
Plane’s size will depend on what ratio has device that we use. If ratio is smaller than 0.7, plane will be 150 x 75, if greater than 0.7 – 200 x 100.
Let’s sum up the above:
This loop is used to make a cursor from a 3D ball. First of all we need to hide a default cursor, it can be easy implemented using CSS:
Then we should make our ball follow the hidden cursor. For that we need to have 2 3D points:
- Point where a ball should be.
- Current ball position.
Second one we already have, but where should be first one? To find that we should use THREE.Raycaster. We use current cursor’s X and Y to get a point where our projected ray intersects “plane for raycasting”. Let’s modify createScene() a little:
APP.planeForRaycasting should be a Math Plane, not a plane geometry. On image below you can see that this plane is highlighted blue:
We’ll talk about triggerLevelMenu() and goBackToLevel() later, when we’ll make a Level Menu section.
We can load app faster
Some things can be done after app is loaded, but before them be used. I created one more init… : initLevelMenu()
…but the difference between initMenu() and initLevelMenu() is that first one is called before app started and second one only after we score a goal. Of course it’s not necessary to do such things, but it’s up to you.
So what we prepare in this part?
- Level grid — some planes with generated texture (by number of level)
- Level indicator – used in checkForLevel loop. Will be shown when player’s cursor is over the level plane.
- LI progress — this is a progressbar for Level Indicator.
On this image you can see that ball (cursor) is over Level plane.
Level indicator is a little white sphere in ball center.
LI progress is a torus around Level Indicator.
And we need to make 2D textures depending on level data again:
This file will store an array of level objects, that will store miscellaneous parameters such as distance to basket, basket color, backboard texture. You may add your own ones.
Example of my levelData.js you can find at Github.
The most interesting part: Transitions
Before we start
We need to add some more things: onGoal and onLevelStart. We had a line in keep_ball loop from Part II. So, what this function does?
- Records goal data. Time, accuracy, attempts.
- Modifies APP.goal variable.
- The most important: calls goToMenu();
Moving from Main Game to second section
Before the switching animation starts we need to stop keep_ball loop and disable controls used in throwBall().
Then we can easily understand what mark player achieves. In this example if accuracy is more than 60, time is less than 2 seconds, and 1 attempt — “Excellent”, accuracy is more than 40, time is less than 5 seconds and 1 attempt — “Good”, other — “OK”
To make a nice animation while going from Main Game to Goal details section i used GSAP’s TweenLite. I don’t know what rotation should I use for camera’s destination so i will use .lookAt() method for cloned camera that is already on destination position. Then I simply get those data from camera as cameraDest.rotation and start loop_raycaster when animation is complete.