🎮

RQt Tic-Tac-Toe ~rqtで遊べるマルバツゲーム~

2023/12/11に公開

サンタさんから久しくゲームをもらっていない皆さん、こんにちは。

私からのクリスマスプレゼントです。

rqt_tic_tac_toeとは

rqt_tic_tac_toeは、Tic-Tac-Toe(○×ゲーム、あるいは三目並べ)のRQt用プラグインです。

簡単に言うと、RQtで動く○×ゲームです。

この記事では、まずインストール方法と遊び方を説明して、その後に技術的な話をします。

インストール

ソースコードはGitHubに公開しています。これをダウンロードしてビルドします。

https://github.com/ShotaAk/rqt_tic_tac_toe

cd ~/ros2_ws/src
git clone https://github.com/ShotaAk/rqt_tic_tac_toe.git
rosdep install -r -y -i --from-paths .

cd ~/ros2_ws
colcon build --symlink-install

RQtにプラグインを認識させるため、次のコマンドを実行します。

source ~/ros2_ws/install/setup.bash
rqt --force-discover

RQtのPluginsTic-Tac-Toeが追加されたらインストール完了です。

遊び方

黒色のボードをポチポチ押して遊んでください。
リセット機能もあるので無限に遊べます。やったね!

ボードサイズの変更

ボードサイズを変えることもできます。
2x2 は短期決戦におすすめです。

2x2に飽きたらボードサイズを大きくしましょう。頭を使いすぎないように注意してね。

対戦モード

対戦モードも搭載してます。
ネットワーク(同じROS_DOMAIN_ID)に接続して白熱したオンライン対戦を楽しもう!

frame IDにあなたの名前を入力したら、ボードをポチポチして遊んでください。
対戦相手が見つかると、sync IDに名前が表示されます。

戦いたい相手をsync IDにセットして、Resetしたらゲーム開始です!

どちらが先手を取るのか、緊張の一手を楽しんでください。

マウスカーソルのリアルタイム表示

また、対戦モードでは相手の動きがリアルタイムで表示されます。
カーソルのその先を読む、まさに心理戦

まとめ

いかがでしたか?
これからRQtを起動するのが楽しみになりますね!

https://github.com/ShotaAk/rqt_tic_tac_toe/tree/main

ここから技術的な話

rqt_tic_tac_toeの技術的な内容を解説します。

RQtプラグイン作成の始め方

RQtプラグインは、ROS 2パッケージとして作成することで使用できます。
パッケージの作り方が分かればよいのですが、RQtについて丁寧に解説された公式ドキュメントはありません。
そのため、web記事を参照するか、他のRQtパッケージを流用して開発することになります。

私は下記の記事とrqt_topicパッケージを参考にしました。

https://robotry.hatenablog.com/entry/2020/01/05/003939

https://github.com/ros-visualization/rqt_topic/tree/humble

RQtプラグインの作り方はブログで紹介されているので、ここでは重要な項目だけ解説します。

Pythonで作るかC++で作るか

RQtプラグインはC++、Pythonのどちらでも作成できます。

先程のrqt_topicはPythonで実装されています。

rqt_image_viewパッケージはC++で実装されています。画像の処理時間を気にするため、C++を選択したのだと思います。

https://github.com/ros-visualization/rqt_image_view

rqt_tic_tac_toeは重い処理を実行しないので、Pythonで開発することにしました。

Qt DesignerでGUIを作ると簡単

GUI画面の作り方はコードで生成する、またはQt Designerで作成するの2択です。

コードで生成する場合、拡張性や自由度は高くなりますが、どのようなデザインになっているのか理解しづらくなるという難点も生まれます。

そのため、今回はQt DesginerでGUIを作成しました。
Qt DesignerはROS 2インストール時に一緒にインストールされます(未検証)。

例えば、rqt_tic_tac_toeのGUIは次のコマンドで編集できます。

$ cd rqt_tic_tac_toe/resource
$ /usr/lib/qt5/bin/designer TicTacToeWidget.ui 

コマンドを実行すると次のような画面が表示されます。

Qt Desginerの使い方は公式ドキュメントを参照してください。

https://doc.qt.io/qt-5/qtdesigner-manual.html

ボードをカスタムウィジェットとして作成

Tic-Tac-Toeのボードの作り方が特殊なので解説します。

先程のQt Desginerの画面を見るとTic-Tac-Toeのボードが描かれてないことがわかります。

ボードはQtのQWdigetを継承したカスタムウィジェットとして作成しています。
Qt Desginerに用意されているボタンやラベルでは、ボードを表現するのが難しいのでカスタムウィジェットとして作ることを選択しました。

カスタムウィジェットの場合、ウィジェットに何をさせるか、というのをコードで実装します。
今回は詳しく解説しませんが、実装を見たい場合はboard_wdiget.pypaintEventから見ることをおすすめします。

https://github.com/ShotaAk/rqt_tic_tac_toe/blob/1d814a89e3f4763ee8ab0f76281d1adc8e65bb9b/rqt_tic_tac_toe/src/rqt_tic_tac_toe/board_widget.py#L50-L63

