【Arduinoで自作コントローラーを作る①】ジョイパッドから信号入力するためのスクラッチ拡張機能


※ 当ページには【広告/PR】を含む場合があります。
2021/12/19
合同会社タコスキングダム|TacosKingdom,LLC.

スクラッチゲームはキーボードから操作する基本的になっています。

でも、やっぱりゲームと言ったらコントローラーで操作したい、と感じられる方も多いでしょう。

今回から数回に渡って、スクラッチで使える自作コントローラーを作成して、ゲームを動かせるまでを解説していきます。

初回となる今回は、Arudinoで
スクラッチゲーム用の自作「ジョイパッド」を操作する方法を考えていこうと思います。


Arduino Uno Rev3

ジョイスティックブレイクアウトモジュール

合同会社タコスキングダム|タコキンのPスクール【Pschool厳選】お子様に通わせたいロボットプログラミング教室&教材・学習サービスまとめ

材料の調達

まずはコントローラーの中身としてArduinoが一台必要です。

今回は、Arduinoシリーズの中でももっともベーシックな
Arduino Uno Rev3
をチョイスしてみます。

かつての据え置き型ゲーム機では十字キーが主流でしたが、今では
ジョイスティックブレイクアウトモジュール
のほうが簡単に入手出来るため、この部品でジョイパッドを作成します。

最近だと5個入のブレークアウト基板に載った商品でも1000円未満とかなりお手頃な値段で購入できます。


Arduino Uno Rev3

ジョイスティックブレイクアウトモジュール

合同会社タコスキングダム|タコキンのPスクール【Pschool厳選】お子様に通わせたいロボットプログラミング教室&教材・学習サービスまとめ

Arduinoで基本機能をテスト実装

最初に今回の自作ジョイパッドを組み立てる上で、Arduinoを使った配線図を考えてみます。

合同会社タコスキングダム|タコキンのPスクール

とりあえずこれをブレッドボード上に配置すると以下のようになります。

合同会社タコスキングダム|タコキンのPスクール

見てくれはともかく、なんとなくゲームパッドの実験機としてちゃんと動くところまでを以降で確認しましょう。

Arduinoでジョイスティックモジュールの使い方

先ほど紹介した
ジョイスティックブレイクアウトモジュール
の外観は以下の写真のようになっています。

合同会社タコスキングダム|タコキンのPスクール

合同会社タコスキングダム|タコキンのPスクール

端子は全部で5本あり、
Vcc(+5V) / GND / VRx / VRy / SWとなっています。

端子名

備考

+5V

+5VDC電源

GND

グラウンド(コモン)

VRx

X軸方向のアナログ読み取り

VRy

Y軸方向のアナログ読み取り

SW

センタークリック(ON/OFF)

とりあえず最初の確認事項として、このジョイスティックモジュールとArduinoを接続し、基本的な信号を拾ってみるところから始めましょう。

Arudinoプログラムの実装

今回Arduino側にインストールするソースコードは以下のようになります。

            
            /**
  デジタルピン#4(Aボタン)
  デジタルピン#5(Bボタン)
  デジタルピン#6(スタートボタン)
  デジタルピン#7(セレクトボタン)
  デジタルピン#8(センタースイッチのON/OFF)
  アナログピンA0(X軸方向入力用)
  アナログピンA1(Y軸方向入力用)
*/
const int A_PIN = 4, B_PIN = 5, ST_PIN = 6, SL_PIN = 7,
          SW_PIN = 8, X_PIN = A0, Y_PIN = A1;
char buf[48];

void setup() {
    pinMode(X_PIN, INPUT); // アナログ0ピンは通常入力
    pinMode(Y_PIN, INPUT); // アナログ1ピンは通常入力
    // デジタル4-8ピンはプルアップ入力
    pinMode(A_PIN, INPUT_PULLUP);
    pinMode(B_PIN, INPUT_PULLUP);
    pinMode(ST_PIN, INPUT_PULLUP);
    pinMode(SL_PIN, INPUT_PULLUP);
    pinMode(SW_PIN, INPUT_PULLUP);
    Serial.begin(9600);
    delay(200);
}

