GCコンは良いぞぉ!!!
このフォルム、この軽さ、スマブラに特化したボタン配置、ボタンを押し込んだ時の感触、これこそ任天堂が開発した至高のコントローラー、そうは思いませんか!?
1. 研究の背景
久しぶりにスマブラSPで遊びたくて2月の初めあたりにGCコンを買いました。昔と違って1時間もトレモで遊ぶと満足してしまい枕元にSwitch共々片付けてしまいました。
2/8くらいに別記事で1月の間に研究していた内容をまとめた後次はロボットカーの学びを深めてミニ四駆を改造することとかも考えてましたが去年買うかどうか悩んだ挙句結局買わなかったロボットボールSphero Boltのイメージが脳裏に降りてきました。と同時にミニ四駆コースの一角でロボットボールを転がして友人たちを驚かす青写真が。速さを競ってマシンがひたすら走るのを尻目に淡々とコースアウトしないように人の手で操縦されていくロボットボール…良い…!!そしてすぐさまコースを走らせても差し支えない大きさのSphero MiniをAmazonでポチりました。
後は専用のアプリで制御しても良かったかもしれませんが元ゲーマーとしてはそれだけだと何か味気ないと感じました。
せっかくなら自分が良いと思ったコントローラーでロボットボールを動かし、ラジコン感覚で遊べるようにしたい…!!
以上が研究の動機になります。
用意するもの
- Spheroロボットボール
- ゲームパッド(ゲームキューブコントローラーでもSwitchプロコンでも好きなものを)
- Bluetooth対応PC(大体のノートPCなら標準搭載しているはず)
2. 制御スクリプトを作ってみた
Spheroシリーズはアプリ上ではJavaScriptとPythonのコーディングに対応しています。ですが致命的なことに、外部のコントローラーの入力を受け取るためのライブラリーは使えないためGCコンの入力を受け取り、かつSpheroを制御するためにはオリジナルのスクリプトを組みアプリ外で動かす必要がありました。
調べてみましたところPythonであればそれについてspherov2やpygameといったゲームパッドの入力値の検出やspheroの制御に必要なライブラリー一式が揃っていることが判明しましたのでPythonで構築してみることにしました。
そして、こちらのサンプルコードを拡充して制御スクリプトを組んでみました。npakaさんのコードわかりやすくてありがたかったです。
from spherov2 import scanner
from spherov2.sphero_edu import SpheroEduAPI
from spherov2.types import Color
import pygame
import time
# 初期化
pygame.init()
pygame.joystick.init()
toy = scanner.find_toy()
# ジョイスティックの取得
if pygame.joystick.get_count() == 0:
print("ジョイスティックが見つかりません")
exit()
joy = pygame.joystick.Joystick(0)
joy.init()
print("使用デバイス:", joy.get_name())
# 状態保持
prev_btnA = False
prev_btnB = False
prev_btnC = False
with SpheroEduAPI(toy) as droid:
try:
droid.set_main_led(Color(r=0, g=0, b=255))
# メインループ
while True:
# イベントの更新
pygame.event.pump()
# アナログ入力
x = joy.get_axis(0) # 左右
y = joy.get_axis(1) # 上下
# デジタル入力
btnA = joy.get_button(0) == 1 # A
btnB = joy.get_button(1) == 1 # B
btnC = joy.get_axis(5) > 0.5 # C
# 結果表示
if x < -0.5:
print("←")
droid.roll(90, 255, 0.0167)
elif x > 0.5:
print("→")
droid.roll(270, 255, 0.0167)
if y < -0.5:
print("↑")
droid.roll(0, 255, 0.0167)
elif y > 0.5:
print("↓")
droid.roll(180, 255, 0.0167)
if btnA and not prev_btnA:
print("A")
droid.set_main_led(Color(r=0, g=255, b=0))
if btnB and not prev_btnB:
print("B")
droid.set_main_led(Color(r=255, g=0, b=0))
if btnC and not prev_btnC:
print("C")
droid.set_main_led(Color(r=0, g=0, b=255))
# 状態保持
prev_btnA = btnA
prev_btnB = btnB
prev_btnC = btnC
# スリープ
time.sleep(0.05)
except KeyboardInterrupt:
droid.set_speed(0)
droid.set_main_led(Color(r=0, g=0, b=0))
print("終了しました")
2/14にこちらのスクリプトで動かした結果が以下のポストです。え、バレンタイン?知らぬ。
バーチャル嫁×ロボットアームの次に始めた研究
— ウラン (@hexanitrobenzen) February 14, 2026
PC上で動くGCコンとspheroの連携スクリプトはすんなり作れたけれども pythonista上でも動かそうとして沼
iOSのBLEくっそむずい pic.twitter.com/sRpufjyqi7
この時のポストでも触れていますがiOSのPythonistaでも動かそうとしました。ですが最新のバージョン(v3.4)では依存ライブラリーをいじくり回してもBluetoothの壁を突破できず断念しました。
当面はPCからの制御だけでいいかと考え、本記事を執筆し始めた2/21に遊んでいましたところまだ課題として以下の問題が残っていました。
- 上下左右にLスティックを倒し切った時にしか反応しないため操作性がいまいち
- 向きを調整し直してもspheroが予期しない方向に転がってしまう
この辺についてまだ改善の余地があると考えスクリプトを修正することにしました。
3. スクリプトを修正してみた
3.1. 操作性の改善
まず、Lスティックについて操作性の改善をするための制御を考えてみました。イメージとしてはカービィのエアライドのウエライドモードで使える赤いウエライドマシン(Lスティックを上下左右に倒すとその方向にキャラクターが向く)です。エアライダー?Switch2はまだ買う気ない。
そのためには、スティックが上下左右に倒された場合のみ反応するのではなく、右上とか左下とかの方向に倒したとしてもその方向にspheroが転がる必要がありました。
pygameでは、スティックがX軸方向やY軸方向でどのくらい倒されているかの入力値を取得できますのでXとYの成分からベクトルを求め、そこから0~360の度数に変換できないかと考えました。
そのあたりベクトルが分かれば何とかなるのではないかと考えリサーチついでにAIに聞いてみましたところ逆正接(アークタンジェント)で簡単に求められるとのことでした。文系出身エンジニアだからXY成分からのベクトルの算出は思いついても逆正接の発想はなかった…。
逆正接でスティックの方向は算出できることは判明しましたが、実際に動かしてみるとスティックは上に倒しているのにロボットが前進するのではなく左に移動してしまいました。これはspherov2の実装で転がる方向が0°の方向だと前進するわけですが、スティックの上方向の入力値をそのまま逆正接で変換すると90°に変換されてしまうからでした。
そこで、そこについては逆正接でスティックの方向を0~360に変換後に差分の90を差し引くロジックにしました。
ただ、これだけでは左右が反転してしまう問題が残ってしまっていましたので以下の形で左右の反転も解決しました。
angle_rad = math.atan2(-y, -x)
これにより、操作性は著しく改善でき、直感的な操作ができるようにはなりました。
3.2. spheroの進行方向のリセットの必要性
もう一つの問題は操作性は改善されたけれどもプログラムを実行してLスティックを上に倒してみると、意図しない方向に依然として転がり続けてしまう事でした。カプセルを開けてロボットボールの本体の向きを素手で修正してもすぐに間違った方向に向いてしまいました(例:北方向に前進させたいけれどもLステ上入力で南に向かって転がる、カプセルを開けて本体の向きを是正しても南方向に向き直ってしまう)。
この問題自体は2/14にスクリプト動かした段階で薄々気づいていましたが手作業で向きを補正したら良いかくらいで考えていました。しかし、今回さらに調べていてそれでは解決しないと気づき、制御スクリプトの改善を余儀なくされました。
改めてspherov2のコードを確認してみましたところ求めていた処理もきちんと用意されていました。それがreset_aimメソッドです。多分これspheroの進行方向をリセットするための処理じゃないかと推測し、実装に組み込んでみたところ狙った通りにYボタンを押したら押す前に進んでいた方向を基準にリセットし、押下後スティックを上方向に倒したところ意図した通りに前進したい方向に前進するようになりました。
任天堂ゲームファンとしてはこのリセット処理ニュートラルポジションのコマンド(Lトリガー + Rトリガー + start)で動作するようにしたかったですがpygameで目的のボタンの入力を取得できるかどうかよくわからなかったですので一旦はYボタン入力でリセットされるようにしました。
3.3. 修正後のスクリプト
修正後のスクリプトは以下の通りになります。
from spherov2 import scanner
from spherov2.sphero_edu import SpheroEduAPI
from spherov2.types import Color
import pygame
import time
import math
toy_name: str | None = None
# 初期化
pygame.init()
pygame.joystick.init()
toy = scanner.find_toy(toy_name=toy_name)
# ジョイスティックの取得
if pygame.joystick.get_count() == 0:
print("ジョイスティックが見つかりません")
exit()
joy = pygame.joystick.Joystick(0)
joy.init()
print("使用デバイス:", joy.get_name())
# 状態保持
prev_btnA = False
prev_btnB = False
prev_btnC = False
prev_btnD = False
with SpheroEduAPI(toy) as droid:
try:
droid.set_main_led(Color(r=0, g=0, b=255))
# メインループ
while True:
# イベントの更新
pygame.event.pump()
# アナログ入力
x = joy.get_axis(0) # 左右
y = joy.get_axis(1) # 上下
# デジタル入力
btnA = joy.get_button(0) == 1 # A
btnB = joy.get_button(1) == 1 # B
btnC = joy.get_axis(5) > 0.5 # C
btnD = joy.get_button(3) == 1 # D
# ベクトルの大きさを計算(スティックの傾き量)
magnitude = math.sqrt(x**2 + y**2)
# スティックが一定以上傾いている場合のみ移動
if magnitude > 0.3: # デッドゾーンの設定
# ベクトルの角度を計算(ラジアン)
angle_rad = math.atan2(-y, -x) # x軸を反転して左右の方向を修正
# 角度を度数法に変換し、スティック上向きが0度になるよう調整
angle_deg = math.degrees(angle_rad) - 90
if angle_deg < 0:
angle_deg += 360
# 速度をベクトルの大きさに基づいて調整(最大255)
speed = min(int(magnitude * 255), 255)
# ロボットを移動
droid.roll(math.floor(angle_deg), speed, 0.0167)
print(f"方向: {angle_deg:.1f}度, 速度: {speed}")
else:
# スティックが中央の場合は停止
droid.set_speed(0)
if btnA and not prev_btnA:
print("A")
droid.set_main_led(Color(r=0, g=255, b=0))
if btnB and not prev_btnB:
print("B")
droid.set_main_led(Color(r=255, g=0, b=0))
if btnC and not prev_btnC:
print("C")
droid.set_main_led(Color(r=0, g=0, b=255))
if btnD and not prev_btnD:
print("D")
droid.reset_aim()
# 状態保持
prev_btnA = btnA
prev_btnB = btnB
prev_btnC = btnC
prev_btnD = btnD
# スリープ
time.sleep(0.05)
except KeyboardInterrupt:
droid.set_speed(0)
droid.set_main_led(Color(r=0, g=0, b=0))
print("終了しました")
修正後のスクリプトの動作確認がこちらのポストです。
Qiita記事掲載用
— ウラン (@hexanitrobenzen) February 21, 2026
スクリプト改良後の動作確認
走らせすぎてバッテリー残量わずか pic.twitter.com/au5GQ8lUyh
4. まとめ
Sphero社のロボットボールも触ってみたいなあと考えていたものでしたのでこの機会に色々研究してみましたが、子供用プログラミング教育のおもちゃだけにとどまらず大の大人が本気で遊ぼうとするとこういうこともできるからこそ可能性に満ちていると思いました。
それこそ、子持ち世帯とかで父親がロボットボール買ってラジコン感覚で遊んで見せて興味を持った子供がプログラミングにのめり込むとかも子供の可能性を広げる出来事になるかもしれないです。例えば、子供が遊んでいるSwitchのコントローラーをノートPCとペアリングさせて今回の記事執筆に際して作成したスクリプトでロボットボール制御して子供に遊ばせるとか。
スマブラSPで遊びたくて買ったGCコンがゲームよりも研究開発の方でがっつり使ってしまうことは私も予想していませんでした。
今回作成したスクリプトはこちらからダウンロードして使用できます。遊んでみたい方はどうぞ。最低限の機能しか追加していないですので適宜機能拡張もご自由にどうぞ。ロボットボールを動かすことができたら「すごーい!!」と子供から尊敬される可能性は高いかもしれません。
そして最後にもう一度、
GCコンは良いぞぉ!!!
追記:ミニ四駆コース走らせてみたら坂道が登れない致命的な問題発生。完走させるためにそのうち機体の設計からフルスクラッチ開発もう一度頑張るか…。

