JS 游戏引擎 - 实现单片机逻辑

本文要介绍的内容比较有意思,笔者尝试在团队自研的 JS 游戏引擎里复刻大学时期做过的电子设计大赛中的一道题目:基于 LDC1000 的循迹小车,最终实现的效果如下:

在这里插入图片描述

题目描述

地面上存在着一个由金属围成的轨道圈,有一辆小车,车头有个 LDC1000。LDC1000 就是一种金属传感器,能够检测到靠近的金属,并产生涡电流。金属轨道有一定宽度,LDC1000 是一个小矩形线圈,轨道与线圈重合的面积越大,产生的涡电流也就越大。现在需要让这辆小车能够自动沿着轨道圈跑,当然方式是单片机编程。

算法描述

算法的基本思路是实时读取 LDC1000 的涡电流,通过涡电流的变化,判断小车是否偏离轨道,继而调整小车的运动行为,伪代码如下:

  1. 读取涡电流,若涡电流高于某个阈值,则前进一小段距离,否则进入 2;
  2. 小角度右转弯,读取涡电流,若涡电流比 1 中的大,则进入 1,否则进入 3;
  3. 回正方向,小角度左转弯,读取涡电流,若涡电流比 1 中的大,则进入 1,否则进入 4;
  4. 降低速度,前进一小段距离,进入 1。

可以看出,这是一段循环逻辑。

问题描述

循迹算法并不难,笔者面临的真正问题是:如何使当前的游戏引擎支持实现这种带循环的单片机逻辑?

游戏引擎的基本逻辑是以 requestAnimationFrame 驱动 tick 函数,tick 函数里会去执行修改状态数据的逻辑,然后再将状态数据同步到物理引擎、渲染引擎。在笔者团队自研的游戏引擎里,修改状态数据的逻辑可以通过编写游戏脚本注入。

简单来说,就是将循迹算法封装为一个 javascript 函数,然后交由游戏引擎去执行,这个函数对应的就是单片机的逻辑。

function main({
     scene, car }) {
   
    // 控制小车的逻辑
}

那么现在的问题就是:游戏引擎该如何去运行这个 main 函数呢?

同步实现

游戏引擎的 tick 机制本身就具有循环性,所以比较直接的思路是每一帧运行一次 main 函数,并且 main 是个同步函数。

这里有个问题:当前的游戏引擎是基于 ECSM 架构,由于这个架构,在 main 函数里设置小车的状态并不是立即生效的,至少在 main 函数执行完前不会生效。而且实际的情况是,比如在当前帧设置小车转过某个角度,要在下一帧才能看到小车真的转过这个角度。

有种办法是增加标记变量,在当前帧标记已经设置小车转过某个角度,在下一帧根据标记变量来判断小车已经转过这个角度。

// 示例代码
function main({
     scene, car }) {
   
    const dataThreshold = 0.22;
    const data = car.getEddyCurrent();
    
    if (data > dataThreshold) {
    // move
      car.setSpeed(10);
      car.store.isScanning = false;
    } else {
   
      const scanRadBase = 2;
      const scanRadStep = 1;
      if (!car.store.isScanning) {
   
        car.stop();
        car.store.isScanning = true;
        car.store.dataMiddle = data;
        car.store.scanCount = 0;
        car.store.scanRad = scanRadBase;
      } else {
   
        if (car.store.scanCount === 0) {
    // scan right
          car.turn(-car.store.scanRad);
          car.store.scanCount++;
        } else if (car.store.scanCount === 1) {
    // check right
          if (data >= car.store.dataMiddle) {
    // right pass
            car.store.dataMiddle = data;
            car.store.scanCount = 0;
            car.store.scanRad = scanRadBase;
          } else {
    // scan left
            car.turn(2 * car.store.scanRad);
            car.store.scanCount++;
          }
        } else if (car.store.scanCount === 2) {
    // check left
          if (data >= car.store.dataMiddle) {
    // left pass
            car.store.dataMiddle = data;
            car.store.scanCount = 0;
            car.store.scanRad = scanRadBase;
          } else {
   
            car.turn(-car.store.scanRad);
            car.setSpeed(5);
            car.store.dataMiddle = data;
            car.store.scanCount = 0;
            car.store.scanRad += scanRadStep;
          }
        }
      }
    }
}

上面这种写法存在两个问题:

  1. 因为受限于 ECSM 架构,对小车的状态设置是延迟到下一帧生效的,所以需要额外地增加标记变量(如 scanCount),这使得编写、理解代码变得很复杂,要知道算法伪代码里就 4 个步骤;
  2. 在这每一帧都会被执行的 main 函数里,临时变量只能被记录在 car 的 store 上面,导致缺少一定的编码自由度(灵活性),这个可以通过对比现实单片机里的运行程序来感受:
function main({
     scene, car 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值