|ハイブリッドOS|File System|ARM|Android|Java|制御システム|オープンシステム

 

技術者コラム

 
フォーム
 
第34回目:Haskellでテトリス(Part2)
2014-11-08
筆者:村田
 
こんにちは。
 
今回のテーマはdo記法を用いたモナドの使い方です。モナドと一口に言っても様々なモナドがあります。Maybeモナド、Stateモナド、IOモナド、Writerモナドなどなど。テトリスでは少々IOモナドを使うことになりますので、IOモナドを中心に話を進めていきたいと思います。
 
それではまずモナド関数(モナドを使った関数)の見分け方を覚えましょう。関数の型の調べ方は覚えていますか?ghciというREPLの中で調べることができます。:tの後に関数名を書きます。いくつか関数の型を表示させてみましょう。
 
$ ghci
> :t putStrLn
putStrLn :: String -> IO ()
 
> :t getLine
getLine :: IO String
 
これらはIOモナド関数です。putStrLnはStringをもらってIO ()を返すモナド関数です。getLineは引数なしでIO Stringを返すモナド関数です。こんな風に戻り値の型が「IO ほにゃらら」となっている関数がIOモナド関数です。次の関数の例を見てください。
 
> :t head
head :: [a] -> a
 
> :t map
map :: (a -> b) -> [a] -> [b]
 
> :t length
length :: [a] -> Int
 
これらは純粋関数であってIOモナド関数ではありませんね。それでは前回のGUIサンプルコードを読み込んで、他にもIOモナド関数の例を探してみましょう。
 
> :load GtkHello.hs
> :t main
main :: IO ()
 
> :t windowNew
windowNew :: IO Window
 
> :t widgetShowAll
widgetShowAll :: WidgetClass self => self -> IO ()
 