void loop(){
    /**
        x: X軸座標(0 - 1023)
        y: Y軸座標(0 - 1023)
        sw: センタースイッチのON/OFF
        a: AボタンのON/OFF
        b: BボタンのON/OFF
        st: スタートボタンのON/OFF
        sl: セレクトボタンのON/OFF
    */
    sprintf(buf, "{x:%d,y:%d,sw:%d,a:%d,b:%d,st:%d,sl:%d}",
        analogRead(X_PIN), analogRead(Y_PIN), digitalRead(SW_PIN),
        digitalRead(A_PIN), digitalRead(B_PIN), digitalRead(ST_PIN), digitalRead(SL_PIN)
    );
    Serial.println(buf);
    delay(100);
}
        
これをArduinoIDEのシリアルモニタで試してみると、

合同会社タコスキングダム|タコキンのPスクール

というように一定時間ごとの各スイッチの状態を一括して文字列にして送信していることが分かります。


Arduino Uno Rev3

ジョイスティックブレイクアウトモジュール

合同会社タコスキングダム|タコキンのPスクール【Pschool厳選】お子様に通わせたいロボットプログラミング教室&教材・学習サービスまとめ

スクラッチ拡張機能の作成

以前の記事で、「Web Serial API」機能に対応したブラウザから直接シリアル通信を操作する拡張機能の作り方を解説していました。

合同会社タコスキングダム|タコキンのPスクール
【scratch-vm応用講座】Scratchの拡張機能でブラウザから直接シリアル通信を操作する

「Web Serial API」を利用してスクラッチの拡張機能からシリアル通信が使えるようにしてみます。

今回も基本的にはこの記事で説明したスクラッチの拡張機能に少し機能を継ぎ足して、改造したコードを以下に示します。

            
            const ArgumentType = require('../../extension-support/argument-type');
const BlockType = require('../../extension-support/block-type');
const Cast = require('../../util/cast');
const log = require('../../util/log');

class Scratch3GamepadController {
    constructor (runtime) {
        this.runtime = runtime;
        this.stopFlag = false;
        this.serialReader = null;
        //👇メッセージ更新時のトリガーとして利用
        this.isRecieveMessage = false;
        //👇最新のゲームパッド状態を保存
        this.x = 0;
        this.y = 0;
        this.sw = false;
        this.ab = false;
        this.bb = false;
        this.st = false;
        this.sl = false;
    }

    getInfo () {
        return {
            id: 'gamepadcontroller',
            name: 'Gamepad Controller',
            blocks: [
                {
                    opcode: 'connectSerial',
                    blockType: BlockType.REPORTER
                },
                {
                    opcode: 'disconnectSerial',
                    blockType: BlockType.COMMAND
                },
                //👇メッセージ受信時のイベント(HATブロック)
                {
                    opcode: 'updateMessage',
                    blockType: BlockType.HAT
                },
                //👇ゲームパッド状態の取得(REPORTERブロック)
                {
                    opcode: 'getX',
                    blockType: BlockType.REPORTER
                },
                {
                    opcode: 'getY',
                    blockType: BlockType.REPORTER
                },
                {
                    opcode: 'getSW',
                    blockType: BlockType.BOOLEAN
                },
                {
                    opcode: 'getStart',
                    blockType: BlockType.BOOLEAN
                },
                {
                    opcode: 'getSelect',
                    blockType: BlockType.BOOLEAN
                },
                {
                    opcode: 'getA',
                    blockType: BlockType.BOOLEAN
                },
                {
                    opcode: 'getB',
                    blockType: BlockType.BOOLEAN
                }
            ],
            menus: {}
        };
    }

