three.js添加人物实现第一人称视角控制,类似于cf或绝地求生游戏控制人物

本文通过three.js的PointerLockControls介绍如何创建类似CF或绝地求生的游戏人物控制。首先参照官方例子,然后加载模型并同步更新位置和观察方向。最终展示实现的第一人称视角效果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

three.js实现第一人称视角控制

思路

查看官方例子,使用PointerLockControls可以实现该功能,官方参考地址
然后可以将模型加载进来同步修改位置和观察方向就可以了

具体代码如下

<!DOCTYPE html>
<html lang="en">
	<head>
		<title>three.js - pointerlock controls</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<link type="text/css" rel="stylesheet" href="main.css">
		<style>
			#blocker {
				position: absolute;
				width: 100%;
				height: 100%;
				background-color: rgba(0,0,0,0.5);
			}

			#instructions {
				width: 100%;
				height: 100%;

				display: flex;
				flex-direction: column;
				justify-content: center;
				align-items: center;

				text-align: center;
				font-size: 14px;
				cursor: pointer;
			}
		</style>
	</head>
	<body>
		<div id="blocker">
			<div id="instructions">
				<p style="font-size:36px">
					Click to play
				</p>
				<p>
					Move: WASD<br/>
					Jump: SPACE<br/>
					Look: MOUSE
				</p>
			</div>
		</div>

		<script type="module">

			import * as THREE from '../build/three.module.js';

			import { PointerLockControls } from './jsm/controls/PointerLockControls.js';
			import { GLTFLoader } from './jsm/loaders/GLTFLoader.js';

			let camera, scene, renderer, controls;

			const objects = [];

			let raycaster;

			let moveForward = false;
			let moveBackward = false;
			let moveLeft = false;
			let moveRight = false;
			let canJump = false;
			//根据navigation属性,开始计算毫秒数,通过两次时间相减可以计算某个操作的间隔时间
			let prevTime = performance.now();
			const velocity = new THREE.Vector3();
			const direction = new THREE.Vector3();
			const vertex = new THREE.Vector3();
			const color = new THREE.Color();
			let person=null,mixer,stateList={},currentAction,previousAction,lastkey
			let clock = new THREE.Clock();
			init();
			animate();

			function init() {

				camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 1, 1000 );
				camera.position.set(1,200,1);

				scene = new THREE.Scene();
				scene.background = new THREE.Color( 0xffffff );
				scene.fog = new THREE.Fog( 0xffffff, 0, 750 );

				const light = new THREE.HemisphereLight( 0xeeeeff, 0x777788, 0.75 );
				light.position.set( 0.5, 1, 0.75 );
				scene.add( light );

				controls = new PointerLockControls( camera, document.body );

				const blocker = document.getElementById( 'blocker' );
				const instructions = document.getElementById( 'instructions' );

				instructions.addEventListener( 'click', function () {
					camera.position.set(1,10,1);
					camera.lookAt(new THREE.Vector3(1,1,1));
					controls.lock();

				} );

				controls.addEventListener( 'lock', function () {
					//camera.position.y = 1;
					instructions.style.display = 'none';
					blocker.style.display = 'none';

				} );

				controls.addEventListener( 'unlock', function () {

					blocker.style.display = 'block';
					instructions.style.display = '';

				} );
				controls.addEventListener( 'change', function (evt) {

					//console.log('change',evt)
					let dir1=new THREE.Vector3()
					controls.getDirection(dir1)
					console.log('change getDirection',dir1)
					console.log('change position',controls.getObject().position)

				} );
				scene.add( controls.getObject() );

				const onKeyDown = function ( event ) {
					if (lastkey !== event.code) {
						lastkey = event.code;
						fadeToAction('Walking', 0.2);
					}
					switch ( event.code ) {

						case 'ArrowUp':
						case 'KeyW':
							moveForward = true;
							break;

						case 'ArrowLeft':
						case 'KeyA':
							moveLeft = true;
							break;

						case 'ArrowDown':
						case 'KeyS':
							moveBackward = true;
							break;

						case 'ArrowRight':
						case 'KeyD':
							moveRight = true;
							break;

						case 'Space':
							if ( canJump === true ) velocity.y += 350;
							canJump = false;
							break;

					}

				};

				const onKeyUp = function ( event ) {
					lastkey = null;
					fadeToAction('Standing', 0.2);

					switch ( event.code ) {

						case 'ArrowUp':
						case 'KeyW':
							moveForward = false;
							break;

						case 'ArrowLeft':
						case 'KeyA':
							moveLeft = false;
							break;

						case 'ArrowDown':
						case 'KeyS':
							moveBackward = false;
							break;

						case 'ArrowRight':
						case 'KeyD':
							moveRight = false;
							break;

					}

				};

				document.addEventListener( 'keydown', onKeyDown );
				document.addEventListener( 'keyup', onKeyUp );

				raycaster = new THREE.Raycaster( new THREE.Vector3(), new THREE.Vector3( 0, - 1, 0 ), 0, 10 );

				// floor

				let floorGeometry = new THREE.PlaneGeometry( 2000, 2000, 100, 100 );
				floorGeometry.rotateX( - Math.PI / 2 );

				// vertex displacement

				let position = floorGeometry.attributes.position;

				for ( let i = 0, l = position.count; i < l; i ++ ) {

					vertex.fromBufferAttribute( position, i );

					vertex.x += Math.random() * 20 - 10;
					vertex.y += Math.random() * 2;
					vertex.z += Math.random() * 20 - 10;

					position.setXYZ( i, vertex.x, vertex.y, vertex.z );

				}

				floorGeometry = floorGeometry.toNonIndexed(); // ensure each face has unique vertices

				position = floorGeometry.attributes.position;
				const colorsFloor = [];
				for ( let i = 0, l = position.count; i < l; i ++ ) {

					color.setHSL( Math.random() * 0.3 + 0.5, 0.75, Math.random() * 0.25 + 0.75 );
					colorsFloor.push( color.r, color.g, color.b );

				}

				floorGeometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colorsFloor, 3 ) );

				const floorMaterial = new THREE.MeshBasicMaterial( { vertexColors: true } );

				const floor = new THREE.Mesh( floorGeometry, floorMaterial );
				scene.add( floor );

				// objects

				const boxGeometry = new THREE.BoxGeometry( 20, 20, 20 ).toNonIndexed();

				position = boxGeometry.attributes.position;
				const colorsBox = [];

				for ( let i = 0, l = position.count; i < l; i ++ ) {

					color.setHSL( Math.random() * 0.3 + 0.5, 0.75, Math.random() * 0.25 + 0.75 );
					colorsBox.push( color.r, color.g, color.b );

				}

				boxGeometry.setAttribute( 'color', new THREE.Float32BufferAttribute( colorsBox, 3 ) );

				for ( let i = 0; i < 500; i ++ ) {

					const boxMaterial = new THREE.MeshPhongMaterial( { specular: 0xffffff, flatShading: true, vertexColors: true } );
					boxMaterial.color.setHSL( Math.random() * 0.2 + 0.5, 0.75, Math.random() * 0.25 + 0.75 );

					const box = new THREE.Mesh( boxGeometry, boxMaterial );
					box.position.x = Math.floor( Math.random() * 20 - 10 ) * 20;
					box.position.y = Math.floor( Math.random() * 20 ) * 20 + 10;
					box.position.z = Math.floor( Math.random() * 20 - 10 ) * 20;

					scene.add( box );
					objects.push( box );

				}
				// 添加辅助
				var axesHelper = new THREE.AxesHelper(30);
				scene.add(axesHelper);


				// 添加人物
				const gltfloader=new GLTFLoader()
				let modelConfig={
					url:'models/gltf/RobotExpressive/RobotExpressive.glb',
					scale:1,
					position:[0,0,0],
					openAnimate:true,
				}
				gltfloader.load( modelConfig.url,  ( gltf )=> {
					person=gltf.scene
					if(modelConfig.position){
						gltf.scene.position.set(modelConfig.position[0], modelConfig.position[1], modelConfig.position[2])
					}
					if(modelConfig.scale){
						gltf.scene.scale.set(modelConfig.scale, modelConfig.scale, modelConfig.scale)
					}
					if(modelConfig.rotateX){
						gltf.scene.rotateX(modelConfig.rotateX)
					}
					if(modelConfig.rotateY){
						gltf.scene.rotateX(modelConfig.rotateY)
					}
					if(modelConfig.rotateZ){
						gltf.scene.rotateX(modelConfig.rotateZ)
					}
					mixer = new THREE.AnimationMixer(person);
					stateList.Walking = mixer.clipAction(gltf.animations[10]);
					stateList.Standing = mixer.clipAction(gltf.animations[7]);
					// 设置下面两项主要是站着的时候,别抖了
					stateList.Standing.clampWhenFinished = true;
					stateList.Standing.loop = THREE.LoopOnce;

					currentAction = stateList.Standing;
					currentAction.play();
					scene.add( gltf.scene );
					//person=gltf.scene
				},null,(error)=>{
					console.error(error)
				} );

				renderer = new THREE.WebGLRenderer( { antialias: true } );
				renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				document.body.appendChild( renderer.domElement );

				//

				window.addEventListener( 'resize', onWindowResize );

			}

			function onWindowResize() {

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				renderer.setSize( window.innerWidth, window.innerHeight );

			}

			function animate() {

				requestAnimationFrame( animate );

				const time = performance.now();

				if ( controls.isLocked === true ) {

					raycaster.ray.origin.copy( controls.getObject().position );
					raycaster.ray.origin.y -= 10;

					const intersections = raycaster.intersectObjects( objects, false );

					const onObject = intersections.length > 0;

					const delta = ( time - prevTime ) / 1000;

					velocity.x -= velocity.x * 10.0 * delta;
					velocity.z -= velocity.z * 10.0 * delta;

					velocity.y -= 9.8 * 100.0 * delta; // 100.0 = mass

					direction.z = Number( moveForward ) - Number( moveBackward );
					direction.x = Number( moveRight ) - Number( moveLeft );
					direction.normalize(); // this ensures consistent movements in all directions

					if ( moveForward || moveBackward ) velocity.z -= direction.z * 400.0 * delta;
					if ( moveLeft || moveRight ) velocity.x -= direction.x * 400.0 * delta;

					if ( onObject === true ) {

						velocity.y = Math.max( 0, velocity.y );
						canJump = true;

					}

					controls.moveRight( - velocity.x * delta );
					controls.moveForward( - velocity.z * delta );

					controls.getObject().position.y += ( velocity.y * delta ); // new behavior

					if ( controls.getObject().position.y < 10 ) {

						velocity.y = 0;
						controls.getObject().position.y = 10;

						canJump = true;

					}
					if(person){

						// 同步移动人物位置
						let personPosition =camera.position
						person.position.set(personPosition.x,personPosition.y-8,personPosition.z-2)

						// 同步修改人物方向
						let dir1=new THREE.Vector3()
						// todo 注意此处获得的是方向向量,需要添加该处的position才能获得目标位置
						controls.getDirection(dir1)
						let targetPt=person.position.clone()
						// 获取目标位置
						targetPt.add(dir1)
						person.lookAt(targetPt);
					}

				}

				prevTime = time;
				roleAction()
				renderer.render( scene, camera );

			}
			function roleAction() {
				let dt = clock.getDelta();
				if (mixer) mixer.update(dt);
				//handleRoleAction();
			}
			function fadeToAction(name, duration) {
				previousAction = currentAction;
				currentAction = stateList[name];
				if (previousAction !== currentAction) {
					previousAction.fadeOut(duration);
				}
				if (currentAction) {
					currentAction
							.reset()
							.setEffectiveTimeScale(1)
							.setEffectiveWeight(1)
							.fadeIn(duration)
							.play();
				}
			}

		</script>
	</body>
</html>

最终效果

效果

如有不足之处欢迎来指正,交流QQ 1826356125

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值