読者です 読者をやめる 読者になる 読者になる

Lenny`s Dairy Notes

とある大学生のブログ

モンキーハンティングのシミュレーション

約1年ぶりの投稿になります。新2年生になりました。

年度初の投稿は、学校で発信する新入生向け通信(一部変更あり)です。電子媒体だと、コードもコピペしやすいし、gifも載せられて便利ですね。

以下本編です。

モンキーハンティングとは

木にぶら下がっている猿がいます。その猿を猟銃で狙っている猟師がいます。銃の発射とともに、(発射音に驚いた)猿が木から手を離しますが、どんなに離れていても、銃弾は必ず猿に命中するという話です。

まず、この話の問題設定をまとめてみましょう。

  1. 自由落下をする物体(猿)と放物運動をする物体(銃弾)が存在します。
  2. 放物運動をする物体の運動の向きは、自由落下をする物体の位置の方向です。
  3. 2つの物体は同時に運動を開始します。

しかし、実際は、銃の発射と銃声が届くタイミングがずれていたり、銃弾が届く前に地面に着いたりしてしまうので、上の3項目だけを満たした理想な状態を考えましょう。

理論

最初に、モンキーハンティングがどういう理論で成り立っているのかを、数式を用いてみていきましょう。

f:id:LennyQ:20170401191237p:plain 

図1:説明図

モンキーハンティングを簡単な図に表すと、図1のようになります。青ボールは銃弾、緑ボールは猿を表しています。では、必要な文字を定義していきましょう。2つのボールの水平の距離を \( l \) 、垂直の距離を \( h \) とします。青のボールは水平面に対して仰角 \( \theta \) 、初速度 \( v \) で打ち出します。重力加速度は \( g \) とします。

この問題で大切なことは、初速度 \( v \) を水平方向と垂直方向に、それぞれ \( v\cos\theta \) と \( v\sin\theta \) に分解することです。これを踏まえた上で、この問題を証明する手順を説明していきましょう。

何を証明するのかといいますと、この2つのボールは本当にぶつかるのかということ証明します。

2つのボールがぶつかるというのはどういうことか。これをもっと単純化して、2つのボールが同じ位置にいるときという認識にしましょう。それはつまり、

  • 青ボールが水平方向に \( l \) 進んだとき、高さが緑ボールと同じになる

ということです。これを用いて、証明してみましょう。

証明

まず、青ボールが水平方向に \( l \) だけ進んだときの時間を求めましょう。放物運動の水平方向の運動は等速直線運動な\begin{align*} l = v\cos\theta \cdot t \end{align*} という式が書けますね。したがって、 \begin{align*} t = \frac{l}{v\cos\theta} (*) \end{align*} という式ができます。つまり、2つのボールはこの時間に同じ高さにいないといけないというわけですね。

次に、時間 \( t \) のときのそれぞれのボールの高さを調べてみましょう。青ボールの初期位置を基準として、青ボールの高さを \( y_1 \) 、緑ボールの高さ \( y_2 \) としますと、 \begin{align*} y_1 = v\sin\theta \cdot t - \frac{1}{2}gt^2 \end{align*}

\begin{align*} y_2 = h - \frac{1}{2}gt^2 \end{align*}

と表せます。この二つの高さが等しい、つまり、\( y_1 = y_2 \) を示せば証明することができます。これを、 \begin{align*} v\sin\theta \cdot t - \frac{1}{2}gt^2 = h - \frac{1}{2}gt^2 \end{align*} と書けかえると、両辺に同じ \( - \frac{1}{2}gt^2 \) があるので消去して、 \begin{align*} v\sin\theta \cdot t = h \end{align*} となります。つまり、この式の (左辺) = (右辺) を示すことになります。左辺の式に式 \( (*) \) を代入すると、 \begin{align*} (左辺) = v\sin\theta \cdot \frac{l}{v\cos\theta} = l\tan\theta = l \cdot \frac{h}{l} = h \end{align*} となり、(左辺) = (右辺) が示され、すなわち、\( y_1 = y_2 \) が示されたのです。これで、モンキーハンティングが証明されました。

しかし、最初の問題設定でも書かれていた通り、青ボールの運動方向は緑ボールに向かっていなければならないのです。このことを表している条件が、 \begin{align*} \tan\theta = \frac{h}{l} \end{align*} という式です。この式を満たしていれば、2つのボールは必ずぶつかるということです。

シミュレーション

そうはいっても、現実でそんな場面に遭遇することもないだろうし、イメージが湧きにくいですよね。そんなときに用いるのが、物理演算によるシミュレーションです。今回は、Processingという描画専門の統合開発環境を用いて、プログラミングの力でこの問題を確かめてみましょう。

Processingについて

Processingでは描画が簡単にできますが、静止画はもちろん、動画も作ることができます。シミュレーションはものが動いていないとできませんので、今回は動画を作ります。

さて、この動画の仕組みですが、簡単に説明しますと、実は画像をパラパラ漫画のように1枚ずつ描画しています。Processingの基本設定だと60fpsですので、1秒間に60枚の画像、つまり、1/60秒ごとに1つの画像を表示しているんです。この1つの画像のことを1フレームといいます。

この知識を踏まえて次をみていきましょう。

コードを書く前に

まずは下のことを頭に入れておきましょう。

  • 現在の速度=直前の速度+加速度

  • 現在の位置=直前の位置+現在の速度

「現在」というのは今のフレームにおいて、「直前」というのは1コ前のフレームにおいてと考えましょう。

実はこれ、何も難しい話ではなく、数式に直してみると当たり前だと感じるはずです。

現在の速度を\( v \)、直前の速度を\( v' \)とし、現在の位置を\( y \)、直前の位置を\( y' \)とします。また、加速度を\( a \)とします。そしてここで、隠れた要素である、1フレームの時間を\( t \)(= 1/60秒)とします。役者がそろったところで、数式に直してみましょう。

  • \( v = v' + at \)
  • \( y = y' + vt \)

こうしてみるとどれも見たことあるような数式ばかりですね。もっと簡単に説明すると、

  • (速度の変化)=(加速度)×(経過時間)
  • (移動距離)=(速度)×(経過時間)

ということを表しているんですね。

コード

いよいよシミュレーションを作るためのコードを書いていきましょう。

/********************/
/** メインファイル ****/
/********************/

//PVectorはProcessingにおいてベクトルを扱うクラス
PVector acc;                    //重力加速度ベクトル
boolean isClicked = false;        //マウスのクリック判定用変数

FObject[] obj = new FObject[2];  //2つのボールの生成宣言

/* 前準備 */
void setup() {
  size(1200,600); //キャンバスの大きさ(横,縦)
  background(255); //キャンバスの背景の色(白)
  
  //1つ目のボールの生成とそのパラメータ
  obj[0] = new FObject(20, height-100, 30, 10,0,255);  
  //2つ目のボールの生成とそのパラメータ
  obj[1] = new FObject(width-200, 20, 30, 20,200,20);  
  //1つ目のボール:銃弾(青)
  //2つ目のボール:猿(緑)
  //パラメータはクラスファイルを参照(下にある)
  
  //猿の初速度と射角(クラスファイル参照)
  obj[1].move(0.0, 0.0);             
  
  //重力加速度をベクトルとして生成(x方向,y方向)
  acc = new PVector(0.0/60, 9.8/60);  
  //パラメータを1/60倍している理由は、Processingのfpsに合わせることで物体の動きを見
  //やすい速度に落とすため。
}

/* メイン関数(1/60秒ごとに実行) */
void draw() {
  //2つの球の初期位置を描画(クラスファイル参照)
  for(int i=0; i<2; i++) {
    obj[i].position();
  }
  
  //マウスをクリックしたらボールが動き出す(クラスファイル参照)
  if(isClicked) {
    for(int i=0; i<2; i++) {
      obj[i].diplay();
    }
  }
  
  //2つのボールがぶつかったら動きを止める(クラスファイル参照)
  if(isHitted(obj[0].getPos(), obj[0].getRadius(), 
    obj[1].getPos(), obj[1].getRadius())) {
    noLoop();  
  }
}

/* マウスがクリックされたら発動 */
void mouseClicked() {
  obj[0].move(1200.0, -atan2(mouseY-(height-100), mouseX-20)); 
  //銃弾の初速度:1200
  //銃弾の射角 :銃弾を原点にした水平面に対するマウスカーソルと原点を結んだ直線の仰角
  //          つまり、銃弾をマウスカーソルの方向に打ち出せる。
  isClicked = true;
}

/* 2つのボールがぶつかったかどうかを判定 */
boolean isHitted(PVector pos1, float r1, PVector pos2, float r2) {
  if(dist(pos1.x, pos1.y, pos2.x, pos2.y) <= r1+r2)  
    //dist()は2点間の距離を求める関数
    return true;
  return false;
}
/****************************/
/**** ボールのクラスファイル ****/
/****************************/

class FObject {
  //メンバ変数の宣言
  PVector pos;      //位置ベクトル
  PVector vel;      //速度ベクトル
  int diameter;        //ボールの直径
  int R, G, B;     //ボールの色の要素
  PVector tempos;   //記憶用変数
  
  /* ボールのパラメータの設定(コンストラクタ) */
  //(左から順に)位置のX座標、位置のy座標、直径、色の要素(赤)、色の要素(緑)、色の要素(青)
  FObject(float posX, float posY, int tempDiameter, int RED, int GREEN, int BLUE) {
    pos = new PVector(posX, posY);    //位置ベクトルの生成
    tempos = pos;                   //初期位置を記憶
    diameter = tempDiameter;        
    R = RED;
    G = GREEN;
    B = BLUE;
  }
  
  /* 初速度と射角を決めるメソッド */
  void move(float vel0, float rad) {
    vel = new PVector(vel0*cos(rad)/60, -vel0*sin(rad)/60);
  }
  
  /* 描画するメソッド */
  void diplay() {
    vel.add(acc);   //現在の速度=直前の速度+加速度
    pos.add(vel);   //現在の位置=直前の位置+現在の速度
    
    //キャンバスの底に着いたら上に戻る
    if(pos.y > height) {
      pos.y = 20;
    }
    
    //ボールの描画
    fill(255,20);noStroke();rect(0,0,width,height); //軌跡を残すためのぼかし
    stroke(R,G,B); fill(R,G,B);                     //線色・面色
    ellipse(pos.x, pos.y, diameter, diameter);      //円の描画
  }
  
  /* 初期位置を描画するメソッド */
  void position() {
    stroke(R,G,B); fill(R,G,B);
    ellipse(tempos.x, tempos.y, diameter, diameter);
  }
  
  /* 今の位置をベクトルで返すメソッド */
  PVector getPos() {
    return pos;
  }
  
  /* 半径を返すメソッド */
  float getRadius() {
    return diameter/2.0;
  }  
}

注意点

途中で出てくる「キャンバス」という言葉がありますが、シミュレーションを実行する画面を指しています。f:id:LennyQ:20170401200025p:plain

図2:キャンバス

図2のように、キャンバスの原点は左上の角にあり、幅は右方向に、高さは下方向に伸びています。そのため、ベクトルを考えるときは、この方向に合わせて考えなければいけません。

実行結果

それでは、このコードで書かれたシミュレーションがどのような動きをするのかを見てみましょう。 f:id:LennyQ:20170401215023g:plain 見事にぶつかりました!

次に、青ボールの初速度を半分に落としてみましょう。もし証明が正しければ、初速度に関係なく2つのボールはぶつかるはずです。コードにある青ボールの初速度を1200から600に書き換えましょう。

さてどうなるでしょうか。 f:id:LennyQ:20170401215405g:plain こちらも見事にぶつかりました!

これでシミュレーションによって、モンキーハンティングが確かめられましたね。

最後に

今回は物理とプログラミングを融合させてシミュレーションというものをやってみました。すでに気づいている人もいるかもしれませんが、シミュレーションには現実ではできないメリットがあります。それはパラメーターを色々いじることで、容易に動作を確かめることができることにあります。パラメーターというのは、シミュレーションを形作る様々な数値だと思ってください。今回でいう、銃弾の初速度や重力加速度がそれです。

今までプログラミングを学んできた人や、これから学ぶ人もこの考えを大事にして欲しいのですが、何度でもパラメーターやコードを変えて、「実験」を繰り返してみましょう。色々実験してみることで理解を深めたり、新しく学ぶこともあると思います。

シミュレーションを使って問題を確かめることは、今まででは思いもしなかった新鮮なことだったかもしれません。しかし、プログラミングを知っていても、「プログラミングという知識」と「物理という知識」を分けてしまっていたら、今回のシミュレーションはやろうとしなかったと思います。

プログラミングは決して単独な知識ではなく、手段にすぎません。自然科学(数学や物理など)の問題でも、身近の問題でもなんでもいいです。自分が理解を深めるための手段という視点で、ぜひプログラミングを学んでみてください。

追記(2017/4/1/22:06)

スマートフォンだと数式がうまく見れないかもしれないので、どーしてもみたい方だけ(←ここ重要)PCでみてください。