OGIMOノート~家族のためのモノづくり~

OGIMOテックノート ~家族のためのモノづくり~

重度障害の息子を持つ父親エンジニアの備忘録。自作の電子工作おもちゃ/リハビリ器具/ロボット関係の製作記録、思った事を残していきます

Nintendo Switchゲームを物理スイッチで操作できるコントローラを作ってみた

f:id:motokiinfinity8:20191205212426j:plain

はじめに(製作動機)

ゲーム好きな我が家族、妻と娘は時間を見つけてはNintendo Switchのゲーム(スプラトゥーンマリオカートなど)で毎日楽しく遊んでいます! 息子もその横で、楽しそうに画面を見て喜んでいます。

しかし、見てるだけで、操作できない。

ゲーム依存が社会問題になっている様ですが、我が家はゲームから多くを学んでます。
消極的だった娘はゲームをやる様になって、チャレンジ精神、失敗する事の大切さを学び、学校の友達とも積極的に遊ぶ様になりました!

なので、息子にも息子もゲームを通して、
「自分で操作する楽しさ」「失敗してやり直せる楽しさ」を感じて欲しい!


まず最初の関門は、市販のコントローラでは「手の不自由な息子は操作できない」という事。
・ボタンが多すぎて、どれを押せばいいか分からない
・コントローラを持てないので、狙ったボタンを押せない

という訳で、まずは第一歩として

「息子でも操作できる様な物理スイッチで操作できる
 Nintendo Switchコントローラの開発」

の自作に取り組みました!


[要求仕様]
3.5mmジャック対応スイッチで操作できる事
 また、我が家で使っている無線対応ジョイスティックでも操作できる事
・操作ボタンはゲームごとに変更できる事
・操作ボタンは最小限として、
 それ以外の操作は自動 or 介助者による代替操作でフォローできる事
 (ただし、本人が操作してる感覚になれる事)
・介助者によるサポート操作が同時にできる事


製作物

上記要求仕様を踏まえた結果、効率良い手段として、
「市販品のNintendo Switchコントローラを改造して外部ハックする」
という方針で、ハックモジュールを開発・改造しました。

f:id:motokiinfinity8:20191205212259j:plain


操作イメージを以下の動画にまとめました。


今のところ、息子でも遊べそうなゲーム(少ボタンでも出来るゲーム)として、
マリオカート
ゼルダの伝説 ~夢を見る島~
太鼓の達人

に対応しております。

製作方法

材料

改造するコントローラとしては、安価なコントローラとしてHORI製有線コントローラを使いました。

【Nintendo Switch対応】ホリパッド for Nintendo Switch

【Nintendo Switch対応】ホリパッド for Nintendo Switch

  • 作者:
  • 出版社/メーカー: ホリ
  • 発売日: 2017/07/13
  • メディア: Video Game

これに、お馴染みの無線対応マイコンモジュール"M5Stack" 及び 周辺デバイスを使います。
筐体は 3Dプリンタで作成しました。

部品名 個数 購入先 値段
M5Stack Basic 1 Switch Science ¥3575
M5Stack プロトモジュール 1 Switch Science ¥1300
GPIO拡張IC MCP23017 1 秋月電子 ¥110
3.5mmステレオジャック 4 秋月電子 ¥60
全体システム

システムイメージは以下の図になります。

f:id:motokiinfinity8:20191216222436j:plain

市販のコントローラ内の基板上のボタン入力信号&ジョイスティック入力信号に外部から配線を追加接続して、外部マイコンから強制的に信号を切り替えるやり方です。ボタン制御用には8本接続し、ボタン押し制御を実施する際は外部からL(GND)信号を印加してやります。
ジョイスティックの場合、1つのジョイスティックに上下用と左右用の2本を接続します。この場合、ジョイスティックの制御がない場合は、信号は1.65V(中間値)になっていますので、
 上 or 左に設定する場合 → H(3.3V)信号を出力
 下 or 右に設定する場合 → L(0V)信号を出力
 設定しない場合  → Hi-Zにする = 入力設定にする
 
という事で対応しました。これだとアナログジョイスティックならではの中間値が表現できませんが、まぁゲーム精度を求める事が目的ではないので、今回は妥協しました。
(こういう妥協も、スピード感あるモノづくりでは結構大事ですよね)


