LoginSignup
1
2

More than 3 years have passed since last update.

戦車ラジコン:ブラウザ+GamePadで動かす

Last updated at Posted at 2020-02-01

最近、戦車ラジコンネタ が続いていますが、今回は、ブラウザ+マルチタッチで戦車を動かすのではなく、ブラウザ+GamePadで動かしてみます。
ですので、スマホからではなく、PCから操作できるようになります。
HTML5のGamePadを使いますので、なんらかのゲームコントローラが必要です。私の場合は、SONYのDUALSHOCK 4を使いました。

a.png

毎度の通り、GitHubに上げておきました。
 https://github.com/poruruba/obniz_motor

以下からもページを参照できます。(Obnizにつながないと使えないですが)。
 https://poruruba.github.io/obniz_motor/
※M5Cameraがhttp接続なので、https上にある上記ページからは解像度の変更ができないようです。

HTML5 Gamepad API

ブラウザから、PCに接続されたゲームコントローラを使うためのAPIです。

Gamepad APIを使用したコントロールの実装
 https://developer.mozilla.org/ja/docs/Games/Techniques/Controls_Gamepad_API

ゲームパッドでブラウザゲームを操作できるGamepad APIが楽しい【俺聞け11】
 https://mzsm.me/2015/02/14/orekike11-gamepadapi/

SONYのDUALSHOCK 4をUSB接続したら認識してくれました。
ボタンや十字キーだけでなく、アナログジョイスティックにも対応しています。

基本的には、requestAnimationFrame で呼ばれるコールバック関数の中で、Gamepadの状態を参照します。
今回の実装では、関数check_gemapadがそれに該当し、その部分のソースを抜粋します。

start.js
        check_gamepad: function(playing){
            var gamepadList = navigator.getGamepads();
            for(var i=0; i<gamepadList.length; i++){
                var gamepad = gamepadList[i];
                if(gamepad){
                    if( playing ){
                        this.power_right = Math.floor(-gamepad.axes[3] * this.power_max);
                        this.power_left = Math.floor(-gamepad.axes[1] * this.power_max);
                        this.motor_change_right();
                        this.motor_change_left();

                        if( !button_pressed && gamepad.buttons[0].pressed ){
                            button_pressed = true;
                            this.battle_fire();
                        }else if( button_pressed && !gamepad.buttons[0].pressed ){
                            button_pressed = false;
                        }
                    }else{
                        if( gamepad.buttons[9].pressed )
                            this.battle_start();
                        if( gamepad.buttons[1].pressed )
                            this.dialog_close('#result_dialog');
                    }

                    break;
                }
            }
        },

ループを回しているのは、gamepadList[i] がundefined となっているものが含まれているためです。
一番最初に発見したコントローラを採用しています。

アナログジョイスティックは、 gamepad.axes[] にあります。
各ボタンは、 gamepad.buttons[]の配列にあるので、どのボタンが何番目になるかは、 実際に押して試してみてください。
ちなみに、DUALSHOCK 4では以下でした。

 ・0: ×ボタン
 ・1: 〇ボタン
 ・9: OPTIONSボタン

M5Cameraで取得できる画像解像度の変更

PCでも操作できるようにることにしたので、大きな解像度で表示させたいところです。
そこで、画像解像度を変更できるようにしました。
以下のようなGET呼び出しをすれば、(少しウェイトを挟んで)以降取得される画像の解像度が変更されます。

 http://[M5CameraのIPアドレス]:80/control?var=framesize&val=X

Xの部分には、解像度に合わせて以下の数字を指定します。
 ・UXGA(1600x1200) : 10
 ・SXGA(1280x1024) : 9
 ・XGA(1024x768) : 8
 ・SVGA(800x600) : 7
 ・VGA(640x480) : 6
 ・CIF(400x296) : 5
 ・QVGA(320x240) : 4
 ・XQVGA(240x176) : 3
 ・QQVGA(160x120) : 0

ちなみに、画像は以下のURLなので、Webページから入力してもらうのはM5CameraのIPアドレスだけに変更しました。

 http://[M5CameraのIPアドレス]:81/stream

ソースコード

Javascriptのソースコードを載せておきます。その他のソースコードは、GitHubを参照してください。

start.js
'use strict';

//var vConsole = new VConsole();

