本シリーズでは、このブログで紹介してきた「まずこれ」のシリーズを凝縮して、LabVIEWによるプログラミングはこんな風にできるんだ、ということを比較的すぐに体験できるよう、内容を絞って紹介しています。
また、紹介する画面はLabVIEW Community Editionという、非商用の目的であれば無料で使用できるエディションを使っています。なので、LabVIEWに興味を持ったらCommunity Editionを実際にダウンロード、インストールして触りながらプログラムを作り、LabVIEWの楽しさに触れてもらえればと思います。
第六回の今回では、より具体的なステートマシンの例を紹介していきます。
LabVIEWを使用してプログラムを作る目的は人によって様々なので、万人受けする例を紹介するのは難しいのですが、今回と次回で、
- 数当てゲーム
- 疑似データ測定プログラム
の二つを題材にしたいと思います。
ゲームは、LabVIEWを手軽に楽しみたい、という方向けの内容で、ステートマシンの考え方の例としては比較的単純なものです。
もう一つの疑似データ測定プログラムは、その名の通りデータ測定を模したプログラムで、実際にLabVIEWを使用して何か測定を行うといったことをする場合の基本的な形を想定しています。
今回は数当てゲームを扱っていきます。
なお、前の記事はこちらです。
数当てゲームとは
では、今回題材とする数当てゲームとはどういったものなのか、についてですが、以下のルールに沿ったプログラムを作ることを考えます。
- 最初に、当てる数の範囲を指定します。
- 範囲を決定したら、LabVIEWがランダムでその範囲の中から当たりの数を一つ決めるのでそれをプレイヤーが当てます。
- プレイヤーは予想した数を一つずつ入力してそれが当たりの数と一致するか判定させます。
- もし予想した数が当たりの数より大きい場合には「大きいです」、予想した数が当たりの数より小さい場合には「小さいです」といったヒントを表示させ、それらヒントの履歴が残ります。
- 予想した数と当たりの数が一致すればプログラムが終了します。
- 予想した数と当たりの数が一致しなくてもいつでも正しくプログラムを終了できるようにします。
このようなルールで実行できるゲームを作っていきますが、先に完成後のプログラム実行の様子を紹介します。
ユーザーインタフェースを作る
実際にプログラムを作るにあたって、ユーザーインタフェース部分を作る作業は結構重要です。
このユーザーインタフェースの作りによって、実際にそのプログラムを動かすユーザーの使いやすさが決まるためです。
これまでの回でLabVIEWの使い方を説明してきたときにそうであったように、LabVIEWはユーザーインタフェースを手軽に構成することができます。
「ユーザーが画面を操作する」プログラムを作る場合、このLabVIEWの特徴はとても重宝します。
「とりあえずどういうボタンや表示画面がどこにあってどういう使い方をする」といったことを、まず最初に具体的なイメージとして持つことができるためです。
なお、これまでの説明では、制御器パレットのデフォルトであるModernの項目(スタイル)から制御器や表示器を扱ってきましたが、今回はModernではなくSilverにある制御器、表示器を使っています。
単純に、ブールの項目についてSilverの方が多くの見た目を備えているため、よりわかりやすいインタフェースを作ることができるためです。
今回のプログラムは練習なので深く考える必要はないですが、この数当てゲームのルールにあった使い方をするためのユーザーインタフェースとしては、例えば以下の図で示したようなことを考える必要があるはずです。
その結果、冒頭で紹介したユーザーインタフェースのプログラムを作ろう、と思い立つわけです。
なお、以下の図の「最小」、「最大」を指定するための数値制御器の背景にグレーの枠がありますが、これは特に今まで紹介してこなかった装飾体を用意しているだけで、必須ではありません。
もちろん、これが絶対的な正解というわけではなく、自由に設計してもらって構いません(例えば結果表示の文字列表示器や停止ボタンを、これより上に置いた制御器などより右側に置くことで、縦長ではなく横長のインタフェースにする、など)。
ですが少なくともこれらの制御器(control)や表示器(indicator)がないと上記のルールのゲームが成り立たないので最低限何が必要かはしっかり確認するようにしてください。
なお、余談ですが、LabVIEWのプログラムの書き方に慣れると、ユーザーインタフェースの「自由度」はどんどん増えていきます。
例えば、今は全ての機能(数字の範囲を指定する、予想した数字を入力する、履歴を表示させる、など)を一つのユーザーインタフェースにおさめていますが、数字の範囲を指定するのは設定画面であるため、ゲームをプレイするための画面とは分けて表示する、といったことももちろんできます。
あるいは、制御器パレットにある既に用意された制御器や表示器の見た目以外にカスタマイズすることもできます。
ただし、そういったプログラムの書き方は、当然「そういった書き方ができる」ことを知らないと書けないですし、「そういった書き方はこのようにすれば実現できる」という方法を知らないと実装できません。
これらの知識は、あるに越したことはありませんが、プログラムを書く上で最低限必要な知識かと言われるとそうではないので今回のこのシリーズではそこまでは扱いません。
なので、LabVIEWに慣れないうちは、とにかく必要最低限の機能は一通り同じユーザーインタフェース(同じviのフロントパネル)に盛り込み、デザイン(配置やスタイルの種類)の面で工夫してみてください。
ステートマシンの構成として状態を考える
ユーザーインタフェースを作り終えたら、後はブロックダイアグラム上で実際にルールに沿った流れでプログラムが動くようにコードを書いていきます。
前回の記事で紹介しましたが、ステートマシンを意識したコードを書くには、「状態」が必要で、それら個々の状態をケースストラクチャに当てはめていました。
今回の数当てゲームのルールを踏まえて、どのような状態が必要になるかをまずは考えてみてください。
これも絶対的な正解があるわけではないのですが、あくまで今回の記事では以下の状態があるものとして考えていきます。
- ゲーム準備:ユーザーが範囲を指定、決定するための状態
- 選択待ち:ゲーム中、ユーザーが予想した数を指定、決定するための状態
- 結果:ユーザーによって予想された数と、当たりの数の大小を比べるための状態
- 終了:プログラムを終了させるための状態
頭の中のイメージとして、まずこれらの状態があることを考え、何らかの方法でそれらがつながっている(ある状態から別の状態へ移る)ことをまず考えていきます。
この中で意外と忘れがちなのは、終了の状態です。
どんなプログラムも、最終的には「意図した状態で」終わらせるのが望ましいです。言い換えると、緊急停止のような無理やり止めるしか停止させる方法のないプログラムは望ましくないので、終わるべきタイミングで終わるようなプログラムを書く必要があり、そのために終了の状態を用意します。
なお、終了と対になる形で、本来は初期化という状態も必要になる場合が(多く)ありますが、状態が多くなり複雑すぎると最初から負担が大きくなってしまうので今回は無くしています。
初心者の方にとってしてみれば、「なんでこのプログラムが上の4つの状態からできているかわからない」と思われるかもしれません。あるいは「どういう基準でこれら4つの状態を考えたのか知りたい」という疑問を持つ方もいると思います。
正直なことを言ってしまえば、どんな状態を用意すればいいかは慣れればわかってくる、もので、あまり明確なマニュアルがあるわけではない(少なくとも私は知りません・・・)ですが、そうは言ってもいくつか基準はあると思っています。
個人的なこれまでの経験から何とか挙げるとすれば以下のようなことは考えておく必要があると思っています。
- プログラムの最初と最後の状態を決める(既知の状態にする)
- ユーザーの操作、あるいはある状態で処理された結果によってその後複数の処理内容が考えられるときには「状態」を分ける
- ひとつの状態で時間がかかりすぎないようにする
どのように各状態を分けてそれぞれにどのような処理内容を当てはめていくかについて、最初から最適な選択ができるわけではないと思います。
プログラムを作っていくうえで、「あ、この状態の複数の処理、途中で分岐する可能性があるから分けた方が良いな」とか、「この処理とこの処理、同じ状態にできるな」とか、そういったこともあり得るためです。
そうした後からの修正を加えることももちろんできるので、まずは何となく、見様見真似でもプログラムの例を見て作ってみて、感覚をつかんでいければいいかなと思います。
上に挙げた項目の中の「ひとつの状態で時間がかかりすぎないようにする」という判断基準は少し曖昧なのですが、少し違う言い方をすると「ケースストラクチャのラベルで内容を言い表せる程度の処理内容にとどめる」という判断基準も役立つと思います。
プログラムは、その場で書いて終わり、の場合もあれば、一度完成した後に改造、修正を加える必要があるといった場合も存在します。
そんなとき、後から見返したときに「この状態って何やっていたんだっけ?」というのがわからないと、既存のコードを理解するところから始めることになり、とても面倒だったりします。
そんな負担を少しでも軽減するためのベストプラクティスとして、「ケースストラクチャのラベルによってその状態の処理内容を分かりやすく記述する」といった考えがあり、この考えに伴って、ケースストラクチャのラベルで表現できる程度の処理内容しかその状態に含めない、という意識を持つと色々都合がよかったりします。
ステートマシンの構成として条件分岐の仕組みを考える
少し話が脱線してしまいましたが本題に戻ります。
今回のプログラムをステートマシンのデザインで書く際に必要な状態を決めたところで、今度は各状態(ケースストラクチャでそれぞれ定義されている)から別の状態にどのように移るかを考えていきます。
プログラムが大きくなればなるほど複雑になりますが、今回は状態の数もそこまで多くないので、「ある状態から次にどの状態に移る可能性があるか」という観点で一つずつ確認していきます。
実際は、どんな状態があり得るかを考える過程でおのずとわかることが多いと思うのですが、今回については以下のようになります。
- 「ゲーム準備」状態から移る可能性があるのは「ゲーム準備」か「選択待ち」か「終了」状態
- 「選択待ち」状態から移る可能性があるのは「選択待ち」か「結果」状態
- 「結果」状態から移る可能性があるのは「選択待ち」か「終了」状態
- 「終了」状態からは他に移る可能性はない
せっかくなので、先ほど書いた状態の種類の図に、移り方の情報も載せておきます。
ここでこんな疑問を持った方もいると思います。「なんで「ゲーム準備」状態から「ゲーム準備」状態に移る可能性があるの?」と。
ゲーム準備状態というのは「ユーザーが範囲を指定、決定するための状態」でした。
この状態については、すぐ後で具体的なプログラムの内容を紹介する際に見せるのですが、「ユーザーが範囲である数字を決めて決定ボタンを押すまで待つ状態」としています。
ユーザーが、当てる数字の範囲を決めるまでLabVIEWは「待つ」必要があります。でも、LabVIEWはいつまで待てばいいのかわかりません。実際は、ユーザーが範囲である数字を決めるまでです。・・・それはいつ?5秒後?それとも10秒後?答えは「わからない」ですね。
そう、わからないんです。当てる数字の範囲を決めるのはユーザーの自由なので、それをいつ決めるかもユーザーの自由、そのため「わからない時間」を待つ必要があります。
これをプログラム的にどう実装すればいいか?一つの解決策を簡単な例で具体的に以下に示しています。(これ自体は数当てゲームとは関係のないブロックダイアグラムです)
プログラムの中で使用されているのは「Select(選択)」という関数です。
前回のじゃんけんのプログラムの例でも登場しました。
この関数には、入力が3つあり、「条件」の入力と「その条件がTrueだった場合の出力結果」と「その条件がFalseだった場合の出力結果」の入力があります。
例として上で示したプログラムの場合、この選択関数の条件の入力には、Boolean、つまりブールの制御器がワイヤで紐づいています。つまり、このプログラムは、「ブール制御器がFalseのままだと同じ状態が繰り替えされ、Trueになると次の状態に移る」という仕組みにしています。
待機の関数に100と指定してあるので、この部分(数当てゲームでいう「ゲームの準備」に相当)の繰り返しは100ミリ秒ごとに行われます。
なので数宛ゲームの文脈に置き換えると「LabVIEWは100ミリ秒ごとにブール制御器の状態を読み取り、もしブール制御器が押されてTrueになっていれば選択待ち状態に移り、押されていなければゲームの準備状態としてまた次の100ミリ秒待つことを繰り返す」という構造になっています。
こうすることで、ユーザーがいつ数字を決定するかあらかじめわからない状態でも待ち続けることができます。
別に待機の関数への指定は「100」である必要は特にありません。何となくキリがいいのでこうしているだけで、「29」でも「314」でもなんでもいいのですが、あまり変な数字にすると、後で自分以外の人がプログラムを見返したときに「中途半端な値にしているということは何か意図があるのか?」と無駄に勘違いさせないためにも、特別な理由がなければキリのいい数にしておくことをオススメします。
一方で、こんな組み方もあるのでは?と考えた方がいるかもしれません。
確かにこれも一案です。この方法であっても、ブールの制御器が押されたら次の状態に進むということを表現できます。ただし、個人的にはこの組み方はあまりオススメできません。
その理由としては、プログラムがこのWhileループで停滞してしまうことです。
ここはLabVIEWの並列実行の概念がわかっているとより理解しやすいのですが、この内側のWhileループを抜けない限り、プログラムの他の箇所で何か意図しない状態が発生したとしても、「絶対に」このステートマシンが他の状態に移ることが無くなってしまいます。
それよりも、常に状態を一定の間隔で更新し続けるようなプログラムの方が柔軟性があり、他の処理との連携も取りやすくなります。
今回のプログラムでも具体的にこの柔軟性に関連した部分が後で出てきます。(プログラムの要件にあった、「いつでも正しくプログラムを終了できる」ようにするため)
話を戻して、次の状態に移るために選択関数を使用するということを説明しましたが、この選択関数は前回の記事のじゃんけんプログラム同様、ステートマシンでの使い勝手がとてもいいのでこの後にも登場してきます。
ある状態から別の状態への移り変わりを説明した部分にあった
- 「選択待ち」状態から移る可能性があるのは「選択待ち」状態か「結果」状態
についても、全く同様で、選択待ちの状態が続くかあるいは結果の状態に移るかを同じような選択関数の使い方で実現することができます。
これを踏まえ、状態から状態への移り変わり(遷移)の条件を言葉で書いてみます。
- 「ゲームの準備」状態からは、ゲームスタートのボタンが押されたら「選択待ち」状態に移り、押されなかったら「ゲームの準備状態」状態を繰り返す
- 「選択待ち」状態からは決定ボタンが押されたら「結果」状態に移り、押されなかったら「選択待ち」状態を繰り返す
- 「結果」状態からはあらかじめ決まった数とユーザーが予想した数が一致すれば「終了」状態に移り、一致していなければ「選択待ち」状態に戻る
- 「終了」状態になってからはプログラムが終了する
ここまで言葉で書いたら、あとはこれをLabVIEWのプログラムとして落とし込んでいくだけです。
ステートマシンの実装
では、各ステートをケースストラクチャのそれぞれのケースの中身としてそれぞれ実装した際の例を紹介します。
既に言葉で紹介しているので、それぞれの状態は簡単に説明していくにとどめています。
まず、ゲーム準備の状態です。
Whileループの外側に「ゲーム準備」と書かれた列挙体の定数があります。これがWhileループのシフトレジスタに入力されているので、Whileループの一番最初の回はこの「ゲーム準備」がケースストラクチャに渡されます。
中では、ユーザーが最大、最小の範囲を決められるようになっています。そしてその範囲の中でLabVIEWが当たりの数を決めるために処理(最大から最小を引いた数に乱数を掛けて丸め込みを行い、その値を最小に足している)を行っています。
このLabVIEWが決めた当たりの数はこれ以降の状態で使用されるためシフトレジスタに渡しています。
また、あとの状態で使用する「予想した数」を適当な値で初期化しています。
ゲームスタートのボタンが押されたら次に選択待ち状態に進みます。
ケースストラクチャの中にさらにケースストラクチャがありますが、これはこの選択待ち状態が初めて実行されたときのみTrueになるFirst Call ?という関数を使用しています。(関数パレットとしてはSynchronizationのパレットにあります)
この状態で「ゲームスタート」という文字列を使用することで、結果表示の文字列表示器に「ゲームスタート」という文字を表示させることができるようになります。
後の理屈はゲーム準備状態と同じで、決定のボタンが押されるまでこの状態を100ミリ秒おきに繰り返すことになります。
ユーザーが予想した数は次の結果状態にシフトレジスタを介して渡されます。
結果状態では、予想した数と当たりの数の大小を比べて、その結果次第で結果表示に何と表示するか、次にどの状態に進むかを決めています。
結果表示の文字列表示器に履歴を残すためにConcatenate Stringsという関数を使用して、文字の履歴をconcatenate(連結)していきます。
無事、予想した数と当たりの数が一致したら終了状態に移り、ゲーム終了の文字を結果表示に出しつつWhileループを止めます。
なお、ここで使用されているFormat Into Stringという関数は、他のデータタイプ(数字など)を織り交ぜて一つの文章を作るのに適しています。
それぞれのデータタイプでどう指定するかは決まっていますが、比較的よく使用されるのは以下の図で示したものだと思います。
終了状態では特になにもしません。
ゲーム終了というメッセージを結果表示の文字列表示器の最後に表示してWhileループを止めてプログラムが終了します。
なお、ケースストラクチャの外側には下の図の赤枠の部分があることに気づいたでしょうか。
この処理があるおかげで、プログラムが今どの状態であろうと、停止ボタンが押されたタイミングで終了状態に移ることができるようになります。
上で、「なんで「ゲーム準備」状態から「ゲーム準備」状態に移る可能性があるの?」の話の中で紹介したように、ある状態から別の状態へ移る際に「Whileループを配置してそのWhileループから抜け出したら次の状態に進む」という処理を設けていると、こうした「いつでもある特定の状態(終了)に移る」という書き方ができなくなります。
プログラムの種類によっては、上で紹介した書き方をする必要がない、ということもあると思いますが、かといって初心者の方にとっては「見たことがないと自力で作るのも難しい」と思うので、プログラムはこんな風に作れるんだという参考になるといいなと思います。
(ある状態で待機させるという点に関しては、そもそも書き方を大きく変えて「イベント」という考え方を取り入れることでより効率的なプログラムが書けます。が、例によってこのシリーズではその話は割愛しています)
ステートマシンは怖くない
上のプログラム、実際に各状態の中身を見ていかがでしたでしょうか?あるいは、真似て実際に手を動かして作ってみた方はどんなことを思ったでしょうか?各状態の中身の処理および他の状態への移り方の書き方を見てどう感じたでしょうか?
「こんなの思いつかないよ」、あるいは「何となく自分でもできそう」、見ている方によっていろいろ感じるところはあると思います。
それぞれの状態を考えたり中身の処理を思いつくところはまだ難しくても、少なくとも上の例のそれぞれの状態の中身の処理を見て、何をしているかがわかる、という状態になればまずはいいかなと思います。最初からできる人なんてほとんどいないですし。
自力で書けるかどうかはともかく、読んでくれている方々が「ステートマシンと聞いても怖くないな」と思ってもらえればうれしいです。
同じような動作をするプログラムをもっと効率的に書く方法もあると思います。実際、上のプログラムは初心者の方が作りやすいようにあえて無駄な動作をしている部分もあるので、効率を上げるプログラム上の工夫やテクニックを盛り込めば十分改良の余地はあります。
それでも、まずはステートマシンというプログラムの作り方についてまずは苦手意識を持つことなく、ひいては「LabVIEW自分にもできそう!」と思ってもらえればこの記事を書いた甲斐があります。
次回の記事も、ステートマシンの具体例を紹介します。LabVIEWを使用する目的としては、何か測定を行ってそのデータを保存するという処理を行わせたいという場面もありえるので、そんな用途に近いプログラムの組み方について扱っていきます。
よければ次の記事もみていってください。
ここまで読んでいただきありがとうございました。
コメント