見出し画像

スト6出来ないからレバーレスコントローラー作った話

概要

スト6買ったら嫁に奪われた無聊を慰めるために、思い切ってレバーレスを自作してみようと思いました。何を言っているかわからねーと思うが以下略。

わたくし す゛こう すき すき た゛いすき


経緯説明

作成の経緯説明です。

シン・なんで作ろうと思ったのか

クラシックでレバーレスでガっとやるの格好いいからです。なお操作できるとは言っていない。

とはいえレバーレスコントローラーってお高いんですよね。有名どころのHitBoxはお値段3万だの5万だの……ちょっとお財布には優しくない。

ちょっと触るならサードパーティー製の安価なものと思ったんですが……こちらも最低1万2千ぐらいからのお値段。ちょっとまえは5千円ぐらいでなかったか???

でもこれらを見てると構造的に作れそうなんですよね。

ようは『ボタン』から『コントロール基盤』で解析した結果を『PCに伝達』するハードウェアなわけです。サーボモーターを使うでもなし、複数セグメントのLEDを制御して画面を構築するでもなし。超単純です。

ここでIOT御用達、安価な基盤の『RaspberryPi pico』という神マイコンがあるわけですよ。なんですかこれ、千円って安すぎませんか。

さらにプログラムなんて商業でなければ開発ツールは無料ですし、処理内容からしても問題ない肌感覚。ようは1秒の1/60がフレーム=17ミリ秒弱で入力を処理すればいいわけですしね。100万件データのバッチ処理でなし有り余る猶予ではないでしょうか。

しかし物理ボタン単品がやばい。よく使われるボタンが9千円超えてました。個数なかったので下手すると1個の価格は正気の沙汰では……。じゃあノーブランド品はと思えばこちらも1個600円。

ちなみにこれらは『ボタンケース+銀軸キースイッチ』なんですよね。なおそれなりの銀軸キースイッチなら1個200円……ならもうケースも作ったほうが良さそうです。さらにホットスワップも対応して面白くしましょう。

基本設計

やれるとはいえある程度のプラン立ては必要、なのでイメージを図示化していきましょう。

全体構成

今回やりたい構造を簡単に図にしてみました。

システム構成図

やりたいことはレバーでのキー入力の実現です。

ハード構成は『ボタン入力を検出』して『Raspberry Pi』で解析、『USBケーブル』伝で送信』します。

ソフトウェア構成はInputとしてピン情報を読んで、パソコン用に認識コードに置き換えて出力します。このときSOCDクリーナーという仕様があるのでそれにも対応します。

なおRaspberry Piについては凡例が数多あるので、あまり迷う事もなさそうです。

ハードウェア制作

材料集め

基本構成は次のような形で構築します。
ちなみに最安構成というよりは、回路構築を楽しむという方面で揃えています。

  • 筐体

    • A4サイズの厚型ツールボックス:百均のケースでも良かったんですが、ちょっとペラいのでちゃんとしたやつを買います。ただ切削を行うため、ポリ素材のやつをピック。

    • MDFボード:内部構造の土台、および、天板として4mm厚と2mm厚を購入。なんだかんだ丈夫で加工がし易いのはあまりに強い。

    • 各種ネジ:固定のための皿ネジ。なんだかわからないが皿ネジはスマートな印象がある。いいよね皿ネジ。

    • プラバン:アクリル板クッソ高いのでケチった結果とりあえず保護膜的な形で購入。後日後悔筆頭

  • 回路系

    • RaspberryPi pico:今回のコア。この汎用性で1000円はやはり神なのでは???

    • ブレッドボード:トライアンドエラーが多くなりそうなので、ブレッドボードによるピン差し回路構築が楽そうなので採用

    • ピン付きケーブル:ブレッドボード使うので合わせて購入。

    • 並列40ピンヘッダー:RasberryPiにはんだ付けして利用する拡張ピン。大幅に余るので、キースイッチからの配線でピン化するのにも利用。

    • 被覆銅線:キースイッチから伸ばすための線として購入。

    • USBケーブル(microメス/microオス):RasberryPiのUSB端子の摩耗を避けるためのスペーサー的な意味で購入。また箱の外に端子を持ってくるとき、配置を自在にする目的もある。

    • 銀軸キースイッチ:とりあえず銀軸という点だけを考慮したものを20購入。まぁ怪しいベンダーのやつじゃないから多分問題なし。

    • ホットスワップ用キーソケット:Amazon調べで怪しいベンダーからしか買えなかった……あまりに悲しい。とはいえ用は足りてるのでヨシ

  • 工具系

    • 今回追加購入した工具はありませんでした。結果で言えば大戦犯です