これは前職(家電関連の電気設計)でのエージング試験用としてよく行っていた手法です。
家電のリモコンを改造して、定期的な信号を発行し続けさせて、耐久性試験の効率化として活用していました。
ここにきて、前職での設計スキルが役に立ったー!

ハードウェア

それでは、HORI製のコントローラを分解してみる。裏側のネジを外すと、緑色の基板が出てきます。
f:id:motokiinfinity8:20191223235302j:plain

この基板の上で、比較的はんだ付けしやすそうな場所を選んで半田をつけていきます。
f:id:motokiinfinity8:20191224001643p:plainf:id:motokiinfinity8:20191224001651p:plain


A/B/X/Yボタンは残念ながら裏面にはんだ付けしやすそうな場所がなかったので、
表面側に直接付けます。(上級者であれば、配線のところのレジストを削る手段を取る方がラクかも)
ただし、ボタン面はコーティングされているので、カッターの刃裏面を使い、コーティング部を削っていきます。
f:id:motokiinfinity8:20191224002134j:plain

キレイな銅のピカピカした色が見えてきたら、その上にはんだを乗せてみて、無事にひっつけばOKです

配線するとこんな感じになります。(汚くてスイマセン)

f:id:motokiinfinity8:20191224002321j:plainf:id:motokiinfinity8:20191224002328j:plain


また、ハックユニット側には、3.5mmジャック用スイッチを4つ筐体に並べます。
残念ながら、M5Stack プロトモジュールの一端面には4つ並ばなかったので、専用筐体を3Dプリンタで製作してみました。

f:id:motokiinfinity8:20191224003358p:plainf:id:motokiinfinity8:20191224003828j:plain

ソフトウェア

GPIO拡張IC MCP23017

今回の様に8本以上のスイッチ入力やON制御出力を実施する場合、信号本数が足りなくなる事が想定されます。
そんな時に便利なのが、『GPIO拡張IC MCP23017』です。

チップの詳細は以下のサイトをご覧ください。

project59.blog.fc2.com


※使う際の注意
 ・ICのリセット端子が必ずPU抵抗になっているか?
 ・アドレス信号(A0/A1/A2)は必ずGND or PU抵抗で接続されているか?

私は上記を忘れていた結果、不安定動作(IC近辺を手で触ると挙動が変わる)に悩まされました…


制御については、Arduinoライブラリとして提供されています。
これを使えば、通常GPIOと近いソフトウェアの記述イメージで製作できます。

github.com

ソースコード

全体を公開すると冗長になるため、必要部のみ抜粋します

#include <M5Stack.h>
#include <WiFi.h>
#include <WiFiUdp.h>
#include "esp_system.h"
#include "Adafruit_MCP23017.h"

//MCP23017 (GPIOエキスパンダ)のピン割り当て
#define L_H_JOY  7
#define L_V_JOY  6
#define R_H_JOY  5
#define R_V_JOY  4
#define A_BTN    8
#define B_BTN    9
#define X_BTN   10
#define Y_BTN   11
#define R_BTN   12
#define RZ_BTN  13 
#define L_BTN   14
#define LZ_BTN  15 
#define INPUT1_BTN   0
#define INPUT2_BTN   1
#define INPUT3_BTN   2
#define INPUT4_BTN   3

#define RENDA_JUDGE_TIME 250
#define RENDA_KANKAKU_MS 60

Adafruit_MCP23017 mcp;

void setup()
{
  M5.begin();
  Serial.begin(115200);
  M5.Lcd.fillScreen(BLACK);
  
  mcp.begin();      // use default address 0

  //PIN設定
  mcp.pinMode(L_V_JOY, INPUT);
  mcp.pinMode(L_H_JOY, INPUT);
  mcp.pinMode(R_V_JOY, INPUT);
  mcp.pinMode(R_H_JOY, INPUT);
  
  mcp.pinMode(A_BTN, OUTPUT);
  mcp.digitalWrite(A_BTN, 1);
  
  mcp.pinMode(B_BTN, OUTPUT);
  mcp.digitalWrite(B_BTN, 1);
  
  mcp.pinMode(X_BTN, OUTPUT);
  mcp.digitalWrite(X_BTN, 1);
  
  mcp.pinMode(Y_BTN, OUTPUT);
  mcp.digitalWrite(Y_BTN, 1);
  
  mcp.pinMode(L_BTN, OUTPUT);
  mcp.digitalWrite(L_BTN, 1);
  
  mcp.pinMode(LZ_BTN, OUTPUT);
  mcp.digitalWrite(LZ_BTN, 1);
  
  mcp.pinMode(R_BTN, OUTPUT);
  mcp.digitalWrite(R_BTN, 1);
  
  mcp.pinMode(RZ_BTN, OUTPUT);
  mcp.digitalWrite(RZ_BTN, 1);

  mcp.pullUp(0, HIGH);
  mcp.pullUp(1, HIGH);
  mcp.pullUp(2, HIGH);
  mcp.pullUp(3, HIGH);
}