上記も全てIOモナド関数です。最後の例について少し説明します。まず、selfは「型変数」と呼ばれるものです。型変数は小文字で始まります。head関数やmap関数の型定義にあるaやbも型変数です。selfやaは「型」ではなく、型変数は任意の型を扱える「型の変数」として扱われます。他の言語ではジェネリクスとかテンプレートとか、<T>とかそんな感じ。実際の型はBoolとかIntとかIOとか大文字で始まります。次にWidgetClass self => の部分ですが、selfという型変数に入る実際の型はWidgetClassという「型クラス」に属していることを表しています。今度は型クラス・・・ぬ〜ややこしいですね。型も型クラスも大文字から始まりますが、型と型クラスは全く別モノです。型クラスについて詳しくは説明しませんが、型とは違うモノって覚えておいていただければ良いです。(ちなみにクラスという言葉からJavaやC#のclassが想起されますが、どちらかというとinterfaceに近いものです) ともあれ着目すべきはself -> IO ()という部分です。widgetShowAll関数も引数にselfをもらってIO ()を返すIOモナド関数ということが分かりますね。
 
少し他のモナドも見てみましょう。例えば失敗を扱うMaybeモナドってのがあります。Maybeモナド関数がどういう型で定義されるか想像できますか?こんな感じです。
maybeRead :: Read a => String -> Maybe a  (Read a =>という型クラス制限は無視してください)
 
次は状態を扱うStateモナド関数の例です。
get :: State s s
put :: s -> State s ()
 
並列処理を扱うEvalモナド関数の例も見てみましょう。
rpar :: a -> Eval a
rseq :: a -> Eval a
 
先程見たIOモナド関数と同じように必ず戻り値の型が「モナドの型 ほにゃふにゃ」になっていますね。Maybe a、State s ()、Eval aしかりです。今回はモナド関数の見分け方が何となく分かれば十分です。この後の説明は少々余分ですが付け加えておきます。MaybeやStateやEvalは正しくは型構築子(型コンストラクタ)というもので、それ単体では(具体)型になりません。MaybeとEvalは後ろに具体的な型をもう一つとって本当の型になれます。例えばMaybe Int、Eval Boolのように。Stateは2つ型をとって本当の型になります。例えばState [Int] Intとか、State Bool Charのように。すべてのモナドはこのように型構築子を持つので、モナド関数は一般的に「a -> モナドの型構築子 x」という形になります。
 
さて、見分け方の次に覚えることはモナド関数の組み合わせ方です。組み合わせ方は2通りあり、bind関数(>>=)を使う方法とdo記法を使う方法があります。今回のテトリスではdo記法を覚えて使っていきたいと思います。do記法で覚えることは4つありますので順に見ていきます。まずは合成の方法です。例を見てください。
 
do
  putStrLn "hoge"
  putStrLn "piyo"
 
doの後にインデントして二つのモナド関数を並べるだけで順に実行(合成)されます。もう少し一般的に書くと以下の形になります。
 
do
  IOモナド
  IOモナド
  ・・・
  IOモナド
 
この形はIOモナド以外の他のモナドでも同じです。
 
do
  Stateモナド
  Stateモナド
  ・・・
  Stateモナド
 
とか
 
do
  Evalモナド
  Evalモナド
  ・・・
  Evalモナド
 
などです。do記法を使うと普通の手続き言語のように、上から順番にモナド関数を実行するという書き方ができるのです。但し以下のように異なるモナドを混ぜて書くことはできません。doでまとめられるのは一種類のモナドだけです。
 
do
  IOモナド
  Stateモナド
  Evalモナド
 
ところで、以下はコンパイルエラーになるのですが理由は分かりますか?
 
do
  putStrLn
 
putStrLnの型はString -> IO ()でしたね。doの中にはIOモナドを並べることができるのであって、String -> IO ()というIOモナド(を生み出す)関数は置けません。ちゃんとStringを与えて、例えばputStrLn "abc" としてください。こうすれば全体の型はIO () となり、めでたくIOモナドとして並べることができます。さて、面白いことにモナドを並べてdoでまとめたものは、それ自体が一つのモナドになるのです。つまり・・・
 
do
  putStrLn "hoge"
  putStrLn "piyo"
 
これで一つのIOモナドになります。ですからこんな書き方もできます。(わざとらしい書き方ですが)
 
do
  putStrLn "hoge"
  do
    putStrLn "piyo"
    putStrLn "fuga"
  putStrLn "mooo”
 
それではdo記法について覚えることの2つ目です。getLineというIOモナド関数の型を思い出して下さい。 getLine::IO Stringです。doの中ではIOで包まれた中身の型のデータを取り出すことができます。<-という記号を使って以下のように取り出せます。なお、”doの中でだけ取り出せる”ということを頭の片隅に入れておいて下さい。
 
do
  line <- getLine
  putStrLn line
 
getLineがIO StringというIOモナドなので、lineはString型の値です。getLineは標準入力から文字列を取り出すIOモナドであり、line <- getLineの部分でキーボードからの入力を待っています。文字列を入力してエンターをッターンすると、lineに文字列が取り出されます(束縛)。そしてputStrLnに渡してまた画面に表示しているというわけです。以下はちょっと中途半端ですがEvalモナドの例です。以下のfはf :: a -> a という関数のつもりです。
 
do
  a <- rpar (f x)  関数にxを与えてaを得る処理と、
  b <- rpar (f y)  関数にyを与えてbを得る処理を、並列に実行! (Evalモナドの効果)
  ・・・
 
とまあ、IOモナドはI/O処理を扱うのに対し、Evalモナドは並列処理を扱えるという違いがありますが、ともかく<-でIO StringやEval Intなどの中身を取り出せるということです。
 
do記法について覚えることの3つ目にまいりましょう。今度は逆に普通の値をdoの中でモナドに包み込む方法です。returnを使います。
 
do
  line <- getLine
  return line
 
と書きます。lineはString型の値ですが、return lineと書くとIO Stringという型になりIOに包まれます。純粋な値もIOで包んでIOモナドにすることでdoの中に並べることができるようになるのです。これまたややこしいのですが、普通の言語でreturnと言ったら”関数から戻る/抜ける”という意味ですが、Haskellでは”普通の値をモナドに包む”という意味だけです。doの中から外に出られるわけではありません。例えばこんな風に書くと・・・
 
do
  line <- getLine
  return line
  line2 <- getLine
  putStrLn line2
 
3行目のreturn lineで終了してしまうなんてことにはならず、ちゃんと最後まで順に実行されます。ちょっとヘンテコなコードですが、こんな感じにも使います。
 
do
  line1 <- getLine
  combinedline <- do                        ② ①で包んだStringをまた剥がしてcombinedlineに取り出す
                    line2 <- getLine
                    return (line1 ++ line2) ① line1とline2を結合してIOモナドに包む
  putStrLn combinedline
 
do記法について覚えることの4つ目は、純粋な関数の呼び出しとモナドを混ぜる方法です。こんな風に書きます。
 
do
  line <- getLine
  let line2 = reverse line   入力された文字列をひっくり返す
  putStrLn line2
 
reverse関数は[a]->[a]という型の純粋な関数です。do記法の中では「let 変数名 = 純粋関数」と書くと純粋関数を呼び出し、その戻り値を変数に束縛できます。以上がモナドの使い方の基本的なところです。まとめると、
 
①モナド関数の見極め方
②do記法の中でモナドの並べ方(合成)
③do記法の中でモナドに包まれた値の取り出し方
④do記法の中で値をモナドに包むやり方
⑤do記法の中で純粋関数を呼び出すやり方
 
を覚えていただければと思います。では前回のGUIコードを再掲してもう一度読んでみます。分かるところが少しでも増えているといいのですが・・
 
main = do
  initGUI   
  window <- windowNew
  windowSetTitle window "Hello World!"
  window `on` deleteEvent $ do
    liftIO mainQuit
    return False
  widgetShowAll window
  mainGUI
 
まずmainはIOモナドです(main :: IO ())。doの中で1行ずつIOモナドを並べ、一つの大きなIOモナドに合成したものをmainと定義しているのです。次にinitGUIやmainGUIを見てください。どちらも型はIO ()という引数なしのIOモナド関数、つまりIOモナド値そのものを並べているだけなのです。次にwindowNewを見てみます。windowNew :: IO WindowというIOモナドから中身のwindowを取り出していることが分かります。windowSetTitleやwidgetShowAllは折角なのでghciで型を調べてみてください。いくつかの引数を渡してIO ()を作るモナド関数であることが分かると思います。最後にwindow `on`〜の部分が残りました。ここが最も難しいところです。まずonという関数の型を調べてみましょう。
 
on :: object -> Signal object callback -> callback -> IO (ConnectId object)
 
となっています。あ”〜複雑ですね。私もよく分かりません:P しかし、じっくり眺めれば読み取れることがあります。まず最後がIO (ほにゃらら)なのでIOモナド関数であることが分かります。on関数の引数の数は3つであることも分かります。小文字から始まる型変数は任意の型なのでいまいちピンときませんが、とりあえず2つ目の引数は「Signal ほげ ぴよ」という型のようですね。
 
ではもう一度サンプルコードを見ます。onは`on`とバッククォートで囲んで中置演算子のように書いています。このテクニックは初出かもしれませんね。文字から始まる関数(onやdivやmod)を中置にして+や*のような2項演算子にしたい場合にバッククォートで囲みます。つまり、window `on` deleteEventは、on window deleteEventと同じことです。もう一個のテクニックを紹介します。$は関数の結合優先順を変えるものです。括弧で結合の様子を表してみると少し分かりやすくなります。$が無いと(((window `on`) deleteEvent) do) という風にon関数の第3引数にdoという関数(?)を渡していることになりコンパイルエラーとなります。$を置くことで(((window `on`) deleteEvent) $ (do〜)) とdo以降のコードの塊をonの引数に渡せます。これらのテクニックについては説明不足ですがこの辺にしておきます。重要なのは、on関数の第2引数に渡しているのがdeleteEvent関数である点です。ではdeleteEvent関数の型を見てみましょう。
 
deleteEvent :: WidgetClass self => Signal self (EventM EAny Bool)
 
うげぇ。また色々複雑そうです・・が、これもじっくり眺めましょう。=>より前の部分はselfに対する型クラス制限ですのでここでは無視します。deleteEventは「Signal ほげ ぴよ」という型なので、onの型宣言の第2引数とぴったり合っていることが分かりますね。ちゃんと型が合っていることに喜びを感じたらもう気分はHaskellerです(!?) onの定義をもう一度見ましょう。Signalの2つ目の型変数callbackと、onの第3引数のcallbackは型変数名が同じなので同じ型が入ることが分かります。そしてdeleteEvent関数の型と見比べると・・・はいっ、callbackは(EventM EAny Bool)型であることが分かりました。少しずつonの第3引数の姿が見えてきましたね。EventMという名前のMはモナドのMです。このネーミングルールは慣習なのでHaskellに慣れて勘を働かせるしかありませんが、onの第3引数にはイベントモナドというモナドを渡すようです。新しいモナドですね。どんな機能があるモナドか詳しく調べていないのでここで解説できませんが、とにかくモナドなのでdo記法が使えるはずです。EventMモナドを並べたり、EventMモナドの中身を取り出したり、普通の値をreturnで包んでEventMモナド化する、といった前述①〜⑤のテクニックはIOモナドだけでなくEventMモナドでも通用します。さあ、大分ごちゃごちゃしてきましたが、もう一度問題のコードを見ましょう。
 
window `on` deleteEvent $ do
  liftIO mainQuit
  return False
 
ひとまずliftIO mainQuitは無視しましょう。
 
window `on` deleteEvent $ do
  return False
 
こう見れば、純粋な値(Bool)をreturnでEventMに包んでEventMモナドを作り、onの第3引数に渡していると読み取れますでしょうか。では最後にliftIO mainQuitの部分を解説します。もう一踏ん張りです。mainQuitの型はmainQuit :: IO ()ですのでIOモナドです。ところが今書いているdoはEventMモナドですね。モナドの合成で説明した通り、異なるモナドをdoの中で並べることはできません。そこでliftIOの登場です。liftIOはIOモナドを別のモナド、ここではEventMモナドに変換する関数です。こういった変換を「持ち上げる(lift)」と呼ぶ慣習がありliftIOという名前が付いています。今回、変換の仕組みまでは紹介できませんが、興味のある方は”モナド変換”や”モナド変換子”を調べてみて下さい。ともあれliftIO mainQuitと書くことで、IOモナドの処理(イベントループ終了処理)を、立派なEventMモナドに変換することができ、無事doの中で並べることができました。めでたしめでたし。
 
ここまで読んで頂いた方、お付き合い有難うございました。モナドの使い方に焦点を当て、do記法が分かれば何とかGUIサンプルコードも読めるぞい!ということを説明したかったのです。しかし、C言語のポインタしかり、Javaのクラスベース開発しかり、Haskellのモナドもきっと読むだけではよく分からないと思います。モナドに興味のある方は是非Haskellをインストールしてモナド関数に触ってみて下さい。様々なモナドに精通するのはなかなか難しいですが、まずはIOモナドをdo記法の中で使うことに慣れましょう。IOモナドが使えるとHaskellで色々なことができるようになりますので、是非楽しんで下さい。では次回からテトリス作成に入っていきたいと思います。
 
最後に、モナドの仕組みに興味がある方には「すごいHaskellたのしく学ぼう!」という書籍がオススメです。モナドの説明がとても分かりやすいです。英語が得意な方は原著(Learn You a Haskell)をWebで読むことができます。
http://learnyouahaskell.com
 
以上。
 
第33回目:Haskellでテトリス(Part1)
2014-11-02
筆者:村田
 
こんにちは。
 
今回からまたHaskellにチャレンジしたいと思います。Haskellは半年程前の第10〜13回でProject Euler問題に取り組んで以来となります。前回は再帰を使ったループ処理や、計算の型を考えながらのプログラミングといったテーマを扱いました。すっかり忘れちゃっているかもしれませんが、Haskellを触りながらゆっくり思い出したいと思います。それではしばらくお付き合い宜しくお願いいたします。
 
テトリスについては説明不要なので早速ゲーム作り開始といきたいところですが、Part1のテーマは環境準備です。そしてPart2ではdo記法を用いたモナドの使い方をテーマにする予定でして、具体的なテトリス作成はもう少し先になります。
 
さて、純粋関数型言語と呼ばれるHaskellでは入出力のような副作用の扱いに癖があり、ゲームのようなイベントドリブン処理と純粋関数を結びつける部分が少々難しいのです。具体的にはモナド(Monad)という仕組みを使うのですが、これが何といいいますか・・・とっつきにくいシロモノなのです。ひとことで言えば『文脈付き計算の合成』という感じなんですが、きちんと説明できるほど私が理解していないので、理論/仕組みについてはWebや書籍を参考にしてください。ですがまあ、新しいモナドを生み出すのではなく、とりあえずIOモナドを使うだけならば仕組みに詳しくなくても何とかなります。私の場合も色々ネットのコードをコピペして、いじくって、コンパイルエラーと格闘して、な〜んとなく使っているという感じです;-)
 
