Arduino×Pythonでリアルタイム握力計測ゲーム|コピペOKのコードと応用

プログラミング・電子工作






【リアルタイム握力計測ゲーム!】Python×Arduino×Pygameで作る握力計測ゲームを徹底解説 | YOFUKASI LAB


目次


ARDUINO × PYTHON × PYGAME / DIY計測ゲーム

【リアルタイム握力計測ゲーム!】

〜Python×Arduino×Pygameで作る握力計測ゲームを徹底解説〜

握力計測ゲームの実機検証の様子
実機で組み上げた握力計測ゲームのテスト風景

01はじめに

みなさん、ゲームの世界ではボタンを押す強さはほとんど関係ありませんよね。しかしもし現実の「握力」を使ってゲームができたらどうでしょう? Arduino を使ってセンサーから取得した「握力」データを Python で受け取り、Pygame を使って可視化・ゲーム化する。想像しただけでワクワクする実験的なプロジェクトです。

本記事では、初心者の方でも理解できるように、シリアル通信(Arduino → Python)や Pygameを使ったPythonゲーム開発の基礎 を中心に、この握力ゲームのコードを徹底的に分解・解説します。理学療法士として臨床現場で握力測定や機能評価に携わってきた筆者が、実機検証をしながらまとめました。

内部リンク・関連記事【究極ガイド】Arduinoメインボード徹底解説 ― 初心者からプロまで、これだけ読めば全てがわかる!


02全体の流れ

  1. Arduinoからのデータ受信(シリアル通信)
    Arduino側でアナログセンサーから得た握力データを数値化し、シリアルポート経由でPythonへ送信します。
  2. Pygameによる画面描画
    Python側で受信した握力データをもとに、リアルタイムでグラフ(棒グラフ)の高さと色を変更します。ゲーム開始前(READY → GO)→ ゲーム中(RUNNING)→ 終了(FINISH)といった状態遷移を管理し、画面上に大きく文字を表示します。
  3. 制限時間管理と終了処理
    制限時間30秒間、グラフを動かし続けます。時間が経過したら「FINISH」を表示し、最終的にはゲームを停止します。

「制限時間内にどれだけ強く握力を出せるか」を測るゲームにするも良し、一定の値を狙うゲームにするも良し。いろいろとアレンジ可能です。本記事のサンプルコードをベースに、自由に発想を広げてみてください。仕組みの土台になっている Arduino-PC間のシリアル通信入門 も合わせて読むと理解が深まります。

03事前準備

ハードウェア

  • Arduino(UNOやNanoなどお好みでOK)
  • 握力センサー
    ここでは最大値を 0〜1023 で計測する単純なセンサーを想定しています。Arduinoのアナログ入力(A0など)に接続し、analogRead() で値を取得できるイメージです。※実際には圧力センサーやグリップセンサーなどを使うと良いでしょう。

内部リンク・関連記事【完全攻略】圧センサーを用いた電子工作の基礎編|圧センサーの種類、配線、読み取り値など徹底解説

握力・圧力のようなアナログ量をリハビリや動作分析に応用したい方は、以下の記事も参考になります。

ソフトウェア

  • Python 3.x
    Python 2.x ではなく3系を使いましょう。
  • Pygame(バージョンは2.0以上推奨)
    pip install pygame でインストールできます。詳細はPygame公式ドキュメントを参照してください。
  • PySerial
    pip install pyserial でインストールできます。API仕様はPySerial公式リファレンスで確認できます。
  • Arduino IDE または PlatformIO など
    Arduino側のスケッチを書き込むために使用。インストールがまだの方はArduino IDEのインストール手順まとめをどうぞ。

04Arduino側のコード(例)

本記事ではArduino側の詳しいコードは割愛しますが、シリアルモニタに対して握力センサーの値をそのまま出力するだけのシンプルなものを想定しています。例えば以下のようなイメージです。

内部リンク・関連記事【2025年版】Arduinoのコードの書き方|基礎から応用まで全てがわかる初心者の手引き

void setup() {
  Serial.begin(9600);
}

void loop() {
  int sensorValue = analogRead(A0); // センサーをA0に繋いでいる場合
  Serial.println(sensorValue);
  delay(50); // 50msごとに送信
}

このように常に握力値を送信しています。今回のPython側コードは、基本的にこの形式で送られてくる値を受信すると仮定します。analogRead() や delay() の挙動をもっと詳しく知りたい方は、analogRead / analogWriteの使い方まとめとdelay()とmillis()の違い・使い分けもあわせてどうぞ。

05Python側コード(完成版)

以下が今回解説するPythonコードの全体です。なお、後の章で細かく分解解説していきますので、いったんざっと眺めておきましょう。

import serial
import pygame
import sys
import time

# --- シリアル通信の設定 ---
SERIAL_PORT = 'COM3'  # ご利用の環境に合わせて変更してください
BAUD_RATE = 9600

