#nomenubar
ロボットに複雑な動作をさせようとすると、 当然プログラムの方も複雑になってきます。 そこでプログラムが複雑になっても デバッグ(プログラムの間違い探し)がしやすく、 また改良もしやすいように、 なるべく一連の動作を部品にわけて記述しましょう。 以下部品に分かる方法をいくつか紹介します。
ちなみに多くのプログラミング言語では、一連の機能をまとめたプログラムの部品をサブルーチンと言ったり、関数と言ったり、メソッドと言ったり、マクロと言ったりしますが、以下で説明するようにNQCの場合には、これらのおのおのについてNQC独自の使い分けが必要なこともあります。
目次
以下のような、直進したり左右に曲がるロボットのプログラムを考えましょう。 (以下の説明のために、既に正回転しているモータをさらに正回転させるなど、 不必要な命令も多少含まれています。)
#define TURN_TIME 200 // 旋回に必要な時間 task main() { OnFwd(OUT_A+OUT_C); Wait(300); // 3秒間直進 OnRev(OUT_A); OnFwd(OUT_C); // 左に曲がる Wait(TURN_TIME); Off(OUT_A+OUT_C); OnFwd(OUT_A+OUT_C); Wait(400); // 4秒間直進 OnFwd(OUT_A); OnRev(OUT_C); // 右に曲がる Wait(TURN_TIME); Off(OUT_A+OUT_C); }
たしかにこの程度の簡単なプログラムであれば、ロボットにさせたい動作を順番に書き下していってもそれほど大変ではありません。しかし、もう少し複雑なプログラムになってくると、同じ動作を記述する命令を何度も繰り返して記述するのが面倒になってきたり、その一連の動作を少し変更させたいときにも、関連するすべての箇所を書き換えなければいけません。
そこでまず登場するのが「サブルーチン」です。サブルーチンを使うことで、 プログラムが見やすくなるだけでなく、改良も加えやすくなります。 以下は、左に曲がるという動作を記述した turn_left という名前のサブルーチンと、右に曲がるという動作を記述した turn_right という名前のサブルーチンを定義したサンプル・プログラムです。
#define TURN_TIME 200 // 旋回に必要な時間 sub turn_left() { OnRev(OUT_A); OnFwd(OUT_C); Wait(TURN_TIME); Off(OUT_A+OUT_C); } sub turn_right() { OnFwd(OUT_A); OnRev(OUT_C); Wait(TURN_TIME); Off(OUT_A+OUT_C); } task main() { OnFwd(OUT_A+OUT_C); Wait(300); // 3秒間直進 turn_left(); // 左に曲がる OnFwd(OUT_A+OUT_C); Wait(400); // 4秒間直進 turn_right(); // 右に曲がる }
このようにサブルーチンを定義するには sub というキーワードを使い、 そのサブルーチンをメインルーチン(メインタスク)から呼び出すときには、 サブルーチン名のあとに()をつけます。
★ 練習問題
上記の turn_left, turn_right のようなサブルーチンを使って、
これまでのプログラムを書き換えなさい。
★ 練習問題
上の例題について、「直進する」というサブルーチンを go_straight という名前で定義して書き換えなさい。
上の例を、今度は「関数」を使って書き直して見ましょう。 ここで言う「関数」とは、中学校や高校の数学で習った、ある独立変数(入力)に 対して従属変数(出力)が決まる、y=f(x) のような形の場合もありますが、 この例のように、入力された値に従って何かの動作をするだけで何の値も返さない、そういう関数もあります。このことは他のプログラミング言語でも同様です。
#define TURN_TIME 200 void turn_left() { OnRev(OUT_A); OnFwd(OUT_C); Wait(TURN_TIME); Off(OUT_A+OUT_C); } void turn_right() { OnFwd(OUT_A); OnRev(OUT_C); Wait(TURN_TIME); Off(OUT_A+OUT_C); } void go_straight(int t) // 関数は引数を取ることができる { OnFwd(OUT_A+OUT_C); Wait(t); // t/100 秒直進する Off(OUT_A+OUT_C); } task main() { go_straight(300); // 3秒間直進 turn_left(); // 左に曲がる go_straight(400); // 秒間直進 turn_right(); // 右に曲がる }
C言語などでは、関数が返す値の型 (例えば、整数型を表す int や 倍精度の小数を表す double) を関数の名前の前に書きます。 一方、例えば、ある文字列を表示するだけ、という関数のように値を返さない関数の場合には void というキーワードを指定します。
ところで実は NQC の場合、値を返す関数というのがサポートされていないので、 全ての関数が void という型なります。
もし何かの値を返したい場合には、その値を以下で説明するグローバル変数として定義しておいて関数の中でその変数に値を代入するようにすれば、一応同等のことができます (グローバル変数を使ったこのような方法は、現在では一般に推奨されない方法ですが)。
また、サブルーチンと違って関数はコンパイル時に展開されてしまうので、何度も関数を呼ぶようなプログラムを書けば、プログラムのサイズがとても大きくなってしまいます。
プログラムを部品化するもう一つの方法は、定数を定義したのと同じ、#define を使う方法です。#define で定義した一連の動作はマクロと呼ばれます。
#define TURN_TIME 200 #define turn_left OnRev(OUT_A);OnFwd(OUT_C);Wait(TURN_TIME);Off(OUT_A+OUT_C); #define turn_right OnFwd(OUT_A);OnRev(OUT_C);Wait(TURN_TIME);Off(OUT_A+OUT_C); #define go_straight(t) OnFwd(OUT_A+OUT_C);Wait(t);Off(OUT_A+OUT_C); task main() { go_straight(300); // 3秒間直進 turn_left; // 左に曲がる go_straight(400); // 4秒間直進 turn_right; // 右に曲がる }
ただし、サブルーチンや関数と違ってマクロは必ず一行で書く必要があります。 マクロを実行させる場合にはマクロ名を書くだけでよく、()は必要ありません。
また関数と同様にマクロは引数を取ることができます。 そして、マクロも関数と同様にコンパイル時に展開されるので、その分、プログラムのサイズは大きくなります。
★ 練習問題
これまでに書いたプログラムを サブルーチンや関数やマクロを使って わかりやすいプログラムに書き換えなさい。
(作成中)
これまでは、変数を定義する位置についてあまり触れませんでした。 ここでは、定義する場所によって、変数の働きがまったく違う、ということを説明します。
さて、タスクやサブルーチン、関数の内部で定義した変数はローカル変数と呼ばれ、他のタスク、サブルーチン、関数の中では使用(アクセス)できません。 一方、タスクやサブルーチン、関数の外部で定義した変数はグローバル変数と呼ばれ、すべてのタスク、サブルーチン、関数から使用できます。
グローバル変数は一見便利なように思われますが、タスクやサブルーチンを個々に改良していく際には足枷となってしまうので、一般にはあまり推奨されません。
ちなみに RCX2 ではローカル変数が 16 個、グローバル変数が32個使用できます。