Three.js做了一个网页版的我的世界

在这里插入图片描述

前言

笔者在前一阵子接触到 Three.js 后, 发现了它能为前端 3D 可视化 / 动画 / 游戏方向带来的无限可能, 正好最近在与朋友重温我的世界, 便有了用 Three.js 来仿制 MineCraft 的想法, 正好也可以通过一个有趣的项目来学习一下前端 3D 领域

介绍

游戏介绍

相信大家对我的世界应该都不太陌生, 他是一款 3D 像素风的生存类游戏, 本项目是模仿我的世界的来进行实现的, 目前大致支持了以下的功能:

  • 方块的放置 / 破坏
  • 选择不同的方块类型
  • 移动和碰撞检测
  • 随机的地形和树木生成
  • 无限的世界
  • 保存 / 读取游戏
  • 音效和背景音乐
  • 可调节的渲染距离和视野范围
  • 基本的 UI

除此之外, 笔者目前也在尝试着为项目添加一些别的特性:

  • 生成水
  • 更多的保存栏位
  • 手机支持

玩法介绍

玩法介绍

玩法介绍也可以在 游戏体验地址 中的操作介绍下找到

技术栈介绍

因为项目的初衷便是探索一下 Three.js, 所以除了 Three.js 之外并没有别的第三方库依赖, 因此最后的打包大小其实是十分轻量级的, gzipped 后仅有 140kb 左右.

在此之上笔者还添加了 TypeScript 来进行类型检测, 并且使用了 Vite 进行的开发

源码介绍

源码结构

上图是整体项目的源码架构, 开发主要基于了 Class 写法, 主要有五大类以及一些子类, 分别为:

  • Core: 包含了 Three.js 中的一些核心内容, 并且进行一些初始化设定

  • Player: 包含了玩家的一些基础属性以及当前模式(行走, 奔跑, 作弊等)

  • Audio: 主要进行声音的导入, 并且暴露了一些用于播放音乐的 api

  • Terrain
    

    : 包含了与地形相关的各种内容:

    • Noise: 通过柏林噪音实现了地形, 树木, 方块类型的随机生成算法

    • GenerateWorker: 通过 web worker 的方式实现了地形动态生成的算法

    • Mesh
      

      : 场景中基础的网格体(方块)

      • Blocks: 自定义方块类
      • Materials: 各种方块的材质加载
    • Highlight: 实现了实时高亮准心位置方块的算法

  • Control
    

    : 包含了各种与操作相关的算法, 比如移动, 镜头转动, 碰撞检测等

    • CollideWorker: 在 web worker 中实现碰撞检测以提高运行效率
  • ui:包含了 ui 界面以及其功能:

    • Bag: 放置不同方块的背包
    • FPS: 实时展示当前 fps

核心技术点

笔者会在这一部分深入的分析一下项目的核心技术点以及一些遇到的难点, 如果大家只是来试玩看看 / 图一乐儿的话, 这一部分就可以跳过啦~ 不过要是有同学对底层的实现或者 Three.js 感兴趣的话, 也可以看看这一部分

对于文中大部分的涉及代码部分, 笔者尽可能的删去了不相关的内容, 让代码更加的易懂, 有兴趣的小伙伴也可以直接去 GitHub 上查看源码

随机的地形生成

地形生成采用了柏林噪音来进行实现的, Three.js 中自带了噪音的底层算法实现, 所以笔者只对其进行了一下简单的封装:

ts复制代码import {
    ImprovedNoise } from 'three/examples/jsm/math/ImprovedNoise'

export default class Noise {
   
  noise = new ImprovedNoise()
  seed = Math.random()
  stoneSeed = this.seed * 0.4
  coalSeed = this.seed * 0.5
  treeSeed = this.seed * 0.7
  leafSeed = this.seed * 0.8
  
  get = (x: number, y: number, z: number) => {
   
    return this.noise.noise(x, y, z)
  }
}

然后在地形生成的模块下, 首先为不同的方块类型创建了对应数量的 InstancedMesh, 然后将其存放到名为 blocks 的数组中:

ts复制代码const blocks: THREE.InstancedMesh[] = []
blocks[i].instanceMatrix = new THREE.InstancedBufferAttribute(
  new Float32Array(maxCount * blocksFactor[i] * 16),
  16
)

