学习使用oasis开发一个跳一跳

172 阅读5分钟

参考

选题来自于zhuanlan.zhihu.com/p/533266152,因为觉得很有趣所以一步一步研究了

oasis的适配平台

标题基础建模物理引擎
原生h5支持支持
支付宝小程序支持不支持
微信小程序官方不支持 (有社区版)官方不支持

oasis的基础功能可以参考官方文档,基本与three.js类似,主要功能是在空间坐标中设置光源,相机,对有贴图的网格模型进行渲染,官方提供七种基础的网格模型

oasis还具备的一个特点是物理引擎,给网格模型套上物理,物体会具备重力,刚性碰撞和弹力,可以模拟现实中的物体碰撞场景,官方提供的物理引擎有两种:

  1. physics-lite
  2. physics-physx

官方文档说明:

  1. 功能:追求完整物理引擎功能以及高性能的物理模拟,推荐选择 PhysX 后端,Lite 后端只支持碰撞检测。
  2. 性能:PhysX 会在不支持 WebAssembly 的平台自动降级为纯 JavaScript 的代码,因此性能也会随之降低。但由于内置了用于场景搜索的数据结构,性能比 Lite 后端还是要更加好。
  3. 包尺寸:选择 PhysX 后端会额外引入接近 2.5mb 的 wasm 文件(纯 JavaScript 版的大小接近),增加包的大小的同时降低应用初始化的速度。

但是只有原生h5端才能使用physx,小程序端经检测只支持lite,即只可以检测物体的碰撞,不具备重力,弹力等物理元素,对基础的网格来说,碰撞其实只需要对两者的空间坐标和大小进行对比就可以了,可能对某些复杂的自定义网格来说,碰撞检测会起到作用。

开始

image.png

一个简易版跳一跳由这些基本因素组成:

1、基本的灰色背景,一个俯视角的相机

2、一开始设置两个底座,在小人开始跳动的时候,随机在x轴方向或y轴的负方向生成一个底座,需要保证底座距离当前底座在一屏内

3、屏幕上覆盖一个透明遮罩,用来监听touchstart和touchend事件,根据两者的时间差确定小人的移动距离,在touchend后移动小人

4、相机会在移动完成后移动到当前底座上方

5、每次跳动后需要判断小人与底座的坐标是否有接触,如果没有接触就弹窗提示,并重置整个场景。

    registerCanvas(canvas);
    const engine = new WebGLEngine('canvas');
    this.engine = engine;
    // 适应屏幕
    engine.canvas.resizeByClientSize();
    // 设置原点
    const zeroVector = new Vector3(0, 0, 0);
    // 初始化场景
    const scene = engine.sceneManager.activeScene;
    const rootEntity = scene.createRootEntity();
    this.rootEntity = rootEntity; // 根结点
    scene.background.solidColor.setValue(208 / 255, 210 / 255, 211 / 255, 1);
    scene.ambientLight.diffuseSolidColor.setValue(0.5, 0.5, 0.5, 1);
    // 相机
    this.cameraEntity = rootEntity.createChild('camera');
    const camera = this.cameraEntity.addComponent(Camera);
    // 正交相机,让场景内物体大小相同
    camera.isOrthographic = true;
    camera.nearClipPlane = 0.1;
    camera.farClipPlane = 1000;
    // 设置俯视角,观察原点
    this.cameraEntity.transform.setPosition(-100, 100, 100);
    this.cameraEntity.transform.lookAt(zeroVector);
    // 设置相机移动脚本
    this.cameraScript = this.cameraEntity.addComponent(CameraScript);

    // 场景光源
    const directLightEntity = rootEntity.createChild('directLight');
    const directLight = directLightEntity.addComponent(DirectLight);
    directLight.intensity = 1.0;
    directLightEntity.transform.setPosition(10, 30, 20);
    directLightEntity.transform.lookAt(zeroVector);

    // 添加场景脚本
    this.sceneScript = rootEntity.addComponent(SceneScript);
    this.moveScript = rootEntity.addComponent(MoveScript);
    // 创建小人(这里替换为方块)
    const renderer = rootEntity.addComponent(MeshRenderer);
    renderer.mesh = PrimitiveMesh.createCuboid(engine);
    // 重置脚本场景
    this.sceneScript.reset();
    this.moveScript.reset();
    // 引擎启动
    engine.run();

先进行一个场景的搭建,设置好各项数值,场景就已经被布置好了

在代码中还包含了脚本这一个名词,下面我们来重点讲一下脚本

脚本