void loop(){
  
   M5.update();
   if(M5.BtnA.wasPressed()){  
      //現時点では3モードのみ
      game_idx = (game_idx-1)%3;     
   } 
   if(M5.BtnC.wasPressed()){  
      //現時点では3モードのみ
      game_idx = (game_idx+1)%3;     
   }

   
  //ゲームモード選択
  if(game_idx != game_idx_old){
    //モード0 : マリオカート
    if(game_idx == 0){
      M5.Lcd.drawJpgFile(SD, "/mariocart.jpg");
      M5.Lcd.drawJpgFile(SD, "/mc_accel.jpg", 0, 180);
      M5.Lcd.drawJpgFile(SD, "/mc_item.jpg", 80, 180);
      M5.Lcd.drawJpgFile(SD, "/mc_left.jpg", 160, 180);
      M5.Lcd.drawJpgFile(SD, "/mc_right.jpg", 240, 180);
   }
    **** 中略 ****
    game_idx_old = game_idx;
  }

  //外部ボタンからのハック分をマージ
  int jyuji_val_update = jyuji_val;
  int btn_val_update = btn_val;
  int input_val = mcp.digitalRead(INPUT4_BTN)*8+mcp.digitalRead(INPUT3_BTN)*4+mcp.digitalRead(INPUT2_BTN)*2+mcp.digitalRead(INPUT1_BTN);

  // Mode0 マリオカートの場合
  if(game_idx == 0){
    // ボタン入力
    if(mcp.digitalRead(INPUT1_BTN) == 0){
      btn_val_update |= 0b00000001; //Aボタンを1にする
    }
    if(mcp.digitalRead(INPUT2_BTN) == 0){
      btn_val_update |= 0b01000000; //Lボタンを1にする  
    }
    if(mcp.digitalRead(INPUT3_BTN) == 0){
      jyuji_val_update |= 0b0010; //左ボタンを1にする    
    }
    if(mcp.digitalRead(INPUT4_BTN) == 0){
      jyuji_val_update |= 0b0001; //右ボタンを1にする   
    }

    // ボタン表示変更
    if((input_val & 0b0001) != (input_val_old & 0b0001)){
      if((input_val & 0b0001) == 0) M5.Lcd.drawJpgFile(SD, "/mc_accel_on.jpg", 0, 180);
      else M5.Lcd.drawJpgFile(SD, "/mc_accel.jpg", 0, 180);
    }
    if((input_val & 0b0010)  != (input_val_old & 0b0010)){
      if((input_val & 0b0010) == 0) M5.Lcd.drawJpgFile(SD, "/mc_item_on.jpg", 80, 180);
      else M5.Lcd.drawJpgFile(SD, "/mc_item.jpg", 80, 180);
    }
    if((input_val & 0b0100) != (input_val_old & 0b0100)){
      if((input_val & 0b0100) == 0) M5.Lcd.drawJpgFile(SD, "/mc_left_on.jpg", 160, 180);
      else M5.Lcd.drawJpgFile(SD, "/mc_left.jpg", 160, 180);
    }
    if((input_val & 0b1000) != (input_val_old & 0b1000)){
      if((input_val & 0b1000) == 0) M5.Lcd.drawJpgFile(SD, "/mc_right_on.jpg", 240, 180);
      else M5.Lcd.drawJpgFile(SD, "/mc_right.jpg", 240, 180);
    }

   *** 中略 *****
   ////////////////////////////
   // コントローラボタン制御
   ///////////////////////////
   // 上下ボタン
   if((jyuji_val_update & 0b1000) >> 3){ //上の場合
      mcp.pinMode(L_V_JOY, OUTPUT);
      mcp.digitalWrite(L_V_JOY, 0); 
      Serial.println("push UP");
   }else if((jyuji_val_update & 0b0100) >> 2){ //下の場合
      mcp.pinMode(L_V_JOY, OUTPUT);
      mcp.digitalWrite(L_V_JOY, 1); 
      Serial.println("push DOWN");  
   }else{
      mcp.pinMode(L_V_JOY, INPUT);
   }

   
    // 左右ボタン
   if((jyuji_val_update & 0b0010) >> 1){ //左の場合
      mcp.pinMode(L_H_JOY, OUTPUT);
      mcp.digitalWrite(L_H_JOY, 0);
      Serial.println("push LEFT");
   }else if(jyuji_val_update & 0b0001){  //右の場合
      mcp.pinMode(L_H_JOY, OUTPUT);
      mcp.digitalWrite(L_H_JOY, 1);
      Serial.println("push RIGHT");
   }else{
      mcp.pinMode(L_H_JOY, INPUT);
   }

    // X(青)ボタン
   if((btn_val_update & 0b00000100) >> 2){
      mcp.pinMode(X_BTN, OUTPUT);
      mcp.digitalWrite(X_BTN, 0);
      Serial.println("push X");
   }else{
      mcp.pinMode(X_BTN, INPUT);
   }

   // B(黄)ボタン
   if((btn_val_update & 0b00000010) >> 1){
      mcp.pinMode(B_BTN, OUTPUT);
      mcp.digitalWrite(B_BTN, 0);
      Serial.println("push B");
   }else{
      mcp.pinMode(B_BTN, INPUT);
   }

   // Y(緑)ボタン
   if((btn_val_update & 0b00001000) >> 3){
      mcp.pinMode(Y_BTN, OUTPUT);
      mcp.digitalWrite(Y_BTN, 0);
      Serial.println("push Y");
   }else{
      mcp.pinMode(Y_BTN, INPUT);
   }

   // A(赤)ボタン
   if(btn_val_update & 0b00000001){
      mcp.pinMode(A_BTN, OUTPUT);
      mcp.digitalWrite(A_BTN, 0);
      Serial.println("push A");
//   }else if(digitalRead(INPUT1_BTN) == 0){
//      pinMode(A_BTN, OUTPUT);
//      digitalWrite(A_BTN, 0);
//      Serial.println("push A (Outside)");
//   
   }else{
      mcp.pinMode(A_BTN, INPUT);
   }
    // LZボタン
   if((btn_val_update & 0b10000000) >> 7){
      mcp.pinMode(LZ_BTN, OUTPUT);
      mcp.digitalWrite(LZ_BTN, 0);
      Serial.println("push LZ");
   }else{
      mcp.pinMode(LZ_BTN, INPUT);
   }

   // Lボタン
   if((btn_val_update & 0b01000000) >> 6){
      mcp.pinMode(L_BTN, OUTPUT);
      mcp.digitalWrite(L_BTN, 0);
      Serial.println("push L");
   }else{
      mcp.pinMode(L_BTN, INPUT);
   }

    // RZボタン
   if((btn_val_update & 0b00100000) >> 5){
      mcp.pinMode(RZ_BTN, OUTPUT);
      mcp.digitalWrite(RZ_BTN, 0);
      Serial.println("push RZ");
   }else{
      mcp.pinMode(RZ_BTN, INPUT);
   }

   // Rボタン
   if((btn_val_update & 0b00010000) >> 4){
      mcp.pinMode(R_BTN, OUTPUT);
      mcp.digitalWrite(R_BTN, 0);
      Serial.println("push R");
   }else{
      mcp.pinMode(R_BTN, INPUT);
   }

  // データ更新
  input_val_old = input_val;
  delay(20);
}

終わりに

肢体不自由な息子でも簡単にゲームを楽しむ体験を創るためのコントローラハック。
今後は、画像認識と組み合わせて息子の苦手な操作を自動化する事や、視線入力モジュールと組み合わせる等の簡易入力も視野に入れつつ、使いながら必要な応用例を考えてみたいと思います~

合わせて、他のゲームにも対応できる様に応用例を検討中。
できれば、妻&娘が大好きなスプラトゥーンに対応させたいですが…素直にボタンを割合すると数が多くて操作が大変そうなので、どうやって簡易化&マイコンでのサポート機能を使うかは悩み中~

息子でも遊べそう&家族で楽しめそうなおすすめのゲームあれば教えて下さい~(^-^)