IT女子のラズベリーパイ入門奮闘記

第46回「ラズベリーパイ専用アドオンボード Sense HATで遊ぼう!(3)ジョイスティック編」

raspberrypi46_main
Sense HAT編、第3回目はジョイスティック編です!

過去記事はこちら↓からどうぞ!
第45回「ラズベリーパイ専用アドオンボード Sense HATで遊ぼう!(2)6種類のセンサ編」
第44回「ラズベリーパイ専用アドオンボード Sense HATで遊ぼう!(1)LEDディスプレイの操作」

さて、宇宙で使うことを想定して作られているSense HATは、入力装置として「ジョイスティック」が搭載されています。

raspberrypi46_img01

Sense HATの表面の、ラズベリーのイラストの右隣にあるこの部品がジョイスティックです。バーが短いので一見わかりにくいのですが、指で押してみるとカチッと倒れる感触があります(意外と固いです!)。ゲーム機のコントローラーなどで見かけることが多いですね。

今回はこのジョイスティックからの入力を、Python側で受け取る方法をご紹介します。LEDディスプレイと連動させたゲーム作りにも挑戦しました!

 

ジョイスティックの入力値について

THE JOYSTICK – Raspberry Pi Learning Resources
The Sense HAT joystick is mapped to the four keyboard cursor keys, with the joystick middle-click being mapped to the Return key. This means that moving the joystick has exactly the same effect as pressing those keys on the keyboard. Remember that the down direction is with the HDMI port facing downwards.

ジョイスティックの使い方は、こちらのドキュメントにありました。A Guide to Astro Piとしてまとめられているドキュメントの1ページです。Sense HATの機能別の解説ページも揃っています。

ジョイスティックの向きは、正位置=HDMIポートが下向き、ということなので、

raspberrypi46_img02

図1

このような向きですね!
上下左右の4方向の他、まっすぐ押し込むこともできるので、合計5パターンの入力が可能です。

それではIDELを起動して、最初のサンプルソースを実行してみましょう!

/home/pi/nas/keyboad_mapping.py

import pygame

from pygame.locals import *
from sense_hat import SenseHat

pygame.init()
pygame.display.set_mode((640, 480))
sense = SenseHat()
sense.clear()

running = True

while running:
    for event in pygame.event.get():
        print(event)
        if event.type == QUIT:
            running = False
            print("BYE")
raspberrypi46_img03

図2

実行すると、「pygame window」という黒い画面が立ち上がります。これは、6~7行目で呼び出しているpygameライブラリの画面です。

Pygame – Wikipedia
pygame は、ビデオゲームを製作するために設計されたクロスプラットフォームのPythonモジュール集であり、Pythonでコンピュータグラフィクスと音声を扱うためのライブラリを含んでいる。

pygame.event
event – Pygameドキュメント 日本語訳
この命令を実行するとイベントキューから全てのイベント情報を取得し、取得されたイベントはイベントキューから削除されます。引数にイベントの種類が設定されていた場合は、指定した種類のイベント情報のみがイベントキューから取得されます。

プログラムの14行目の「pygame.event.get()」でイベントを監視し、15行目で内容を表示しています。
「pygame window」の上でマウスを動かしたり、キーボードのキーを押下したりすると、コンソール画面に「イベント」の内容が表示されます。同様に、Sense HATのジョイスティックからの入力イベントも検知されます。

raspberrypi46_img04

図3

図3は、ジョイスティック上、キーボード上キー、ジョイスティック右……というように、交互に時計回りに操作してみたところです。どちらも同じイベント内容が表示されているのが分かるでしょうか?
ジョイスティックは4方向の傾きだけでなく、まっすぐ押し込むことができます。これはキーボードのEnterキーと対応していました。

また、1回の入力操作につき「KYEDOWN」と「KEYUP」の2種類のイベントが発生しています。少し長押しするような操作をしてみると、イベント発生のタイミングが分かりやすいです。

このプログラムでは、ウィンドウを閉じようとすると「BYE」と表示して処理を終了させるようにプログラムされているため(16~18行目)、pygameのウィンドウ自体は閉じられません。終了させたい場合は、IDELのコンソール画面を閉じましょう。

 

ジョイスティックとLEDディスプレイを連動!

ジョイスティックの動きが分かったところで、LEDディスプレイと連動させてみましょう!

raspberrypi46_img05

図4

