F#とXNAで試しに何か作ってみる
『hello, worldでもOK!』とのことだったので、F# Advent Calendar jp 2010参加してみました。
この記事は第9回目です。
とはいえF#初心者*1なので、ブログに書けることが何もない。
あまりに何もないので、とりあえず何かプログラム作ってから考えることに。
というわけでなんとなくMicrosoft XNA Game Studio4.0とあわせて何か作ることにしました。
ちなみにF#の勉強をメインに据えていたため+初心者のため、無駄な(そして余計な)部分が多いです。
そして文章も読みにくいと思われるので先に謝っておきますごめんなさい。
とりあえず参照設定
空のF#プロジェクトを立ち上げた後、参照設定に以下の項目を追加します。
Microsoft.Xna.Framework Microsoft.Xna.Framework.Game Microsoft.Xna.Framework.Graphics
たぶんそこまで頑張れないからこの3つだけでなんとかなるでしょうという勢いで。
画面を表示する
まず、Gameクラスを継承したTestGameを作成します。
type TestGame() as this = inherit Game() static let fps = 16 let manager = new GraphicsDeviceManager(this) do this.Window.Title <- "F#でXNA" this.TargetElapsedTime <- new TimeSpan(0,0,0,0,fps) () override this.Update(gameTime : GameTime) = base.Update gameTime override this.Draw(gameTime : GameTime) = base.Draw gameTime
UpdateとDrawはあらかじめオーバーライド実装しておきます。
今回は60fpsで更新をかけるようにしています。
次にエントリポイントを作成。
[<EntryPoint>] let main (args : string[]) = let game = new TestGame() game.Run() game.Dispose() 0
プレーヤー機を作る
さすがに画面だけではさみしいので、プレーヤーもど機を作ろうということに。
用意した画像はこちら。
このFは形がお気に入りです。
さておき、TestGameクラスを拡張していきます(これ以降は追加・変更部分のみ記述します)。
type TestGame() as this = ... static let speed = 6.0f let sprite = lazy begin new SpriteBatch(this.GraphicsDevice) end let texture = lazy begin Texture2D.FromStream(this.GraphicsDevice, File.Open("f.png",FileMode.Open)) end let mutable keyPos = new Vector2() let operateKey key = match key with | Keys.Up when keyPos.Y > 0.0f -> keyPos.Y <- keyPos.Y - speed | Keys.Down when keyPos.Y < float32 (manager.PreferredBackBufferHeight - texture.Value.Height) -> keyPos.Y <- keyPos.Y + speed | Keys.Left when keyPos.X > 0.0f -> keyPos.X <- keyPos.X - speed | Keys.Right when keyPos.X < float32 (manager.PreferredBackBufferWidth - texture.Value.Width) -> keyPos.X <- keyPos.X + speed | Keys.Escape -> this.Exit() | _ -> () let rec operateKeys keys = match keys with | [] -> () | hd :: tail -> operateKey hd operateKeys tail ... override this.Update(gameTime : GameTime) = this.GetPressedKeys() |> Array.toList |> operateKeys base.Update gameTime override this.Draw(gameTime : GameTime) = this.DrawTexture(texture.Value, keyPos) base.Draw gameTime member this.GetPressedKeys() = Keyboard.GetState().GetPressedKeys() member this.DrawTexture (texture:Texture2D, position:Vector2) = sprite.Value.Begin() sprite.Value.Draw(texture, position, Color.White) sprite.Value.End()
リスト処理やパターンマッチを使ってみました。
スプライトやテクスチャをlazyとしているのは、TestGameオブジェクト生成時点ではGraphicsDeviceが取得できていない可能性があるためです、おそらく。
弾を撃ちたいよね!
という欲がでてしまったので実装します。画像はこちら。
「F#!F#!」ということで、Fが#を発射します。
まず、新たにPellet(小弾)クラスを作成します。
type Pellet(pos:Vector2, playerTexture:Texture2D, device:GraphicsDevice) = static let speed = 10.0f static let file = File.Open("sharp.png",FileMode.Open) let texture = lazy begin Texture2D.FromStream(device, file) end let mutable position = new Vector2 begin pos.X + float32 (playerTexture.Width/2 - texture.Value.Width/2), pos.Y + float32 (playerTexture.Height/2 - texture.Value.Height/2) end override this.Texture = texture.Value override this.Position = position override this.OnScreen() = position.Y > - float32 texture.Value.Height override this.Update() = position.Y <- position.Y - speed
特段珍しいことはしていないので、次にTestGameクラスに変更を加えます。
type TestGame() as this = ... static let timelag = 100 let mutable pellets = [] let mutable wait = 0; let operateKey key = ... | Keys.Z when wait > timelag -> pellets <- new Pellet(keyPos, texture.Value, this.GraphicsDevice) :> Pellet :: pellets wait <- 0 ... override this.Update(gameTime : GameTime) = wait <- wait + fps this.GetPressedKeys() |> Array.toList |> operateKeys for pellet in pellets do pellet.Update() pellets <- pellets |> List.filter (fun x -> x.OnScreen()) base.Update gameTime override this.Draw(gameTime : GameTime) = this.DrawTexture(texture.Value, keyPos) for pellet in pellets do this.DrawTexture(pellet.Texture, pellet.Position) base.Draw gameTime ...
operateKeyでは「Zを押していると弾を発射」という処理を追加しています。timelagやwaitは弾が連続しすぎないようにするための調整用です。
Updateでは画面内の弾の座標更新し、弾List内が画面内の弾のみになるようフィルターをかけています。
Drawでは弾の描画を追加で行っています。
実行結果は以下の通り。
別の弾も撃ちたいよね!
と思って仕様変更してみたのですが、ここで勉強不足が露呈する結果に。
先ほど作成したPelletクラスをNormalPelletへrenameし、新たにPellet抽象クラスとCosPellet(cos軌道を描く弾)クラスを実装しようとしました。
しかし、サブクラスからスーパークラスのフィールドにアクセスできるのかどうかわからなかったので、以下の通りごり押しする結果に。
[<AbstractClass>] type Pellet(pos:Vector2, playerTexture:Texture2D, device:GraphicsDevice) = abstract Texture : Texture2D abstract Position : Vector2 abstract OnScreen : unit -> bool abstract Update : unit -> unit type NormalPellet(pos:Vector2, playerTexture:Texture2D, device:GraphicsDevice) = inherit Pellet(pos,playerTexture,device) static let speed = 10.0f static let file = File.Open("sharp.png",FileMode.Open) let texture = lazy begin Texture2D.FromStream(device, file) end let mutable position = new Vector2 begin pos.X + float32 (playerTexture.Width/2 - texture.Value.Width/2), pos.Y + float32 (playerTexture.Height/2 - texture.Value.Height/2) end override this.Texture = texture.Value override this.Position = position override this.OnScreen() = position.Y > - float32 texture.Value.Height override this.Update() = position.Y <- position.Y - speed type CosPellet(pos:Vector2, playerTexture:Texture2D, device:GraphicsDevice) = inherit Pellet(pos,playerTexture,device) static let speed = 1.0f static let file = File.Open("sharp2.png",FileMode.Open) static let pi = 3.14159265358979323846264f let texture = lazy begin Texture2D.FromStream(device, file) end let mutable position = new Vector2 begin pos.X + float32 (playerTexture.Width/2 - texture.Value.Width/2), pos.Y + float32 (playerTexture.Height/2 - texture.Value.Height/2) end let mutable i = 0 override this.Texture = texture.Value override this.Position = position override this.OnScreen() = position.Y > - float32 texture.Value.Height override this.Update() = position.X <- position.X - 10.0f * cos (pi / 100.0f * float32 i) i <- i + 1 position.Y <- position.Y - speed
F#でのOOPはまだよくわかってないです。
ちなみにGameTestはoperateKeyの変更のみで済みました。
let operateKey key = match key with ... | Keys.Z when wait > timelag -> pellets <- new NormalPellet(keyPos, texture.Value, this.GraphicsDevice) :> Pellet :: pellets wait <- 0 | Keys.X when wait > timelag -> pellets <- new CosPellet(keyPos, texture.Value, this.GraphicsDevice) :> Pellet :: pellets wait <- 0 ...
まとめ
- F#は文法がきちんと理解できればわりと書きやすい気がした
- ロジック部分は理解しやすいイメージ
- letとmemberの使い分けに戸惑ったりする(文法に振り回されてる気分)
- 要学習(抽象クラスあたりがまだよくわかっていない)
- もっとたくさんプログラムを書いてみれば、色々なことが見えてきそう
- 私の「フィールドやメソッドの命名のへたくそさ」が改めて露呈する結果となってしまった・・・なんとかしないと!