运用 Three.js 开发 3D 版 2048 小游戏

2,253 阅读4分钟

前言

最近在捣鼓 Three.js 相关知识,同时觉得微信小游戏是个不错的试错场呢。来个 2048 小游戏踩踩坑。

先来一张 场景图

乍一看,感觉就是 2D 的么,与 Three.js 没半毛线关系。请仔细看,有阴影,有透明度,有立体感,一切都在细节中,3D 的没错。

抢先体验请扫码

微信二维码

这里吐槽一哈,微信部分版本,进游戏部分机型可能会闪退,具体原因不详。

Three 开发前置条件

  1. 这里运行环境实在微信小游戏。如果浏览器环境,用谷歌准没错, 具体兼容性,请查看 https://caniuse.com,
  2. 假定你一定程度上掌握了 JavaScript,最好有一定三维图形学知识。

基本概念

  • 场景:提供一个带三维坐标轴的空间
  • 相机: 相当于人的眼睛,就是视角。相机分两种,正投影相机 OrthographicCamera 和透视投影相机 PerspectiveCamera。 针对不同应用的三维场景需要使用不同的投影方式,比如平面领域常常采用正投影(平行投影), 游戏场景往往采用透视投影(中心投影)
  • 光源 Threejs 虚拟光源是对自然界光照的模拟。光源分环境光(AmbientLight), 平行光(DirectionalLight), 半球光(HemisphereLight),点光源(PointLight),平面光光源(RectAreaLight),聚光灯(SpotLight)等等
  • 几何体:模型的几何形状,如:立方体,圆柱体,或是 自定义几何体之类。three.js 封装了一些几何模型 立方几何体(BoxGeometry),圆柱几何体(CylinderGeometry)等
  • 材质:几何体的外观。three.js 封装了一些材质,如具有镜面高光的光泽表面的材质(MeshPhongMaterial),没有镜面高光的网格材质(MeshLambertMaterial)等等
  • 纹理:材质表面的贴图, 可以是图片,也可以是canvas绘制出来的图形
  • 渲染器 就是将场景相机光照以及模型以某方式结合并呈现出来

相信上面的概念,足够用了。

场景实例

  • new 场景,我这里是 new 了两个场景,游戏界面需要立体感,采用中心投影,其余分数等对象,需要平铺在界面上,采用平行投影。中心投影场景中添加辅助线,灯光和背景色。

createScene() {
    this.scene = new THREE.Scene();
    this.hudScene = new THREE.Scene();
    const axesHelper = new THREE.AxesHelper(1000);
    const lights = this.createLight();
    Object.keys(lights).forEach((value) => {
      this.scene.add(lights[value]);
    });
    this.scene.add(axesHelper);
    this.scene.background = new THREE.Color(0x3498db);
  }
  • new 相机,也是两个,一个正投影相机, 一个透视相机。设置朝向,相机位置,以及聚焦位置。