ジョイスティックを使って、光っている部分を動かすプログラムを組んでいきます。
必要な処理は大きく分けて2つ。

  • 現在点灯しているLEDを消灯する
  • 移動先のLEDを点灯させる

同時に2つの処理を行うことで、移動しているように見せることが可能ですね。

LEDを個別に制御するには、第44回「ラズベリーパイ専用アドオンボード Sense HATで遊ぼう!(1)LEDディスプレイの操作」で紹介した「set_pixel」関数が便利です。

set_pixel
Sets an individual LED matrix pixel at the specified X-Y coordinate to the specified colour.

第1引数はX座標(0~7)、第2引数はY座標(0~7)、第3~5引数には色を指定することができます。点灯と消灯は、次のように記述すればOKです。

#点灯(白)
sense.set_pixel(0, 0, 255, 255, 255)
#消灯(黒)
sense.set_pixel(0, 0, 0, 0, 0)
raspberrypi46_img06

図5 ラズベリーパイ公式サイトより

LEDディスプレイの座標は、図5のように、左上を基準に0からはじまります。従って、ジョイスティックの操作とX座標、Y座標の変化は次のようになります。

ジョイスティックの操作 座標の変化
y-1
y+1
x-1
x+1

座標はそれぞれ0~7の間となるように制御する必要があります。ちなみに、無効な座標が設定された場合は下記のようなラーが発生してしまいます。

ValueError: Y position must be between 0 and 7

このエラー回避にも対応したサンプルソースが公開されているので、早速実行してみましょう!

import pygame

from pygame.locals import *
from sense_hat import SenseHat

pygame.init()
pygame.display.set_mode((640, 480))

sense = SenseHat()
sense.clear()

running = True

x = 0
y = 0
sense.set_pixel(x, y, 255, 255, 255)

while running:
    for event in pygame.event.get():
        if event.type == KEYDOWN:
            sense.set_pixel(x, y, 0, 0, 0)  # Black 0,0,0 means OFF

            if event.key == K_DOWN and y < 7:
                y = y + 1
            elif event.key == K_UP and y > 0:
                y = y - 1
            elif event.key == K_RIGHT and x < 7:
                x = x + 1
            elif event.key == K_LEFT and x > 0:
                x = x - 1

        sense.set_pixel(x, y, 255, 255, 255)
        if event.type == QUIT:
            running = False
            print("BYE")

16行目までは初期処理です。最初は左上のLED(X座標0、Y座標0)が点灯した状態から始まります。
入力があると、まず、現在の位置のLEDをオフにします(21行目)。そして座標が有効範囲(0~7)であるかどうかを判定して、32行目で移動先のLEDを点灯させます(23~30行目)。無効な値が設定された場合(たとえば右端からさらに右に移動させようとした場合)、動きがないように見えますが、同じLEDの消灯・点灯が行われています。

 

ゲームを作ろう!

Sense HAT Pong | Raspberry Pi Learning Resources
Worksheet – Sense HAT Pong | Raspberry Pi Learning Resources

動くボールをバーで打つ、なつかしのレトロゲーム、ポンゲームです。
こちらのドキュメントでは、キーボードの矢印キーを使って操作するプログラムが紹介されていますが、ジョイスティックで操作できるようにカスタマイズしてみたいと思います。

まずはカスタマイズ前のゲームを動かしてみましょう。完成版のプログラムソースは、8章の最後に掲載されています。

/home/pi/nas/pong.py

from time import sleep
from sense_hat import SenseHat
import curses
import threading

sense = SenseHat()
sense.clear(0,0,0)

screen = curses.initscr()
screen.keypad(True)
curses.cbreak()
curses.noecho()

y = 4
ball_position=[6,3]
ball_speed=[-1,-1]

def drawbat():
    sense.set_pixel(0,y,255,255,255)
    sense.set_pixel(0,y+1,255,255,255)
    sense.set_pixel(0,y-1,255,255,255)

def moveball():
    global game_over
    while True:
        sleep(0.15)

        sense.set_pixel(ball_position[0],ball_position[1],0,0,0)

        ball_position[0] += ball_speed[0]
        ball_position[1] += ball_speed[1]

        if ball_position[1] == 0 or ball_position[1] == 7:
            ball_speed[1] = -ball_speed[1]
        if ball_position[0] == 7:
            ball_speed[0] = -ball_speed[0]
        if ball_position[0] == 1 and y-1 <= ball_position[1] <= y+1:
            ball_speed[0] = -ball_speed[0]
        if ball_position[0] == 0:
            break

        sense.set_pixel(ball_position[0],ball_position[1],0,0,255)

    game_over = True

