小学校教員のためのプログラミング入門

インタラクティブに図形を動かす

 前回は,壁にぶつかると跳ね返るボールのシミュレーションを作りました.前回までのスケッチでは,転がし始める場所や方向を変えるにはスケッチのコードを変更する必要がありました.今回は,これをインタラクティブ(対話的)に指示できるようにしてみます.

転がし始める場所を指示する

 まず,前回演習時間を設けて作成してもらったスケッチの完成例を次に示します.これを元に転がし始める場所を指示できるように変更していきます.

int r = 10;
float x0 = r, y0 = r;
float v = 30;
float theta = (PI/8)*3;

float t = 0;
float x = x0, y = y0; 
float vx = v * cos(theta), vy = v * sin(theta);

void setup()
{
  size(300,300);
  colorMode(RGB,255,255,255,100);
  ellipseMode(RADIUS);
  background(0,0,0);
  frameRate(30);
}

void draw()
{
 stroke( 0, 0, 0, 10 );
 fill( 0, 0, 0, 10 );
 rect( 0, 0, width, height );

 x = x0 + vx*t;
 y = y0 + vy*t;

 stroke( 0, 0, 100, 100 );
 fill( 0, 200, 255, 100 );
 ellipse( x, y, 10, 10 );
 t = t + 0.1;
 
 if( y > height-r )
 {
   vy = -vy;
   x0 = x;
   y0 = height-r;
   t = 0;
 }
 else if( y < r )
 {
   vy = -vy;
   x0 = x;
   y0 = r;
   t = 0;
 }
 
 if( x > width-r )
 {
   vx = -vx;
   x0 = width-r;
   y0 = y;
   t = 0;
 }
 else if( x < r )
 {
   vx = -vx;
   x0 = r;
   y0 = y;
   t = 0;
 }
}

  マウスクリックで指示するようにしてみる

 転がし始める場所の指示にはマウスボタンを利用することにしてみます.具体的には画面の中にマウスカーソルを置いてマウスボタンを押す(マウスダウンする)と,マウスカーソルの位置からボールが転がし始められるということです.

07-1.jpg

 このためにはマウスダウンされたら〜するという処理を書く必要があります.このマウスダウンされたのような事象のことをイベントと呼びます.イベントにはこの例以外にも,マウスが動く,キーボードが押されるなどがあります.イベントを検知してなんらかの処理を行う仕組みをイベントハンドラと呼び,Processing ではイベントに対応した組み込みメソッドを定義することで,イベントハンドラの定義を行います.

 マウスダウンに対応するメソッドは mousePressed です.コードの中に

void mousePressed()
{
  マウスダウンされたときに実行する処理
}

のように書くことで,マウスダウンされたときに任意の処理を実行できます.今回行いたいのは,マウスダウンされたら,その場所からボールを転がす動作にしたいので,初期座標をマウスダウンされた場所(座標)に書き換え,時間 t を 0 に,初速度や方向を初期化すればよいはずです.

 マウスダウンされた場所座標は mouseX と mouseY で取得することができます.これらは常にマウスカーソルの位置を示す座標が格納されている組み込み変数です.

 これらのことから次のコードを先のコードの中(最後部)に入れればよいはずです.

void mousePressed()
{
  x0 = mouseX;
  y0 = mouseY;
  v = 30;
  theta = PI/8;
  vx = v * cos(theta);
  vy = v * sin(theta);
  t = 0;
}

  前の軌跡を消す

 ボールをころがしたときには,前のボールの軌跡を消したいですね.今までは draw メソッドが呼び出されるたびに不透明度 10% の黒色で塗りつぶしていたため,残像が残りました.よって,マウスダウンされたときには,不透明度 100% の黒色で塗りつぶす処理

stroke( 0, 0, 0, 100 );
fill( 0, 0, 0, 100 );
rect( 0, 0, width, height );

を mousePressed メソッドの中に入れてあげれば,残像がすべて消えるはずです.

転がす方向を指示する

 次に転がす方向を指示できるようにしてみましょう.転がす方向の指示にはマウスドラッグを利用してみることにします.具体的には,マウスダウンされた場所から,マウスアップされた(マウスボタンが離された)場所を直線で結んだ方向を転がす方向として利用してみます.なお,マウスアップイベントに対応したメソッドは mouseReleased です.