それでは環境準備を始めましょう。
 
OSはMacOSXかLinux(Ubuntu14.04)を前提に進めます。Windowsでもできると思いますがまだ試していません。テトリス完成までにWindows版環境構築も情報展開できるかもしれませんが今のところ未定です。関係無い話ですがWindows8.1にしてからあまり触らなくなってしまいました。どうも操作に慣れなくて・・・。Haskellの開発環境は公式サイトからHaskell Platform (現在のバージョンは2014.2.0.0)をインストールしてください。んで問題はGUI画面を作るためのライブラリ(ToolKit)をどうするかです。Haskell Platformに付いてくるGLUTを使うこともできますがOpenGLに精通していないとキツそうです。ちょっと調べてみると結構色々なライブラリがあるのですが、有名どころはGtk2Hs、HGL、wxHaskellあたりでしょうか。どれを使ってもテトリスを作れると思いますし、それぞれ書き方や思想が異なるので色々触ってみてお気に入りを選んでもらえば良いのですが、今回はGtk2Hsを使いたいと思います。
 
さて、こいつのインストールが結構大変です。こちらのサイト
https://www.haskell.org/haskellwiki/Gtk2Hs/Installation
の通りに進めれば良いのですが、結構記述が分散していて分かりにくいので、以下に私の環境で行った手順をメモしておきます。100%うまくいくとは限らないので、トラブルがあったらgoogle先生に聞いてみましょう。
 