game_over = False

thread = threading.Thread(target=moveball)
thread.start()

while not game_over:
    drawbat()

    key = screen.getch()    
    sense.clear()
    if key == curses.KEY_UP:
        if y > 1:
            y -= 1

    if key == curses.KEY_DOWN:
        if y < 6:
            y += 1

sense.show_message("You Lose", text_colour=(255,0,0))

screen.keypad(0)
curses.nocbreak()
curses.echo()
curses.endwin()

3行目でインポートしている「curses」は、キーボードからの入力を検知するために使用しています。cursesはIDELでは動作しないようなので、コマンドラインから実行しましょう。

実行コマンド
python3 /home/pi/nas/pong.py

左側の白いバーを、キーボードの上下キーで動かして、青いボールをはね返します。返し損ねると、「You Lose」という文字が流れてゲームオーバーとなります。ボールは0.15秒間隔で移動していくのですが(26行目)、意外と速くてむずかしいです!

それでは、このプログラムを、ジョイスティックから操作できるように調整していきましょう。

/home/pi/nas/pong_joy.py

from time import sleep
from sense_hat import SenseHat
import pygame
from pygame.locals import *
import threading

sense = SenseHat()
sense.clear(0,0,0)

pygame.init()
pygame.display.set_mode((320, 240))
y = 4
ball_position=[6,3]
ball_speed=[-1,-1]

def drawbat():
    sense.set_pixel(0,y,255,255,255)
    sense.set_pixel(0,y+1,255,255,255)
    sense.set_pixel(0,y-1,255,255,255)

def moveball():
    global game_over
    while True:
        sleep(0.5)

        sense.set_pixel(ball_position[0],ball_position[1],0,0,0)

        ball_position[0] += ball_speed[0]
        ball_position[1] += ball_speed[1]

        if ball_position[1] == 0 or ball_position[1] == 7:
            ball_speed[1] = -ball_speed[1]
        if ball_position[0] == 7:
            ball_speed[0] = -ball_speed[0]
        if ball_position[0] == 1 and y-1 <= ball_position[1] <= y+1:
            ball_speed[0] = -ball_speed[0]
        if ball_position[0] == 0:
            break

        sense.set_pixel(ball_position[0],ball_position[1],0,0,255)

    game_over = True

game_over = False

thread = threading.Thread(target=moveball)
thread.start()

while not game_over:
    drawbat()

    for event in pygame.event.get():
        if event.type == KEYDOWN:
            if event.key == K_UP and y > 1:
                sense.set_pixel(0,y+1,0,0,0)
                y -= 1
            if event.key == K_DOWN and y < 6:
                sense.set_pixel(0,y-1,0,0,0)
                y += 1

sense.show_message("You Lose", text_colour=(255,0,0))

実行
python3 /home/pi/nas/pong_joy.py

pygameは、SSH接続やリモートデスクトップ接続ではうまく動作しないので、コマンドラインから直接実行するようにしましょう。

cursesライブラリをpygameライブラリに置き換え、ジョイスティックからの入力を受け取れるように変更しました(キーボードの上下キーも使用できます)。
主な変更点は、3~4行目のpygame呼び出し部分、10~11行目のpygameの初期処理、そして53~60行目の入力値の判定です。どれもこれまでに学んできた内容なので、簡単にカスタマイズできました!

ボールの動きが分かりやすいように、スピードを0.5秒に変えてみました。動きを遅くすると「sense.clear()」によるちらつきが目立ってしまったので(元ソース55行目)、必要な部分だけを書き換えるように修正しています。

 

まとめ

Sense HATのジョイスティックはボード上に搭載されているので、直接触れるというのが最大の特徴!ラズベリーパイの入力装置といえば外部接続が当たり前だったので、新鮮な印象がありました。また、GPIOポートからボタンを取り付けることもできるので、ディスプレイなしで使っていきたい方にはこちらもオススメです。

さらに今回はゲーム作りにも挑戦しました!
ディスプレイの小ささという制限はありますが、このLEDディスプレイだからこその味わいがあるように思います。スピードを変更できるようにしたり、スコアを出せるようにしたりなどがあると、よりゲームらしいものに仕上がりそうですね!

円山ナカノ

プロフィール:プログラミング暦通算4年、最近IT業界に舞い戻ってきたプログラマーです。女子です。