然后在具体的循环中首先依据前面的种子来判断地形的高度, 然后依据具体方块类型的种子来判断具体该渲染什么样的方块类型, 最后将每一个方块的位移量写入对应的 InstancedMesh 中:

ts复制代码
  for (
    let x = -chunkSize * distance + chunkSize * chunk.x;
    x < chunkSize * distance + chunkSize + chunkSize * chunk.x;
    x++
  ) {
   
    for (
      let z = -chunkSize * distance + chunkSize * chunk.y;
      z < chunkSize * distance + chunkSize + chunkSize * chunk.y;
      z++
    ) 
      const yOffset = Math.floor(
        noise.get(x / noise.gap, z / noise.gap, noise.seed) * noise.amp
      )
      matrix.setPosition(x, y + yOffset, z)
      // 如果为草方块
      blocks[BlockType.grass].setMatrixAt(
        blocksCount[BlockType.grass]++,
        matrix
      )
      // 如果为其他方块
      ...
    }
  }

除了地形外, 对于树和树叶的生成也是大同小异, 这里就不展开了.

无限动态地形生成

除去最基本的地形外, 笔者还添加了无限动态地形生成的算法从而实现的一个无限大小的世界, 这样就不至于说会走到地形的边界然后掉出世界了 XD

具体的实现的话是通过在 requestAnimationFrame (以下简称 raf) 的回调函数中判断玩家是否移动到了新的区块, 如果区块发生了变化, 则会触发一次渲染:

ts复制代码  update = () => {
   
    this.chunk.set(
      Math.floor(this.camera.position.x / this.chunkSize),
      Math.floor(this.camera.position.z / this.chunkSize)
    )

    // 当进入新的区块时, 触发一次渲染
    if (
      this.chunk.x !== this.previousChunk.x ||
      this.chunk.y !== this.previousChunk.y
    ) {
   
      this.generate()
    }

    this.previousChunk.copy(this.chunk)
  }

对于具体的 generate 部分, 因为对于地形的位置的计算是一个比较耗时的过程, 所以如果直接在主线程中进行运算的话则会带来卡顿的感觉, 所以具体计算的部分则是移动到了 web worker 中去实现的, 然后只在主线程中行进最后的渲染.

笔者也是在这个项目中发现了 web worker 不允许传输函数, 所以像各种 Three.js 中的类都无法直接进行传输, 最后不得不封装了一些自定义的数据结构来进行数据的沟通:

ts复制代码  // 将数据传入 web worker
  generate = () => {
   
    this.blocksCount = new Array(this.blocks.length).fill(0)
    this.generateWorker.postMessage({
   
      distance: this.distance,
      chunk: this.chunk,
      noiseSeed: this.noise.seed,
      treeSeed: this.noise.treeSeed,
      stoneSeed: this.noise.stoneSeed,
      coalSeed: this.noise.coalSeed,
      idMap: new Map<string, number>()
好的,这个问题我可以回答。首先,你需要先创建一个模型,可以使用Three.js中提供的几何体(Geometry)或导入外部模型文件。然后,你可以使用Three.js中的向量(Vector3)来实现平移。 具体步骤如下: 1. 创建一个模型 这里以创建一个盒子(BoxGeometry)为例: ```javascript var geometry = new THREE.BoxGeometry(1, 1, 1); var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); var cube = new THREE.Mesh(geometry, material); scene.add(cube); ``` 2. 平移模型 使用模型的位置(position)属性和向量的add方法实现平移: ```javascript var offset = new THREE.Vector3(1, 0, 0); // 平移向量 cube.position.add(offset); // 平移模型 ``` 这里将模型沿着X轴平移了1个单位长度。 完整代码示例: ```javascript var scene = new THREE.Scene(); var camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000); var renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); document.body.appendChild(renderer.domElement); var geometry = new THREE.BoxGeometry(1, 1, 1); var material = new THREE.MeshBasicMaterial({ color: 0x00ff00 }); var cube = new THREE.Mesh(geometry, material); scene.add(cube); camera.position.z = 5; var animate = function () { requestAnimationFrame(animate); var offset = new THREE.Vector3(0.01, 0, 0); // 平移向量 cube.position.add(offset); // 平移模型 renderer.render(scene, camera); }; animate(); ``` 这里使用了requestAnimationFrame函数来实现动画效果,每帧平移模型一个固定的距离。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Elivis Hu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值