try:
    ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=1)
except Exception as e:
    print(f"シリアルポート {SERIAL_PORT} に接続できませんでした: {e}")
    sys.exit()

# --- Pygame初期設定 ---
pygame.init()
WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Grip Force Meter Game")
clock = pygame.time.Clock()

# フォント設定
large_font = pygame.font.SysFont("Comic Sans MS", 72)
medium_font = pygame.font.SysFont("Comic Sans MS", 48)
small_font = pygame.font.SysFont("Comic Sans MS", 32)

# ゲーム状態とタイミング
READY_DURATION = 2000   # 2秒
GO_DURATION = 1000      # 1秒
GAME_DURATION = 30000   # 30秒

game_state = "READY"  # "READY", "GO", "RUNNING", "FINISH"
state_start_time = pygame.time.get_ticks()

# センサー(握力)関連
sensor_value = 0        # 現在の握力値(例:0~1023)
MAX_FORCE = 1023        # 握力計の最大値
MAX_THRESHOLD = 900     # 閾値。これ以上なら棒グラフの色を変える

# 棒グラフの設定
BAR_WIDTH = 100
BAR_MAX_HEIGHT = 300
BAR_X = WIDTH // 2 - BAR_WIDTH // 2
BAR_Y_BOTTOM = HEIGHT - 100  # 棒グラフの底位置

# カラー設定
BG_COLOR = (245, 245, 255)   # 明るいパステルブルー
BAR_COLOR_NORMAL = (100, 200, 100)  # グリーン
BAR_COLOR_MAX = (255, 100, 100)     # レッド(閾値超え時)
BAR_BORDER_COLOR = (50, 50, 50)

# 枠の描画関数
def draw_frame(surface):
    pygame.draw.rect(surface, BAR_BORDER_COLOR, (BAR_X-5, BAR_Y_BOTTOM - BAR_MAX_HEIGHT - 5, BAR_WIDTH+10, BAR_MAX_HEIGHT+10), 5)

