基于WebSocket和Python服务端的CSI检测可视化网页上位机 - 续 - Chrome小恐龙

0. 前言

之前那个CSI可视化之后,我收到反馈说这个可视化还不够直观,能不能再直观一点。好吧,灵感来了,让CSI的读值去控制Chrome小恐龙。


1. 系统框图

系统框图 系统框图没有变,仍然假设服务端通过串口收到CSI信息,然后通过WebSocket把CSI的结果传给客户端。这次只需要修改客户端,让Chrome小恐龙的网页去接收CSI的结果。


2. 小恐龙的实现

网络上有不少对小恐龙源码的解析[1] [2],或者直接问AI也行。简而言之小恐龙游戏的核心逻辑是:

  • 碰撞检测机制用于判断是否Game Over

  • 按下空格时跳跃

  • 速度(currentSpeed)是一个随时间增长的变量,它通过动画被用户感知到:

    • 小恐龙本身的绝对位置不变,改变背景景物的移动速度让人以为小恐龙在往前跑。背景景物的移动速度与currentSpeed成正比。

    • 小恐龙两只脚交替的动画帧率越高,用户以为小恐龙跑得越快。帧率和currentSpeed的关系可以自定义函数来计算。

要让小恐龙可视化CSI的读值,首先要禁用碰撞检测,否则会严重影响游戏体验。然后有两种思路:

  1. 用挥手(CSI读数为1)代替空格控制跳跃,

  2. CSI检测到用户动得越快(读数持续为1),小恐龙跑得越快

本文接下来会基于[2]的简化版代码做修改来实现这几件事。

禁用碰撞检测

正常的代码会先进行碰撞检测,如果没有撞到才会更新下一帧。这里直接把if (!collison)后面那段整个注释掉,碰撞就不会有任何效果了。

注释掉这一段同时也会让速度不再随时间增长。

// Check for collisions.
var collision = hasObstacles &&
    checkForCollision(this.horizon.obstacles[0], this.tRex);

if (!collision) {
    this.distanceRan += this.currentSpeed * deltaTime / this.msPerFrame;

    if (this.currentSpeed < this.config.MAX_SPEED) {
        this.currentSpeed += this.config.ACCELERATION;
    }
} else {
    this.gameOver();
}

CSI控制移动速度

在入口函数里,和Runner有关的函数都定义完之后,建立一个WebSocket连接并在回调函数里把currentSpeed设置成接收到的值。

var socket = new WebSocket("ws://localhost:4200");
socket.onmessage = function(event) {
var data = JSON.parse(event.data);
var currentSpeed=data.x * 12;
window['Runner'].instance_.currentSpeed=currentSpeed; 
window['Runner'].instance_.tRex.updateMSPerFrame(currentSpeed);
};

另外找到updateMSPerFrame这个函数,它的作用是修改小恐龙的脚蹬得有多快。这里靠目测拟合了一个msPerFrame关于currentSpeed的指数函数。

updateMSPerFrame: function (currentSpeed) {
    if (this.status === Trex.status.RUNNING) {
        //this.msPerFrame = 2000 - 550 * Math.sqrt(currentSpeed);
        this.msPerFrame = 2000 * Math.exp(-0.268 * currentSpeed);
    }
},

修改后的代码在git仓库里。

CSI触发跳跃

这件事的本质是要用“CSI接收到1”这个事件代替“按下空格”。所以只需要把“按下空格”的响应函数复制到WebSocekt的回调函数里。

“按下空格”的响应函数如下,其中if (!this.crashed && (Runner.keycodes.JUMP[e.keyCode] || e.type == Runner.events.TOUCHSTART))的部分用来初始化游戏,以及让非跳跃状态的小恐龙开始跳跃。

onKeyDown: function (e) {
    // Prevent native page scrolling whilst tapping on mobile.
    if (IS_MOBILE && this.playing) {
        e.preventDefault();
    }

    if (e.target != this.detailsButton) {
        if (!this.crashed && (Runner.keycodes.JUMP[e.keyCode] ||
            e.type == Runner.events.TOUCHSTART)) {
            if (!this.playing) {
                this.loadSounds();
                this.playing = true;
                this.update();
                if (window.errorPageController) {
                    errorPageController.trackEasterEgg();
                }
            }
            //  Play sound effect and jump on starting the game for the first time.
            if (!this.tRex.jumping && !this.tRex.ducking) {
                this.playSound(this.soundFx.BUTTON_PRESS);
                this.tRex.startJump(this.currentSpeed);
            }
        }

        if (this.crashed && e.type == Runner.events.TOUCHSTART &&
            e.currentTarget == this.containerEl) {
            this.restart();
        }
    }

把这一段响应复制到WebSocket的回调函数里即可。

var socket = new WebSocket("ws://localhost:4200");
socket.onmessage = function(event) {
var data = JSON.parse(event.data);
// var currentSpeed=data.x * 12;
// window['Runner'].instance_.currentSpeed=currentSpeed; 
// window['Runner'].instance_.tRex.updateMSPerFrame(currentSpeed);
var runner=window['Runner'].instance_;
if (!runner.crashed && data.x>0.5) {
    if (!runner.playing) {
        runner.loadSounds();
        runner.playing = true;
        runner.update();
        if (window.errorPageController) {
            errorPageController.trackEasterEgg();
        }
    }
    //  Play sound effect and jump on starting the game for the first time.
    if (!runner.tRex.jumping && !runner.tRex.ducking) {
        runner.playSound(runner.soundFx.BUTTON_PRESS);
        runner.tRex.startJump(runner.currentSpeed);
    }
}
};

参考资料

  1. Chrome自带恐龙小游戏的源码研究

  2. 简化版T-Rex Runner