ブラウザゲームをローカルのSocket.IO経由でArduinoとつなげてインターフェースを拡張します。
ということでCdSセルという、光をアナログで受け取ってくれる部品(1個100円!)を使って光をインプットにして弾道を作ってみました。
↓実行結果(youtube : https://www.youtube.com/watch?v=RpPLE0tSp6M)
光を手で緩和させるとボールの軌道が曲がります
元々やりたかったことは、Arduinoのアナログin-outをブラウザから制御できるようにしたかった。これで4Dゲームなども作れる!(今回はinだけだったけど、outも作れますからね)
たとえば・・・
・ゲームオーバー時に、部屋中の明かりを派手に明滅させる
・サーボモーターでゲージを作る
・サーボモーターでポルターガイスト的な
・気温に応じてゲームが変化する
・ドローンが飛ぶ・・・
・米が炊ける・・・
などArduino、マイコン関連のパーツは無数にあるので、これがつながることで可能性が広がるわけです。照明、人感センサ、赤外線センサ、モーター、温度、湿度、機械制御、やろうと思えばなんでもありです。
構成イメージ
Arduinoとつないだマシン(Mac)のNode.jsをホストとしてCylon.jsから制御します。Cylon.jsの機能でSocket.IOの接続があるので、接続して、同じポートでブラウザから見に行きます。ArduinoにCylon.jsからあらかじめコマンドを仕込んでおいて、Socket.IO経由でブラウザからemitします。逆の流れもできます。
しかしCylon.jsからつながれるソケットはローカルのみのようなんですよ。(ドキュメントをみる限り)ただ、Socket.IOは複数のソケットとつながれるので、ブラウザからArduinoと、リモート両方につながればリモートからArduinoも制御できますね。(やってみたけどできた。これはちょっと現状ハック的な感じか)
Cylon.js
今回の主役であるJavaScriptでArduinoが制御できるライブラリ。サイトもイカしてますね。名前は『宇宙空母ギャラクティカ』の人工知能?から取ってるらしい。メインの関数がCylon.robot()とかなんだかそれっぽい感じ。ラズパイやARDRONE、mbedなども制御できるらしい。
最近Socket.IOに対応したようで、まだドキュメントも少なく分かりにくい状態(しかもサンプルが変というかソース不足?)だが、ちょっとやればブラウザから制御できる。
gortとかいうコマンドなど入れるのが若干大変だったが・・
Arduino
いわずと知れたマイコンボードとソフトウェアプログラミングが融合したようなマシン。もはや紹介いらずか
Socket.IO
Node.jsをベースに動くリアルタイム通信のライブラリ
Cocos2d-js
HTML5のゲームライブラリ。pixi.jsやenchant.jsなどと同じような志向。ちなみに現時点でクロスブラウザ対応(特にIE9以下)がちょっと懸念点
作ってみた
まずCdSセルをセットする。(SWITCH SCIENCEさんのArduinoをはじめようキットが一通り揃ってるのでおすすめ。若松通商とかで売ってます)
配線はよくあるチュートリアルにあるような感じ。(0番アナログイン、5V電源、GND)
一通りかじったところで、Cylon.jsへ・・・
次はCylon.jsの導入(環境設定などは今回は省略)
実装コードは以下のようにとてもシンプルです。
Cylon.jsのフローは
1 . cylonをrequire
2 . Cylon.robot({});
3 . name,events,connections,devicesなどをセットする
4 . work()内にメインの処理を書く。underscoreやjQueryライクにeveryとか使う
'use strict';
var Cylon = require('cylon');
var analogValue = 0; //cdsセルから受け取る値
Cylon.robot({
name: 'hp0bot',
events: ['from_sensor'],
//ブラウザからの指令の登録
commands: function() {
return {
check: this.check
};
},
connections: {
arduino: { adaptor: 'firmata', port: '/dev/tty.usbmodem1421' }
},
//cdsセル
devices: {
sensor: { driver: 'analog-sensor', pin: 0, lowerLimit: 100, upperLimit: 900 }
},
//メイン実行部
work: function(my) {
//0.02秒ごとにアナログセンサーの値をインプットする
every((0.02).second(), function() {
analogValue = my.sensor.analogRead();
console.log(analogValue);
});
},
//定期的にブラウザ側にアナログセンサーの値を送る
check: function() {
this.emit('from_sensor',analogValue);
}
});
Cylon.api(
'socketio',
{
host: '0.0.0.0',
port: '3000'
});
Cylon.start();
これの場合は以下でRUN
node motion.js
Cocos2d-js(ブラウザ側)
Socket.IOがMacとArduino間でつながったので、次はブラウザからそちらにアクセスします。ちなみにArduinoがサーバ、ブラウザがクライアントというイメージのようです。
Cocos2d-jsの基本は色んな記事があるようなのでここでは割愛します。
<!DOCTYPE html>
<html>
<head>
<title>Arduino Pong</title>
<script type="text/javascript" src="cocos2d-js-v3.6-lite.js" charset="UTF-8"></script>
<script src="https://cdn.socket.io/socket.io-1.2.0.js"></script>
</head>
<body>
<canvas id="gameCanvas" width="500" height="500"></canvas>
<script type="text/javascript">
window.onload = function(){
var offs = 0;
//Arduinoから受け取る部分
var robot;
robot = io('http://127.0.0.1:3000/api/robots/hp0bot');
robot.on('from_sensor', function(data){
//その時の明るさによって数値を変えると良い
//offs = (data - 55)*20;
offs = (data -110)/3;
});
setInterval(function() {
//定期的にセンサーの値を送ってよと指令を出す
robot.emit('check');
}, 10);
//Cocos2d-js部分
cc.game.onStart = function(){
var size;
var timerMax = 5;
var blocks = [];
var player;
var ball;
var ballV;
var sensorVX;
var v;
var gameOverOnce = 0;
//load resources
cc.LoaderScene.preload(["ball.png","block.png","board.png","bg.png"], function () {
var MyScene = cc.Scene.extend({
onEnter:function () {
this._super();
size = cc.director.getWinSize();
var bg = cc.Sprite.create("bg.png");
bg.setPosition(size.width/2,size.height/2);
this.addChild(bg);
//ボール(センサーで移動)
ball = cc.Sprite.create("ball.png");
ball.setPosition(150,300);
this.addChild(ball);
//プレイヤー(マウスで移動)
player = cc.Sprite.create("board.png");
player.setPosition(250,100);
player.setScale(0.3);
this.addChild(player);
//ボールの速度ベクトル
ballV = cc.p(5,5);
//update
this.schedule(this.myUpdate, 0.01);
cc.eventManager.addListener(cc.EventListener.create({
event: cc.EventListener.MOUSE,
//マウス入力でプレイヤー移動リスナー
onMouseMove: function(evt) {
console.log(evt);
player.x = evt._x;
}
}), this);
},
myUpdate:function(){
//player.x += offs;
//player.setPositionX(-offs);
//ボールの位置
ball.setPosition(ball.getPositionX()+ballV.x+offs,ball.getPositionY()+ballV.y);
//端にいくと折り返す
if(ball.x < ball.getScaleX()){
ballV.x *= -1;
}else if(ball.x > size.width){
ballV.x *= -1;
}else if(ball.y > size.height){
ballV.y *= -1;
}else if(ball.y < 0){
//if(gameOverOnce==0)//alert("gameover");
//gameOverOnce = 1;
ballV.y *= -1;
}
//あたり判定(ボールとプレイヤー)
var ballBoundingBox = ball.getBoundingBox();
var playerBoundingBox = player.getBoundingBox();
if(cc.rectIntersectsRect(ballBoundingBox,playerBoundingBox)){
//ballV.x *= -1;
ballV.y *= -1;
}
}
});
cc.director.runScene(new MyScene());
}, this);
};
cc.game.run("gameCanvas");
};
</script>
</body>
</html>
座標操作に関して
センサーの値は若干荒っぽいことが分かりました。その時点で座標に直接センサーの値を代入してしまうのはナンセンスで、速度の値にセンサーの値を足すイメージだとスムーズに動くね。こういう時は速度や加速度という形で座標から一歩二歩離れてみるとスムーズになる。