pygameでXBOXコントローラの入力を取得してPyQt6に表示する

2022/05/28 categories:Python| tags:Python|pygame|PyQt6|XBOX|

pygameでXBOXコントローラの入力を取得して、PyQt6に表示するプログラムを作成してみました。

処理内容

QtにはQGamepadなどのゲームパッド用のクラスが用意されているようですが、PyQt6でそれらのクラスを見つけられませんでした。そこで、QThread内の無限ループでpygameによるXBOXコントローラの入力状態を監視して、その入力に応じてシグナルを発行するような処理にしました。これにより、メインのスレッドとは別でコントローラの状態を監視できるので、メインのUIの処理なども問題なく行うことが出来ました。

動作の様子

録画に使用したソフトや機器の関係でコントローラのボタンを押すタイミングとソフト上の表示にラグがありますが、実際にはラグは体感できないレベルです。

ソースコード

# -*- coding: utf-8 -*-
import sys
from PyQt6 import QtWidgets, QtCore, QtGui, QtSerialPort
import pygame
import pygame.locals

class Mainwindow(QtWidgets.QMainWindow):
    def __init__(self) -> None:
        super().__init__()

        self.setWindowTitle('PyQt6 and pygame XBOX controller test')

        self.setStyleSheet('''
            QPushButton {
                min-height: 32px;
            }
            QDoubleSpinBox {
                min-height: 32px;
            }
            QPushButton:checked {
                background-color: red; 
            }
        ''')

        self.joystick = Joystick(self)
        self.A_button = QtWidgets.QPushButton('A')
        self.B_button = QtWidgets.QPushButton('B')
        self.X_button = QtWidgets.QPushButton('X')
        self.Y_button = QtWidgets.QPushButton('Y')
        self.R_button = QtWidgets.QPushButton('R')
        self.L_button = QtWidgets.QPushButton('L')
        self.UP_button = QtWidgets.QPushButton('UP')
        self.DOWN_button = QtWidgets.QPushButton('DOWN')
        self.LEFT_button = QtWidgets.QPushButton('LEFT')
        self.RIGHT_button = QtWidgets.QPushButton('RIGHT')
        self.VIEW_button = QtWidgets.QPushButton('VIEW')
        self.MENU_button = QtWidgets.QPushButton('MENU')

        self.left_horizontal = QtWidgets.QDoubleSpinBox()
        self.left_horizontal.setMinimum(-1)
        self.left_horizontal.setMaximum(1)
        self.left_vertical = QtWidgets.QDoubleSpinBox()
        self.left_vertical.setMinimum(-1)
        self.left_vertical.setMaximum(1)
        self.right_horizontal = QtWidgets.QDoubleSpinBox()
        self.right_horizontal.setMinimum(-1)
        self.right_horizontal.setMaximum(1)
        self.right_vertical = QtWidgets.QDoubleSpinBox()
        self.right_vertical.setMaximum(1)
        self.right_vertical.setMinimum(-1)
        self.left_trigger = QtWidgets.QDoubleSpinBox()
        self.left_trigger.setMinimum(-1)
        self.left_trigger.setMaximum(1)
        self.right_triggr = QtWidgets.QDoubleSpinBox()
        self.right_triggr.setMinimum(-1)
        self.right_triggr.setMaximum(1)
        
        self.A_button.setCheckable(True)
        self.B_button.setCheckable(True)
        self.X_button.setCheckable(True)
        self.Y_button.setCheckable(True)
        self.R_button.setCheckable(True)
        self.L_button.setCheckable(True)
        self.UP_button.setCheckable(True)
        self.DOWN_button.setCheckable(True)
        self.LEFT_button.setCheckable(True)
        self.RIGHT_button.setCheckable(True)
        self.VIEW_button.setCheckable(True)
        self.MENU_button.setCheckable(True)

        self.setCentralWidget(QtWidgets.QWidget())
        self.centralWidget().setLayout(QtWidgets.QGridLayout())
        self.centralWidget().layout().addWidget(self.UP_button,       0, 0, 1, 1)
        self.centralWidget().layout().addWidget(self.DOWN_button,     1, 0, 1, 1)
        self.centralWidget().layout().addWidget(self.LEFT_button,     2, 0, 1, 1)
        self.centralWidget().layout().addWidget(self.RIGHT_button,    3, 0, 1, 1)
        self.centralWidget().layout().addWidget(self.R_button,        4, 0, 1, 1)
        self.centralWidget().layout().addWidget(self.L_button,        5, 0, 1, 1)
        self.centralWidget().layout().addWidget(self.left_horizontal, 6, 0, 1, 1)
        self.centralWidget().layout().addWidget(self.left_vertical,   7, 0, 1, 1)
        self.centralWidget().layout().addWidget(self.right_horizontal,8, 0, 1, 1)

        self.centralWidget().layout().addWidget(self.A_button,        0, 1, 1, 1)
        self.centralWidget().layout().addWidget(self.B_button,        1, 1, 1, 1)
        self.centralWidget().layout().addWidget(self.X_button,        2, 1, 1, 1)
        self.centralWidget().layout().addWidget(self.Y_button,        3, 1, 1, 1)
        self.centralWidget().layout().addWidget(self.VIEW_button,     4, 1, 1, 1)
        self.centralWidget().layout().addWidget(self.MENU_button,     5, 1, 1, 1)
        self.centralWidget().layout().addWidget(self.right_vertical,  6, 1, 1, 1)
        self.centralWidget().layout().addWidget(self.left_trigger,    7, 1, 1, 1)
        self.centralWidget().layout().addWidget(self.right_triggr,    8, 1, 1, 1)

        self.joystick.A_PRESSED.connect(self.A_button.toggle)
        self.joystick.B_PRESSED.connect(self.B_button.toggle)
        self.joystick.X_PRESSED.connect(self.X_button.toggle)
        self.joystick.Y_PRESSED.connect(self.Y_button.toggle)
        self.joystick.R_PRESSED.connect(self.R_button.toggle)
        self.joystick.L_PRESSED.connect(self.L_button.toggle)
        self.joystick.UP_PRESSED.connect(self.UP_button.toggle)
        self.joystick.DOWN_PRESSED.connect(self.DOWN_button.toggle)
        self.joystick.LEFT_PRESSED.connect(self.LEFT_button.toggle)
        self.joystick.RIGHT_PRESSED.connect(self.RIGHT_button.toggle)
        self.joystick.VIEW_PRESSED.connect(self.VIEW_button.toggle)
        self.joystick.MENU_PRESSED.connect(self.MENU_button.toggle)

        self.joystick.A_RELEASED.connect(self.A_button.toggle)
        self.joystick.B_RELEASED.connect(self.B_button.toggle)
        self.joystick.X_RELEASED.connect(self.X_button.toggle)
        self.joystick.Y_RELEASED.connect(self.Y_button.toggle)
        self.joystick.R_RELEASED.connect(self.R_button.toggle)
        self.joystick.L_RELEASED.connect(self.L_button.toggle)
        self.joystick.UP_RELEASED.connect(self.UP_button.toggle)
        self.joystick.DOWN_RELEASED.connect(self.DOWN_button.toggle)
        self.joystick.LEFT_RELEASED.connect(self.LEFT_button.toggle)
        self.joystick.RIGHT_RELEASED.connect(self.RIGHT_button.toggle)
        self.joystick.VIEW_RELEASED.connect(self.VIEW_button.toggle)
        self.joystick.MENU_RELEASED.connect(self.MENU_button.toggle)
        self.joystick.AXIS_MOVED.connect(self.joystick_axis_moved)
        
        self.joystick.start()

    def joystick_axis_moved(self, left_horizontal, left_vertical, right_horizontal, right_vertical, left_trigger, right_triggr):
        self.left_horizontal.setValue(left_horizontal)
        self.left_vertical.setValue(left_vertical)
        self.right_horizontal.setValue(right_horizontal)
        self.right_vertical.setValue(right_vertical)
        self.left_trigger.setValue(left_trigger)
        self.right_triggr.setValue(right_triggr)

    def closeEvent(self, a0: QtGui.QCloseEvent) -> None:
        self.joystick.terminate()
        return super().closeEvent(a0)