1. 必要な外部ライブラリをごっそりインストールしておきます
$ sudo apt-get install libgtk2.0-dev libpango1.0-dev libglib2.0-dev libcairo2-dev
 
2. もし環境変数PKG_CONFIG_PATHを通していなければ*.pcのありかを追加しておく
$ export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
 
3. Haskellのパッケージ管理ツール(cabal)のリポジトリを最新にします
$ cabal update
 
ここでハマることがあります。もし、cabal: Codec.Compression.Zlib〜というエラーがでる場合はリポジトリデータのダウンロードに失敗しています。一度このエラーが出ると再度updateしてもうまくいきません。まずは~/.cabal/packags/hackage.haskell.orgを全部削除して再度試してください。私だけかもしれませんが結構頻繁にcabal updateに失敗するのです。通信環境があまり良くないのかも・・
 
4. ちょっと手間ですが先にcabal自体をアップデートしておく
$ cabal install cabal-install
 
5. Gtk2Hsのビルドツールをインストール
$ cabal install gtk2hs-buildtools
 
6. ビルドツールのパスを通す
$ export PATH=~/.cabal/bin:$PATH
 
7. Gtk2Hsをインストール
$ cabal install gtk
 
それでは以下のサンプルコードをGtkHello.hsとして書いて動作確認してみます。
 