工作について

今回はMDF材を切り出して箱に嵌め込む形にします。
さらにボタン穴は自分の手指の位置から場所を決定し、次の思想で開けてます。

  1. 左手が方向で4つ(薬指、中指、人差し指、親指)

  2. 右手が各種攻撃要素で8つ(小指、薬指、中指、人差し指)

  3. 拡張位置で4つ(右手親指、両手親指の下、左手小指、左手中指の上)

  4. コントロール系で5つ(画像の左上位置のポチボタン)

レバーレスの筐体

キースイッチ16ボタン、コントロール系5ボタンの、変則21ボタンの構成となります。

なおPCからの認識としてはスティックなしのジョイコントローラー扱い(A、B、X、Y、L1、L2、L3、R1、R2、R3、SELECT、HOME、START、キャプチャ、↑、↓、→、←で18ボタン)なので、機能しないボタンが3つほど発生する形になります。

なおこの死にボタン、完成まで全く気づいていませんでした。これもまた試作の醍醐味です……。

ちなみに今回の切削加工ですが、ホールソーが無い(いっこ三千円にビビり、ケチって買いませんでした)ので、古き良き小型ドリルでくり抜いてヤスリ処理と言う古典方式でやりました。

当然ですが、穴は開いたが精度が悪い。

ワタクシプラモをよく作るので、ある程度工具は揃ってるんです。でもこうした大物加工を精度よく作るほどの設備じゃないんですよね。とはいえ普段から切削加工している訳ではないのでこんなものです。穴なんて開きゃあいいんすよ開きゃあ(この後開けた16穴分無限調整編が始まる予定)。

ボタン開発

ボタンもフルスクラッチします。ようは原型を作り、型を作ってレジン複製していきます。

なお原型を作るに当たり、ちゃんと加工しやすい素材を選ぶべきなんですが……なんとプラ板ケチって厚紙の積層で作り始めました。いや、ちょうど手元に厚紙があったので……。ちなみにプラ板は山程ありました。なんでケチったのか自分でも全く覚えていません。

作成した原型のした第一敲、第二敲がこちら。
左がボタン30mm、右がボタン26mmで作成しています。隙間は瞬間接着剤で硬化後ヤスリで削ったり、溶きパテで埋めたりとしています。

ボタン原型

しかし厚紙。切り貼りはし易いんですが、細かい研磨や切削は柔らかすぎて難しいですね。瞬間接着剤で硬化すればヤスリがけできるとはいえ……やっぱプラ板しか勝たん

とはいえ原型としては成立してはいますので、最終的に第二敲を採用。以下パーツ分割で決定しました。

ボタン原型最終稿
  • 右上:ボタン押下部

  • 左上:スペーサー

  • 左下:ソケット本体(キーソケット埋め込み用の土台)

  • 右下:キーソケット蓋

なお銀軸とボタンを接続するパーツは、レジン用キートップシリコン型があるので活用します。

ここからフィニッシャーとして、ラッカー塗料のクリアを吹いて保護膜を形成しました。流石に厚紙のままだと、型取りするときシリコンにくっついて都合が悪いし、表面がガタつくのを多少は抑え込めます。

ボタン原型

これで原型は出来上がったので、シリコンで型を取ります。ちなみにシリコンをケチったので後ほど山のように後悔します。

ボタンのシリコン型

ケチったのは左のフタ部分ですね。薄くしすぎて安定しなくなっていました。結果、

  1. 薄いため密着時の圧迫がたりず、レジンのおもらしが頻発

  2. 蓋への圧迫を強化

  3. 型が歪む

  4. 出力するレジンパーツの精度が落ちる

という悪循環に陥りました。

型取り中の光景
出力したレジンパーツ

これらパーツのバリ取りなど行って最終的にボタン及びソケットが完成しました。

複製したボタンケースとボタン

とはいえ大分歪んでいるので、個別に調整していく必要があります。削っては合せ、削っては合せ……。ちなみに平均誤差1ミリは、このサイズでは致命的と言って良いヤバさです。