let obniz;
var motor_right;
var motor_left;
var power_left_sign;
var power_right_sign;
var camera_image;
var button_pressed = false;

const COOKIE_EXPIRE = 365;
const POWER_MARGIN = 10;
const POWER_MAX = 40;
const TIMER_COUNT = 60.0;
const SHELL_COUNT = 10;

var vue_options = {
    el: "#top",
    data: {
        progress_title: '',

        obniz_id: '',
        obniz_connected: false,
        power_left: 0,
        power_right: 0,
        power_max: POWER_MAX + POWER_MARGIN,
        power_min: -(POWER_MAX + POWER_MARGIN),
        camera_ipaddress: '192.168.1.248',
        camera_resolution: 6,
        qrcode_context: null,
        qrcode_canvas: null,
        qrcode_list: [],
        qrcode: '',
        counter: 0.0,
        num_of_fail: 0,
        num_of_total: 0,
        shells: 0,
        lockon: false,
    },
    computed: {
    },
    methods: {
        battle_fire: function(){
            console.log('fire');
            if( this.shells <= 0 )
                return;

            this.shells--;
            var fire = $('#snd_fire')[0];
            fire.pause();
            fire.currentTime = 0;
            fire.play();
            if( this.lockon ){
                if( this.qrcode.endsWith('.mp3') ){
                    console.log("fail");
                    this.num_of_fail++;
                    setTimeout(() => {
                        var audioElem = new Audio();
                        audioElem.src = this.qrcode;
                        audioElem.play();
                    }, 500 );
                    return;
                }

                this.qrcode_list.push(this.qrcode);
                setTimeout(() => {
                    var bomb = $('#snd_bomb')[0];
                    bomb.pause();
                    bomb.currentTime = 0;
                    bomb.play();
                }, 700 );
            }
        },
        battle_start: function(){
            this.counter = TIMER_COUNT;
            this.shells = SHELL_COUNT;
            this.num_of_fail = 0;
            this.qrcode_list = [];
            this.lockon = false;
            this.qrcode = null;
            this.timer = setInterval(() =>{
                this.counter -= 0.1;
                if( this.counter <= 0.0){
                    clearInterval(this.timer);
                    this.counter = 0.0;
                    this.motor_reset();
                    this.num_of_total = this.qrcode_list.length - this.num_of_fail * 3;
                    if( this.num_of_total < 0)
                        this.num_of_total = 0;
//                    alert('終了ーっ!!');
                    this.dialog_open('#result_dialog', true);
                }
            }, 100);
        },
        check_gamepad: function(playing){
            var gamepadList = navigator.getGamepads();
            for(var i=0; i<gamepadList.length; i++){
                var gamepad = gamepadList[i];
                if(gamepad){
                    if( playing ){
                        this.power_right = Math.floor(-gamepad.axes[3] * this.power_max);
                        this.power_left = Math.floor(-gamepad.axes[1] * this.power_max);
                        this.motor_change_right();
                        this.motor_change_left();

                        if( !button_pressed && gamepad.buttons[0].pressed ){
                            button_pressed = true;
                            this.battle_fire();
                        }else if( button_pressed && !gamepad.buttons[0].pressed ){
                            button_pressed = false;
                        }
                    }else{
                        if( gamepad.buttons[9].pressed )
                            this.battle_start();
                        if( gamepad.buttons[1].pressed )
                            this.dialog_close('#result_dialog');
                    }

                    break;
                }
            }
        },
        camera_draw() {
            if(this.qrcode_canvas == null ){
//                this.qrcode_canvas = document.createElement('canvas');
                this.qrcode_canvas = $('#camera_canvas')[0];
                this.qrcode_canvas.width = camera_image.width;
                this.qrcode_canvas.height = camera_image.height;
                this.qrcode_context = this.qrcode_canvas.getContext('2d');
                this.qrcode_context.strokeStyle = "blue";
                this.qrcode_context.lineWidth = 3;
            }

            this.qrcode_context.drawImage(camera_image, 0, 0, this.qrcode_canvas.width, this.qrcode_canvas.height);

            if( this.counter > 0.0 ){
                const imageData = this.qrcode_context.getImageData(0, 0, this.qrcode_canvas.width, this.qrcode_canvas.height);
                const code = jsQR(imageData.data, this.qrcode_canvas.width, this.qrcode_canvas.height);
                if( code && code.data != "" ){
                    console.log(code);
                    if( this.qrcode_list.indexOf(code.data) < 0){
                        this.qrcode = code.data;
                        this.lockon = true;

                        var pos = code.location;
                        this.qrcode_context.beginPath();
                        this.qrcode_context.moveTo(pos.topLeftCorner.x, pos.topLeftCorner.y);
                        this.qrcode_context.lineTo(pos.topRightCorner.x, pos.topRightCorner.y);
                        this.qrcode_context.lineTo(pos.bottomRightCorner.x, pos.bottomRightCorner.y);
                        this.qrcode_context.lineTo(pos.bottomLeftCorner.x, pos.bottomLeftCorner.y);
                        this.qrcode_context.lineTo(pos.topLeftCorner.x, pos.topLeftCorner.y);
                        this.qrcode_context.stroke();
                    }else{
                        this.lockon = false;
                        this.qrcode = null;
                    }
                }else{
                    this.lockon = false;
                    this.qrcode = null;
                }

                this.check_gamepad(true);
            }else{
                this.check_gamepad(false);
            }

            requestAnimationFrame(this.camera_draw);
        },
        obniz_connect: function(){
            obniz = new Obniz(this.obniz_id);
            this.progress_open('接続試行中です。', true);

            obniz.onconnect = async () => {
                this.progress_close();
                Cookies.set('obniz_id', this.obniz_id, { expires: COOKIE_EXPIRE });
                this.obniz_connected = true;

                motor_left = obniz.wired("DCMotor", {forward:0, back:1});
                motor_right = obniz.wired("DCMotor", {forward:2, back:3});
                this.motor_reset();

                await do_get('http://' + this.camera_ipaddress + ':80/control', { var: 'framesize', val: this.camera_resolution });

                setTimeout(() => {
                    camera_image = new Image();
                    camera_image.crossOrigin = "Anonymous";
                    camera_image.addEventListener("load", this.camera_draw, false);
                    camera_image.src = 'http://' + this.camera_ipaddress + ':81/stream';
                }, 1000);
            }
        },
        motor_reset: function(){
            if( this.obniz_connected ){
                motor_right.power(0);
                motor_right.move(true);
                motor_left.power(0);
                motor_left.move(true);
            }

            power_right_sign = 0;
            this.power_right = 0;
            power_left_sign = 0;
            this.power_left = 0;
        },
        motor_change_right: function(){
            if( !this.obniz_connected ){
                this.power_right = 0;
                return;
            }

            var sign = Math.sign(this.power_right);
            var power = Math.abs(this.power_right);
            if( power <= POWER_MARGIN )
                power = 0;
            else
                power -= POWER_MARGIN;

            motor_right.power(power);
            if( sign != power_right_sign ){
                motor_right.move(sign >= 0);
                power_right_sign = sign;
            }
        },
        motor_change_left: function(){
            if( !this.obniz_connected ){
                this.power_left = 0;
                return;
            }

            var sign = Math.sign(this.power_left);
            var power = Math.abs(this.power_left);
            if( power <= POWER_MARGIN )
                power = 0;
            else
                power -= POWER_MARGIN;

            motor_left.power(power);
            if( sign != power_left_sign ){
                motor_left.move(sign >= 0);
                power_left_sign = sign;
            }
        },
        motor_end_right: function(){
            if( !this.obniz_connected ){
                this.power_right = 0;
                return;
            }

            motor_right.power(0);
            motor_right.move(true);
            power_right_sign = 0;
            this.power_right = 0;
        },
        motor_end_left: function(){
            if( !this.obniz_connected ){
                this.power_left = 0;
                return;
            }
            motor_left.power(0);
            motor_left.move(true);
            power_left_sign = 0;
            this.power_left = 0;
        },
    },
    created: function(){
    },
    mounted: function(){
        proc_load();

        this.obniz_id = Cookies.get('obniz_id');
    }
};
vue_add_methods(vue_options, methods_utils);
var vue = new Vue( vue_options );

function do_get(url, qs) {
    var params = new URLSearchParams(qs);
    var url2 = new URL(url);
    url2.search = params;

    return fetch(url2.toString(), {
        method: 'GET',
      })
      .then((response) => {
        if (!response.ok)
          throw 'status is not 200';
      });
  }

以上

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2