基于WebSocket和Python服务端的CSI检测可视化网页上位机 - 续 - Chrome小恐龙
0. 前言
继之前那个CSI可视化之后,我收到反馈说这个可视化还不够直观,能不能再直观一点。好吧,灵感来了,让CSI的读值去控制Chrome小恐龙。
1. 系统框图
系统框图没有变,仍然假设服务端通过串口收到CSI信息,然后通过WebSocket把CSI的结果传给客户端。这次只需要修改客户端,让Chrome小恐龙的网页去接收CSI的结果。
2. 小恐龙的实现
网络上有不少对小恐龙源码的解析[1] [2],或者直接问AI也行。简而言之小恐龙游戏的核心逻辑是:
碰撞检测机制用于判断是否Game Over
按下空格时跳跃
速度(currentSpeed)是一个随时间增长的变量,它通过动画被用户感知到:
小恐龙本身的绝对位置不变,改变背景景物的移动速度让人以为小恐龙在往前跑。背景景物的移动速度与currentSpeed成正比。
小恐龙两只脚交替的动画帧率越高,用户以为小恐龙跑得越快。帧率和currentSpeed的关系可以自定义函数来计算。
要让小恐龙可视化CSI的读值,首先要禁用碰撞检测,否则会严重影响游戏体验。然后有两种思路:
用挥手(CSI读数为1)代替空格控制跳跃,或
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);
}
}
};