# ゲーム状態のテキスト描画関数
def draw_state_text(surface, text):
    rendered = large_font.render(text, True, (255, 50, 50))
    surface.blit(rendered, (WIDTH // 2 - rendered.get_width() // 2, HEIGHT // 4 - rendered.get_height() // 2))

# メインループ
def main():
    global game_state, state_start_time, sensor_value

    running = True
    while running:
        dt = clock.tick(60)  # 60FPS
        current_time = pygame.time.get_ticks()

        # イベント処理
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

        # ゲーム状態の遷移
        if game_state == "READY" and current_time - state_start_time >= READY_DURATION:
            game_state = "GO"
            state_start_time = current_time
        elif game_state == "GO" and current_time - state_start_time >= GO_DURATION:
            game_state = "RUNNING"
            state_start_time = current_time
        elif game_state == "RUNNING" and current_time - state_start_time >= GAME_DURATION:
            game_state = "FINISH"
            state_start_time = current_time

        # センサー値の更新
        if game_state == "RUNNING":
            if ser.in_waiting:
                try:
                    line = ser.readline().decode('utf-8').strip()
                    sensor_value = int(line)
                except Exception:
                    pass  # 読み取りエラーの場合は無視

        # 画面背景の描画
        screen.fill(BG_COLOR)

        # ゲーム状態ごとの描画
        if game_state in ["READY", "GO", "FINISH"]:
            if game_state == "READY":
                draw_state_text(screen, "READY")
            elif game_state == "GO":
                draw_state_text(screen, "GO")
            elif game_state == "FINISH":
                draw_state_text(screen, "FINISH")
        elif game_state == "RUNNING":
            # ゲーム中は、棒グラフをリアルタイムに更新
            pass

        # 棒グラフ描画
        draw_frame(screen)
        bar_height = int((sensor_value / MAX_FORCE) * BAR_MAX_HEIGHT)
        if sensor_value >= MAX_THRESHOLD:
            bar_color = BAR_COLOR_MAX
        else:
            bar_color = BAR_COLOR_NORMAL
        bar_y = BAR_Y_BOTTOM - bar_height
        pygame.draw.rect(screen, bar_color, (BAR_X, bar_y, BAR_WIDTH, bar_height))

        # 棒グラフの数値表示
        value_text = small_font.render(f"{sensor_value}", True, (0, 0, 0))
        screen.blit(value_text, (BAR_X + BAR_WIDTH // 2 - value_text.get_width() // 2, bar_y - 40))

        # ゲームタイマーの表示
        if game_state == "RUNNING":
            remaining_time = max(0, GAME_DURATION - (current_time - state_start_time))
            seconds = remaining_time // 1000
            timer_text = medium_font.render(f"{seconds} sec", True, (50, 50, 255))
            screen.blit(timer_text, (WIDTH - timer_text.get_width() - 20, 20))

        pygame.display.flip()

    pygame.quit()
    ser.close()
    sys.exit()

if __name__ == "__main__":
    main()

06コード分解:シリアル通信の設定

import serial
import pygame
import sys
import time

# --- シリアル通信の設定 ---
SERIAL_PORT = 'COM3'  # ご利用の環境に合わせて変更してください
BAUD_RATE = 9600

try:
    ser = serial.Serial(SERIAL_PORT, BAUD_RATE, timeout=1)
except Exception as e:
    print(f"シリアルポート {SERIAL_PORT} に接続できませんでした: {e}")
    sys.exit()

解説

  1. import serial
    PySerialライブラリをインポート。pip install pyserial でインストールしておきましょう。
  2. SERIAL_PORT
    Arduinoが接続されているポートを指定します。Windowsなら COM3 や COM4、MacやLinuxなら /dev/tty.usbmodemXXXX や /dev/ttyACM0 等に変更が必要です。ポートが見つからない場合はArduinoのポートが認識されない時の対処法を参照してください。
  3. BAUD_RATE
    Arduino側の Serial.begin(9600); に合わせる必要があります。ここでは9600bps。
  4. ser = serial.Serial(...)
    実際にシリアルポートをオープンします。エラーが出る場合(ポートが開けない等)は except で例外処理し、プログラムを終了 (sys.exit()) しています。
  5. timeout=1
    1秒待っても読み取れない場合は処理を先に進める設定。無限に待ち続けないようにするための保険です。

シリアル通信の全パラメータは PySerial公式APIリファレンス に一覧があります。あわせてArduinoシリアルモニタでのデバッグ方法も読んでおくと、通信トラブル時の切り分けがスムーズです。

07コード分解:Pygameの初期設定

# --- Pygame初期設定 ---
pygame.init()
WIDTH, HEIGHT = 800, 600
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Grip Force Meter Game")
clock = pygame.time.Clock()

解説

  1. pygame.init()
    Pygameを使う際に必ず最初に呼び出す初期化関数。
  2. WIDTH, HEIGHT = 800, 600
    ウィンドウの幅と高さ。ここでは横800px、縦600pxに設定しています。
  3. screen = pygame.display.set_mode((WIDTH, HEIGHT))
    Pygameのメイン画面(Surface)を作成します。詳細はpygame.display公式ドキュメントを参照してください。
  4. pygame.display.set_caption("Grip Force Meter Game")
    ウィンドウのタイトルバーに表示される文字列を設定。
  5. clock = pygame.time.Clock()
    フレームレート管理に使うClockオブジェクトを生成します。後ほど clock.tick(60) を使って「60FPS」を目安に処理を進める形になります(pygame.time公式ドキュメント)。

Pygame以外の選択肢として、レトロゲーム風の表現がしやすいPythonゲームエンジン「Pyxel」の紹介記事やPyxelの入門ガイドも参考になります。

08コード分解:フォントやゲーム状態の管理

# フォント設定(好みのフォント名に変更可能)
large_font = pygame.font.SysFont("Comic Sans MS", 72)
medium_font = pygame.font.SysFont("Comic Sans MS", 48)
small_font = pygame.font.SysFont("Comic Sans MS", 32)

# ゲーム状態とタイミング
READY_DURATION = 2000   # 2秒
GO_DURATION = 1000      # 1秒
GAME_DURATION = 30000   # 30秒

game_state = "READY"  # "READY", "GO", "RUNNING", "FINISH"
state_start_time = pygame.time.get_ticks()

解説

  1. pygame.font.SysFont("Comic Sans MS", サイズ)
    Pygameのフォントを取得します。第一引数はフォント名、第二引数はサイズ。日本語フォントを使いたい場合は MSGothic 等を指定することもできますが、環境によっては文字化けするかもしれません(詳しい仕様はpygame.font公式ドキュメントを参照)。
  2. 状態遷移用の定数
    • READY_DURATION = 2000 → 「READY」状態を2秒間
    • GO_DURATION = 1000 → 「GO」状態を1秒間
    • GAME_DURATION = 30000 → 「RUNNING」状態を30秒間
  3. game_state = "READY"
    初期状態は「READY」に設定。
  4. state_start_time = pygame.time.get_ticks()
    状態が始まった時刻(ミリ秒単位)を保管。経過時間を計算するために使用します。後ほど if current_time - state_start_time >= READY_DURATION のように比較して状態遷移を管理します。

09コード分解:センサー関連の変数

# センサー(握力)関連
sensor_value = 0        # 現在の握力値(例:0~1023)
MAX_FORCE = 1023        # 握力計の最大値(Arduinoの値に合わせる)
MAX_THRESHOLD = 900     # 閾値。これ以上なら棒グラフの色を変える

解説

sensor_value
Arduinoから受け取った値を格納します。0〜1023の範囲を想定。

MAX_FORCE
センサーの最大値を設定。Arduinoの analogRead() は通常は 0〜1023 なので1023に設定しています。

MAX_THRESHOLD
この値以上の握力なら「最大レベル(赤色の棒グラフ)を表示させる」などの演出を入れたいときに使用します。

同じくアナログ入力を扱う例として、ポテンショメータとanalogReadの使い方やジョイスティックのアナログ値取得も同じ考え方で読み替えられます。


10コード分解:棒グラフの設定

# 棒グラフの設定
BAR_WIDTH = 100
BAR_MAX_HEIGHT = 300
BAR_X = WIDTH // 2 - BAR_WIDTH // 2
BAR_Y_BOTTOM = HEIGHT - 100  # 棒グラフの底位置
変数名 意味
BAR_WIDTH 棒グラフの幅(px)
BAR_MAX_HEIGHT 棒グラフの最大高さ(px)
BAR_X 棒グラフ描画の左上X座標
BAR_Y_BOTTOM 棒グラフの底辺のY座標(画面下から100px上)

たとえば握力が MAX_FORCE の時にこの BAR_MAX_HEIGHT(300px)を表示し、0の時は高さ0で描画するイメージです。

11コード分解:カラー設定

# カラー設定(背景や棒グラフ)
BG_COLOR = (245, 245, 255)   # 明るいパステルブルー
BAR_COLOR_NORMAL = (100, 200, 100)  # グリーン
BAR_COLOR_MAX = (255, 100, 100)     # レッド(閾値超え時)
BAR_BORDER_COLOR = (50, 50, 50)
  • RGB のタプル形式で色を指定。
  • PythonやPygameでは (R, G, B) が 0〜255 の範囲で色を表します。

12コード分解:フレーム描画関数

def draw_frame(surface):
    # やわらかいパステルカラーの枠
    pygame.draw.rect(surface, BAR_BORDER_COLOR, (BAR_X-5, BAR_Y_BOTTOM - BAR_MAX_HEIGHT - 5, BAR_WIDTH+10, BAR_MAX_HEIGHT+10), 5)

解説

  • 棒グラフの外枠を描画する関数。
  • 棒グラフより少し大きめの矩形を描画し、枠線の太さを引数の最後(4番目)に指定 (5) しています。図形描画全般の詳細はpygame.draw公式ドキュメントを参照してください。

13コード分解:ゲーム状態テキスト描画関数

def draw_state_text(surface, text):
    rendered = large_font.render(text, True, (255, 50, 50))
    surface.blit(rendered, (WIDTH // 2 - rendered.get_width() // 2, HEIGHT // 4 - rendered.get_height() // 2))

解説

  1. large_font.render(text, True, (255, 50, 50))
    指定した文字列をフォントでレンダリングし、Surface オブジェクトにします。第2引数の True はアンチエイリアスの有効化、第3引数 (255, 50, 50) は文字色を指定。
  2. テキストの中央揃え
    rendered.get_width() と rendered.get_height() を使い、画面の中央付近に文字を表示するように計算。ここでは画面横方向は中央揃え、縦方向は画面全体の1/4の位置に表示しています。

14コード分解:メインループの構造

def main():
    global game_state, state_start_time, sensor_value

    running = True
    while running:
        dt = clock.tick(60)  # 60FPS
        current_time = pygame.time.get_ticks()

        # イベント処理
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

イベントループ

  • while running: でゲームループを回します。
  • clock.tick(60) は「1秒間に最大60回ループを回す」目安です。Pygameで定番の制御方法。
  • for event in pygame.event.get(): でウィンドウの×ボタンなどのイベントを処理。pygame.QUIT イベントがあれば running = False にしてループを抜ける→ゲーム終了。

15コード分解:ゲーム状態の遷移

if game_state == "READY" and current_time - state_start_time >= READY_DURATION:
    game_state = "GO"
    state_start_time = current_time
elif game_state == "GO" and current_time - state_start_time >= GO_DURATION:
    game_state = "RUNNING"
    state_start_time = current_time
elif game_state == "RUNNING" and current_time - state_start_time >= GAME_DURATION:
    game_state = "FINISH"
    state_start_time = current_time

ここがゲームの流れを管理する重要な部分です。

  • READY → GO → RUNNING → FINISH のように順番に状態が移っていきます。
  • それぞれの状態に応じて表示されるテキストを変えたり、センサー読み取りのON/OFFをコントロールします。
状態 持続時間(ミリ秒) 画面表示 センサー読み取り
READY 2000 「READY」 しない
GO 1000 「GO」 しない(ここではOFF)
RUNNING 30000 握力グラフをリアルタイム更新 する
FINISH – 「FINISH」 しない

state_start_time を更新しないと、状態が切り替わった後も同じ経過時間が使われてしまいます。忘れずに切り替えのたびに current_time を代入。


16コード分解:センサー値の更新

if game_state == "RUNNING":
    if ser.in_waiting:
        try:
            line = ser.readline().decode('utf-8').strip()
            sensor_value = int(line)
        except Exception:
            pass  # 読み取りエラーの場合は無視

if game_state == “RUNNING”:
ゲーム中(RUNNING状態)のみセンサー値を更新します。ゲーム前や終了後は値を固定したまま。

ser.in_waiting
受信バッファに読み込めるデータがあるかチェック。1行分のデータが来ていなければ読み取りをスキップ。

line = ser.readline().decode('utf-8').strip()
シリアルから1行読み取って文字列に変換し、両端の空白や改行を削除。

sensor_value = int(line)
数値化して sensor_value に代入。ここでエラーが起きた場合(不正な文字列など)は except Exception で無視するだけにしてあります。int() の詳しい仕様はPython公式ドキュメントを、変数の型変換全般はint・float・stringの違いと変換方法を参照してください。

17コード分解:画面背景と状態別描画

# 画面背景の描画
screen.fill(BG_COLOR)

# ゲーム状態ごとの描画
if game_state in ["READY", "GO", "FINISH"]:
    if game_state == "READY":
        draw_state_text(screen, "READY")
    elif game_state == "GO":
        draw_state_text(screen, "GO")
    elif game_state == "FINISH":
        draw_state_text(screen, "FINISH")
elif game_state == "RUNNING":
    # ゲーム中は、棒グラフをリアルタイムに更新
    pass

screen.fill(BG_COLOR) で画面全体をリセットしてから、各状態に応じた描画をします。

  • READY, GO, FINISH であれば大きな文字をど真ん中に表示。
  • RUNNING は一時的に pass になっていますが、後で棒グラフやタイマーを描画する流れになっています。

18コード分解:棒グラフ描画と数値表示

# 棒グラフの描画
draw_frame(screen)
bar_height = int((sensor_value / MAX_FORCE) * BAR_MAX_HEIGHT)
if sensor_value >= MAX_THRESHOLD:
    bar_color = BAR_COLOR_MAX
else:
    bar_color = BAR_COLOR_NORMAL
bar_y = BAR_Y_BOTTOM - bar_height
pygame.draw.rect(screen, bar_color, (BAR_X, bar_y, BAR_WIDTH, bar_height))

# 棒グラフの数値表示
value_text = small_font.render(f"{sensor_value}", True, (0, 0, 0))
screen.blit(value_text, (BAR_X + BAR_WIDTH // 2 - value_text.get_width() // 2, bar_y - 40))

解説

  1. bar_height = int((sensor_value / MAX_FORCE) * BAR_MAX_HEIGHT)
    握力(0〜1023)を、棒グラフ高さ(0〜300)にスケーリング。仮に sensor_value が 512 のとき、高さは約150px になります。
  2. 色分け(MAX_THRESHOLD)
    sensor_value >= 900 の場合は赤色、それ以外は緑色を指定。これにより握力が900を超えたらバーが赤くなる。
  3. bar_y = BAR_Y_BOTTOM - bar_height
    Pygameの座標系では「上が0、下に行くほど数値が大きい」ため、バーの「下端」は BAR_Y_BOTTOM を固定し、上方向に伸ばすには底辺から高さ分を差し引く形になります。
  4. テキスト表示
    棒グラフの上付近( bar_y - 40 )にセンサーの数値を表示。中央揃えしたいので、 (BAR_X + BAR_WIDTH // 2 - value_text.get_width() // 2, bar_y - 40) のように計算します。

19コード分解:ゲームタイマー

if game_state == "RUNNING":
    remaining_time = max(0, GAME_DURATION - (current_time - state_start_time))
    seconds = remaining_time // 1000
    timer_text = medium_font.render(f"{seconds} sec", True, (50, 50, 255))
    screen.blit(timer_text, (WIDTH - timer_text.get_width() - 20, 20))

解説

remaining_time
残り時間(ミリ秒)。GAME_DURATION(30000)から現在の経過時間を引き、最低でも0に保つ (max(0, ...))。

seconds = remaining_time // 1000
ミリ秒を秒に変換(整数)。

画面の右上付近に表示する形で blit しています。

20コード分解:最後の処理

pygame.display.flip()

フレームバッファを画面に反映。これを呼ばないと画面が更新されません。

pygame.quit()
ser.close()
sys.exit()

ループを抜けた後は、Pygameを終了し、シリアルポートを閉じてからプログラムを終了。

21コード全体の流れまとめ

ここで改めて、ゲームのメインループをフローチャート的にまとめます。

┌───────── Start ─────────┐
│ │
│ game_state = READY │
│ state_start_time = 0 │
│ │
├──────── [Main Loop] ────┤
│ 1) イベント確認 │
│ – もしQUITなら終了 │
│ 2) 状態遷移確認 │
│ – READY → GO → │
│ RUNNING → FINISH │
│ │
│ 3) センサー値受信 │
│ – RUNNING時のみ │
│ │
│ 4) 画面描画 │
│ – 背景クリア │
│ – 状態表示 │
│ – 棒グラフ描画 │
│ – タイマー描画 │
│ │
│ 5) 画面更新 │
│ – pygame.display.flip() │
│ │
├───── ループ継続 or 終了 ─────┤
└──────────────────────────────┘

22実行方法

  1. Arduinoを接続
    シリアルモニタ等で、センサー値が出力されていることを確認(0〜1023)。文字化けが起きる場合はシリアルモニタの文字化け対処法を確認しましょう。
  2. Pythonスクリプトを実行
    コマンドラインで python filename.py (Windowsなら python、Mac/Linuxなら python3)。
  3. Pygameのウィンドウが開く
    最初は「READY」→「GO」→「RUNNING」→「FINISH」と順番に切り替わるアニメーションを確認。
  4. 握力センサーを握る
    「RUNNING」状態の間、センサー値によって棒グラフの高さと色が変化します。閾値以上で赤くなる演出を楽しみましょう。
  5. 終了後
    30秒後(デフォルト設定)に「FINISH」が表示されます。ウィンドウの×ボタンでゲーム終了。

23応用アイデア(実装例つき)

ここからは、完成版コードに追加できる5つのアイデアを、実際に動くコードとあわせて紹介します。どれもpython-full章の完成版コードに、数行〜数十行を追加・変更するだけで組み込めます。まずは1つずつ試してみて、慣れてきたら複数を組み合わせてみてください。

01得点表示|最高値と平均値をFINISH画面に出す

「RUNNING」状態の間、受信したセンサー値を毎回リストに記録しておけば、ゲーム終了時に最高値・平均値を計算して表示できます。まずは完成版コードのグローバル変数に、記録用のリストを1つ追加します。

# グローバル変数に追加
score_history = []   # RUNNING中に受信した値をすべて記録するリスト
final_max = 0
final_avg = 0

次に「センサー値の更新」の処理に、記録の1行を追加します。

# センサー値の更新(記録つき)
if game_state == "RUNNING":
    if ser.in_waiting:
        try:
            line = ser.readline().decode('utf-8').strip()
            sensor_value = int(line)
            score_history.append(sensor_value)  # ここを追加
        except Exception:
            pass

RUNNINGからFINISHへ切り替わる瞬間(状態遷移のelif game_state == "RUNNING" and ...のブロック内)で、最高値と平均値を計算して確定させます。

elif game_state == "RUNNING" and current_time - state_start_time >= GAME_DURATION:
    game_state = "FINISH"
    state_start_time = current_time
    # ここで得点を確定する
    global final_max, final_avg
    if score_history:
        final_max = max(score_history)
        final_avg = sum(score_history) / len(score_history)

最後に、FINISH状態の描画部分に得点表示を追加します。

elif game_state == "FINISH":
    draw_state_text(screen, "FINISH")
    result_text = medium_font.render(
        f"MAX: {final_max}  AVG: {final_avg:.0f}", True, (0, 0, 0)
    )
    screen.blit(result_text, (WIDTH // 2 - result_text.get_width() // 2, HEIGHT // 4 + 80))

ポイント:max()やsum()は、リストが空の状態で呼ぶとエラーになります。if score_history:で件数を確認してから計算しているのは、そのための保険です。

02複数人対戦|2つのセンサーで左右対決

Arduino側にセンサーをもう1つ追加し、A0とA1の2つの値をカンマ区切りで送信すれば、2人同時プレイが実現できます。まずArduino側のコードを変更します。

// Arduino側:2つのセンサー値をカンマ区切りで送信
void setup() {
  Serial.begin(9600);
}

void loop() {
  int p1 = analogRead(A0); // プレイヤー1
  int p2 = analogRead(A1); // プレイヤー2
  Serial.print(p1);
  Serial.print(",");
  Serial.println(p2);
  delay(50);
}

Python側は、sensor_valueを1つの変数から2つに分けます。棒グラフも横に2本並べて描画します。

# グローバル変数(2人分に変更)
sensor_value_p1 = 0
sensor_value_p2 = 0

# 棒グラフの位置(左右にずらして2本表示)
BAR_X_P1 = WIDTH // 2 - BAR_WIDTH - 40
BAR_X_P2 = WIDTH // 2 + 40

# センサー値の更新(2人分をカンマで分割)
if game_state == "RUNNING":
    if ser.in_waiting:
        try:
            line = ser.readline().decode('utf-8').strip()
            parts = line.split(",")
            if len(parts) == 2:
                sensor_value_p1 = int(parts[0])
                sensor_value_p2 = int(parts[1])
        except Exception:
            pass

# 棒グラフの描画(2本分)
bar_height_p1 = int((sensor_value_p1 / MAX_FORCE) * BAR_MAX_HEIGHT)
bar_height_p2 = int((sensor_value_p2 / MAX_FORCE) * BAR_MAX_HEIGHT)
bar_y_p1 = BAR_Y_BOTTOM - bar_height_p1
bar_y_p2 = BAR_Y_BOTTOM - bar_height_p2

pygame.draw.rect(screen, BAR_COLOR_NORMAL, (BAR_X_P1, bar_y_p1, BAR_WIDTH, bar_height_p1))
pygame.draw.rect(screen, (100, 150, 255), (BAR_X_P2, bar_y_p2, BAR_WIDTH, bar_height_p2))

# FINISH画面で勝敗を表示
if game_state == "FINISH":
    winner = "PLAYER 1 WINS!" if sensor_value_p1 > sensor_value_p2 else "PLAYER 2 WINS!"
    win_text = medium_font.render(winner, True, (200, 0, 0))
    screen.blit(win_text, (WIDTH // 2 - win_text.get_width() // 2, HEIGHT // 4 + 80))

ポイント:len(parts) == 2のチェックは必須です。通信の途中でデータが欠けると、partsの要素数が1つや3つになることがあり、これを確認せずにparts[1]へアクセスすると、範囲外エラーでプログラムが止まってしまいます。

03アニメーション追加|握力に応じて画面を揺らす

握力が閾値を超えたときに画面全体を小刻みに揺らすと、演出の迫力が大きく増します。randomモジュールを使い、描画位置に毎フレームわずかなズレを加える仕組みです。

import random  # ファイル先頭のimportに追加

# 画面を揺らす関数
def get_shake_offset(sensor_value, threshold):
    if sensor_value >= threshold:
        # 閾値を超えている間は、上下左右に±6px程度ランダムに揺らす
        return random.randint(-6, 6), random.randint(-6, 6)
    return 0, 0

メインループの描画部分で、棒グラフや枠を描く座標に、このオフセットを加算します。

# 揺れを計算してから描画する
shake_x, shake_y = get_shake_offset(sensor_value, MAX_THRESHOLD)

# 枠と棒グラフに揺れを反映(BAR_Xやbar_yにshake分を加算)
pygame.draw.rect(
    screen, BAR_BORDER_COLOR,
    (BAR_X - 5 + shake_x, BAR_Y_BOTTOM - BAR_MAX_HEIGHT - 5 + shake_y, BAR_WIDTH + 10, BAR_MAX_HEIGHT + 10), 5
)
pygame.draw.rect(
    screen, bar_color,
    (BAR_X + shake_x, bar_y + shake_y, BAR_WIDTH, bar_height)
)

ポイント:揺れ幅(random.randint(-6, 6)の数値)を大きくしすぎると、演出というよりただの表示崩れに見えてしまいます。まずは±3〜6px程度から試して、好みの強さに調整しましょう。

04センサーデータの記録|CSVに保存してリハビリ記録に

受信した値を、日時つきでCSVファイルに書き込みながらプレイできるようにします。ゲーム開始前にファイルを開き、RUNNING中のセンサー更新処理に1行追記するだけです。

import csv
from datetime import datetime

# ファイルの先頭付近(グローバル変数)に追加
csv_file = open("grip_log.csv", "w", newline="", encoding="utf-8")
csv_writer = csv.writer(csv_file)
csv_writer.writerow(["timestamp", "sensor_value"])  # ヘッダー行

センサー値の更新処理に、記録用の1行を追加します。

if game_state == "RUNNING":
    if ser.in_waiting:
        try:
            line = ser.readline().decode('utf-8').strip()
            sensor_value = int(line)
            # ここでCSVに1行追記する
            csv_writer.writerow([datetime.now().isoformat(), sensor_value])
            csv_file.flush()  # すぐディスクに反映させる
        except Exception:
            pass

最後の処理(pygame.quit()の直前)で、ファイルを閉じるのを忘れないようにします。

pygame.quit()
ser.close()
csv_file.close()  # 追加
sys.exit()

ポイント:csv_file.flush()を省略すると、Pythonの内部バッファに記録が一時的に溜まったままになり、ウィンドウを強制終了したときに直近のデータが失われることがあります。長時間の記録を取る用途(リハビリの経過記録など)では、この一手間を入れておくと安心です。記録したCSVをあとから折れ線グラフで振り返りたい場合は、ArduinoのSDカードへのCSV保存の記事で紹介している考え方がそのまま応用できます。

05可変のゲーム時間|起動時に秒数を選べるようにする

GAME_DURATIONを固定の30秒にせず、プログラム起動時にコマンドラインから秒数を指定できるようにします。標準ライブラリのargparseを使うと、簡単に実装できます。

import argparse  # ファイル先頭のimportに追加

# main()の外、setup部分に追加
parser = argparse.ArgumentParser(description="握力計測ゲーム")
parser.add_argument(
    "--seconds", type=int, default=30,
    help="ゲームの制限時間(秒)。デフォルトは30秒"
)
args = parser.parse_args()

GAME_DURATION = args.seconds * 1000  # 元の「GAME_DURATION = 30000」をこの行に置き換える

実行するときは、次のようにオプションを指定します。オプションを省略すると、これまで通り30秒でプレイできます。

# 15秒版で遊ぶ場合
python filename.py --seconds 15

# 60秒版で遊ぶ場合
python filename.py --seconds 60

ポイント:argparseは、Python標準ライブラリに最初から含まれているため、追加インストールは不要です。オプションの数を増やせば、--threshold(閾値)や--port(COMポート)なども、同じ仕組みで起動時に指定できるようになります。

理学療法士の視点から:握力データをリアルタイムで可視化できる仕組みは、そのままリハビリの評価・フィードバックツールにも応用できます。特に④のCSV記録と①の得点表示を組み合わせると、セッションごとの握力の推移を数値として残せるようになり、経過観察の一助になります。関連して力(フォース)計測に関する電子工作の基礎や、スマートフォンを使ったTUGテスト計測、リハビリ×テクノロジーの取り組みもあわせてご覧ください。

24トラブルシューティング

  1. シリアルポートが開けない
    COM3 などポート番号があっているかを確認。OSによって異なるので要注意。Arduino IDEのシリアルモニタは閉じましたか? 1つのポートを同時に2つのプログラムで使うことはできません。詳しくはポートが見つからない時の対処法を参照。
  2. センサー値が常に0または1023
    Arduinoの配線やセンサーが正しく接続されているか確認。analogRead() のピン番号を間違えている可能性があります。
  3. Pygameウィンドウがフリーズする
    無限ループに陥っていないか。毎フレームで pygame.display.flip() が呼ばれるようになっているか確認。
  4. エラーが出る (ValueErrorなど)
    Arduino側のシリアル出力が想定と違っている可能性あり。strip() したあとに int() 変換が失敗するパターンかもしれません。無駄な文字を送信していないかチェックしましょう。書き込み時のエラーが別途出る場合はavrdudeの書き込みエラー対処法も確認してください。
  5. ②の複数人対戦で、片方の値だけ更新されない
    Arduino側が本当に2値をカンマ区切りで送っているか、シリアルモニタで直接確認してください。len(parts) == 2のチェックを外していると、不完全な行を受け取った際に古い値のまま止まって見えることがあります。
  6. ④のCSV記録で、ファイルの中身が空になる
    csv_file.flush()を入れ忘れていないか確認してください。また、ser.close()より先にcsv_file.close()を書いてしまうと、直前の書き込みが反映されないまま閉じてしまう場合があるため、記載の順序を参考にしてください。

25まとめ

以上で「PythonとArduinoで作るリアル握力計測ゲーム」の詳細な解説をしました。ポイントは以下の通りです。

  • ArduinoでセンサーデータをSerial.println() し、Pythonで受け取る
    シリアル通信の基本設定はしっかり押さえましょう。
  • Pygameでの状態管理と描画
    READY → GO → RUNNING → FINISH の流れで分かりやすい画面切り替えを実装しています。RUNNING中のみセンサー値を反映し、棒グラフを動的に描画します。
  • 棒グラフの高さや色の制御
    sensor_value を MAX_FORCE で割ってスケーリング。閾値を超えれば色を変化。
  • 初心者に優しい構造
    状態遷移を明確にし、見通し良くゲームを制御。
  • 5つの応用アイデアも、実際に動くコードとして紹介
    得点表示、複数人対戦、画面の揺れ演出、CSV記録、可変のゲーム時間まで、それぞれ数行〜数十行の追加で組み込めます(23章参照)。

本記事のサンプルはあくまでも基本形です。「得点の可視化」「ランキング表示」「センサーの校正」など様々なカスタマイズが可能です。アイデア次第で、握力だけでなく、他のアナログセンサーを使ったゲーム開発にも応用できます。

さあ、あなたならではの発想を加えて、オリジナルの握力バトルを盛り上げてみましょう!

  • 「どこまで握力を上げられるか」に挑戦するストイックなゲーム
  • 一定値をキープする必要があるゲーム
  • 連打ならぬ“連続握り”でキャラクターが跳ねたり進んだりするアクションゲーム

などなど、可能性は無限大です。ぜひ、今回の解説をもとに、あなたのアイデアを実現してください!さらに体系的にArduinoを学びたい方はArduino学習ロードマップもぜひ参考にしてください。

以上、Python × Arduino × Pygame で作る握力計測ゲームの超ロング解説でした。ここまで読んでいただきありがとうございます。この記事が、あなたの創造力を刺激して、新しいデバイスゲームの可能性を切り開く一助となれば幸いです。ぜひ楽しみながら、ハードウェア×ソフトウェアの世界にチャレンジしてみてください!


関連記事

特集記事

コメント

この記事へのコメントはありません。

TOP
CLOSE