07-2.jpg

  マウスダウン&アップ時の座標取得

 先ほどはマウスボタンが押されたときに投げ直しを行いましたが,今回はマウスアップされたときに投げ直しを行うことになります.したがって,mouseReleased でころがし直す処理を実行することになります.転がす方向は次の図のようにして求めることができます.

7-3.jpg

なお,逆正接関数(アークタンジェント)は atan メソッドで求めることができます.ただし,atan は x 軸方向の成分が正であるとき -PI/2 〜 PI/2 の値を返す仕様になっているため,x 軸方向の成分が負の場合は,y 軸方向の成分の符号が反転した場合の値が求まってしまうので,求まった値に PI を加える必要があります.

6-11.jpg

 マウスアップされたときの座標 (X2,Y2) は mouseReleased の中において mouseX, mouseY から取り出すことができます.一方マウスダウンされた時の座標 (X1,Y1) は mousePressed の中では mouseX, mouseY に格納されていますが,mouseReleased の中ではすでにどこにも保存されていません.よって,mouseReleased の中でもこの値を利用するために,mousePressed の中で変数に保存しておく必要があります.よって,マウスダウンされたときとマウスアップされたときの座標は次のようにして取得することになります.

float x1 = 0, y1 = 0;
void mousePressed()
{
  x1 = mouseX;
  y1 = mouseY;
}

void mouseReleased()
{
  float x2 = mouseX;
  float y2 = mouseY;
}

  グローバル変数とローカル変数

 ここで,マウスダウンされたときの座標を保存しておくための変数 x1, y1 の宣言が,mousePressed の中,つまり

