当前位置: 首页 > article >正文

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

vue使用three.js实现仿黑神话悟空跑动-脚下波纹特效

玩家角色的正面始终朝向鼠标方向,且在按下 W 键时,玩家角色会朝着鼠标方向前进

空格建跳跃

<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>

http://www.kler.cn/news/317950.html

相关文章:

  • 【云原生安全篇】一文掌握Harbor集成Trivy应用实践
  • Eclipse如何调整编辑器中的字体大小?
  • 科研绘图系列:R语言误差连线图(errobar linechart)
  • dockerfile 添加arthas 监控插件。容器添加arthas监控
  • 哪款品牌充电宝性价比比较高?五款性价比绝佳充电宝推荐
  • java反射的应用场景与限制
  • 记软件开发者画图(UML),使用WPS应用制图
  • 如何使用ssm实现基于ssm框架的车辆出租管理系统+vue
  • 前端——JavaScript综合练习 下拉框样式实现(2)
  • 110Redis 简明教程--Redis 数据类型
  • 手写Spring第三篇,原来Spring容器是使用反射来初始化对象的
  • 考前须知:Oracle OCP考试流程和准备
  • 从零开始,Docker进阶之路(三):Docker镜像与命令
  • cmaklist流程控制——调试及发布
  • 深度学习(5):torch.nn.Module
  • 实战OpenCV之几何变换
  • 【学习笔记】exkmp(Z函数)
  • 关于C++的备忘录
  • Qt-QComboBox输入类控件(31)
  • 说一说Zookeeper的应用场景及其原理
  • pandas中数据的合并
  • 200Kg大载重多旋翼无人机应用前景详解
  • Solidity——抽象合约和接口详解
  • 【路径规划】 通过使用前向动态规划算法在地形上找到最优路径
  • 运维工程师面试整理-沟通能力
  • Spring Security 详解:保护Java应用的强大盾牌
  • linux下不同库出现符号冲突的解决方式
  • LLM - 理解 多模态大语言模型(MLLM) 的 幻觉(Hallucination) 与相关技术 (七)
  • Jenkins基于tag的构建
  • Redis: 特色,业务场景举例,底层原理,持续进阶等问题梳理