ケチる場所を間違(白目

まあいいんです、自分しか使わないので、好きなようにやればいいんです。

なおワタクシの家には3Dプリンタがあるので、Blenderなり使えば原型作れたはずでした。16個作り終えた後に気づいた……次に活かしましょう。

ケースが出来たらソケットを実装していきます。ここからは電子工作ですね。

ソケットパーツ

今回は基盤がないので、ホットスワップ用のパーツを、被覆導線に直接はんだ付けしました。またブレッドボード運用するので、40ピンヘッダーの2ピンを割りとってはんだ付けしています。

すべて合体させると、ようやっとボタン一個ができるという形です。

完成品のソケット

筐体、ボタンが出来たので実装していきましょう。然るべき場所にネジ止めして、配線は底面に逃がします。

ボタンを筐体に実装

逃がした配線は、ブレッドボード上に配備したRaspberryPiへと接続します。

Raspberry Piからの配線
配線の実装

うーん、なんかスマートでない感じが……とはいえ、結線で行う以上仕方ない面はあります。これを基盤でやると、クソデカ汎用基板の外注になるので断念しました。流石にはCAD無いので設計出来ないんですよね。

今回はエラーが多いトライアンドエラーなので、ここは割り切ります。

ソフトウェア実装

Raspberry Piくんは現在中身がからっぽなので、内容をプログラミングしていきます。

前提

まずレバーレスのファームウェアとして有名なMulti-Platform Gamepad Firmware for Raspberry Pi Pico and other RP2040 boardsこと、GP2040-CEくんがつかえません

何故ならボタンが18個しかないから。あたりまえだよなぁ?
調子こいて21ボタンにしたのがもう終わりの合図でした。悲しいなあ。

となるとイチからつくるか、このプログラムを改造するかどちらかになりますが、全容理解よりとりあえず動かしたかったので自作することにしました。

より単純なファームウェアサンプルがを見つけたので、先人に倣ってベースを組み立てていきます。

参考サイト

ベースとしてはこちらの記事を参考にしています。

Raspberry Pi Pico で薄型レバーレスコントローラーの自作

こちらはC/C++環境であるRaspberry Pi上に、Rust言語で実装するという内容でした。まずRust言語事態が触ったこと無いので、勉強がてら触ってみます。

また開発のためのVSCodeのセットアップは適宜実施していきます。

実装のポイント

実装にあたり次の点を目標とします。

  • 21ボタンのコントローラーにしたい。

  • SOCDクリーナーは実装する。

  • 不可解な点やバグは修正する。

P1.21ボタンにするには

以下の部分が該当するようです。

// HID descriptor for Gamepad
// usage_maxがボタン数の定義情報。
#[gen_hid_descriptor(
	(collection = APPLICATION, usage_page = GENERIC_DESKTOP, usage = GAMEPAD, ) = {
		(usage_page = BUTTON, usage_min = 1, usage_max = 21) = {
			#[packed_bits 21] #[item_settings data,variable,absolute,not_null,no_wrap,linear] buttons = input;
		};
	}
)]
// 配列をu8型、2バイトから4バイト指定にして、21ボタン指定可能にする。
struct GamepadReport {
	buttons: [u8; 4],
}

上記の通り、usage_maxを21に指定することでボタン数を拡張。
すると構造体の配列buttonsの容量が足りなくなるのでbuttonsの定義情報は拡張しておきます。

またボタン定義も5つ追加を行います。extension button部ですね。


// HID descriptor for GamePad
struct GamePad {
	// left, up, right, down
	btnl: Pin<Gpio11, PullUpInput>,
	btnd: Pin<Gpio10, PullUpInput>,
	btnr: Pin<Gpio9, PullUpInput>,
	btnu: Pin<Gpio8, PullUpInput>,

	// button 1 ~ 8
	btn01: Pin<Gpio3, PullUpInput>,
	btn02: Pin<Gpio2, PullUpInput>,
	btn03: Pin<Gpio1, PullUpInput>,
	btn04: Pin<Gpio0, PullUpInput>,
	btn05: Pin<Gpio7, PullUpInput>,
	btn06: Pin<Gpio6, PullUpInput>,
	btn07: Pin<Gpio5, PullUpInput>,
	btn08: Pin<Gpio4, PullUpInput>,