createCamera() {
    this.camera = new THREE.PerspectiveCamera(
      45,
      screenWidth / screenHeight,
      0.1,
      5000
    );
    this.camera.up.set(0, 1, 0);
    this.camera.position.set(0,0,2000);
    this.camera.lookAt(this.scene.position);
  }
  createHudCamera() {
    this.hudCamera = new THREE.OrthographicCamera(
      -screenWidth / 2,
      screenWidth / 2,
      screenHeight / 2,
      -screenHeight / 2,
      -1000,
      1000
    );
    this.hudCamera.up.set(0, 1, 0);
    this.hudCamera.position.set(0, 0, 1000);
    this.hudCamera.lookAt(new THREE.Vector3(0, 0, 0));
  }
  • new光源,这里设置了环境光和平行光,其中平行光可以设置阴影。
 createLight() {
    const directionalLight = new THREE.DirectionalLight(0xffffff, 0.3);
    directionalLight.position.set(400, 100, 500);
    directionalLight.castShadow = true;
    directionalLight.shadow.camera.near = 0.5;
    directionalLight.shadow.camera.far = 1000;
    directionalLight.shadow.camera.left = -500;
    directionalLight.shadow.camera.right = 500;
    directionalLight.shadow.camera.top = 500;
    directionalLight.shadow.camera.bottom = -500;
    directionalLight.shadow.mapSize.width = 3024;
    directionalLight.shadow.mapSize.height = 3024;

    return {
      ambientLight: new THREE.AmbientLight(0xffffff, 0.7),
      directionalLight,
    };
  }
  • new 渲染器
 createRenderer() {
    let { devicePixelRatio, innerWidth, innerHeight } = window;
    this.renderer = new THREE.WebGLRenderer({
      canvas,
      antialias: true,
      precision: "highp",
    });

    this.renderer.setClearColor(0xffffff, 1);
    this.renderer.setPixelRatio(devicePixelRatio);
    this.renderer.setSize(innerWidth, innerHeight);
    this.renderer.shadowMap.enabled = true;
    this.renderer.autoClear = false;
  }
  • 渲染
animate() {
    this.renderer.render(this.scene, this.camera);
    this.renderer.render(this.hudScene, this.hudCamera);
    requestAnimationFrame(() => {
      this.animate();
    });
  }

创建模型,这里纹理运用 canvas 绘制,

// 这里创建平面模型
createPlane() {
  const geometry = new THREE.PlaneGeometry(width, length);
  const material = new THREE.MeshPhongMaterial({
    color: 0xdddddd,
    specular: 0xdddddd,
    map: this.texture(),
  });

  this.instance = new THREE.Mesh(geometry, material);
  this.instance.name = "plane";
  // 设置阴影
  this.instance.castShadow = true;
  this.instance.receiveShadow = true;
}
// 创建纹理
texture() {
    let w = width;
    const canvas = wx.createCanvas();
    canvas.width = canvas.height = w;
    const context = canvas.getContext("2d");

    ...

    let map = new THREE.CanvasTexture(canvas);
    map.minFilter = THREE.LinearFilter;
    return map;
  }

添加事件

  • 点击事件,将需要添加点击事件的对象放在一个group里,用户点击屏幕的时候,threejs提供Raycaster实例,会从触碰点会发射一条“激光”,激光扫到的所有对象记录在返回的数组里。
getEventObject(event, camera = null, group = []) {
  var raycaster = new THREE.Raycaster();
  var mouse = new THREE.Vector2();
  mouse.x = (event.clientX / screenWidth) * 2 - 1;
  mouse.y = -(event.clientY / screenHeight) * 2 + 1;
  raycaster.setFromCamera(mouse, camera);
  var intersects = raycaster.intersectObjects(group, true);
  return intersects;
}
  • 滑动事件,通过微信提供的 onTouchStartonTouchEnd, 获取起始坐标点和结束坐标点,然后通过滑动距离和角度来判断滑动方向。

fn(callback) {
  if (this.endId === this.startId) {
    let w = this.xt - this.x0;
    let h = this.yt - this.y0;
    let k = h / w;
    if (k > 2 || k < -2) {
      if (h < -20) callback(0); /*向上*/
      if (h > 20) callback(1); /*向下*/
    }
    if (k < 0.5 && k > -0.5) {
      if (w < -20) callback(2); /*向左*/
      if (w > 20) callback(3); /*向右*/
    }
  }
}

优化性能

  • 几何体,材质,纹理等尽量共用,考虑使用记忆函数
  • 移除模型时,及时调用 dispose(), 包括几何体,材质及纹理等

总结

通过 Three.js 来构建一个小游戏场景, 过程比较简单的,大概四步走。

  1. 新建场景,打好灯光,设置摄像机位置
  2. 将几何图形和材质的组合,构建出不同的模型
  3. 在场景中将模型渲染出来。一个 3D 界面就出来了
  4. 根据业务需求添加事件,调整场景

体验扫码

微信二维码