I am participating in the nuggets Community game creative submission Contest. For details, please see: Game Creative Submission Contest
Disclaimer: This article is only used for personal study, research and appreciation, please do not modify, illegally spread, reprint, publish, commercial use, and other profit behavior.
background
In kepler-1028, a galaxy 2,545 light-years away that is home to a colorful, habitable planet called 🌑, interstellar migrants 👨🚀 must wear base-issued radiation suits to survive. Ali 🦊 to drive the interstellar aircraft 🚀 to arrive here, quickly help it in the limited time to use the wheel to move to find the base to obtain radiation protection suit!
In this paper, Three. Js + React + CANNON technology stack is used to implement a low-poly low-polygon style mini-game that controls the model to move in a 3D world by sliding the screen. This article mainly covers Three. Js shadow types, creating particle systems, basic usage of cannon.js, creating terrain using cannon.js Heightfield Heightfield, controlling model animation through wheel movement, etc.
The effect
- Gameplay: Click the start game button, move ali by operating the wheel at the bottom of the screen, and find the base within the limited time of the countdown.
- Main quest: Find the shelter within the time limit.
- Side quest: Explore the open world freely.
Online preview:
👀
Address 1:3d-eosin.vercel.app/#/metaverse👀
Address 2:Dragonir. Making. IO / 3 d / # / metave…
Adaptation:
💻
PC
端📱
The mobile terminal
🚩 tip: the higher you stand, the farther you can see. Vaguely, I heard that the base is located in the west of the initial position.
design
The game process is as follows: after the page is loaded, the player 👨🚀 clicks the start button, and then moves the model by controlling the wheel 🕹 at the bottom of the page within a limited time to find the location of the target base. Search for success or failure will display the result page 🏆, there are two buttons on the result to try again and free exploration, click try again time will reset, and then return to the starting point to start the countdown. Click free to explore without timing, the player can manipulate the model in 3D open world to explore freely. In addition, the in-game page also provides a rewind button, which allows players 👨🚀 to manually reset the countdown ⏳ before they fail to start the game again.
implementation
Load resources
Load the necessary resources for development: GLTFLoader for loading fox 🦊 and base 🏠 models, CANNON is the physics engine for creating 3D worlds; CannonHelper is a wrapper around some of the uses of CANNON; JoyStick is used to create a roulette wheel that controls model movement by listening for mouse movement positions or shifts generated by touching the screen 🕹.
import * as THREE from 'three';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader';
import CANNON from 'cannon';
import CannonHelper from './scripts/CannonHelper';
import JoyStick from './scripts/JoyStick';
Copy the code
Page structure
The page structure is relatively simple. Webgl is used to render webGL; .tool is an in-game toolbar that resets the game and displays hints. Loading is the game loading page, used to display the game loading progress, introduce the game rules, display the game start button; .result is the game results page, used to display success or failure of the game, and provides two buttons: try again and explore freely 🔘.
(<div id="metaverse">
<canvas className='webgl'></canvas>
<div className='tool'>
<div className='countdown'>{ this.state.countdown }</div>
<button className='reset_button' onClick={this.resetGame}>Back in time</button>
<p className='hint'>The higher you stand, the farther you can see</p>
</div>
{ this.state.showLoading ? (<div className='loading'>
<div className='box'>
<p className='progress'>{this.state.loadingProcess} %</p>
<p className='description'>Game description</p>
<button className='start_button' style={{'visibility': this.state.loadingProcess= = =100 ? 'visible' : 'hidden'}} onClick={this.startGame}>Start the game</button>
</div>
</div>) : '' }
{ this.state.showResult ? (<div className='result'>
<div className='box'>
<p className='text'>{ this.state.resultText }</p>
<button className='button' onClick={this.resetGame}>Try again</button>
<button className='button' onClick={this.discover}>Free inquiry</button>
</div>
</div>) : "'}</div>)
Copy the code
Data initialization
Data variables include loading progress, whether to display the loading page, whether to display the result page, result page copywriting, countdown, whether to open free exploration, etc.
state = {
loadingProcess: 0.showLoading: true.showResult: false.resultText: 'failure'.countdown: 60.freeDiscover: false
}
Copy the code
Scene initialization
Initialization scene 🏔, camera 📷, and light source 💡.
const renderer = new THREE.WebGLRenderer({
canvas: document.querySelector('canvas.webgl'),
antialias: true.alpha: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const scene = new THREE.Scene();
// Add the main camera
const camera = new THREE.PerspectiveCamera(45.window.innerWidth / window.innerHeight, .01.100000);
camera.position.set(1.1, -1);
camera.lookAt(scene.position);
// Add ambient light
const ambientLight = new THREE.AmbientLight(0xffffff.4.);
scene.add(ambientLight)
// Add parallel light
var light = new THREE.DirectionalLight(0xffffff.1);
light.position.set(1.1.1).normalize();
scene.add(light);
Copy the code
💡
Three. Js shadow type
This article uses three.pcfsoftShadowMap to enable softer shadows. Three.js provides the following shadow types:
THREE.BasicShadowMap
: Provides unfiltered shadow maps for fastest performance but lowest quality.THREE.PCFShadowMap
Use:Percentage-Closer Filtering (PCF)
Algorithm filter shadow map, the default type.THREE.PCFSoftShadowMap
Use:PCF
The algorithm filters softer shadow maps, especially when using lower resolution shadow maps.THREE.VSMShadowMap
: Use the variance shadow mapVSM
Algorithmically filtered shadow maps. useVSMShadowMap
, all shadow receivers will also cast shadows.
To create the world
Initialize the physical world with cannon.js 🌏.
// Initialize the physical world
const world = new CANNON.World();
// Test rigid body collision on any axis of multiple steps
world.broadphase = new CANNON.SAPBroadphase(world);
// Set the gravity of the physical world to -10 meters per second along the Y-axis
world.gravity.set(0, -10.0);
// Create a default contact material
world.defaultContactMaterial.friction = 0;
const groundMaterial = new CANNON.Material("groundMaterial");
const wheelMaterial = new CANNON.Material("wheelMaterial");
const wheelGroundContactMaterial = new CANNON.ContactMaterial(wheelMaterial, groundMaterial, {
// Friction coefficient
friction: 0.// Recovery coefficient
restitution: 0.// Contact stiffness
contactEquationStiffness: 1000
});
world.addContactMaterial(wheelGroundContactMaterial);
Copy the code
💡
Cannon.js
Cannon.js is a physics engine library implemented in JavaScript that works with any browser-enabled rendering or game engine and can be used to simulate rigid bodies for more realistic physical forms of movement and interaction in the 3D world 🌏. More cannon.js API documentation and examples can be found at the end of this article.
Create the starry sky
Create 1000 particles to model the starry sky ✨ and add them to the scene. In this example, particles are created in shader form, which is more efficient for GPU rendering.
const textureLoader = new THREE.TextureLoader();
const shaderPoint = THREE.ShaderLib.points;
const uniforms = THREE.UniformsUtils.clone(shaderPoint.uniforms);
uniforms.map.value = textureLoader.load(snowflakeTexture);
for (let i = 0; i < 1000; i++) {
sparkGeometry.vertices.push(new THREE.Vector3());
}
const sparks = new THREE.Points(new THREE.Geometry(), new THREE.PointsMaterial({
size: 2.color: new THREE.Color(0xffffff),
map: uniforms.map.value,
blending: THREE.AdditiveBlending,
depthWrite: false.transparent: true.opacity: 0.75
}));
sparks.scale.set(1.1.1);
sparks.geometry.vertices.map(spark= > {
spark.y = randnum(30.40);
spark.x = randnum(-500.500);
spark.z = randnum(-500.500);
return true;
});
scene.add(sparks);
Copy the code
Create the terrain
Create a visual gradient terrain of 128 x 128 x 60 with the CANNON.Heightfield Heightfield. Is the state of the concave and convex of terrain ups and downs through the following a height map HeightMap implementation, it is a black and white picture 🖼, from the pixel color shades to record height information, according to the information to create the terrain height map data grid. Random height maps can be generated online via the link provided at the end of the article. The terrain is generated and added to the world 🌏, and then the check method is called when the page is redrawn in the animate method, which detects and updates the position of the model on the terrain.
const cannonHelper = new CannonHelper(scene);
var sizeX = 128, sizeY = 128, minHeight = 0, maxHeight = 60, check = null;
Promise.all([
// Load the height map
img2matrix.fromUrl(heightMapImage, sizeX, sizeY, minHeight, maxHeight)(),
]).then(function (data) {
var matrix = data[0];
/ / form
const terrainBody = new CANNON.Body({ mass: 0 });
// Terrain shape
const terrainShape = new CANNON.Heightfield(matrix, { elementSize: 10 });
terrainBody.addShape(terrainShape);
// Terrain position
terrainBody.position.set(-sizeX * terrainShape.elementSize / 2, -10, sizeY * terrainShape.elementSize / 2);
// Set the Angle from the axis
terrainBody.quaternion.setFromAxisAngle(new CANNON.Vec3(1.0.0), -Math.PI / 2);
world.add(terrainBody);
// Visualize the resulting terrain rigidbody
cannonHelper.addVisual(terrainBody, 'landscape');
var raycastHelperGeometry = new THREE.CylinderGeometry(0.1.5.1.5);
raycastHelperGeometry.translate(0.0.0);
raycastHelperGeometry.rotateX(Math.PI / 2);
var raycastHelperMesh = new THREE.Mesh(raycastHelperGeometry, new THREE.MeshNormalMaterial());
scene.add(raycastHelperMesh);
// Use Raycaster to detect and update the position of the model on the terrain
check = () = > {
var raycaster = new THREE.Raycaster(target.position, new THREE.Vector3(0, -1.0));
var intersects = raycaster.intersectObject(terrainBody.threemesh.children[0]);
if (intersects.length > 0) {
raycastHelperMesh.position.set(0.0.0);
raycastHelperMesh.lookAt(intersects[0].face.normal);
raycastHelperMesh.position.copy(intersects[0].point);
}
target.position.y = intersects && intersects[0]? intersects[0].point.y + 0.1 : 30;
var raycaster2 = new THREE.Raycaster(shelterLocation.position, new THREE.Vector3(0, -1.0));
var intersects2 = raycaster2.intersectObject(terrainBody.threemesh.children[0]);
shelterLocation.position.y = intersects2 && intersects2[0]? intersects2[0].point.y + . 5 : 30;
shelterLight.position.y = shelterLocation.position.y + 50;
shelterLight.position.x = shelterLocation.position.x + 5shelterLight.position.z = shelterLocation.position.z; }});Copy the code
💡
CANNON.Heightfield
The rugged terrain in this example is achieved through cannon.heightfield, which is the Heightfield of the cannon.js physics engine. In physics, the distribution of a physical quantity in a region of space is called a field, and a height field is a field that is highly correlated. The HEIGHT of a Heightfield is a function of two variables, expressed as HEIGHT(I,j).
Heightfield(data, options)
Copy the code
data
Is aY value
Array that will be used to build the terrain.options
Is a configuration item with three configurable parameters:minValue
Is the minimum value of a data point in a data array. If not given, it will be calculated automatically.maxValue
The maximum value.elementSize
是The x axis
The world spacing between data points in the direction.
Load Progress Management
Use LoadingManager to manage the loading progress. When the page model is loaded, the loading progress page displays the start game menu.
const loadingManager = new THREE.LoadingManager();
loadingManager.onProgress = async (url, loaded, total) => {
this.setState({ loadingProcess: Math.floor(loaded / total * 100)}); };Copy the code
Create base model
A shelterLocation grid is created to hold the base model before loading the base model 🏠. This grid object is also used for subsequent terrain detection. Then load the base model using GLTFLoader and add it to the shelterLocation grid. Finally, add a PointLight 💡 to add colored PointLight to the base model and a DirectionalLight 💡 to generate shadows.
const shelterGeometry = new THREE.BoxBufferGeometry(0.15.2.0.15);
const shelterLocation = new THREE.Mesh(shelterGeometry, new THREE.MeshNormalMaterial({
transparent: true.opacity: 0
}));
shelterLocation.position.set(this.shelterPosition.x, this.shelterPosition.y, this.shelterPosition.z);
shelterLocation.rotateY(Math.PI);
scene.add(shelterLocation);
// Load the model
gltfLoader.load(Shelter, mesh= > {
mesh.scene.traverse(child= > {
child.castShadow = true;
});
mesh.scene.scale.set(5.5.5);
mesh.scene.position.y = -. 5;
shelterLocation.add(mesh.scene)
});
// Add a light source
const shelterPointLight = new THREE.PointLight(0x1089ff.2);
shelterPointLight.position.set(0.0.0);
shelterLocation.add(shelterPointLight);
const shelterLight = new THREE.DirectionalLight(0xffffff.0);
shelterLight.position.set(0.0.0);
shelterLight.castShadow = true;
shelterLight.target = shelterLocation;
scene.add(shelterLight);
Copy the code
Create a Ali model
The fox 🦊 model is similarly loaded, creating a target grid for terrain detection and adding the fox 🦊 model to the target grid. After the fox 🦊 model is loaded, it is necessary to save its animation effects of CLIP1 and CLIP1, and then determine which animation to play by judging the moving state of the wheel 🕹. Finally, add a DirectionalLight 💡 light source to create the shadow.
var geometry = new THREE.BoxBufferGeometry(. 5.1.. 5);
geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(0.. 5.0));
const target = new THREE.Mesh(geometry, new THREE.MeshNormalMaterial({
transparent: true.opacity: 0
}));
scene.add(target);
var mixers = [], clip1, clip2;
const gltfLoader = new GLTFLoader(loadingManager);
gltfLoader.load(foxModel, mesh= > {
mesh.scene.traverse(child= > {
if (child.isMesh) {
child.castShadow = true; child.material.side = THREE.DoubleSide; }});var player = mesh.scene;
player.position.set(this.playPosition.x, this.playPosition.y, this.playPosition.z);
player.scale.set(008..008..008.);
target.add(player);
var mixer = new THREE.AnimationMixer(player);
clip1 = mixer.clipAction(mesh.animations[0]);
clip2 = mixer.clipAction(mesh.animations[1]);
clip2.timeScale = 1.6;
mixers.push(mixer);
});
const directionalLight = new THREE.DirectionalLight(new THREE.Color(0xffffff), . 5);
directionalLight.position.set(0.1.0);
directionalLight.castShadow = true;
directionalLight.target = target;
target.add(directionalLight);
Copy the code
Control ali movement
When using the wheel controller to move ali 🦊 model, update the direction of the model in real time. If the wheel has displacement, update the model displacement and play the running animation; otherwise, animation is prohibited. At the same time, according to the location of the model target, the location of the camera 📷 is updated in real time to generate a third-person perspective. The wheel movement control model movement function is realized by introducing JoyStick class. Its main realization principle is to monitor the mouse or touch position, and then map to the model position change through calculation.
var setup = { forward: 0.turn: 0 };
new JoyStick({ onMove: (forward, turn) = > {
setup.forward = forward;
setup.turn = -turn;
}});
const updateDrive = (forward = setup.forward, turn = setup.turn) = > {
let maxSteerVal = 0.05;
let maxForce = 15.;
let force = maxForce * forward;
let steer = maxSteerVal * turn;
if(forward ! = =0) {
target.translateZ(force);
clip2 && clip2.play();
clip1 && clip1.stop();
} else {
clip2 && clip2.stop();
clip1 && clip1.play();
}
target.rotateY(steer);
}
// Generate third person view
const followCamera = new THREE.Object3D();
followCamera.position.copy(camera.position);
scene.add(followCamera);
followCamera.parent = target;
const updateCamera = () = > {
if (followCamera) {
camera.position.lerp(followCamera.getWorldPosition(new THREE.Vector3()), 0.1);
camera.lookAt(target.position.x, target.position.y + . 5, target.position.z); }}Copy the code
The JoyStick class of 🚩 wheel controller can be specifically implemented by referring to the Codepen link [5] at the end of the article.
Animation updates
Update the camera, model state, Cannon world, scene rendering, and more in page redraw animation.
var clock = new THREE.Clock();
var lastTime;
var fixedTimeStep = 1.0 / 60.0;
const animate = () = > {
updateCamera();
updateDrive();
let delta = clock.getDelta();
mixers.map(x= > x.update(delta));
let now = Date.now();
lastTime === undefined && (lastTime = now);
let dt = (Date.now() - lastTime) / 1000.0;
lastTime = now;
world.step(fixedTimeStep, dt);
cannonHelper.updateBodies(world);
check && check();
renderer.render(scene, camera);
requestAnimationFrame(animate);
};
Copy the code
Page scaling adaptation
The page generates a zoom when updated render scene 🏔 and camera 📷.
window.addEventListener('resize'.() = > {
var width = window.innerWidth, height = window.innerHeight;
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}, false);
Copy the code
At this point, the game’s 3d world 🌏 has been fully realized.
Add game logic
According to the previous game flow design, now add the game logic, reset the data and start the 60s countdown when the game starts ⏳; Reset the game set ali 🦊 position, direction and camera position to the initial state; Open the free exploration state while exploring freely and clear the countdown. ⏳
startGame = () = > {
this.setState({
showLoading : false.showResult: false.countdown: 60.resultText: 'failure'.freeDiscover: false
},() = > {
this.interval = setInterval(() = > {
if (this.state.countdown > 0) {
this.setState({
countdown: -this.state.countdown
});
} else {
clearInterval(this.interval)
this.setState({
showResult: true}); }},1000);
});
}
resetGame = () = > {
this.player.position.set(this.playPosition.x, this.playPosition.y, this.playPosition.z);
this.target.rotation.set(0.0.0);
this.target.position.set(0.0.0);
this.camera.position.set(1.1, -1);
this.startGame();
}
discover = () = > {
this.setState({
freeDiscover: true.showResult: false.countdown: 60
}, () = > {
clearInterval(this.interval);
});
}
Copy the code
Frosted glass effect
The Loading page, the results page and the back to the Past button all use the frosted glass effect style 💧. Through the following lines of style code, you can achieve amazing frosted glass.
background rgba(0.67.170.5)
backdrop-filter blur(10px)
filter drop-shadow(0px 1px 1px rgba(0.0.0.25))
Copy the code
conclusion
The new knowledge points involved in this paper mainly include:
Three.js
Shadow type- Creating a particle system
cannon.js
Basic usage- use
cannon.js
Height fieldHeightfield
Create the terrain - Control the model animation by moving the wheel
To learn more about scene initialization, lighting, shadows, base geometry, meshes, materials, and more about three.js, read my previous articles. Please indicate the original address and author. If you think the article is helpful to you, don’t forget a key three link oh 👍.
The appendix
- [1].three.js flame effect to achieve the dynamic logo of Elden Ring
- [2].three.js to achieve magical 3D text suspension effect
- [3].three.js implementation makes 2d images have 3D effect
- [4].three.js to achieve the 2022 Winter Olympics theme 3D interesting page, Bing Dwen Dwen 🐼
- [5].three.js to create an exclusive 3D medal
- [6].three.js to achieve the Year of the Tiger Spring Festival 3D creative page
- [7].three.js to implement the 3D dynamic Logo of facebook metasomes
- [8].three.js to implement 3D panoramic detective game
- [9].three.js to achieve cool acid style 3D pages
The resources
- [1] threejs.org
- [2] cannonjs.org
- [3] heightmap-generator
- [4] Three cannon. Js Physics engine Heightfield
- [5] Joggin’ version 0.1