mbedJS+Wiiヌンチャクでスロットカーを作った

I2Cなデバイスで真っ先に思い付いたのが、Wiiヌンチャクだった訳で。

何かできないかなと考えた。
ヌンチャクを手に持って小一時間・・・この感覚、あれだ!車が走るやつ!

/media/uploads/ban4jp/slotcar.png

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.