仿黑神话悟空跑动-脚下波纹特效(键盘wasd控制走动)

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>
相关推荐
w64372864 小时前
关于 Visual Studio Code 如何插入自定义快捷方式
vscode·vue
叫我DPT4 小时前
chapter3-基于jwt的分布式认证流程
python·django·vue
Mr.史10 小时前
Axios 封装网络请求
网络·ajax·vue·axios
寒山李白10 小时前
VuePress搭建文档网站/个人博客(详细配置)主题配置-侧边栏配置
前端·vue.js·vue·博客·vuepress·网站
大龙@、11 小时前
Error: error:0308010C:digital envelope routines::unsupported
vue
QGC二次开发12 小时前
Vue3:快速生成模板代码
前端·javascript·vue.js·前端框架·vue
天涯学馆12 小时前
Svelte Store与Vuex:轻量级状态管理对比
前端·vue·vuex·svelte
喜好儿aigc14 小时前
DrawingSpinUp:单个平面2D角色绘图的3D动画转换
平面·3d·aigc·drawingspinup
jimumeta14 小时前
企业搭建VR虚拟展厅,如何选择搭建平台?
3d·vr·虚拟展厅·虚拟现实·视创云展·云展厅