前回の物を持ってきてくれるロボットに続き、複数台のロボットが字を描く書道ロボットアプリケーションをサンプルとしてご紹介します。これは、もともと一台に字をなぞらせていたLEGO書道を複数台に拡張してソースコードを少し綺麗にしたものです。前回同様、配布バイナリはHakoniwaシミュレータ上で動作します。
- RobotCalligraphy.zip (Mac OSX, Linux用)
- RobotCalligraphy-exe.zip (Windows用)
アプリケーションのソースコードと実行可能なバイナリが含まれています。例によってバイナリはライブラリの正式配布前のα版を利用して動作するもので、ライブラリ部分のソースコードがオープンソースでなく、また、完全に無保証です。
今回はソースコードが7つのファイルに分かれていてちょっと複雑に見えますが、GUI部分のコードが大部分を占め、ロボットに指示を出す処理は一つのメソッドの中に書かれています。
まず、各クラス、インタフェースの概要を説明します。
- Path (Path.java)
- ユーザが描く連続した一本のパスを表すクラスです。マウスイベントで得た生の座標値列を適当な間隔でリサンプルする機能などを持っています。
- WizardFrame, WizardComponent (WizardFrame.java, WizardComponent.java)
- ウィザード形式の(Nextボタンで画面遷移する)ウィンドウを表すクラスと、その中で切り替わる各画面を表すインタフェースです。
- StrokePainterPanel (StrokePainterPanel.java)
- ユーザに手本となる文字を描いてもらう画面を表すクラスです。アプリケーション起動直後にWizardFrameに貼り付けられています。PathsProvider.javaで定義されているPathsProviderインタフェースを実装しています。
- StrokePlayerPanel (StrokePlayerPanel.java)
- 手本となる文字をロボットになぞらせ、その様子を映す画面を表すクラスです。StrokePainterPanelの次に表示される画面です。
ユーザはまず、左のようにStrokePainterPanelに文字を描きます。
Nextをクリックすると右のようにStrokePlayerPanelに表示が切り替わり、ロボットが動き始めます。
待っていると、赤いインクでロボットが字をなぞっていきます。最終的に、ユーザの手本と似ているけれどちょっと違う、味のある(?)書道作品ができあがるはずです。
さて、実際にロボットに指示を出す処理はStrokePlayerPanel.play()メソッドの中に書かれています。
今回のように、ロボットが順序立てて、または並列に複数のタスクを実行していく場合、各タスクの終了をイベントリスナを用いて検知し、次に実行すべきタスクを改めてロボットに割り当てていくようなコードを書くと、リスナが入れ子になったり再帰呼び出しする格好になって見通しが非常に悪くなります。
そこでmaterealでは、アクティビティ図というフローチャートの拡張の一種を組み上げ、それを実行するようなコードを書けるようになっています。
アクティビティ図とは、左のようにどのロボットがどのタイミングでどのタスクを実行するかを表した有向グラフの一種です。
赤いハイライトで示されたノードは現在実行中のタスクを表しています。
少し時間をおいて2枚スクリーンキャプチャを撮ってみました。
2枚の図の左側1/3くらいを拡大したのが右の画像です。赤いハイライトが進んでいる様子が分かると思います。
ノードには、タスクの実行を表す丸で表されたアクションノードと、実行の流れをコントロールする四角で表されたコントロールノードの2種類があります。
この図で見えているコントロールノードはForkノードといって、一つの処理フローを5本に分け、5台のロボットで並列して処理を実行させる目的で使われています。他に、複数の処理フローを同期をとって1本にまとめるJoinノードなどがあります。
アクティビティ図は正式にはUML1.0という標準規格の中で定義されていますので、詳しくはその関連情報をご覧ください。なお、アクティビティ図の可視化に際してノードを配置するアルゴリズムが賢くないので、見た目があまり綺麗ではありません…そのうち改善したいと思います。現状でも、マウスのドラッグでグラフの要素を再配置し、手動で見栄えを整えることは可能です。
以降、少し長いですが、アクティビティ図を構築して実行するまでの処理を詳しい注釈付きで掲載します。
paths = pathProvider.getPaths(); // ロボットの台数で描くパスを分担します。パスのほうが数が少なければ、パスの数しかロボットを使いません。 int forks = robots.length > paths.size() ? paths.size() : robots.length; // 「アクティビティ図」を作ります。 final ActivityDiagram ad = new ActivityDiagram(); Action[] inits = new Action[forks]; int offset = 0; int base = paths.size() / forks; // どのロボットも最低base本のパスを描きます。 int rest = paths.size() - forks * base; // rest台のロボットはbase+1本のパスを描きます。 // 各ロボットごとにアクティビティ図の部品を作って繋げます。 for (int r = 0; r < forks; r ++) { // このロボットはmy本のパスを描きます。 int my = base + (r < rest ? 1 : 0); Action tail = null; for (int i = 0; i < my; i ++) { // (略) // p に、ロボットが辿るべきi本目の連続したパスを表す実世界座標のリストが入ります。 // i本目のパスをなぞって描くためのアクティビティ図の部品(ノード)を作ります。 // まず、パスの先頭の位置へ移動します。 Action head = new Action(robots[r], new Move(p.get(0))); // 次に、パスを描き始めるのに適した方向を向きます。 Action a = new Action(robots[r], new Rotate(p.get(1))); // 筆を下してパスをなぞります。 Action b = new Action(robots[r], new DrawPath(p)); // 作ったノードをアクティビティ図に挿入します。 ad.add(head); ad.add(a); ad.add(b); if (i == 0) { // 各ロボットについて最初に実行すべきタスクを表すノードはinits[r]に保存されます。 inits[r] = head; } else { // 一本前のパスを描き終えたら今のパスを描くように、二つのノードを繋ぎます。 ad.addTransition(new Transition(tail, head)); } // 上の方で作ったノードhead, a, bを、この順に繋ぎます。 ad.addTransition(new Transition(head, a)); ad.addTransition(new Transition(a, b)); // 次のループで使うためにノードbをtailに退避します。 tail = b; } offset += my; } // 各ロボットを並列で動かすため、各ロボットについて最初に実行すべきタスクを全て並列実行するようにForkノードを作ります。 Fork fork = new Fork(inits); // Forkノードを作ったら、アクティビティ図に挿入し、 ad.add(fork); // 最初に実行するイニシャルノードとして指定します。 ad.setInitialNode(fork); // アクティビティ図が組み上がったので実行を開始します。 ad.start(); |
ピンバック: Tweets that mention Robot calligraphy | matereal -- Topsy.com