モンキーハンティングのシミュレーション
約1年ぶりの投稿になります。新2年生になりました。
年度初の投稿は、学校で発信する新入生向け通信(一部変更あり)です。電子媒体だと、コードもコピペしやすいし、gifも載せられて便利ですね。
以下本編です。
モンキーハンティングとは
木にぶら下がっている猿がいます。その猿を猟銃で狙っている猟師がいます。銃の発射とともに、(発射音に驚いた)猿が木から手を離しますが、どんなに離れていても、銃弾は必ず猿に命中するという話です。
まず、この話の問題設定をまとめてみましょう。
- 自由落下をする物体(猿)と放物運動をする物体(銃弾)が存在します。
- 放物運動をする物体の運動の向きは、自由落下をする物体の位置の方向です。
- 2つの物体は同時に運動を開始します。
しかし、実際は、銃の発射と銃声が届くタイミングがずれていたり、銃弾が届く前に地面に着いたりしてしまうので、上の3項目だけを満たした理想な状態を考えましょう。
理論
最初に、モンキーハンティングがどういう理論で成り立っているのかを、数式を用いてみていきましょう。
モンキーハンティングを簡単な図に表すと、図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; } }
注意点
途中で出てくる「キャンバス」という言葉がありますが、シミュレーションを実行する画面を指しています。
図2のように、キャンバスの原点は左上の角にあり、幅は右方向に、高さは下方向に伸びています。そのため、ベクトルを考えるときは、この方向に合わせて考えなければいけません。
実行結果
それでは、このコードで書かれたシミュレーションがどのような動きをするのかを見てみましょう。 見事にぶつかりました!
次に、青ボールの初速度を半分に落としてみましょう。もし証明が正しければ、初速度に関係なく2つのボールはぶつかるはずです。コードにある青ボールの初速度を1200から600に書き換えましょう。
さてどうなるでしょうか。 こちらも見事にぶつかりました!
これでシミュレーションによって、モンキーハンティングが確かめられましたね。
最後に
今回は物理とプログラミングを融合させてシミュレーションというものをやってみました。すでに気づいている人もいるかもしれませんが、シミュレーションには現実ではできないメリットがあります。それはパラメーターを色々いじることで、容易に動作を確かめることができることにあります。パラメーターというのは、シミュレーションを形作る様々な数値だと思ってください。今回でいう、銃弾の初速度や重力加速度がそれです。
今までプログラミングを学んできた人や、これから学ぶ人もこの考えを大事にして欲しいのですが、何度でもパラメーターやコードを変えて、「実験」を繰り返してみましょう。色々実験してみることで理解を深めたり、新しく学ぶこともあると思います。
シミュレーションを使って問題を確かめることは、今まででは思いもしなかった新鮮なことだったかもしれません。しかし、プログラミングを知っていても、「プログラミングという知識」と「物理という知識」を分けてしまっていたら、今回のシミュレーションはやろうとしなかったと思います。
プログラミングは決して単独な知識ではなく、手段にすぎません。自然科学(数学や物理など)の問題でも、身近の問題でもなんでもいいです。自分が理解を深めるための手段という視点で、ぜひプログラミングを学んでみてください。
追記(2017/4/1/22:06)
スマートフォンだと数式がうまく見れないかもしれないので、どーしてもみたい方だけ(←ここ重要)PCでみてください。