Rubyのグラフライブラリ(gnuplot)で砲台ゲームを実装しよう
今日の風景
寿司の電子工作
はじめに
今は廃刊してしまった、昔懐しい『ベーシックマガジン』という雑誌には、大抵簡単なゲームのコードが載っていた。俺を含めたパソコン少年は、そのコードを見ながらパチパチとコードを打っていたわけだが、そういったサンプルコードリストの一つに、大抵は「砲台ゲーム」というものが存在していた。
仕様としては、ある範囲が的になっており、角度と強さを指定し、投射線を描写する。的の範囲に入っていたら的中となり、的の外になっていたら外れということになる。
大抵、このゲームに関しては、30代から40代にBASICを触っていたおじさんであるならば、何処かで見たことがあると思うのだけれども、舞台がWebになった現代には、このような「砲台ゲーム」を作るといったようなサンプルを見ることは殆どなくなった。
で、前回にRubyからgnuplotを触るエントリを書いたわけだけれど、「あれ、グラフを使えば砲台ゲームが作れるんじゃないか」と思って、実装してみることにした。実行環境はJupyter Notebookを参考にしているので、上のリンクからインストールして欲しい。
投射線を描写する
ここからは殆ど『Pythonからはじめる数学入門』のパクリとなるので、正確な説明が欲しい場合には、実際に買ってみて試して欲しい。
投射線というのは「ある物体を投げたときに描く軌道のこと」と説明することができる。さて、このとき「ものを投げる」ということを考えた場合、「その物体を投げる力」と、「その物体をどの方向に投げるのか」という二つの物差しで考えることができる。
さて、このように考えたとき、我々の世界には「時間」という概念が存在している。つまり、「ある物体を、任意の力と角度で投げた場合の、ある時間の一点」というのが存在している。この詳しい原理について気になる人は、別途物理の教科書を参照して頂くとして、これを算出する式は、コードであるならば次のように書ける。
def parabora(u, t) t = t / 180.0 * Math::PI g = 9.8 t_flight = 2 * u * Math.sin(t) / g intervals = (0.0..t_flight).step(0.001) return [ intervals.map {|x| u * Math.cos(t) * x}, intervals.map {|y| u * Math.sin(t) * y - 0.5*g*y*y } ] end
一から順番に追っていく。まず、最初の「t」はラジアン変換している部分だ。ラジアンは、2πを360度とするので、角度をまず最初に180度で割る。そうすると、nπのn
の部分が出てくるので、それを使う。
次のgは単なる重力なので、特に気にしなくてもよい。
次に気になるのは、t_flight
だが、これはボールの軌道をいつ止めるかの式になる。これは、ちょうどy軸が0になる範囲を示している。
あとは、u * Math.cos(t) * x
の部分だけれども、これは要するにパワーと角度、そして時間をかけて、cos
で求めている。一方、yのほうに関しては、重力補正がかかるので、ちゃんと- 0.5 * g * y * y
といったように、時間軸にあわせて重力の値が補正できるようにしておかないと、どこまでも飛んでいってしまう。ちなみに、ここの解説が雑なのは、自分もよく理解していないせいもあるので、実際に知りたい人は物理の本を読むといいと思う。
class GameField #... def parabora_only(u, t) Gnuplot.open do |gp| Gnuplot::Plot.new( gp ) do |plot| ball = Gnuplot::DataSet.new(parabora(u, t)) do |ds| ds.with = "lines lt rgb\"blue\"" ds.linewidth = 1 end plot.data << ball IRuby.display(plot) end end end #... end
と書くことによって、下のような図を作画することができる。
ゲーム作るぞー
投射線の考え方はわかった。ポイントは的を描写することである。gnuplot
にグラフを流しこむことは簡単で、一次元の配列の場合、要素数が x
となる。これを利用し、途中までの外れの部分の配列は「当たり」の部分の配列よりも短くするようにする。これを重ねることによって、ズレが発生し、当たりの部分がはみだして見えるわけですね。要するにズルです。
# ... def initialize @first = Random.rand(100...400) @last = @first + Random.rand(50..100) end def show Gnuplot.open do |gp| Gnuplot::Plot.new( gp ) do |plot| plot.title 'Game' # gnuplotの場合、配列の要素がxになるので、挟まないといけない fix = Gnuplot::DataSet.new([600]) { |ds| ds.with = "lines lt rgb \"#ffffff\"" } field = Gnuplot::DataSet.new([*0..@first].map {|x| 0 }) do |ds| ds.with = "lines lt rgb\"green\"" ds.linewidth = 10 end hole = Gnuplot::DataSet.new([*0..@last].map {|x| 0 }) do |ds| ds.with = "lines lt rgb\"red\"" ds.linewidth = 5 end plot.data << fix plot.data << hole plot.data << field IRuby.display(plot) end end end # ...
諸事情によりDRY、つまり重複コードばっかりなのは御愛嬌として、こうすることによって、下のようなグラフができる:
右上に点を付けているのは、グラフが最大の値に合うようになるので、こうすることによって、よりゲームらしい広がりのある表現を作りたかったためである。
さて、これを描写すると次のようになる
おっ、砲台ゲームっぽいですね。
発射メソッドを作る
というわけで弾を発射します。こんな感じでいいのではないかと。
Gnuplot.open do |gp| Gnuplot::Plot.new( gp ) do |plot| # gnuplotの場合、配列の要素がxになるので、挟まないといけない fix = Gnuplot::DataSet.new([600]) { |ds| ds.with = "lines lt rgb \"#ffffff\"" } field = Gnuplot::DataSet.new([*0..@first].map {|x| 0 }) do |ds| ds.with = "lines lt rgb\"green\"" ds.linewidth = 10 end hole = Gnuplot::DataSet.new([*0..@last].map {|x| 0 }) do |ds| ds.with = "lines lt rgb\"red\"" ds.linewidth = 5 end ball = Gnuplot::DataSet.new(parabora(u, t)) do |ds| ds.with = "lines lt rgb\"blue\"" ds.linewidth = 1 end plot.data << fix plot.data << hole plot.data << field plot.data << ball t2 = t / 180.0 * Math::PI last = u * Math.cos(t2) * (2 * u * Math.sin(t2) / 9.8) if last.abs.between?(@first, @last) plot.title '成功' else plot.title '残念' end IRuby.display(plot) end end end
結果として、何処に落ちたかどうかは計算しなければいけなかったので、別途u * Math.cos(t2) * (2 * u * Math.sin(t2) / 9.8)
みたいなかたちで、最後のx軸の部分を計算している。ただし、マイナスになることもあるので、abs
でちゃんと判定できるようにすると吉。
これは残念なパターンです。
やりましたね。
ちなみに、全体のソースはこちらになります。
まとめ
数学だと思ったらいきなり物理の話が出てきて、とまどったし、実際なんでこういう方程式になるのかというのかを説明できない馬鹿なので、今回はポコー(心が凹んだときの音)した。しかし、グラフが書けると、このように砲台ゲームを作ることができる。今だとR言語がグラフを作るのに丁度いいので、こういう砲台ゲームを作ってみると楽しいのではないか、と思ったりした。だれか書いてくれませんかね(お前が書け)

- 作者: Amit Saha,黒川利明
- 出版社/メーカー: オライリージャパン
- 発売日: 2016/05/21
- メディア: 単行本(ソフトカバー)
- この商品を含むブログ (2件) を見る