LabVIEWを触ったことがない方に向けて、それなりのプログラムが書けるようになるところまで基本的な事柄を解説していこうという試みです。
シリーズ26回目として今までの知識を総動員して簡単なゲームのプログラムを作成してみます。
この記事は、以下のような方に向けて書いています。
- LabVIEWのいろいろなことを見てきたけれどプログラムが実際に作れない
- キューやFGVを実際に使ってプログラムを書きたい
もし上記のことに興味があるよ、という方には参考にして頂けるかもしれません。
なお、 前回の記事はこちらです。
これまでの内容でゲームを作る
さて、機能的グローバル変数についても紹介し、ここまでの内容でかなりLabVIEWでプログラムを作る際の必要な知識はそろってきました。もちろんまだ紹介していない機能などあるにはあるのですが、ここまでの内容でも十分ある程度のプログラムはかけてしまいます。
そこで、まずこれシリーズの今までの内容の総復習をかねて、一つLabVIEWでゲームを作ってみます。以下のような、反射神経を試すゲームです。
- ユーザーがスタートボタンを押すとゲームスタート
- ゲームスタート後、ランダムなタイミングでLEDが点灯したらすぐにボタンを押す
- ボタンを押すと再びLEDが消え、またLEDが点灯するのを待つ。これを合計3回繰り返す
- LEDがついてからボタンを押すまでの時間が測られ、累計時間が表示される
- 3回のゲームのそれぞれが終わると、それぞれが終了したことを表わすブールが光る。また状況説明として今何回目なのかを文字列表示器に表す
- もしLEDがついていないときにボタンを押してしまうとペナルティ。200ミリ秒が追加される
このゲームを作るために
- ステートマシン
- キュー
- 機能的グローバル変数
の知識を使います。まさに今までの内容の仕上げにぴったりだと思います。
今回の記事で紹介するプログラム例ではデザインパターンとしてはイベント駆動型ステートマシンの考え方に生産者消費者デザインパターンを盛り込んだ形としました。
もちろん、今回の記事で紹介する以外のプログラムもあり得ると思うので、上記のデザインパターンを使用せずともプログラムを組むことができるかもしれません。ただ、少なくとも今まで扱ってきた上記の各機能を使う方法でもできるはずだよ、ってことです。
いきなりプログラムの完成例を紹介してもふーん、で終わってしまうので、ここからは私がプログラムを作るにあたって考えていた過程を書いてみます。
プログラム構築の過程:デザインパターンの決定
そもそもは何もないところからプログラムを組み始めますし、このデザインパターンを使用する、という答えはありません。
ただ今回のプログラムの動作としては、以下の単純な理由からデザインパターンを選定しました。
- ユーザーが操作するボタンが多く、それらに反応してプログラムの動作が決まる→イベントストラクチャが便利
- ある程度決まった処理の流れがある→ステートマシンが便利
これだけだと、別にキューを使用しないイベント駆動型のステートマシンだけでもよさそうです。ただ、今回のプログラムの場合にはユーザーの操作による「処理の割り込み」が入ります。
LEDが点灯した後、ユーザーがボタンを押すまでの間は、プログラムは経過時間を測る必要があります。しかしその一方で、ユーザーがボタンを押す(というイベントを発生させた)とLEDがついているかどうかの判定を行ったり、その結果によっては経過時間にペナルティ分を加算するかどうかの判定を行ったりしなければいけません。
こういった状態に臨機応変に対応するためには、メインの処理ループで経過時間を測る際に処理を滞らせてはならず常にループを回し、ユーザーがイベントを発生させた時点で判定処理のステートに移る処理を割り込ませる必要があります。
処理を割り込ませるのにぴったりの関数はキューの関数パレットの中にある先頭に要素をエンキューで実現できます。
え、そもそもキューはどこで使用しているのかって?これはステートマシンの構造のシフトレジスタの代わりに使用します。
それぞれのステートにおいて要素をエンキューの関数を使用して、次のループでデキューされるステートを用意する、ということになります。
ステートマシンとしてシフトレジスタを使用するのが基本的な形ですが、キューを組み合わせたステートマシンとすることで、他のループとの連携が行えるのが便利です。
なので大枠としてはイベントストラクチャを監視するループと、メインの処理ループ、これらをキューでつないで、メインの処理ループはステートマシンとして動作させるように考えました。
プログラム構築の過程:ステートの決定
デザインパターンの大枠が決まったら、今回はステートマシンを使用するので、どんなステートが必要かを考えます。
ユーザーがフロントパネルで特定のボタンを押すタイプのプログラムでは、まずそれらのボタンの「状態」が適切でないと話になりません。例えば、プログラムの途中で、ブールボタンの無効プロパティにDisabled and Grayed Outの入力を書き込んでいたとします。これが書き込まれているとユーザーは操作ができず、そのブールに関連したイベントも発生させられません。
しかし、意図しない形でプログラムが終了(例えば強制終了ボタンを押すなど)した場合、次にプログラムを再開する時点でもDisabled and Grayed Outになっていると困る場合があります。
そこで、ステートマシンでInitializeのステートを設けて、「プログラム実行時にこうあるべき」という状態や値を決めることがあります。実際今回のプログラムでは誤作動を防ぐため、ユーザーに余計な操作をさせないよう無効プロパティを一部に使用しているため、初期化のステートが必須です。
こんな具合で必要なステートを列挙していきます。私が考えたプログラム例では全部で7つのステートを用意しました。それぞれのステートの役割は以下のように考えました。
initialize:フロントパネルのブールボタンの状態の初期化を行う。また使用するFGVの初期化も行う
start:ゲームの開始の前にフロントパネルのブールボタンの無効プロパティの状態を変える。具体的には、「スタート」のボタンを押せなくさせて、「ボタン」のボタンが押せるようにする
stay:ある条件が満たされてLEDが光るまで待つためのステート。このステートの時には経過時間のカウントはしない。また、何回目かを表示するための「ブール」の表示(3つ)の状態が正しい状態になっているかを確認
wait:ある条件が満たされたことでLEDが光っている状態を表すためのステート。このステートの時には経過時間はカウントしている。また、何回目かを表示するための「ブール」の表示(3つ)の状態が正しい状態になっているかを確認
clicked:ユーザーが起こした「ボタン」のボタンをクリックしたというイベントにより移行するステートで、LEDが光っている状態で「ボタン」が押されたかをチェックする。LEDが光っていないのに押されていた場合には経過時間のカウントにペナルティ分の時間を加算し、何回目という情報は更新しない。LEDが光っている場合にはペナルティの加算はせず、何回目という情報を更新する。このステートの後は、この何回目という値が3であった場合にはresultのステートに、3未満だったら再びLEDが光るのを待つstayのステートに移行する
result:これまでの時間経過の結果を表示させるステート。また、ゲーム開始の「スタート」のボタンが押される前の状態にするため、「スタート」と「ボタン」のボタンの無効プロパティを変更する
finish:プログラムを終了するステート
これらの設計を軸に、各ステートに必要な機能を盛り込みます。
プログラム構築の過程:FGVの用意
さて、上記のステートの役割のいくつかで、「経過時間を測る」という機能が出てきました。この時間を測るという操作は、各ステートにまたがっていながら、それぞれに異なる操作(経過時間を測る、測らない、ペナルティを加算する)を行う必要があります。ここで機能的グローバル変数(FGV)の出番です。
経過時間を測るFGVについても、初期化のステートが必要です。ゲームを繰り返すたびに(「スタート」のボタンが押されるたび)経過時間は0から数え始めないといけないためです。
その他もちろん、時間を測るステートや、時間を測らないステート、そしてLEDが光っていないのに「ボタン」を操作したというお手付きへのペナルティを与えるステートがあるので、全部で4つのステートを考える必要があります。
メインのVIからFGVに渡すパラメタとしては、FGVのどのステートにするかの指定と、お手付きがあったかどうか(ブール)の二つになります。
時間を測るFGVは前回のまずこれシリーズ第25回のときに既に出ていました。ベースはこれと同じでよさそうで、あとはペナルティ用のステートを用意します。
以下が4つのステートのブロックダイアグラムです。
プログラム構築の過程:各ステートの実装
あとは各ステートの実装です。ここはもう先ほどの「ステートの決定」で各ステートでやるべきことを考えているのでその通りに実装するだけです。
ここまでの過程を経て、それぞれ以下のようなステートを用意しました。注意点としては、何回目かを表示するブールの表し方です。これは、「何回目という情報」を保持した数値を使って何とか表示させてみます。処理がそれなりに煩雑になったのでサブVIとしました。
上記のFGVおよびサブVIを踏まえ、メインVIの一例をこれから紹介していきます。
全体像はこちら。
イベントストラクチャの入っているループは3つのイベントを拾うだけです。
ステートマシンとなっているループの7つのステートのうち、初期化であるinitializeは既に紹介していますので割愛します。
プログラム実行後、ユーザーがフロントパネル上のスタートのボタンを押すことで実行されるstartのステートが以下のようにしています。
このステートを実行した時点で、スタートのボタンは押せなくさせます。代わりに、ボタンのブールを押せるようにして、LEDが光ったらボタンを押してイベントを発生させることができるようにしています。
ここにある回数カウンタとは、スタートのボタンが押されてから3回ある時間計測のうちの何回目かを測るためのカウンタ機能を持ち、上で用意したサブVIとも連携してブールの表示3つの状態を決めたり、後々のステートの移行の判断に使用します。
さて、startステートの後にはstayステートが続きます。
このステートは、LEDが光るタイミングを決めます。stayステートでランダムな時間経過後にLEDを光らせる、という動作を、「乱数で0.95以上の値が出たらwaitステートに移行する」ということで実現しています。
もちろん、0.95という数字は本質的ではなく、どんな数字でも構いません。また、適度に間を空かせるために待機関数に50ミリ秒という指定をしていますが、ここも50である必要は全くありません。
このステートはあくまでLEDが光るのを待つ部分となるので、FGVはnotimecountのステートとして経過時間を測定していないことに注意します。
乱数が条件を満たして移る先が以下のwaitステートです。
このステートは、ユーザーがイベントとしてブールボタンを押さない限り繰り返されます。そのため、エンキューでひたすらwaitの列挙体値を入れ続けます。また、このwaitの時間を測るため、FGVにはtimecountステートを指定しています。
ユーザーがボタンを押すことでイベントストラクチャはキューの先頭にclickedステートを割り込ませ、以下のプログラムが走ります。
キューの中身にはそれまでのwaitの列挙体が入っているかもしれませんが、いったんすべてをリセットするために「キュー排出」の関数でキューの中身を空っぽにしています。
もしLEDが光っていないのにこのステートが実行された場合にはお手付きであるかどうかの判定をする必要があるため、FGVにはpenaltyステートを指定しています。
また、回数カウンタの値も、LEDが光っているときにclickedが実行されたかどうかで1増やすかそのままかを決め、このカウンタが3になったらゲームは終了、resultステートに移行して結果を表示させます。
resultステートに移ったら、フロントパネル上のスタートボタンとボタンのブール制御器の状態をプロパティノードで元に戻し、またゲームができるような状態とします。
また、FGVの方で記録していた累計経過時間の情報を「文字列にフォーマット」関数を使用して特定のメッセージとして表示させるようにしました。
ようやく最後のステートです。こちらは、ユーザーがフロントパネル上で終了ボタンを押した際に実行されるステートで、特に何も行いません。強いて言うのであれば、結果の文字列表示器に「終了」と表示するくらいなものです。
2023/6/7追記
なお、このfinishステートが実行されるタイミングは、ユーザーが終了ボタンを押したタイミングとなりますが、場合によってはこのステートが実行される前にキューが解放されることがあります。
これを防ぐには、キューの中身の要素が実行されてからキューを解放するために、「キューの中身が空になったか」を判定する処理を付け加えるとよりよいプログラムになります。
さて、これで反射神経ゲームの完成です。これだけでも一応プログラムとして遊べるのですが、もっと改良の余地はあります。
例えば、最高スコアを記録させる機能を実装することが挙げられます。記録は外部ファイルに保持する形としておき、そのファイルを読み出して、現在のスコア(反応速度)と比較、ファイルの情報よりも良いスコアであればユーザー名を入力させるポップアップを表示して、ファイルにその記録を残すといった機能です。
こうした外部ファイルへの読み書きも別途FGVを作って行える(一度読み取ったファイルの中身のデータをFGVの中で保持しておく、など)のでFGVに慣れる練習になると思います。
さて、ここまでの内容の知識を駆使して立派なゲームが完成しました。
もちろん、LabVIEWを使用する方は、このようなゲームではなく、何か測定を行った理解席をしたり、それらのログを取るといったような仕事で使用されるケースが多いと思います。そんな場合であっても、ステートマシンやキュー、FGVの組み合わせでそれなりにプログラムを組むことはできるので、工夫して頑張ってみてください。
このブログでは、まずこれシリーズ以外の記事で色々なプログラムの例を紹介しています。中にはステートマシンを使用したプログラムも既にアップしているものもあるので、もしよかったら覗いてみてください。
今回扱った内容以外にもこのシリーズで今後もう少し他の内容も扱うつもりです。
もしよろしければ次の記事も見ていってもらえると嬉しいです。
ここまで読んでいただきありがとうございました。
コメント
LabVIEWまずこれシリーズ、とても参考になりました。ありがとうございます。
今回のviで1点質問があります。
ブログの内容を基に自分なりのviを作ってみたのですが、終了ボタンの処理がうまくいきませんでした。
というのも、終了ボタンの処理をした直後にキュー開放をしてしまうので、fanishステートに入る前のデキューでエラーが出てしまいます。
キュー開放はステートマシン側にあった方が良い気がするんですが、どうでしょうか?
コメントいただきありがとうございます。また、ご質問いただきありがとうございます。
ご指摘の通り、掲載していたプログラムは横着していました、すいません。
この場合、キューの使い方の記事で紹介していたように、キューステータス取得の関数を使用して、終了直前にエンキューした要素も全てデキューされてからキュー解放をする、
というプログラムが一般的(LabVIEWのサンプルでよく見る形)になると思うので、この点追記しました。
キュー解放がステートマシン側にあった方がいいのでは?という点については、もちろんそのような組み方もアリだと思います。
ただ、何となくですが、プログラム全体の構造を見たときに「このプログラムはキューを使用した生産者・消費者デザインだな」といったことが一目でわかるようにするのが
いいと思います。その場合に、キュー生成とキュー解放という、繰り返しを行う必要がない処理についてはループの外に出して、メインの処理部分のみをループに入れるという
構造にした方が、見た目としてわかりやすいので、私のブログ記事ではほとんどこのような形を取っています。
最終的には組みやすい形で実装し、意図したとおりに動作させることができるものであれば、記事で紹介している形にこだわる必要はないと思いますよ。