class Joystick(QtCore.QThread):
    A_PRESSED = QtCore.pyqtSignal()
    B_PRESSED = QtCore.pyqtSignal()
    X_PRESSED = QtCore.pyqtSignal()
    Y_PRESSED = QtCore.pyqtSignal()
    R_PRESSED = QtCore.pyqtSignal()
    L_PRESSED = QtCore.pyqtSignal()
    UP_PRESSED = QtCore.pyqtSignal()
    DOWN_PRESSED = QtCore.pyqtSignal()
    LEFT_PRESSED = QtCore.pyqtSignal()
    RIGHT_PRESSED = QtCore.pyqtSignal()
    VIEW_PRESSED = QtCore.pyqtSignal()
    MENU_PRESSED = QtCore.pyqtSignal()
    
    A_RELEASED = QtCore.pyqtSignal()
    B_RELEASED = QtCore.pyqtSignal()
    X_RELEASED = QtCore.pyqtSignal()
    Y_RELEASED = QtCore.pyqtSignal()
    R_RELEASED = QtCore.pyqtSignal()
    L_RELEASED = QtCore.pyqtSignal()
    UP_RELEASED = QtCore.pyqtSignal()
    DOWN_RELEASED = QtCore.pyqtSignal()
    LEFT_RELEASED = QtCore.pyqtSignal()
    RIGHT_RELEASED = QtCore.pyqtSignal()
    VIEW_RELEASED = QtCore.pyqtSignal()
    MENU_RELEASED = QtCore.pyqtSignal()

    AXIS_MOVED = QtCore.pyqtSignal(float, float, float, float, float, float)

    def __init__(self, parent):
        super().__init__(parent)
        pygame.init()
        pygame.joystick.init()
        self.joystick = pygame.joystick.Joystick(0)
        self.joystick.init()
        self.hat_prev = 0

    def get_hat(self):
        hat = self.joystick.get_hat(0)
        val  = 0
        val += int(hat[1] ==  1) << 0 # up
        val += int(hat[1] == -1) << 1 # down
        val += int(hat[0] ==  1) << 2 # right
        val += int(hat[0] == -1) << 3 # left
        val, self.hat_prev = val - self.hat_prev, val
        return val

    def run(self) -> None:
        while True:
            for e in pygame.event.get():

                if e.type == pygame.locals.JOYAXISMOTION:
                    self.AXIS_MOVED.emit(
                        self.joystick.get_axis(0), #left horizontal
                        self.joystick.get_axis(1), #left vertical
                        self.joystick.get_axis(2), #right horizontal
                        self.joystick.get_axis(3), #right vertical
                        self.joystick.get_axis(4), #left trigger
                        self.joystick.get_axis(5), #right triggr
                    )

                elif e.type == pygame.locals.JOYHATMOTION:
                    h = self.get_hat()
                    if h > 0:
                        if (h >> 0) & 1: self.UP_PRESSED.emit()
                        if (h >> 1) & 1: self.DOWN_PRESSED.emit()
                        if (h >> 2) & 1: self.RIGHT_PRESSED.emit()
                        if (h >> 3) & 1: self.LEFT_PRESSED.emit()
                    else:
                        if (abs(h) >> 0) & 1: self.UP_RELEASED.emit()
                        if (abs(h) >> 1) & 1: self.DOWN_RELEASED.emit()
                        if (abs(h) >> 2) & 1: self.RIGHT_RELEASED.emit()
                        if (abs(h) >> 3) & 1: self.LEFT_RELEASED.emit()

                elif e.type == pygame.locals.JOYBUTTONDOWN:
                    if e.button == 0: self.A_PRESSED.emit()
                    if e.button == 1: self.B_PRESSED.emit()
                    if e.button == 2: self.X_PRESSED.emit()
                    if e.button == 3: self.Y_PRESSED.emit()
                    if e.button == 4: self.L_PRESSED.emit()
                    if e.button == 5: self.R_PRESSED.emit()
                    if e.button == 6: self.VIEW_PRESSED.emit()
                    if e.button == 7: self.MENU_PRESSED.emit()

                elif e.type == pygame.locals.JOYBUTTONUP:
                    if e.button == 0: self.A_RELEASED.emit()
                    if e.button == 1: self.B_RELEASED.emit()
                    if e.button == 2: self.X_RELEASED.emit()
                    if e.button == 3: self.Y_RELEASED.emit()
                    if e.button == 4: self.L_RELEASED.emit()
                    if e.button == 5: self.R_RELEASED.emit()
                    if e.button == 6: self.VIEW_RELEASED.emit()
                    if e.button == 7: self.MENU_RELEASED.emit()
        
if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    w = Mainwindow()
    w.show()
    app.exec()

Share post

Related Posts

Comments

comments powered by Disqus