	// option button 1 ~ 4
	opt1: Pin<Gpio15, PullUpInput>,
	opt2: Pin<Gpio14, PullUpInput>,
	opt3: Pin<Gpio13, PullUpInput>,
	opt4: Pin<Gpio12, PullUpInput>,

	// extension button
	btn09: Pin<Gpio19, PullUpInput>,
	btn10: Pin<Gpio18, PullUpInput>,
	btn11: Pin<Gpio17, PullUpInput>,
	btn12: Pin<Gpio16, PullUpInput>,
	opt5: Pin<Gpio20, PullUpInput>,
}

定義情報はそれぞれのIOピンを定義して……


    let gamepad = GamePad {
        btnl: pins.gpio11.into_pull_up_input(),
        btnd: pins.gpio10.into_pull_up_input(),
        btnr: pins.gpio9.into_pull_up_input(),
        btnu: pins.gpio8.into_pull_up_input(),

        btn01: pins.gpio3.into_pull_up_input(),
        btn02: pins.gpio2.into_pull_up_input(),
        btn03: pins.gpio1.into_pull_up_input(),
        btn04: pins.gpio0.into_pull_up_input(),
        btn05: pins.gpio7.into_pull_up_input(),
        btn06: pins.gpio6.into_pull_up_input(),
        btn07: pins.gpio5.into_pull_up_input(),
        btn08: pins.gpio4.into_pull_up_input(),

        opt1: pins.gpio15.into_pull_up_input(),
        opt2: pins.gpio14.into_pull_up_input(),
        opt3: pins.gpio13.into_pull_up_input(),
        opt4: pins.gpio12.into_pull_up_input(),

        btn09: pins.gpio19.into_pull_up_input(),
        btn10: pins.gpio18.into_pull_up_input(),
        btn11: pins.gpio17.into_pull_up_input(),
        btn12: pins.gpio16.into_pull_up_input(),
        opt5: pins.gpio20.into_pull_up_input(),
    };

押下検知したピンに対して、コードとして返します。
この場合、特定Bitを1↔0指定することで、押下ボタン表現を行っていきます。

// get button input
fn get_btn_input(&self) -> u32 {
    let mut state: u32 = 0;

    if self.btn01.is_low().unwrap() {
        state |= 1_u32 << 4;
    }

    if self.btn02.is_low().unwrap() {
        state |= 1_u32 << 5;
    }

    if self.btn03.is_low().unwrap() {
        state |= 1_u32 << 6;
    }

    if self.btn04.is_low().unwrap() {
        state |= 1_u32 << 7;
    }

    if self.btn05.is_low().unwrap() {
        state |= 1_u32 << 8;
    }

    if self.btn06.is_low().unwrap() {
        state |= 1_u32 << 9;
    }

    if self.btn07.is_low().unwrap() {
        state |= 1_u32 << 10;
    }

    if self.btn08.is_low().unwrap() {
        state |= 1_u32 << 11;
    }

    // addon buttons
    if self.btn09.is_low().unwrap() {
        state |= 1_u32 << 12;
    }

    if self.btn10.is_low().unwrap() {
        state |= 1_u32 << 13;
    }

    if self.btn11.is_low().unwrap() {
        state |= 1_u32 << 14;
    }

    if self.btn12.is_low().unwrap() {
        state |= 1_u32 << 15;
    }

    state
}

// get option button input
fn get_opt_input(&self) -> u32 {
    let mut state: u32 = 0;

    if self.opt1.is_low().unwrap() {
        state |= 1_u32 << 16;
    }

    if self.opt2.is_low().unwrap() {
        state |= 1_u32 << 17;
    }

    if self.opt3.is_low().unwrap() {
        state |= 1_u32 << 18;
    }

    if self.opt4.is_low().unwrap() {
        state |= 1_u32 << 19;
    }

    // addon buttons
    if self.opt5.is_low().unwrap() {
        state |= 1_u32 << 20;
    }

    state
}

P2.SOCDクリーナー

見た時なんなんだこれ? と思っいましたが、正式名称『Simultaneous Opposite Cardinal Direction Cleaner』(反対入力を同時に行った場合の整合措置)とのこと。

機能からしてもそのままの意味で、スト6においてはHitboxと同じく次の要件を満たすものとのことです。

  • 方向キー左と右を同時に入力した時、ニュートラルとして扱う。

  • 方向キー上と下を同時に入力した時、上入力として扱う。

