vue使用three.js实现仿黑神话悟空跑动-脚下波纹特效
玩家角色的正面始终朝向鼠标方向,且在按下
W
键时,玩家角色会朝着鼠标方向前进空格建跳跃
javascript
<template>
<div ref="container" class="container" @click="onClick" @mousedown="onMouseDown"></div>
</template>
<script>
import * as THREE from 'three';
import { PointerLockControls } from 'three/examples/jsm/controls/PointerLockControls';
import TWEEN from '@tweenjs/tween.js';
export default {
name: 'WaterRipple',
data() {
return {
scene: null,
camera: null,
renderer: null,
player: null,
clock: new THREE.Clock(),
rippleMaterial: null,
ripples: [],
controls: null,
moveForward: false,
moveBackward: false,
moveLeft: false,
moveRight: false,
velocity: new THREE.Vector3(),
canJump: true,
attackReady: true,
playerHealth: 100,
npcHealth: 100,
npcList: [],
npcMoveDirection: new THREE.Vector3(),
direction: new THREE.Vector3(),
};
},
mounted() {
this.init();
this.animate();
document.addEventListener('keydown', this.onDocumentKeyDown, false);
document.addEventListener('keyup', this.onDocumentKeyUp, false);
},
beforeDestroy() {
document.removeEventListener('keydown', this.onDocumentKeyDown, false);
document.removeEventListener('keyup', this.onDocumentKeyUp, false);
window.removeEventListener('resize', this.onWindowResize, false);
if (this.controls) {
this.controls.dispose();
}
},
methods: {
init() {
// 创建场景
this.scene = new THREE.Scene();
// 创建相机
this.camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
this.camera.position.set(0, 15, -20);
this.camera.lookAt(0, 0, 0);
// 创建渲染器
this.renderer = new THREE.WebGLRenderer();
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.$refs.container.appendChild(this.renderer.domElement);
// 创建PointerLockControls
this.controls = new PointerLockControls(this.camera, this.renderer.domElement);
this.scene.add(this.controls.getObject());
// 创建平面
const geometry = new THREE.PlaneGeometry(200, 200, 32, 32);
const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, wireframe: true });
const plane = new THREE.Mesh(geometry, material);
plane.rotation.x = -Math.PI / 2;
this.scene.add(plane);
// 创建玩家
this.createPlayer();
// 创建NPC
this.createNPCs();
// 窗口调整
window.addEventListener('resize', this.onWindowResize, false);
},
createPlayer() {
// 创建头部
const headGeometry = new THREE.SphereGeometry(1, 32, 32);
const headMaterial = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const head = new THREE.Mesh(headGeometry, headMaterial);
head.position.set(0, 2.5, 0);
// 创建眼睛
const eyeGeometry = new THREE.EllipseCurve(0, 0, 0.2, 0.4, 0, 2 * Math.PI, false, 0);
const eyeShape = new THREE.Shape(eyeGeometry.getPoints(50));
const eyeExtrudeSettings = { depth: 0.05, bevelEnabled: false };
const eyeGeometry3D = new THREE.ExtrudeGeometry(eyeShape, eyeExtrudeSettings);
const eyeMaterial = new THREE.MeshBasicMaterial({ color: 0x0000ff });
const leftEye = new THREE.Mesh(eyeGeometry3D, eyeMaterial);
const rightEye = new THREE.Mesh(eyeGeometry3D, eyeMaterial);
leftEye.position.set(-0.4, 2.9, 0.9);
leftEye.rotation.set(Math.PI / 2, 0, 0);
rightEye.position.set(0.4, 2.9, 0.9);
rightEye.rotation.set(Math.PI / 2, 0, 0);
// 创建鼻子
const noseGeometry = new THREE.CircleGeometry(0.2, 32);
const noseMaterial = new THREE.MeshBasicMaterial({ color: 0xffff00 });
const nose = new THREE.Mesh(noseGeometry, noseMaterial);
nose.position.set(0, 2.6, 0.95);
nose.rotation.set(Math.PI / 2, 0, 0);
// 创建嘴巴
const mouthShape = new THREE.Shape();
mouthShape.moveTo(-0.5, 0);
mouthShape.quadraticCurveTo(0, -0.3, 0.5, 0);
const mouthExtrudeSettings = { depth: 0.05, bevelEnabled: false };
const mouthGeometry = new THREE.ExtrudeGeometry(mouthShape, mouthExtrudeSettings);
const mouthMaterial = new THREE.MeshBasicMaterial({ color: 0x000000 });
const mouth = new THREE.Mesh(mouthGeometry, mouthMaterial);
mouth.position.set(0, 2.3, 0.95);
mouth.rotation.set(Math.PI / 2, 0, 0);
// 创建上半身
const bodyGeometry = new THREE.BoxGeometry(0.5, 3, 0.5);
const bodyMaterial = new THREE.MeshBasicMaterial({ color: 0x0000ff });
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
body.position.set(0, 1, 0);
// 创建上肢
const armMaterial = new THREE.LineBasicMaterial({ color: 0xffffff });
const armGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-1, 1.5, 0),
new THREE.Vector3(-2, 0.5, 0),
new THREE.Vector3(1, 1.5, 0),
new THREE.Vector3(2, 0.5, 0)
]);
const arms = new THREE.LineSegments(armGeometry, armMaterial);
// 创建下肢
const legGeometry = new THREE.BufferGeometry().setFromPoints([
new THREE.Vector3(-0.25, -0.5, 0),
new THREE.Vector3(-0.25, -2, 0),
new THREE.Vector3(0.25, -0.5, 0),
new THREE.Vector3(0.25, -2, 0)
]);
const legs = new THREE.LineSegments(legGeometry, armMaterial);
// 创建大刀
const swordBladeGeometry = new THREE.BoxGeometry(0.2, 5, 0.05);
const swordBladeMaterial = new THREE.MeshBasicMaterial({ color: 0xc0c0c0, metalness: 0.9, roughness: 0.2 });
const swordBlade = new THREE.Mesh(swordBladeGeometry, swordBladeMaterial);
swordBlade.position.set(1, 1, 0);
const swordHandleGeometry = new THREE.CylinderGeometry(0.1, 0.1, 1, 32);
const swordHandleMaterial = new THREE.MeshBasicMaterial({ color: 0x8b4513 });
const swordHandle = new THREE.Mesh(swordHandleGeometry, swordHandleMaterial);
swordHandle.position.set(1, 3, 0);
const swordGuardGeometry = new THREE.BoxGeometry(0.5, 0.1, 0.1);
const swordGuardMaterial = new THREE.MeshBasicMaterial({ color: 0xd4af37 });
const swordGuard = new THREE.Mesh(swordGuardGeometry, swordGuardMaterial);
swordGuard.position.set(1, 2.5, 0);
const sword = new THREE.Group();
sword.add(swordBlade);
sword.add(swordHandle);
sword.add(swordGuard);
// 创建玩家组
this.player = new THREE.Group();
this.player.add(head);
this.player.add(leftEye);
this.player.add(rightEye);
this.player.add(nose);
this.player.add(mouth);
this.player.add(body);
this.player.add(arms);
this.player.add(legs);
this.player.add(sword);
this.player.position.set(0, 2, 0); // 初始高度
this.scene.add(this.player);
},
createNPCs() {
for (let i = 0; i < 3; i++) {
// 创建与玩家相同的模型
const npc = this.player.clone();
// 放大NPC
npc.scale.set(5, 5, 5);
// 设置颜色为灰色
npc.traverse((child) => {
if (child instanceof THREE.Mesh) {
child.material = child.material.clone();
child.material.color.set(0x888888);
}
});
// 随机位置
const x = Math.random() * 100 - 50;
const z = Math.random() * 100 - 50;
npc.position.set(x, 2, z);
// 为NPC添加名称
npc.name = '张老师';
this.scene.add(npc);
this.npcList.push(npc);
}
},
onWindowResize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
this.renderer.setSize(window.innerWidth, window.innerHeight);
},
createRipple(x, z) {
const rippleGeometry = new THREE.RingGeometry(0.1, 0.5, 32);
const rippleMaterial = new THREE.MeshBasicMaterial({ color: 0x0000ff, transparent: true, opacity: 1, wireframe: true });
const ripple = new THREE.Mesh(rippleGeometry, rippleMaterial);
ripple.position.set(x, 0.1, z);
ripple.rotation.x = -Math.PI / 2;
ripple.scale.set(1, 1, 1);
this.scene.add(ripple);
this.ripples.push({ mesh: ripple, startTime: this.clock.getElapsedTime() });
},
onDocumentKeyDown(event) {
switch (event.code) {
case 'ArrowUp':
case 'KeyW':
this.moveForward = true;
break;
case 'ArrowLeft':
case 'KeyA':
this.moveLeft = true;
break;
case 'ArrowDown':
case 'KeyS':
this.moveBackward = true;
break;
case 'ArrowRight':
case 'KeyD':
this.moveRight = true;
break;
case 'Space':
if (this.canJump) {
this.velocity.y = 10; // 跳跃速度
this.canJump = false;
}
break;
}
},
onDocumentKeyUp(event) {
switch (event.code) {
case 'ArrowUp':
case 'KeyW':
this.moveForward = false;
break;
case 'ArrowLeft':
case 'KeyA':
this.moveLeft = false;
break;
case 'ArrowDown':
case 'KeyS':
this.moveBackward = false;
break;
case 'ArrowRight':
case 'KeyD':
this.moveRight = false;
break;
}
},
onClick() {
this.controls.lock();
},
onMouseDown(event) {
if (event.button === 0) { // 左键点击
this.attack();
}
},
attack() {
if (!this.attackReady) return;
this.attackReady = false;
// 挥刀动作
const sword = this.player.children[8]; // 大刀是玩家的子对象
const initialRotation = sword.rotation.x;
new TWEEN.Tween(sword.rotation)
.to({ x: initialRotation + Math.PI / 2 }, 200)
.onComplete(() => {
// 恢复原始位置
new TWEEN.Tween(sword.rotation)
.to({ x: initialRotation }, 200)
.start();
})
.start();
// 检查 NPC 是否在攻击范围内
this.npcList.forEach(npc => {
const distance = this.player.position.distanceTo(npc.position);
if (distance < 10) { // 攻击范围
this.npcHealth -= 10;
console.log(`${npc.name} Health:`, this.npcHealth);
if (this.npcHealth <= 0) {
this.scene.remove(npc);
}
}
});
// 击退效果
this.velocity.y += 5;
setTimeout(() => {
this.attackReady = true;
}, 500); // 攻击冷却时间
},
npcAttack(player) {
if (this.playerHealth <= 0) return;
const distance = player.position.distanceTo(this.player.position);
if (distance < 10) { // 攻击范围
this.playerHealth -= 1;
console.log('Player Health:', this.playerHealth);
if (this.playerHealth <= 0) {
console.log('Game Over');
}
}
},
animate() {
requestAnimationFrame(this.animate);
const delta = this.clock.getDelta();
const elapsedTime = this.clock.getElapsedTime();
const step = 10 * delta;
// 获取控制器方向
this.controls.getDirection(this.direction);
this.direction.y = 0; // 保持水平朝向
this.direction.normalize();
// 水平方向移动
const moveDirection = new THREE.Vector3();
if (this.moveForward) moveDirection.add(this.direction);
if (this.moveBackward) moveDirection.addScaledVector(this.direction, -1);
if (this.moveLeft) moveDirection.addScaledVector(new THREE.Vector3(-this.direction.z, 0, this.direction.x), 1);
if (this.moveRight) moveDirection.addScaledVector(new THREE.Vector3(this.direction.z, 0, -this.direction.x), 1);
moveDirection.normalize().multiplyScalar(step);
// 更新玩家位置
this.player.position.add(moveDirection);
// 更新玩家朝向
if (this.moveForward || this.moveBackward || this.moveLeft || this.moveRight) {
const targetQuaternion = new THREE.Quaternion().setFromUnitVectors(
new THREE.Vector3(0, 0, -1),
this.direction
);
// 创建一个额外的旋转 180 度的四元数
const extraRotation = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), Math.PI);
// 组合旋转
targetQuaternion.multiply(extraRotation);
this.player.quaternion.slerp(targetQuaternion, 0.1);
}
// 防止穿透地面
if (this.player.position.y < 2) {
this.player.position.y = 2;
this.canJump = true;
this.velocity.y = 0;
} else {
this.velocity.y -= 30 * delta; // 模拟重力
}
// 更新垂直位置
this.player.position.y += this.velocity.y * delta;
this.createRipple(this.player.position.x, this.player.position.z);
// 使相机跟随玩家,并设置合适的角度
const cameraOffset = new THREE.Vector3(0, 15, -20);
const cameraLookAt = new THREE.Vector3(0, 0, 30);
// 计算相机位置和角度相对玩家的位置
const playerDirection = new THREE.Vector3();
this.controls.getObject().getWorldDirection(playerDirection);
playerDirection.y = 0; // 保持水平朝向
// 相机位置基于玩家位置和方向偏移
const cameraPosition = playerDirection.clone().multiplyScalar(cameraOffset.z).add(this.player.position);
cameraPosition.y += cameraOffset.y;
// 更新相机位置和朝向
this.camera.position.copy(cameraPosition);
this.camera.lookAt(this.player.position.clone().add(playerDirection.clone().multiplyScalar(cameraLookAt.z)));
// 更新 NPC 行为
this.npcList.forEach(npc => {
// 随机移动
if (Math.random() < 0.01) {
this.npcMoveDirection.set(Math.random() - 0.5, 0, Math.random() - 0.5).normalize();
}
npc.position.addScaledVector(this.npcMoveDirection, step);
// 攻击玩家
this.npcAttack(npc);
});
// 更新水波纹
this.ripples.forEach((ripple) => {
const age = elapsedTime - ripple.startTime;
ripple.mesh.scale.set(1 + age * 10, 1 + age * 10, 1 + age * 10);
ripple.mesh.material.opacity = Math.max(0, 1 - age / 0.5); // 水波纹消散更快
ripple.mesh.material.color.setHSL(0.6, 1, 0.5 * (1 - age / 0.5)); // 颜色随着扩散变淡
});
this.ripples = this.ripples.filter((ripple) => ripple.mesh.material.opacity > 0);
this.renderer.render(this.scene, this.camera);
TWEEN.update(); // 更新 TWEEN 动画
},
},
};
</script>
<style scoped>
.container {
width: 100vw;
height: 100vh;
overflow: hidden;
}
</style>