Overview
In this part of the series I will go over how to move your character around the X and Z axis of a Threejs scene by right-clicking with your mouse using raycasting. This article assumes you have read the previous two articles consisting of a basic setup and basic scene decoration. You can see a working demo at the bottom of the article.
Getting Started
When moving a character, the first thing you need to know is where to move it. When can capture the target location by using a neat method called raycasting. This detects the X and Y location of a click in 2D space (your screen) and draws a straight link through the scene until it collides with a given set of objects. In this case the only object we need to detect is the floor. We are going to start by creating five new variables to the top of our JS file.
// Track all objects and collisions. var objects = []; // Track click intersects. var mouse, raycaster; // Store movements. var movements = []; var playerSpeed = 5;
Now we want to add the floor of the scene to the objects array since this is what we are going to look for clicks on. You can accomplish this by adding the following code to the bottom of your createFloor() function.
objects.push( plane );
Awesome! Now the floor is ready for detection. We are now going to add a click event handler to gather click information. You can use mousedown or mouseup. In this example I will use mousedown. Add the following code to the bottom of the init() function.
document.addEventListener( 'mousedown', onDocumentMouseDown, false );
Now we need to create the declared onDocumentMouseDown() function. This function is rather complicated but I will explain as much of it as possible.
/** * Event that fires upon mouse down. */ function onDocumentMouseDown( event, bypass = false ) { event.preventDefault(); // Detect which mouse button was clicked. if ( event.which == 3 ) { // Grab the coordinates. mouse.x = ( event.clientX / renderer.domElement.clientWidth ) * 2 - 1; mouse.y = - ( event.clientY / renderer.domElement.clientHeight ) * 2 + 1; // Use the raycaster to detect intersections. raycaster.setFromCamera( mouse, camera ); // Grab all objects that can be intersected. var intersects = raycaster.intersectObjects( objects ); if ( intersects.length > 0 ) { movements.push(intersects[ 0 ].point); } } }
This function will detect a right click (event.which == 3) and grab the 2D coordinates given by the event handler. It will then take those coordinates and push them to the raycaster. If there is an intersect the raycaster will return the objects that were hit (Yes it can detect multiple hits!). If we have a hit we will put the location in our movements array.
Now we need to build two more functions. The move function and a stop moving function (because if we start moving we have to know when to stop). Let's create the stop movement function first because it is quite simple.
/** * Stop character movement. */ function stopMovement() { movements = []; }
Now go back to the onDocumentMouseDown() function and add the stopMovement() function when a right click is detected. So add the function under the conditional statement:
// Detect which mouse button was clicked. if ( event.which == 3 ) { stopMovement();
Alright. Now all we need to do is create the move code and add it to the renderer. The function will simply be called move(). In this function we will use some basic geometry to get the move distance. We must also take into account negative values. I documented the code as well as I could below. This is the move() function.
function move( location, destination, speed = playerSpeed ) { var moveDistance = speed; // Translate over to the position. var posX = location.position.x; var posZ = location.position.z; var newPosX = destination.x; var newPosZ = destination.z; // Set a multiplier just in case we need negative values. var multiplierX = 1; var multiplierZ = 1; // Detect the distance between the current pos and target. var diffX = Math.abs( posX - newPosX ); var diffZ = Math.abs( posZ - newPosZ ); var distance = Math.sqrt( diffX * diffX + diffZ * diffZ ); // Use negative multipliers if necessary. if (posX > newPosX) { multiplierX = -1; } if (posZ > newPosZ) { multiplierZ = -1; } // Update the main position. location.position.x = location.position.x + ( moveDistance * ( diffX / distance )) * multiplierX; location.position.z = location.position.z + ( moveDistance * ( diffZ / distance )) * multiplierZ; // If the position is close we can call the movement complete. if (( Math.floor( location.position.x ) <= Math.floor( newPosX ) + 1.5 && Math.floor( location.position.x ) >= Math.floor( newPosX ) - 1.5 ) && ( Math.floor( location.position.z ) <= Math.floor( newPosZ ) + 1.5 && Math.floor( location.position.z ) >= Math.floor( newPosZ ) - 1.5 )) { location.position.x = Math.floor( location.position.x ); location.position.z = Math.floor( location.position.z ); // Reset any movements. stopMovement(); // Maybe move should return a boolean. True if completed, false if not. } }
Okay that was a lot but the code isn't too hard to decipher. We get the distance using the Pythagorean Theorem based off of the player's current position and destination. Each iteration we add the playerSpeed to the location. Each iteration is completed with a run of the render program. The final thing we must do is add the final code to the render function so we can increment to the destination. Your render function should now look like the following:
/** * Render the scene. */ function render() { renderer.render( scene, camera ); // Don't let the camera go too low. if ( camera.position.y < 10 ) { camera.position.y = 10; } // If any movement was added, run it! if ( movements.length > 0 ) { move( rotationPoint, movements[ 0 ] ); } }
And that is it! Now you can move! This only applies to movement on the X and Z axis at this point. Down the road we can look into creating movement on the Y axis as well (stairs, a hill, etc). In the example below you can right click to move and hold the left click down to look around.
See the Pen Basic Threejs Game Tutorial Part 3: Moving by Bryan Jones (@bartuc) on CodePen.
Add new comment