参考コードでは上下入力がニュートラルになるので、上入力優先に書き換えていきます。

    // SOCD cleaner
    fn socd_cleaner(&self, hat_state: u32) -> u32 {
        let mut state = hat_state;

        // left and right, up and down
        let lr = 1_u32 | (1_u32 << 2);
        let ud = (1_u32 << 1) | (1_u32 << 3);

        // if left and right are pressed at the same time, ignore both of them.
        if (state & lr) == lr {
            state &= !lr;
        }

        // if up and down are pressed at the same time, ignore both of them.
        if (state & ud) == ud {
            state &= !ud; // 上下入力をBit計算で除外
            state |= 1_u32 << 3; // up input指定
        }

        state
    }

既存コードに追加なので、↑↓入力をOFFにしたあと、上入力だけ有効にしています。

が、データ該当時↓入力だけOFFにすればいいのであんまり良くないコードです。

P3.バグ修正

参照コードの仕様上、HORI PADとして認識する設定になっています。このような仕様について、意図しない動作で機器破損の可能性があるようです。

USBのベンダーIDとプロダクトIDの話

そうなるとベンダーID(会社)とプロダクトID(製品)は正規のもの、少なくとも汎用コードに置き換えておいたほうが良さそうとわかりました。

    // Create a USB device with a fake VID and PID
    // 汎用:シリアル番号識別用: VID=0x16C0 PID=0x27DC
    // 仮値: /* HORI RAP3 */0x0F0D, 0x0011
    let usb_dev = UsbDeviceBuilder::new(bus_ref, UsbVidPid(0x16C0, 0x27DC))
        .manufacturer("isofurabonjour")
        .product("pico hitbox extensionVer")
        .device_class(3) // HID
        .build();

コンパイルおよびインストール

仕上がったコードは通常通りコンパイルして、Raspberry Piにインストールします。

その時点でWindowsからの認識とボタンの状態が21ボタンと認識され、上手く動いていることがわかります。

またSOCDクリーナーについても同様に、同時入力で意図した結果になることを確認しました。

しかしこれらのボタンはなんの役割をもたない、入力だけを伝えるだけのものとなっています。たとえばどのボタンを押しても、STARTボタンと同じ動作にはなりません。

ボタン割り当て

ボタン割当については、汎用ジョイコンである点から、Steamのコントローラー設定に依存させる形にします。

なので、こちらは好きなように設定を行いコントローラーとして認識されるようになりました。

完成

以上で自作レバーレスコントローラーが完成しました。
なおプラバンの下に絵を仕込めば、なんかオリジナルのいい感じ装飾ができる形でうs。

完成形

実際にはプラバンが歪んで、指に引っかかってコマンド入力できないという致命的欠陥を抱えることに。これは良くなかったですね。

まとめ

端的に奥が深すぎる

今回ノリと勢いで作って完成までこぎつけましたが、やはり精密機器として精度求められる加工が結構多い。相応に気を使う必要が多くありそうです。

レバーレスとして加工しやすいように分厚い箱で制作したのですが、やはりデカイと取り回しが面倒なので、もっとコンパクトに薄くする検討をしておくべきでした。まぁ初めてレバーレスを触るので仕方なかったといえばそれまでではあります。

それに薄型にする場合剛性の問題で箱の選定なども気を使う必要がありそうです。ホールソーなども利用する前提で設計するべきでした。

表面加工も、操作してみるとわかるのですがつるつるした面で作成するべきだとわかりました。ただしプラバンのようにぺらっぺらの変形しやすい素材は使ってはいけないことも(考えてみれば当たり前ですが)わかりました。そもそもコマンド入力を阻害するのは致命的ですからね。

ソフトウェア面でもまだ改善の余地がありそうです。
まず死にボタンを拡張して、何らかの機能をもたせるのは面白そうです。GP2040-CEくんも、色々拡張ピンが存在していますし、単純にLEDを光らせるなども楽しく出来そうですね。

ちなみに今回の材料費は、結局1万ぐらいかかっている計算になります。とはいえ既存の品を買うより、仕組み学習として楽しかったので非常に良かったと思います。

とりあえず今回作ったものを不満が出るまで使い倒して、次へのナレッジにためていきたいですね。

この記事が気に入ったらサポートをしてみませんか?