import Graphics.UI.Gtk
import Control.Monad.Trans (liftIO)
 
main = do
  initGUI
  window <- windowNew
  windowSetTitle window "Hello World!"
  window `on` deleteEvent $ do
    liftIO mainQuit
    return False
  widgetShowAll window
  mainGUI
 
コンパイルして実行してみましょう。
$ ghc GtkHello.hs
$ ./GtkHello
 
ウインドウが無事表示できましたか?おめでとうございます、8888。では上記のコードをちょっと眺めてみてください。これはモナドを使ったコードです。よく分からないところもあると思いますが、何となくわかる部分も結構ありませんか?個人的にはよく分からないコードを眺める瞬間が好きです。文法とかは置いておいて色々想像するのが楽しいからです。全部分かるコードより、ちょっと分かるくらいのコードが妄想にちょうどいいのです。
 
そして、分かるところから壊していくのも楽しいです。私なら上記のコードが初見だったら・・まずはハローワールドの部分を書き換えてタイトルが変わることを試します。きっと成功すると信じて。次に一行ずつコメントアウトしてもっとミニマムにできないか、必須なのはどこか、役割を想像しながら実験します。あとはreturn Falseも気になりますね。Trueに変えてもコンパイルが通りそうですし、値を変えて試すのは基本ですね。$や<-などのシンボルを消したりしたら、きっとコンパイルできないでしょう。でもどんなエラーがでるか試してみたくなります。その後はマニュアルを紐解きながら遊べそうです。deleteEventってイベントを定義できるコードっぽいですね。$ do 〜って書けばハンドラが書けるっぽい。では何か別のイベントを拾ってハンドラを付け加えてみたくなります。マニュアルでほにゃららEventって名前の関数を探してみたり・・と、まあ上記のコードは短すぎてそのくらいで飽きるかもしれませんが、ネットにはたくさんのサンプルコードがあるのでもっと良いコードをいじくることをお勧めします。以下はGtk2hsのdemoコードです。ちょっと古いのでコンパイルエラーが出るコードもありますが、コメントアウトで凌ぐか、頑張って直してみるのも一興です。
http://code.haskell.org/gtk2hs/gtk/demo/
 
今回はここまでにします。次回Part2ではモナドの『使い方』(理論とか仕組みの話ではありません)についてお話ししたいと思います。
 
以上。
 
第32回目:Processingでシューティング(Part4)
2014-10-18
筆者:村田
 
こんにちは。
 
今回はおまけ編です。Processing.jsというJavaScriptのライブラリを使うとProcessingをブラウザのcanvas上で動かすことができます。公式サイトのリファレンスを見るとファイル操作関連を除いてProcessingAPIがほぼそのまま動くようです。ちょっと面白そうなので紹介したいと思います。
 
まず、公式サイトからprocessing.js (または軽量版processing.min.js)をダウンロードします。同じディレクトリに以下のHTMLを書いてブラウザで開いてみて下さい。なおブラウザはSafari7.1で動作確認しています。IEでも大丈夫だと思いますけど・・
 
<!DOCTYPE html>
<head>
<script src="processing.js"></script>
<script type="text/processing" data-processing-target="canvas">
 
void setup() {
  size(300, 300);
  noStroke();
}
 
void draw() {
  fill(0, 255, 255);
  ellipse(150, 150, 50, 50);
}
 
</script>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
 
canvasの中央に水色で円が描かれましたか?このようにProcessingのコードがそのまま動いちゃいます。楽しいライブラリですね。上記はProcessingの文法で書いていますが、JavaScriptの方が好きな方は以下のようにJavaScriptで書くこともできます。
 
<!DOCTYPE html>
<head>
<script src="processing.js"></script>
<script type="text/javascript">
 
window.onload = function(){
  var canvas = document.getElementById("canvas");
  var p = new Processing(canvas, function(processing) {
    processing.setup = function() {
      processing.size(300, 300);
      processing.noStroke();
    };
    processing.draw = function() {
      processing.fill(255, 0, 255);
      processing.ellipse(150, 150, 50, 50);
    };
  });
};
 
</script>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
 
というわけで前回作ったシューティングプログラムをprocessing.jsを使ってWebにUpしてみました。以下のURLを開き、ゲーム画面を一回クリックすると上下左右キーでプレイヤーを操作できると思います。
 
・Shooting
http://muratamuu.github.io/Processing-ShootingGame/index.html
 