根据文档描述,脚本系统是衔接引擎能力和游戏逻辑的纽带,脚本扩展自 Script 基类,用户可以通过它来扩展引擎的功能,也可以脚本组件提供的生命周期钩子函数中编写自己的游戏逻辑代码。

创建脚本可以继承提供的Script基类,并将该脚本调加到实体(Entity)上,实体在某些情况下就会触发脚本的生命周期函数

其中我们重点关注onAwake,onUpdate这两个生命周期。

onAwake

onAwake 只会被调用一次,并且在所有生命周期的最前面,通常我们会在 onAwake 中做一些初始化相关的操作。

onUpdate

游戏/动画开发的一个关键点是在每一帧渲染前更新物体的行为,状态和方位。这些更新操作通常都放在 onUpdate 回调中。简单来说,添加了脚本的实体,每一帧都会触发该事件,如果不设置渲染帧数,那么一秒钟会触发60次onUpdate

要实现对相机的驱动,新底座的生成,小人和底座是否接触,我们需要把方法全部放在onTouchEnd回调中

    // 计算时间差和跳跃距离
    const touchTime = new Date().getTime() - this.time;
    this.moveScript.moveStep = touchTime * this.step;
    this.moveScript.totalMoveStep = this.moveScript.moveStep;
    this.moveScript.moveTarget = this.moveTarget;

    // 将距离和方向传入脚本,脚本控制实体移动,同时消耗距离直到0
    this.cameraScript.step = touchTime * this.step;
    this.cameraScript.target = this.moveTarget;

    // 随机下一个底座的方向和距离
    this.moveTarget = Math.round(Math.random()) === 0 ? 'x' : 'y';
    const max = 7;
    const min = 3;
    const randomStep = Math.random() * (max - min + 1) + min;
    // 检测小人和新底座是否接触,如果未接触直接结束游戏
    this.checkRes();
    // 创建新底座
    this.sceneScript.createNew(randomStep, this.moveTarget);
  }

检测是否接触,未接触直接游戏结束

    // 获取当前底座和当前小人的坐标
    const targetStep = this.moveScript.getStep();
    const cuboidPos = this.sceneScript.cuboidPos;
    // 底座边长4/3,小人边长0.5,两者任意轴接触距离小于0.3表示没有站稳,游戏失败
    const crossX = Math.abs(targetStep.x - cuboidPos.x) < 4 / 3 + 0.2;
    const crossY = Math.abs(targetStep.z - cuboidPos.z) < 4 / 3 + 0.2;

    if (!crossX || !crossY) {
      const that = this;
      uni.showModal({
        title: '你输了',
        // showCancel: false,
        success: function (res) {
          if (res.confirm) {
            // 重新开始。重置场景
            that.sceneScript.reset();
            that.cameraEntity.transform.setPosition(-100, 100, 100);
            that.cameraScript.pos = {
              x: -100,
              y: 100,
              z: 100,
            };
            that.moveScript.reset();
            that.moveTarget = 'x';
          }
        },
      });
    }
  }

小人的脚本中每一帧都更新小人的位置,同时路程未过半小人上升,过半同步下降

这里写了比较多的判断。。主要是为了保证落下的位置高度相同。。后续会简化判断,如果能上物理引擎通过重力下落会更好

    if (this.moveStep > 0) {
      // 需要移动
      if (this.moveTarget === 'x') {
        this.pos.x = this.pos.x + 0.5;
        if (this.moveStep > this.totalMoveStep / 2) {
          // 路程过半,开始下落
          if (Math.abs(this.moveStep - this.totalMoveStep / 2) < 0.5) {
            this.pos.y += Math.abs(this.moveStep - this.totalMoveStep / 2);
          } else {
            // 开始上升
            this.pos.y += 0.5;
          }
        } else if (this.moveStep >= 0.5) {
          this.pos.y -= 0.5;
        } else {
          this.pos.y = 5 / 3;
        }
        this.moveStep -= 0.5;
      } else {
        this.pos.z = this.pos.z - 0.5;
        // 路程过半,开始下落
        if (this.moveStep > this.totalMoveStep / 2) {
          if (Math.abs(this.moveStep - this.totalMoveStep / 2) < 0.5) {
            this.pos.y += Math.abs(this.moveStep - this.totalMoveStep / 2);
          } else {
            // 开始上升
            this.pos.y += 0.5;
          }
        } else if (this.moveStep >= 0.5) {
          this.pos.y -= 0.5;
        } else {
          this.pos.y = 5 / 3;
        }
        // 消耗传入距离
        this.moveStep -= 0.5;
      }
      // 更新实体位置
      this.moveEntity.entity.transform.worldPosition.setValue(
        this.pos.x,
        this.pos.y,
        this.pos.z
      );
    }
  }