カスタムウィジェットの作り方

カスタムウィジェットの作り方を簡単に紹介します。

  1. GUIにWidgetを置く
  2. Widgetを右クリックし格上げ先を指定を選択
  3. 格上げされたクラス名に、これから作るPythonのクラス名を入力
  4. ヘッダファイルに、これから作るPythonモジュール名を入力
  5. 格上げを押す

  1. コード内でUIファイルを読み込む際に、辞書型でウィジェット名 : クラス名を設定する

https://github.com/ShotaAk/rqt_tic_tac_toe/blob/1d814a89e3f4763ee8ab0f76281d1adc8e65bb9b/rqt_tic_tac_toe/src/rqt_tic_tac_toe/tic_tac_toe.py#L43

以上です。

重要なPythonスクリプトの紹介

rqt_tic_tac_toeで登場する重要なPythonスクリプトファイルを紹介します。
詳細はGitHubを見てください。

https://github.com/ShotaAk/rqt_tic_tac_toe/tree/main/rqt_tic_tac_toe/src/rqt_tic_tac_toe

  • tic_tac_toe.py: プラグインの本体。ROSノードの本体もここにいる。
  • board_widget.py: ボードを描画するカスタムウィジェット。
  • game.py: Tic-Tac-Toeのゲーム部分。

RQtプラグインでトピックをPub/Subする

RQtプラグインの中にもROSノードがいるので、トピックをPub/Subできます。

ノードの作り方:
https://github.com/ShotaAk/rqt_tic_tac_toe/blob/1d814a89e3f4763ee8ab0f76281d1adc8e65bb9b/rqt_tic_tac_toe/src/rqt_tic_tac_toe/tic_tac_toe.py#L37

Pub/Subの作り方:
https://github.com/ShotaAk/rqt_tic_tac_toe/blob/1d814a89e3f4763ee8ab0f76281d1adc8e65bb9b/rqt_tic_tac_toe/src/rqt_tic_tac_toe/tic_tac_toe.py#L58-L63

コードを見ると、tic_tac_toe/commandtic_tac_toe/cursor_posというトピックを、
自分自身でPub/Subしていることがわかります。
ここが通信対戦を実現するポイントです。

通信対戦の同期方法

rqt_tic_tac_toeの通信対戦は、トピック内のframe_idを読むことで実現しています。

それぞれのノードが操作コマンド(tic_tac_toe/command)をPublishし、Subscribeします。
受け取ったframe_idが、GUIで設定しているSync IDと一致したら、相手のコマンドを自身のボードに反映する。という仕組みです。

同じ名前のトピックでやり取りしているので、相手のノード名が何なのかを気にする必要が無いです。

相手カーソルの取得

相手カーソル位置(ボード上の緑色の丸)も同じ仕組みで取得しています。

tic_tac_toe/cursor_posというトピックとしてカーソル位置をPublishし、お互いにSubscribeしています。

ボード描画機能の実装

ボード描画機能は、board_widget.pyに実装しています。
カスタムウィジェットの話で登場したものです。

全て解説するのは大変なので、要点だけまとめます

  • tic_tac_toe.pyから、16 msec周期でupdateされる: リンク
  • updateすると、描画関数paintEvent()が自動で実行される: リンク
  • マウス入力があると、mouse( )Event関数が自動で実行される: リンク

Tic-Tac-Toeのゲーム部分の実装

ゲーム部分はgame.pyに実装しています。

正しく動作しているか確認するため、ユニットテストファイルも作成しました。

例:

https://github.com/ShotaAk/rqt_tic_tac_toe/blob/1d814a89e3f4763ee8ab0f76281d1adc8e65bb9b/rqt_tic_tac_toe/tests/game_test.py#L82-L94

実現できなかったこと

最後に、今回のTic-Tac-Toeで実現できなかったことをまとめます。

サービスによる対戦申し込み

同じ名前のサービスを用意すると、自分自身のサービスサーバにアクセスしちゃうので実装を諦めました。
サービス名にネームスペースを用意して、動的にクライアントを生成したら解決するかな?

特定ノードが出すトピックだけサブスクライブする

今回の実装では、同じ名前のトピックを全てのノードがPublishしてます。
ノードが増えると(Tic-Tac-Toeをたくさん起動すると)、Pub/Subするトピック量も増えます。

おそらく、Subscribeの処理が追いつかなくなります。

こちらも、動的にSubscriberを作れば解決するのかな?

ボードを大きくしても3目並べとして遊べること

今回の実装では、ボードサイズに合わせて、4目並べ、5目並べ、とルールが変わります。

実装してないだけです。

とはいえ、これ以上ゲーム性を高めることに意味があるのか、悩ましいです。

ロボットとの連携

tic_tac_toe/commandトピックをPublishすれば、RQtが無くてもボードを操作できます。
現実世界のロボットでマルバツを描き、画面の向こうの人間と戦う、みたいなことができるかも?

終わりに

長くなりましたが、最後に一言だけ、

良いお年を。

Discussion