今回おまけ編で、もうチョイProcessingっぽいものを作ってみようかなと思いまして、あと一本プログラムを書いてこちらもWebにUpしました。以下のURLを開いて画面をクリックしてみてください。カラフルなにょろにょろがマウスポインタのそばに寄ってきてくるくる回ったりします。
 
・Rainbow Nyoro
http://muratamuu.github.io/Processing-RainbowNyoro/index.html
 
どうでしょう?ちょっとキモイかもしれません・・・。コードに興味のある方はブラウザの右クリックでHTMLソースを表示させてみれば中身を見ることができます。
 
[おわりに]
Processingはいかがでしたか?簡単にビジュアライゼーションできて遊べるとても楽しい言語だと思いました。プログラムの構造化などはあまり気にせずガンガン書いてみて、RUNしてちょっとパラメータいじって・・の繰り返しで結構ハマります。3Dや音楽なども扱えるので、プログラミングでアートしてみたい方はProcessingをお試しあれ。さて日曜は情報処理試験ですね。受験される方、頑張りましょう!!それではPart4までお付き合い頂きまして有り難うございました。
 
以上。
 
参考画像:Rainbow Nyoro
2014-10-18
 
第31回目:Processingでシューティング(Part3)
2014-10-12
筆者:村田
 
こんにちは。
 
今回はプレイヤーを表示させるところから始めましょう。プレイヤーも敵と同じようにクラスにまとめます。
 
class Player {
  float x = width / 2;   // 初期位置は画面下中央
  float y = height - 10;
  
  void draw() {
    triangle(x, y-10, x-10, y+10, x+10, y+10);
  }
}
 
Playerのdrawメソッドで三角形を描いています。triangleの引数は3つの頂点のx,y座標です。三角形の中心をx,yとし、幅、高さは20とした場合の3つの頂点を考えます。1つ目の頂点は(x,y-10)となります。Processingの座標系はY軸が画面上がマイナスであることに注意して下さい。残りの2点は(x-10,y+10)、(x+10,y+10)となります。
 
次は上下左右キーでプレイヤーを移動できるようにしたいと思います。マウス位置はグローバル変数mouseX,mouseYに入っていましたが、キーボードの値はグローバル変数keyCodeを参照して取得します。それと、キーを押している間だけプレイヤーが移動し、離したら止まるようにしたいですね。これもイベントハンドラを書く必要はなく、グローバル変数keyPressedを参照しtrue/falseで判断できます。楽ですね〜。ではPlayerクラスのmoveメソッドを実装しましょう。
 
class Player {
  ...
  void move() {
    if (keyPressed) {  // キーが押されているときだけ・・・
      switch (keyCode) {
        case UP:    y -= 2; break; // UP(38) ()内は実際のkeyCode定数値 (参考までに)
        case DOWN:  y += 2; break; // DOWN(40)
        case LEFT:  x -= 2; break; // LEFT(37)
        case RIGHT: x += 2; break; // RIGHT(39)
      }
    }
    // 画面端から三角形がはみ出ないようにチェックする
    if (x-10 < 0)      x = 10;
    if (x+10 > width)  x = width-10;
    if (y-10 < 0)      y = 10;
    if (y+10 > height) y = height-10;
  }
  ...
}
 
ではPlayerオブジェクトを作って画面に表示させましょう。
 
Player player; // グローバル変数
 
void setup() {
  ...
  player = new Player();
}
 
void draw() {
  ...
  fill(0, 255, 0); // プレイヤーは緑色の三角形
  player.move();
  player.draw();
}
 
実行すると参考画像1のようにプレイヤーを上下左右キーで移動できるようになりました。次はプレイヤーもレーザー弾を発射できるようにします。今回は弾は自動発射にしようと思います。プレイヤーの弾も速度を与えて一定方向に移動するので、前回の弾クラス(Bullet)がそのまま流用できそうです。ただし、図形をちょっとレーザーっぽくしましょう。というわけで、Bulletクラスを継承してLaserクラスを作ります。
 
class Laser extends Bullet {
  float w;
  float h;
 
  Laser (float x, float y, float angle, float w, float h) {
    super(x, y, angle, 3, 0); // 弾速:3 角速度:0に固定
    this.w = w; // レーザー弾は幅と高さをコンストラクタでもらう
    this.h = h;
  }
 
  void draw() {
    rect(x-w/2, y-h/2, w, h); // レーザー弾は長方形
  }
}
 
継承はJavaと同じでextendsで書きます。それではLaserコンストラクタを見ていきます。弾速度と角速度を固定値にしてBulletクラスのコンストラクタを呼びます。スーパークラス(Bullet)はsuperで参照できます。Laserは長方形の図形にしますので、幅と高さはコンストラクタのパラメータでもらいましょう。
 
