📜 ⬆️ ⬇️

Platform on Three.js

The other day, Mr. Oak accepted my first pull request with an example in Three.js , and to my joy I decided to write about him a habroost. If you suddenly want to write a three-dimensional platformer on Three.js, but you don’t have much idea how to do this , this example is for you:



The entire code of the example is less than 300 lines, generously diluted with hyphenation, which it will not be difficult to figure out on your own. However, to further alleviate your fate, I will write a few words below about key points.

Prehistory


We all heard about people who can write a shooter in two days , but can we ourselves be on a par with the legends? To test my strength, I was surrounded by lessons on Three.js by Google and began to sculpt my 2-day masterpiece. However, after an hour or two I got bored, I committed what was there and went to get some fresh air to read the Internet. It was the same every time I returned to this venture. Days passed, then weeks. But the drop continued to sharpen the stone, and somewhere in a month I still carved my shooter, in which you can run in and shoot caravans of monsters from a shotgun.
')
All throw and go shoot a couple of monsters

Now it was time to look back on the road and think what I did well, and where I turned the wrong way. Actually, the example of a platformer, which is discussed in this article is one of the things that I think falls into the first category.

So shooter or platformer?


Maybe you ask me why I persistently call the essentially simplified version of my shooter platformer. Mr. Oak not only asked, but also made me rename the example back to the shooter before accepting a pull request. And yet, I do not consider this example a shooter. At least because in it it is impossible to shoot at anyone. But you can run and jump on a three-dimensional platform. The example code can be easily altered to a third-person game by adding a player model and manipulating it instead of a camera, but it seems to me that it doesn't matter.

No one will argue that, for example, Mario is a platformer?


In short, Sklifosovsky!


Yes, I'm a little distracted from the topic. So, to make a platformer, first of all we have to add at least one platform to the game world. This is a simple matter, I took a 3D model, exported it to my favorite format (from among babylon, ctm, dae, obj, ply, stl, vtk or wrl), uploaded to Three.js editor , exported again, and upload to your health. There are two options:

  1. First load the platform, then create a scene and add a platform there.
  2. Create a scene and add a platform to it, and then load it in the background

The first option is, of course, ideologically more correct, but most of the examples of Three.js (including this one) do not bother and work according to the second scenario. It should be noted that there is no special difference in the code between 1 and 2 - just in the first case, you should transfer the scene initialization call to the load handler, and in the second case, add a check on the platform state in the main loop so as not to fly far down until it boots. I went exactly along this path, and in order to correctly implement the first variant, in the case of preloading a multitude of resources, it would still require a lot more code and / or third-party libraries.

View platform load code?
function makePlatform( jsonUrl, textureUrl, textureQuality ) { var placeholder = new THREE.Object3D(); var texture = THREE.ImageUtils.loadTexture( textureUrl ); texture.anisotropy = textureQuality; var loader = new THREE.JSONLoader(); loader.load( jsonUrl, function( geometry ) { geometry.computeFaceNormals(); var platform = new THREE.Mesh( geometry, new THREE.MeshBasicMaterial({ map : texture }) ); platform.name = "platform"; placeholder.add( platform ); }); return placeholder; }; 


To speed up loading, I deleted the normals from the json file - so you see the computeFaceNormals call here - and platform.name is set for the platform check mentioned above. Without this, all the code would look like this:

 loader.load( jsonUrl, function( geometry ) { placeholder.add( new THREE.Mesh( geometry, new THREE.MeshBasicMaterial({ map : texture }) ) ); }); 


Okay, let's say you created the scene yourself, added a camera and a platform to it. Further, you must force the game character to move along it somehow, without flying through and without falling through it. The Raycaster class will help you in this. As it is easy to guess from the name, it calculates the intersection of a given beam with the selected geometry. In this case, we simply direct the beam down, and find the nearest intersection with the platform:

Simple, but there are nuances. For example, you cannot use the character’s position as the beginning of the beam - in this case you cannot find the intersection with the platform if the character for any reason fails at least a millimeter, and send it to free fall instead of pushing it back onto the platform. Accordingly, the beginning of the beam should be on top, at the height of "bird flight."

In this place in more detail, please ...
 var raycaster = new THREE.Raycaster(); raycaster.ray.direction.set( 0, -1, 0 ); var birdsEye = 100; ... // ,   raycaster.ray.origin.copy( playerPosition ); raycaster.ray.origin.y += birdsEye; var hits = raycaster.intersectObject( platform ); 


In the case of multi-storey level architecture, this height is obviously limited by the minimum vertical distance between the platforms. Next, you should carefully consider when to make a decision about pushing a failed character up. If you do not limit the maximum allowable depth of the "failure", the character will instantly teleport to the platform, simply by entering (or flying) under it; if you limit it too much, the character will be able to easily pass through the platform when landing after jumping.

How does this in code look like then?
 var kneeDeep = 0.4; ... // ,   // ,   ,           if( ( hits.length > 0 ) && ( hits[0].face.normal.y > 0 ) ) { var actualHeight = hits[0].distance - birdsEye; //    ,    if( ( playerVelocity.y <= 0 ) && ( Math.abs( actualHeight ) < kneeDeep ) ) { playerPosition.y -= actualHeight; playerVelocity.y = 0; } } 

An attentive reader will ask why there is a check for playerVelocity.y <= 0? Answer: in order not to create problems with separation from the platform when jumping.

Now, in fact, it is necessary to force the character to move in space, obeying the basic laws of the school physics course. Suppose that at any time the character knows the speed of the playerVelocity and position in the playerPosition space; then the calculation of the movement of the character at first glance might look like this (pseudocode):

 if(   ) playerVelocity.y -= gravity * time; playerPosition += playerVelocity * time; if(   ) playerVelocity *= damping ^ time; 

Alas, and everything is not so simple. Readers with non-school education or veterans igrostroya this pseudocode is known as the "Euler method", and it is also known that this method - just sucks. And that's why (the picture is steal from Wikipedia ):

As you can see, the calculated trajectory with time increasingly diverges from the expected result. In itself, this circumstance is not so scary - one modest variable, time makes it terrible. Imagine how this picture will change if time reduced by 10% (transfer to a faster browser, for example):

As you can see, by running the game in firefox, we get one dynamic, and by running it in the chrome, a completely different one. The behavior of the character will "float" depending on the intensity of the background tasks and the location of the stars. What to do?

There is a solution, and quite simple. It is necessary to replace the calculation with a long variable time step by several calculations with a short fixed step. For example, if two consecutive rendering intervals are 19 and 21 ms, we must calculate 3 steps of 5 ms for the first drawing and, adding the remaining 4 ms to 21, calculate 5 steps of 5 ms for the second.

Uh, what?
somewhere like this:
 var timeStep = 5; var timeLeft = timeStep + 1; ... function( dt ) { //    ;) var platform = scene.getObjectByName( "platform", true ); if( platform ) { timeLeft += dt; //     dt = 5; while( timeLeft >= dt ) { //   ... timeLeft -= dt; } } } 


At this almost everything, you just have to set the movement parameters of the character ( playerVelocity for example) in response to WASD or something like that.

Oh yeah, I completely forgot. The striped ledges in the example send the character to jump across the entire platform. How? Everything is very simple - when a character approaches a ledge, a pre-selected vertical-inclined component is added to playerVelocity , which is guaranteed (thanks to the above-described scheme with a fixed step) to take it to a given point, like an artillery shell. No special tricks are needed - everything is already working.

Now for sure. Read mine, write your own, criticism is welcome. Before communication!

Update : once again links at the end according to the wishes of the lazy workers:

Source: https://habr.com/ru/post/231987/


All Articles