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の使い分けに戸惑ったりする(文法に振り回されてる気分)
  • 要学習(抽象クラスあたりがまだよくわかっていない)
  • もっとたくさんプログラムを書いてみれば、色々なことが見えてきそう
  • 私の「フィールドやメソッドの命名のへたくそさ」が改めて露呈する結果となってしまった・・・なんとかしないと!

余談

私がF#を触ったきっかけは、8月頃にtwitterで「関数型言語の本何かないかな」的なことをつぶやいたら
「月末に『プログラミングF#』って本が発売されますよ」
と、とある親切な御方が教えてくださったことが始まりです。
あと、上記プログラムは参考書とにらめっこ(+だらだら)しながら半日かけて作りました。


というわけで以上です、長文失礼しました。

*1:オライリーの「プログラミングF#」をざっと読んだ程度