次にレーザー弾もリストで管理しましょう。Bulletと同じように画面外に出たら削除します。以下のコードは前回のBulletリストのコードをコピペして作りました。
 
ArrayList<Laser> laserList; // グローバル変数
 
void setup() {
  ...
  laserList = new ArrayList<Laser> ();
  ...
}
 
void draw() {
  ...
  fill(0, 0, 255); // レーザー弾は青色
  for (int i = laserList.size()-1; i >= 0; i--) {
    Laser laser = laserList.get(i);
    laser.move();
    laser.draw();
    if (laser.needRemove()) laserList.remove(i); // 画面外のレーザーは削除
  }
  ...
}
 
これでレーザー弾の描画は行えるようになりました。あとはPlayerクラスが適当なタイミングでレーザー弾を発射できるようにしましょう。
 
class Player {
  ...
  void draw() {
    triangle(x, y-10, x-10, y+10, x+10, y+10);
    if (frameCount % 30 == 0) laserShot(); // 0.5秒毎に自動発射
  }
 
  void laserShot() {
    // レーザー弾は一度に4方向に発射する
    laserList.add(new Laser(x, y, -90, 2, 20)); // 上(-90°) 幅2/高20
    laserList.add(new Laser(x, y,  90, 2, 20)); // 下(90°)  幅2/高20
    laserList.add(new Laser(x, y, 180, 20, 2)); // 左(180°) 幅20/高2
    laserList.add(new Laser(x, y,   0, 20, 2)); // 右(0°)   幅20/高2
  }
}
 
これで参考画像2のようにレーザー弾が出るようになりました。次は当たり判定を実装しましょう。BulletとPlayer、LaserとEnemyと2つの当たり判定があるので、共通関数として衝突判定関数を作りましょう。
 
// 衝突判定(グローバル関数)
boolean collision(float x1, float y1, float w1, float h1,
                  float x2, float y2, float w2, float h2) {
  if (x1 + w1/2 < x2 - w2/2) return false;
  if (x2 + w2/2 < x1 - w1/2) return false;
  if (y1 + h1/2 < y2 - h2/2) return false;
  if (y2 + h2/2 < y1 - h1/2) return false;
  return true;
}
 
まず引数で2つの長方形(x,y,w,h)をもらいます。この2つの長方形が「重ならない」条件を考えていく方法が簡単です。ゆっくり見ていきましょう。最初のif条件を見ます。x1 + w1/2とは「中心+幅の半分」つまり長方形1の右端ですね。そしてx2 - w2/2は「中心ー幅の半分」つまり長方形2の左端です。「長方形1の右端 < 長方形2の左端」という状態をイメージできますか?このとき2つの長方形は絶対に重ならないのです。ということでfalseを返しましょう。2つ目のif条件は長方形1と長方形2の位置関係を入れ替えたものです。「長方形2の右端 < 長方形1の左端」ならば重なりません(false)。3つ目のif条件を見ます。y1 + h1/2とは「中心+高さの半分」つまり長方形1の下端です。そしてy2 - h2/2とは「中心ー高さの半分」つまり長方形2の上端です。ProcessingのY座標は下側がプラスであるという点に注意して「長方形1の下端 < 長方形2の上端」という状態をイメージして下さい。これも2つの長方形が絶対に重ならない状態です。4つ目のif条件は長方形1と2を入れ替えた条件ですね。最後にこの4つの条件を満たさない場合は「重なっている」のでtrueを返します。
 
そうだ忘れていました。弾が当たったら敵/プレイヤーそれぞれHPを減らすようにしましょう。EnemyとPlayerクラスにそれぞれHP属性を加えておきましょう。なお、いわゆるsetter/getterアクセサは作りません。
 
class Enemy {
  ...
  int hitPoint = 30;
  ...
}
 
class Player {
  ...
  int hitPoint = 10;
  ...
}
 
衝突関数を使って敵の弾が当たったかを判定する箇所ですが、draw関数の中で弾リストを表示しているところで判定しちゃいましょう。
 
void draw() {
  ...
  for (int i = bulletList.size()-1; i >= 0; i--) {
    Bullet bullet = bulletList.get(i);
    bullet.move();
    bullet.draw();
    // ここから追加
    if (collision(player.x, player.y, 3, 3, bullet.x, bullet.y, 5, 5)) {
      bullet.hit = true;
      player.hitPoint--;
    }
    // ここまで
    if (bullet.needRemove()) bulletList.remove(i);
  }
  ...
}
 
ループの中でplayerとbulletの衝突判定を行っています。なお、本記事最初でPlayerの緑三角形の幅と高さを20x20としていましたが、当たり判定に使う矩形を3x3と小さくすることで弾を避け易くしています。弾に当たったらbullet.hitをtrueにします。前回の記事の通り、bullet.needRemove()がtrueを返すようになりますので、playerに当たった弾は消えることになります。
 
同じようにenemyとlaserの衝突判定も書きましょう。
 