void mouseReleased()
{
  float sx = mouseX;
             :

ではなく,外に置かれています.今まで変数の宣言の場所をきちんと意識したことはありませんでしたので,ここで説明をしておきます.

 メソッドの外で宣言された変数を グローバル変数 と呼びます.一方,メソッドの中で宣言された変数を ローカル変数 と呼びます.グローバル変数はコードのどこからでも利用が可能です.一方,メソッドの中で宣言されたローカル変数は,メソッドの中でしか利用することはできません.

 一見,グローバル変数だけあれば十分じゃないか考える人も多いと思います.確かに昔のプログラミング言語ではグローバル変数しかありませんでした.それなのになぜローカル変数というものが存在するのでしょうか.それは,プログラムコードの可搬性を向上させるためです.

 まだ解説をしていないのですが,メソッドを定義して利用するようになったとき,メソッドの中でグローバル変数を利用するように作ると,そのメソッドを他のプログラムにコピーして利用するには,グローバル変数の宣言までコピーしなければならなくなります.したがって,そのメソッドでどのようなグローバル変数を利用しているかを確認する必要が生じ,とても面倒です.そこで,メソッドの定義ではグローバル変数を利用しないように作るのが基本的なルールとなります.ただし,今回書いているイベントハンドラの役目をしているメソッドは,どうしてもグローバル変数を利用しなければならないことが多くなります.

 また,メソッド単位ではなく,さらに局所だけで利用できる変数にすることもできます.while や for や if で複数の文を書きたいときに { } で囲みました.この囲んだ部分をブロックと呼び,ブロック内で宣言した変数は,そのブロック内だけでしか利用できません.たとえば,

int i;
for( i = 0 ; i < 10 ; i++ )
{
  int j;

  ここでは i も j も使える
}

while( isplus == true )
{
  ここでは j は使えない
}

if( i == 10 )
{
   int j;

  この j は,さっきの j とは違うもの
}

というように,宣言した変数の利用範囲が決まります.なお,ある変数が利用できる範囲をスコープと呼びます.

  転がす方向の計算

 本題に戻りましょう.

 マウスダウンされたときとマウスアップされたときの座標がわかったので,次に投げる方向(角度)を求めます.角度は,マウスダウンされた場所からマウスアップされた場所へ向かうベクトルの角度を求めることになります.このベクトル (dx, dy) は

float dx = x2 - x1;
float dy = y2 - y1;

として求められますので,このベクトルの向きは

theta = atan( dy / dx ); 
if ( dx < 0 ) theta = theta + PI;

となります.x 軸方向の成分が負の場合の補正は前回説明した通りです.

 転がす方向の角度が求まったら,最後に速度の X 成分,Y 成分 (vx,vy) を求めます.

 これらを踏まえると,一番最初のコードに加える mousePressed メソッドと mouseReleased メソッドの定義は次のようになります.

float x1 = 0, y1 = 0;
void mousePressed()
{
  x1 = mouseX;     // マウスダウンされたときの座標を保存
  y1 = mouseY;
}

void mouseReleased()
{
  float x2 = mouseX;    // マウスアップされた時の座標
  float y2 = mouseY;
  float dx = x2 - x1;      // 投げる方向を表すベクトル
  float dy = y2 - y1;
  theta = atan( dy / dx );
  if( dx < 0 ) theta = theta + PI;  
  
  x0 = x1;
  y0 = y1;
  vx = v * cos(theta);
  vy = v * sin(theta);
  t = 0;
  
  stroke( 0, 0, 0, 100 );
  fill( 0, 0, 0, 100 );
  rect( 0, 0, width, height );
}

 転がし直しの初期位置はマウスダウンされた時の座標なので (x1,y1) を代入します.また,最後に以前のボールの軌跡を消す処理を入れています.

転がす場所と方向を表示する

 今作成したスケッチでは,転がす方向を決めている間にも,前のボールが動いており,転がし始める場所もどの辺だったのかわからなくなってしまう感じがします.そこで,下の図のように,マウスダウンされたら投げる場所にボールを移動させ,マウスアップされるまでのは投げる方向を示す表示を行うようにしてみましょう.

07-5.jpg07-6.jpg

  前に転がしたボールの表示をやめる

 まず,マウスダウンされたら,前のボールの表示を止めることにします.このためには,draw メソッドが呼ばれたときに,転がす方向を決めている最中であるかどうかが判別し,転がす方向を決めている最中であれば,ボールを移動させながら表示するという今までの処理を行わないようにするします.

 この判別のために,isdrag という boolean 型の変数を用意し,転がす方向を決めている最中であれば true を,そうでなければ false を格納しておくことにします.boolean 型は true か false に二つの値だけを格納できる変数の型です.

 転がす方向を決めている期間のはじまりは mousePressed が呼び出されたところで,終わりは mouseReleased が呼び出されたところですので,mousePressed メソッドとmouseReleased メソッドの中で,この変数を書き換えることにします.

boolean isdrag = false;
void mousePressed()
{
   isdrag = true;
   :
}

void mouseReleased()
{
   :
   isdrag = false;
}

 次に draw メソッドの中で isdrag が false のときにだけ,今まで行っていた処理を実行するようにします.これは if 文をつかえばよいでしょう.

void draw()
{
  if( isdrag == false )
  {
  いままでの処理
  }
}

 これで,転がす方向を決めている間,つまり,マウスボタンが押されている間は新たなボールの表示は行われません.ただ,それまでに描いた軌跡が残ってしまっていますので,不透明度 100% の黒色で画面全体を塗りつぶす処理を,マウスボタンが押されてたとき,つまり mousePressed メソッドの中に入れておきましょう.

void mousePressed()
{
   isdrag = true;
   :
   stroke( 0, 0, 0, 100 );
   fill( 0, 0, 0, 100 );
   rect( 0, 0, width, height );
}

  転がす位置にボールを表示する

 次に転がす位置が決まったら,ボールをそこに表示します.これはとても簡単でしょう.マウスボタンが押されたときに,その場所にボールを描けばよいだけです.つまり,mousePressed メソッドを次のようにすればよいはずです.

float x1 = 0, y1 = 0;
boolean isdrag = false;
void mousePressed()
{
  isdrag = true;
  x1 = mouseX;
  y1 = mouseY;
 
  stroke( 0, 0, 0, 100 );
  fill( 0, 0, 0, 100 );
  rect( 0, 0, width, height );

  stroke( 0, 0, 100, 100 );
  fill( 0, 200, 255, 100 );
  ellipse( x1, y1, 10, 10 );
}

  転がす方向に線を描く

 最後に,転がす方向を決めている間,転がす方向になる向きを表す線を描きます.具体的には,マウスダウンされた場所から現在のマウスカーソルの位置まで線分を描きます.

 この線を描くタイミングは,マウスダウンからマウスアップの間ということになります.今,作ろうとしているスケッチでは,マウスカーソルの場所まで線を描くという処理を行いますので,マウスカーソルの位置が変わるたびに描き変えるのがよさそうです.このタイミングに発生するイベントがマウスドラッグイベントで,それに対応するメソッドは mouseDragged です.

 mouseDragged の中でマウスダウンされた場所 (x1,y1) からそのときのマウスの場所まで線を描けばよいのですから mouseDragged は次のような定義でようにすればよいはずです.

void mouseDragged()
{
  stroke( 255, 0, 0, 100 );
  fill( 255, 0, 0 , 100 );
  line( x1, y1, mouseX, mouseY );
}

 ところが,このままでは線がどんどん重なって表示されてしまいますので,毎回画面を塗りつぶして線を消す必要があります.また,mousePressed の中で描いたボールもいっしょに消えてしまいますので,ボールの再描画も必要になります.結局,mouseDragged は次のようにします.

void mouseDragged()
{
  stroke( 0, 0, 0, 100 );
  fill( 0, 0, 0, 100 );
  rect( 0, 0, width, height );

  stroke( 0, 0, 100, 100 );
  fill( 0, 200, 255, 100 );
  ellipse( x1, y1, 10, 10 );

  stroke( 255, 0, 0, 100 );
  fill( 255, 0, 0 , 100 );
  line( x1, y1, mouseX, mouseY );
}

 完成したすべてのコードを再掲します.

int r = 10;
float x0 = r, y0 = r;
float v = 30;
float theta = (PI/8)*3;

float t = 0;
float x = x0, y = y0; 
float vx = v * cos(theta), vy = v * sin(theta);

void setup()
{
  size(300,300);
  colorMode(RGB,255,255,255,100);
  ellipseMode(RADIUS);
  background(0,0,0);
  frameRate(30);
}

void draw()
{
 if( isdrag == false )
 {
   stroke( 0, 0, 0, 10 );
   fill( 0, 0, 0, 10 );
   rect( 0, 0, width, height );

   x = x0 + vx*t;
   y = y0 + vy*t;

   stroke( 0, 0, 100, 100 );
   fill( 0, 200, 255, 100 );
   ellipse( x, y, 10, 10 );
   t = t + 0.1;
 
   if( y > height-r )
   {
     vy = -vy;
     x0 = x;
     y0 = height-r;
     t = 0;
   }
   else if( y < r )
   {
     vy = -vy;
     x0 = x;
     y0 = r;
     t = 0;
   }
 
   if( x > width-r )
   {
     vx = -vx;
     x0 = width-r;
     y0 = y;
     t = 0;
   }
   else if( x < r )
   {
     vx = -vx;
     x0 = r;
     y0 = y;
     t = 0;
   }
 }
}

float x1 = 0, y1 = 0;
boolean isdrag = false;
void mousePressed()
{
  isdrag = true;

  x1 = mouseX;     // マウスダウンされたときの座標を保存
  y1 = mouseY;
  
  stroke( 0, 0, 0, 100 );
  fill( 0, 0, 0, 100 );
  rect( 0, 0, width, height );

  stroke( 0, 0, 100, 100 );
  fill( 0, 200, 255, 100 );
  ellipse( x1, y1, 10, 10 );
}

void mouseReleased()
{
  float x2 = mouseX;    // マウスアップされた時の座標
  float y2 = mouseY;
  float dx = x2 - x1;      // 投げる方向を表すベクトル
  float dy = y2 - y1;
  theta = atan( dy / dx );
  if( dx < 0 ) theta = theta + PI;  
  
  x0 = x1;
  y0 = y1;
  vx = v * cos(theta);
  vy = v * sin(theta);
  t = 0;
  
  stroke( 0, 0, 0, 100 );
  fill( 0, 0, 0, 100 );
  rect( 0, 0, width, height );

  isdrag = false;
}

void mouseDragged()
{
  stroke( 0, 0, 0, 100 );
  fill( 0, 0, 0, 100 );
  rect( 0, 0, width, height );

  stroke( 0, 0, 100, 100 );
  fill( 0, 200, 255, 100 );
  ellipse( x1, y1, 10, 10 );

  stroke( 255, 0, 0, 100 );
  fill( 255, 0, 0 , 100 );
  line( x1, y1, mouseX, mouseY );
}

  draw メソッドの中に描画コードを入れてみる

 上のコードでは,mouseDragged の中などにボールを描くコードがたくさん入っています.これを draw メソッドの中へ持っていく方法もあります.draw メソッドはどんなときにも常に呼び出されるメソッドですから,呼び出されたときにマウスドラッグ中である(isdrag が true)かどうかを判別して,ドラッグ中であれば mouseDragged 内と同じ処理を実行させればよいはずです.

 この変更に伴って,mousePressed の中で行っていたボールを描く処理もいらなくなります.なぜならば,マウスダウンされると isdrag が true になり,次に draw メソッドが呼ばれた時にボールが描かれるからです.mouseDragged はマウスが動いた時に呼び出されるメソッドであるため,もし,マウスダウンされた後にマウスが動かないと,いつまでたっても投げる位置にボールが描かれないことになるため,これまでは mousePressed の中に入れたボールを描く処理を消せなかったのです.

 最終的なコードを次に示します.

int r = 10;
float x0 = r, y0 = r;
float v = 30;
float theta = (PI/8)*3;

float t = 0;
float x = x0, y = y0; 
float vx = v * cos(theta), vy = v * sin(theta);

void setup()
{
  size(300,300);
  colorMode(RGB,255,255,255,100);
  ellipseMode(RADIUS);
  background(0,0,0);
  frameRate(30);
}

void draw()
{
 if( isdrag == false )
 {
   stroke( 0, 0, 0, 10 );
   fill( 0, 0, 0, 10 );
   rect( 0, 0, width, height );

   x = x0 + vx*t;
   y = y0 + vy*t;

   stroke( 0, 0, 100, 100 );
   fill( 0, 200, 255, 100 );
   ellipse( x, y, 10, 10 );
   t = t + 0.1;
 
   if( y > height-r )
   {
     vy = -vy;
     x0 = x;
     y0 = height-r;
     t = 0;
   }
   else if( y < r )
   {
     vy = -vy;
     x0 = x;
     y0 = r;
     t = 0;
   }
 
   if( x > width-r )
   {
     vx = -vx;
     x0 = width-r;
     y0 = y;
     t = 0;
   }
   else if( x < r )
   {
     vx = -vx;
     x0 = r;
     y0 = y;
     t = 0;
   }
 }
 else
 {
   stroke( 0, 0, 0, 100 );  // 画面をきれいに
   fill( 0, 0, 0, 100 );
   rect( 0, 0, width, height );

   stroke( 0, 0, 100, 100 ); // マウスダウンされた(投げ直す)場所にボールを表示
   fill( 0, 200, 255, 100 );
   ellipse( x1, y1, 10, 10 );

   stroke( 255, 0, 0, 100 );  // マウスダウンされた場所から
   fill( 255, 0, 0 , 100 );       // マウスカーソルの場所まで線分を表示
   line( x1, y1, mouseX, mouseY );
 }
}

float x1 = 0, y1 = 0;
boolean isdrag = false;
void mousePressed()
{
  isdrag = true;

  x1 = mouseX;     // マウスダウンされたときの座標を保存
  y1 = mouseY;
}

void mouseReleased()
{
  float x2 = mouseX;    // マウスアップされた時の座標
  float y2 = mouseY;
  float dx = x2 - x1;      // 投げる方向を表すベクトル
  float dy = y2 - y1;
  theta = atan( dy / dx );
  if( dx < 0 ) theta = theta + PI;  
  
  x0 = x1;
  y0 = y1;
  vx = v * cos(theta);
  vy = v * sin(theta);
  t = 0;
  
  stroke( 0, 0, 0, 100 );
  fill( 0, 0, 0, 100 );
  rect( 0, 0, width, height );

  isdrag = false;
}

課題

 初速度を指定できるようにしてみましょう.初速度の指定にはマウスダウンされた場所から,マウスアップされた場所までの距離を使うとよいでしょう.つまり,ベクトル(vx,vy) の長さを使うということです.できたスケッチは 学籍番号_K7 という名前で保存し,pde ファイルを WebClass から提出してください.

発展課題

 空間にボールを投げた場合のボールの動きを表示するスケッチに,インタラクティブに投げる場所,方向,速度を指定する機能をつけてみましょう.