mbedJS+Wiiヌンチャクでスロットカーを作った
I2Cなデバイスで真っ先に思い付いたのが、Wiiヌンチャクだった訳で。
何かできないかなと考えた。
ヌンチャクを手に持って小一時間・・・この感覚、あれだ!車が走るやつ!
Note
編集中です。
【概要】
・ヌンチャクのスティックの押し込み具合で加速する車、スロットカーを作った。
wikiページ
・さすがにグラフィック液晶では迫力もスピード感も出ないので、今回はHTML5+JavaScriptで作ってみる。
・JSと言えばmbedJSがあるので、これを使う。
【使ったもの】
・mbed LPC1768 (青mbed) + ☆ボードオレンジ(LAN接続用)
・Wiiヌンチャク ×2本 (1本でも可、コースを作ってるうちに2レーンできていたので1本追加で買ってきた。)
・拡張コントローラー用の接続アダプタ
最近は海外通販で本物(?)が買えるらしいけど、ピッチが合わず使いにくそうなので、ヌンチャクアダプタがおススメ。
通販用URL
・プルアップ抵抗(10KΩ)
ヌンチャク1本につき2つ。計4つ。
【回路図】
・I2Cのうち、p9 p10 を青い車(1号車)、p28 p27を赤い車(2号車)に割り当てています。
回路図を書いて掲載する。
サンプルページ(mbedJSが接続中でIPが"192.168.0.39"の場合) /media/uploads/ban4jp/slotcar.html
サンプルページ(自動ループ) /media/uploads/ban4jp/slotcar_autoloop.html
素材データ一式 /media/uploads/ban4jp/slotcar.zip
【権利関係】
・素材データ(slotcar.zip)はすべて私(ban4jp)がゼロから作成したものです。
・素材データ(slotcar.zip)は自由に利用、改造してください。商用なども一切気にしません。
・ただし、"AS IS"です。何か問題があっても自己責任でお願いします。
【後で動画を張る】
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>スロットカー for mbedJS</title>
<style type="text/css">
.course_box {
position:relative;
}
.car {
position:absolute;
}
</style>
<script src="http://192.168.0.39/rom/m/mbedJS.all-min.js"></script>
<script>
function Nunchuk(_mcu, _pinnames)
{
this.joyX = 0; // ニュートラルポジションからの相対値
this.joyY = 0;
var instance = this;
var stat = 0;
var neutralJoyX = 127; // ニュートラルポジション
var neutralJoyY = 127;
var timer_wait = 0;
var timer = setInterval(function()
{
switch(stat){
case 1:
case 4:
stat++;
i2c.write(0xA4, [0x00], false);
break;
case 98:
// Read/Writeが失敗した場合、初期化からやり直す
if (timer_wait > 0) {
timer_wait--;
break;
}
stat = 0;
i2c.write(0xA4, [0x40, 0x00], false);
break;
case 99:
// ステータス異常の場合、i2cを再スタートする
if (timer_wait > 0) {
timer_wait--;
break;
}
stat = 0;
console.log("restart:", stat);
i2c.start();
break;
}
}, 1000/60);
var i2c = new mbedJS.I2C(_mcu, _pinnames,
{
onNew:function()
{
i2c.frequency(400000);
},
onStart:function()
{
i2c.frequency(400000);
},
onFrequency:function()
{
i2c.write(0xA4, [0x40, 0x00], false);
},
onWrite:function(v)
{
if (v != 0) {
//console.log("write error(1):", stat, v);
timer_wait = 60 * 0.25;
stat = 98;
return;
}
switch(stat){
case 0:
stat++;
break;
case 2:
case 5:
stat++;
i2c.read(0xA4, 6, false);
break;
default:
i2c.stop();
console.log("stop(1):", stat);
break;
}
},
onRead:function(v)
{
switch(stat){
case 3:
// ヌンチャクの状態が異常となった場合、値が255になる
if (instance.joyX == 255) {
i2c.stop();
console.log("reconnect:", stat, v);
break;
}
instance.neutralJoyX = (v.data[0]^0x17)+0x17;
instance.neutralJoyY = (v.data[1]^0x17)+0x17;
stat++;
break;
case 6:
// ヌンチャクの状態が異常となった場合、値が255になる
if (instance.joyX == 255) {
i2c.stop();
console.log("reconnect:", stat, v);
break;
}
// データをデコード
instance.joyX = (v.data[0]^0x17)+0x17 - instance.neutralJoyX;
instance.joyY = (v.data[1]^0x17)+0x17 - instance.neutralJoyY;
// ボタンや加速度センサーの値が必要な場合は、ここに追記
stat = 4;
break;
default:
i2c.stop();
console.log("stop(2):", stat);
break;
}
},
onStop:function()
{
console.log("onStop:", stat);
timer_wait = 60 * 1.5;
stat = 99;
}
});
}
Nunchuk.prototype = {};
var nun_b;
var nun_r;
var mcu = new mbedJS.Mcu('192.168.0.39',
{
onNew:function()
{
nun_b = new Nunchuk(mcu, [mbedJS.PinName.p9, mbedJS.PinName.p10]);
nun_r = new Nunchuk(mcu, [mbedJS.PinName.p28, mbedJS.PinName.p27]);
},
onClose:function(){
alert("mbedJS close.");
},
onError:function(){
alert("mbedJS Error!");
}
});
</script>
<script>
function Car(func)
{
this.pos = 250; // 走行位置
this.course_func = func; // 走行区間を計算する関数
this.x = 0; // X座標(車の引っ掛け部分の座標)
this.y = 0; // Y座標
this.r = 0; // 回転角度
this.z = 0; // Z座標(0 or 1)
this.accel = 0; // 加速度
this.inertia = 0; // 慣性
}
Car.prototype = {
setAccel:function(val)
{
this.accel = val;
},
tick:function()
{
// アクセルの踏み具合から次の走行距離を計算し、コース上の車の位置を更新
this.inertia = (this.inertia * 7 + this.accel) / 8;
this.pos += this.inertia / 6;
this.course_func(this);
},
course_line:function(x1, y1, x2, y2, rotate, func_nextarea, z_order)
{
// エリアの距離を計算
var l = Math.sqrt(Math.pow(Math.abs(x1-x2), 2) + Math.pow(Math.abs(y1-y2), 2));
// もし、走行距離がエリアの距離を超えていた場合、
// エリア分の走行距離を差し引いて次のエリアへ引き継ぎ
if (this.pos >= l) {
this.pos -= l;
this.course_func = func_nextarea;
this.z = z_order;
this.course_func(this);
return;
}
// 走行距離から、現在座標を計算
this.x = (x2-x1) * this.pos / l + x1;
this.y = (y2-y1) * this.pos / l + y1;
this.r = rotate;
},
course_circle:function(x1, y1, z1, r1, r2, func_nextarea, z_order)
{
// エリアの距離を計算
var l = (z1 * 2 * Math.PI) * Math.abs(r1-r2) / (Math.PI * 2)
// もし、走行距離がエリアの距離を超えていた場合、
// エリア分の走行距離を差し引いて次のエリアへ引き継ぎ
if (this.pos >= l) {
this.pos -= l;
this.course_func = func_nextarea;
this.z = z_order;
this.course_func(this);
return;
}
// 走行距離から、現在座標を計算
var r0 = r1 + (r2-r1) * this.pos / l;
if (r2 > r1) {
this.x = x1 - Math.cos(r0) * z1;
this.y = y1 - Math.sin(r0) * z1;
} else {
this.x = x1 + Math.cos(r0) * z1;
this.y = y1 + Math.sin(r0) * z1;
}
this.r = r0;
}
};
var car_b = new Car(area_b_1);
var car_r = new Car(area_r_1);
var screen_canvas;
var course;
var course_a_img;
var course_b_img;
var car_b_img;
var car_r_img;
var frame_timer;
function onload() {
console.log("onload() start");
screen_canvas = document.getElementById("screen_canvas");
course = screen_canvas.getContext("2d");
course_a_img = document.getElementById("course_a");
course_b_img = document.getElementById("course_b");
car_b_img = document.getElementById("car_b");
car_r_img = document.getElementById("car_r");
update();
frame_timer = setInterval(update, 1000 / 60); // 60Hz
console.log("onload() end");
}
function update() {
if (nun_b != undefined) {
var val;
if (nun_b.joyY < 0) {
val = 0;
} else {
val = nun_b.joyY;
if (val > 90) val = 90;
}
car_b.setAccel(val);
if (nun_r.joyY < 0) {
val = 0;
} else {
val = nun_r.joyY;
if (val > 90) val = 90;
}
car_r.setAccel(val);
data.innerHTML = " val1 = " + nun_b.joyY + " accel1 = " + Math.round(car_b.accel * 100) / 100 + "<br />"
+ " val2 = " + nun_r.joyY + " accel2 = " + Math.round(car_r.accel * 100) / 100;
} else {
console.log("wait...");
}
car_b.tick();
car_r.tick();
course.drawImage(course_a_img, 0, 0);
if (car_b.z != 0) {
course.save();
course.translate(car_b.x, car_b.y);
course.rotate(car_b.r);
course.drawImage(car_b_img, -11, -13);
course.restore();
}
if (car_r.z != 0) {
course.save();
course.translate(car_r.x, car_r.y);
course.rotate(car_r.r);
course.drawImage(car_r_img, -11, -13);
course.restore();
}
course.drawImage(course_b_img, 394, 386);
if (car_b.z == 0) {
course.save();
course.translate(car_b.x, car_b.y);
course.rotate(car_b.r);
course.drawImage(car_b_img, -11, -13);
course.restore();
}
if (car_r.z == 0) {
course.save();
course.translate(car_r.x, car_r.y);
course.rotate(car_r.r);
course.drawImage(car_r_img, -11, -13);
course.restore();
}
}
//----------------------------------------
var CONST_R0 = 270 * Math.PI / 180;
var CONST_R1 = 441 * Math.PI / 180;
var CONST_R2 = 285 * Math.PI / 180;
var CONST_R3 = 138 * Math.PI / 180;
var CONST_R4 = 66 * Math.PI / 180;
//----------------------------------------
function area_b_1(car) {
car.course_line(
835 - Math.cos(CONST_R0) * 85, 620 - Math.sin(CONST_R0) * 85,
178 - Math.cos(CONST_R0) * 105, 600 - Math.sin(CONST_R0) * 105, CONST_R0, area_b_2, 0);
}
function area_b_2(car) {
car.course_circle(178, 600, 105, CONST_R0, CONST_R1, area_b_3, 0);
}
function area_b_3(car) {
car.course_line(
178 - Math.cos(CONST_R1) * 105, 600 - Math.sin(CONST_R1) * 105,
880 + Math.cos(CONST_R1) * 100, 280 + Math.sin(CONST_R1) * 100, CONST_R1, area_b_4, 0);
}
function area_b_4(car) {
car.course_circle(880, 280, 100, CONST_R1, CONST_R2, area_b_5, 0);
}
function area_b_5(car) {
car.course_line(
880 + Math.cos(CONST_R2) * 100, 280 + Math.sin(CONST_R2) * 100,
328 + Math.cos(CONST_R2) * 110, 148 + Math.sin(CONST_R2) * 110, CONST_R2, area_b_6, 0);
}
function area_b_6(car) {
car.course_circle(328, 148, 110, CONST_R2, CONST_R3, area_b_7, 1);
}
function area_b_7(car) {
car.course_line(
328 + Math.cos(CONST_R3) * 110, 148 + Math.sin(CONST_R3) * 110,
646 + Math.cos(CONST_R3) * 105, 492 + Math.sin(CONST_R3) * 105, CONST_R3, area_b_8, 0);
}
function area_b_8(car) {
car.course_circle(646, 492, 105, CONST_R3, CONST_R4, area_b_9, 0);
}
function area_b_9(car) {
car.course_line(
646 + Math.cos(CONST_R4) * 105, 492 + Math.sin(CONST_R4) * 105,
835 - Math.cos(CONST_R4) * 85, 620 - Math.sin(CONST_R4) * 85, CONST_R4, area_b_10, 0);
}
function area_b_10(car) {
car.course_circle(835, 620, 85, CONST_R4, CONST_R0, area_b_1, 0);
}
//----------------------------------------
function area_r_1(car) {
car.course_line(
835 - Math.cos(CONST_R0) * 110, 620 - Math.sin(CONST_R0) * 110,
178 - Math.cos(CONST_R0) * 130, 600 - Math.sin(CONST_R0) * 130, CONST_R0, area_r_2, 0);
}
function area_r_2(car) {
car.course_circle(178, 600, 130, CONST_R0, CONST_R1, area_r_3, 0);
}
function area_r_3(car) {
car.course_line(
178 - Math.cos(CONST_R1) * 130, 600 - Math.sin(CONST_R1) * 130,
880 + Math.cos(CONST_R1) * 75, 280 + Math.sin(CONST_R1) * 75, CONST_R1, area_r_4, 0);
}
function area_r_4(car) {
car.course_circle(880, 280, 75, CONST_R1, CONST_R2, area_r_5, 0);
}
function area_r_5(car) {
car.course_line(
880 + Math.cos(CONST_R2) * 75, 280 + Math.sin(CONST_R2) * 75,
328 + Math.cos(CONST_R2) * 85, 148 + Math.sin(CONST_R2) * 85, CONST_R2, area_r_6, 0);
}
function area_r_6(car) {
car.course_circle(328, 148, 85, CONST_R2, CONST_R3, area_r_7, 1);
}
function area_r_7(car) {
car.course_line(
328 + Math.cos(CONST_R3) * 85, 148 + Math.sin(CONST_R3) * 85,
646 + Math.cos(CONST_R3) * 80, 492 + Math.sin(CONST_R3) * 80, CONST_R3, area_r_8, 0);
}
function area_r_8(car) {
car.course_circle(646, 492, 80, CONST_R3, CONST_R4, area_r_9, 0);
}
function area_r_9(car) {
car.course_line(
646 + Math.cos(CONST_R4) * 80, 492 + Math.sin(CONST_R4) * 80,
835 - Math.cos(CONST_R4) * 110, 620 - Math.sin(CONST_R4) * 110, CONST_R4, area_r_10, 0);
}
function area_r_10(car) {
car.course_circle(835, 620, 110, CONST_R4, CONST_R0, area_r_1, 0);
}
</script>
</head>
<body onLoad="onload()">
<div style="display:none">
<img src="course_a.png" id="course_a" class="course" width="1024" height="768" alt="コース" />
<img src="course_b.png" id="course_b" class="course" width="154" height="99" alt="コース影" />
<img src="car_b.png" id="car_b" class="car" width="23" height="47" alt="1号車" />
<img src="car_r.png" id="car_r" class="car" width="23" height="47" alt="2号車" />
</div>
<canvas id="screen_canvas" width="1024" height="768"></canvas>
<div id="data"></div>
</body>
</html>
【追伸】
・ゲーム性はありません。
・タイムアタックとか、対戦プレイとか面白そうです。誰か追加してください。
Please log in to post comments.