void draw() {
  ...
  for (int i = laserList.size()-1; i >= 0; i--) {
    ...
    if (collision(enemy.x, enemy.y, 20, 20, laser.x, laser.y, laser.w, laser.h)) {
      laser.hit = true;
      enemy.hitPoint--;
    }
    ...
  }
  ...
}
 
敵さんの当たり判定サイズは20x20です。次はHitPointを画面に表示させましょう。
 
void draw() {
  ...
  fill(255, 255, 0); // HP表示は黄色
  text("Player:" + nf(player.hitPoint, 3) , 20, 20);
  text("Enemy:" + nf(enemy.hitPoint, 3)  , 20, 40); 
 
  if (player.hitPoint == 0 || enemy.hitPoint == 0)
    noLoop(); // ゲームオーバー
}
 
text関数を使って画面に文字列を表示することができます。第1引数に文字列を渡します。文字列はJavaのStringと同じです。Processingのnf関数で数値を3桁の文字列に変換しています。第2,3引数は表示する座標です。最後にプレイヤーか敵のHPが0になったらnoLoop関数で描画ループを止めてゲームを終了します。
 
しばらく遊んでみると敵の弾に当たらない安全地帯があることが分かりました。そこに入ってしまえば手を止めていてもいずれ勝ってしまうのです。うーん、もう少し頑張りましょうかね。敵がプレイヤーを狙い撃ちしてくるスナイパー弾を追加しましょう。うーんこのネーミングもイマイチ・・。狙い撃ちとは敵とプレイヤーを結ぶ線の角度で撃ってくる弾です。プレイヤーを直接狙った弾ってことです。このようにA地点とB地点を結ぶ線の角度を求める場合、三角関数のatan(アークタンジェント)を使うのが定石です。サイン・コサイン・タンジェントのタンジェントを思い出しましょう。例えばtan30°は覚えていますか?1/(ルート3) = 0.577です。tanは直角三角形の角度からxとyの比率が出せます。atan(アークタンジェント)はtanの逆関数ですので、xとyの比率から角度が出せます。つまりatan(1/(ルート3))は30°になります。えっと実際に使ってみた方が早いです。
 
class Enemy {
  ...
  void draw() {
    ...
    if (frameCount % 120 == 0) snipeShot(); // 2秒に1回スナイプショット
  }
  
  void snipeShot() {
    float dx = player.x - x;
    float dy = player.y - y;
    float degree = degrees(atan2(dy, dx));
    Bullet bullet = new Bullet(x, y, degree, 2, 0);
    bulletList.add(bullet);
  }
}
 
まず敵から見たプレイヤーの相対位置dx,dyを計算します。x - player.xではありませんので注意。そしてatan2(dy,dx)でxとyの比率から角度(ラジアン)を計算します。ラジアンから度数に変換するためdegrees関数を通してからBulletを生成します。atan2関数はxとyを2引数で渡しますが第1引数がy、第2引数がxであることに注意しましょう。なお、アークタンジェントを1引数で計算するatan関数を使う場合はatan(dy/dx)となります。
 
これで安全地帯は無くなり、じっとしていると敵のスナイプ弾を食らってしまうようになりました。今回のシューティングゲームはここまでになります。えーと少し遊んでみて速攻飽きました:P (参考画像3) なお、ソースコード全体は以下のGitHubを参照してください。
 
https://github.com/muratamuu/Processing-ShootingGame/blob/master/ShootingGame.pde
 
 
先程、Dice Newsというサイトの記事「5 Programming Languages Marked for Death」をちらっと見ました。死にゆく言語としてPerl/Ruby/VB.Net/ActionScript/Delphiの5言語が挙げられています。Rubyが入っていることに驚きました。Twitterの開発言語がRuby(RoR)からScalaに置き換えられた理由でもあるパフォーマンスの問題を指摘しています。これだけでRuby終わったと言ってしまうのも・・と思いましたが、まあそれはさておき、いずれもとても人気のあった言語です。新規プロジェクトで使われる機会が減っているのかもしれませんが、既存の資産としては結構な量のコードがあるのではないでしょうか。私自身も昨年VBとDelphiを業務で触ったのですが、まだまだ“Goodbye World!”には早いですよね。
 
以上。
 

連載記事のソースコード

連載記事のソースコード
 
・Haskellで問題を解く(Part1〜Part4) ソースコード
・Clojureで8クイーン問題にチャレンジ(Part1〜Part5) ソースコード
・OCamlでへびゲームを作る(Part1〜Part5) ソースコード
・Swiftでオセロを作る(Part1〜Part5) ソースコード
・Processingでシューティング(Part1〜Part4) ソースコード 
Haskellでテトリス(Part1〜Part9) ソースコード
・プチコン3号(BASIC)でさめがめ(Part1〜Part3) ソースコード
・Prologでさめがめを解く(Part1〜Part6) ソースコード