    async startSerial() {
        try {
            console.log("INFO: 接続が確立しました");
            this.stopFlag = false;
            const port = await navigator.serial.requestPort();
            await port.open({
                baudRate: 9600,
                dataBits: 8,
                stopBits: 1,
                parity: "none",
                bufferSize: 255,
                flowControl: "hardware"
            });
            while (port.readable) {
                this.serialReader = port.readable.getReader();
                try {
                    const buff_ = [];
                    let lastByte;
                    while (!this.stopFlag) {
                        const { value, done } = await this.serialReader.read();
                        if (done) {
                            console.log("INFO: 読込モード終了");
                            break;
                        }
                        if (value) {
                            for (let i=0;i < value.length;i++) {
                                buff_.push(value[i]);
                                if (value[i] == 10 && lastByte == 13) {
                                    const inputValue = new TextDecoder("utf-8").decode(new Uint8Array(buff_));
                                    buff_.splice(0);
                                    //👇メッセージの更新
                                    console.log(inputValue);
                                    const mtch_ = inputValue.match(/{x:(\d+),y:(\d+),sw:(\d?),a:(\d?),b:(\d?),st:(\d?),sl:(\d?)}/);
                                    if (mtch_) {
                                        if (mtch_[1]) this.x = parseInt(mtch_[1],10);
                                        if (mtch_[2]) this.y = parseInt(mtch_[2],10);
                                        if (mtch_[3]) this.sw = parseInt(mtch_[3],10);
                                        if (mtch_[4]) this.ab = parseInt(mtch_[4],10);
                                        if (mtch_[5]) this.bb = parseInt(mtch_[5],10);
                                        if (mtch_[6]) this.st = parseInt(mtch_[6],10);
                                        if (mtch_[7]) this.sl = parseInt(mtch_[7],10);
                                    }
                                    //👇HATブロックにtrueを流す
                                    this.isRecieveMessage = true;
                                    console.log("INFO: 読込完了");
                                    break;
                                }
                                lastByte = value[i];
                            }
                        }
                    }
                } catch (error) {
                    console.log("ERROR: 読み出し失敗");
                    console.log(error);
                } finally {
                    this.serialReader.releaseLock();
                    await port.close();
                    console.log("INFO: 接続を切断しました");
                }
            }
        } catch (error) {
            console.log("ERRORR: ポートが開けません");
            console.log(error);
        }
    }

    stopSerial() {
        this.stopFlag = true;
        this.serialReader.cancel();
    }

    connectSerial() {
        console.log('Connected!');
        this.startSerial();
        return 'Connected!';
    }

    disconnectSerial() {
        console.log('Disconnected.');
        this.stopSerial();
    }

    //👇HATブロックの中身
    updateMessage() {
        const triggered = this.isRecieveMessage;
        this.isRecieveMessage = false;
        return triggered;
    }

    //👇各ゲームパッドの状態値を返す関数
    getX() { return this.x; }
    getY() { return this.y; }
    getA() { return this.ab; }
    getB() { return this.bb; }
    getSW() { return this.sw; }
    getStart() { return this.st; }
    getSelect() { return this.sl; }

}

module.exports = Scratch3GamepadController;
        
ではこのエクステンションをスクラッチGUIに登録して、まずはデバックコンソールを使ってみましょう。

合同会社タコスキングダム|タコキンのPスクール

良い感じにArduino側からゲームパッドの状態を漏れなく拾っているようです。

ゲームパッドからスクラッチアプリを動かす

折角ですので、スクラッチプログラムを即興で作ってみたいと思います。

ベースのプロジェクトとして、以前
十字キー風スプライトを動かすサンプルを作っていましたので、このプロジェクトをダウンロードしてからゲームパッド機能を盛り込んでいきます。

合同会社タコスキングダム|タコキンのPスクール
【Scratch入門〜中級編】十字キーのスプライトを使ってタッチパネルからゲームを操作する方法

キーボードの無いデバイス(スマートフォンやタブレット)でも動かすときに使える、ゲームパッドの十字キーのような操作ツールを作成してみます。

以下の動画で今回作ったゲームパッド用の拡張機能を使った動作テストを紹介しています。


Arduino Uno Rev3

ジョイスティックブレイクアウトモジュール

合同会社タコスキングダム|タコキンのPスクール【Pschool厳選】お子様に通わせたいロボットプログラミング教室&教材・学習サービスまとめ

まとめ

以上、今回はArduinoをゲームパッドにするエクステンションの作成手順に触れました。

次回以降ではよりゲームパッドらしい見た目にしながら、スクラッチゲームの遊べるようなものを目指して完成度を上げていこうかと思います。

参考サイト

【Arduino入門編⑦】ジョイスティックの制御方法!デジタル・アナログ入力の解説です!