[F# Game Tutorial] Game Abstraction

In last post, we did load an atlas and show a simple sprite from it, this one is mainly on refactoring, extract common logic for all games, which can be reused later.

Game.TexturePacker

Moved the texture packer library from Tank.Content here.

Game.Engine

Move game abstraction here, it’s very simple now, the logic is same from last post, just create base class, and ways for customization.

BaseGame.fs

type BaseGame (param : GameParam) =
    inherit Microsoft.Xna.Framework.Game ()

    let mutable graphicsManager : GraphicsDeviceManager option = None
    let mutable graphics : Graphics option = None
    let mutable atlas : Atlas option = None
    let mutable time : GameTime option = None

    member this.Setup () =
        this.Content.RootDirectory <- ContentRoot
        graphicsManager <- Some <| new GraphicsDeviceManager (this)

    (* Expose properties
     *)
    member __.Graphics = graphics |> Option.get
    member __.Atlas = atlas |> Option.get
    member __.Time = time |> Option.get

    (* Extension points for subclasses, with default implementation
     *)
    abstract member DoInit : unit -> unit
    abstract member DoUpdate : unit -> unit
    abstract member DoDraw : unit -> unit
    default __.DoInit () = ()
    default __.DoUpdate () = ()
    default __.DoDraw () = ()

    override this.Initialize () =
        let spriteBatch = new SpriteBatch (this.GraphicsDevice)
        let spriteSheetLoader = new SpriteSheetLoader(this.Content, this.GraphicsDevice)
        graphics <- Some {
            Device = this.GraphicsDevice
            SpriteBatch = spriteBatch
            SpriteRender = new SpriteRender (spriteBatch)
            SpriteSheetLoader = spriteSheetLoader
        }
        atlas <- Some <| Atlas.Create ^<| spriteSheetLoader.Load (param.AtlasImage)
        base.IsMouseVisible <- param.IsMouseVisible
        this.DoInit ()
        base.Initialize ()

    override this.Update (gameTime : GameTime) =
        time <- Some gameTime
        this.DoUpdate ()
        base.Update (gameTime)

    override this.Draw (gameTime : GameTime) =
        time <- Some gameTime
        param.ClearColor
        |> Option.iter (fun color ->
            this.Graphics.Device.Clear (color)
        )
        this.Graphics.SpriteBatch.Begin ()
        this.DoDraw ()
        this.Graphics.SpriteBatch.End ()
        base.Draw (gameTime)

GameParam.fs

Record type been created for params of game, cleaner and easier to use than individual variables, more params will be added later, such as initial resolution, full screen mode, etc.

type GameParam = {
    AtlasImage : string
    IsMouseVisible : bool
    ClearColor : Color option
} with
    static member Create (atlasImage : string, ?isMouseVisible : bool, ?clearColor : Color) : GameParam =
        {
            AtlasImage = atlasImage
            IsMouseVisible = defaultArg isMouseVisible true
            ClearColor = clearColor
        }

Tank.Core Changes

Game.fs

By using BaseGame, now the subclass only have logic specific to this particular game.

let param = GameParam.Create(Textures.Tank, clearColor = Color.Black)

type Game () =
    inherit BaseGame (param)
    let mutable testSprite : SpriteFrame option = None

    static member CreateAndRun () =
        let game = new Game ()
        game.Setup ()
        game.Run ()
        game
    override this.DoInit () =
        testSprite <- Some <| this.Atlas.SpriteSheet.Sprite (Sprites.TankBody_huge);
    override this.DoDraw () =
        this.Graphics.SpriteRender.Draw (testSprite.Value, Vector2(100.0f, 100.0f))

Summary

Only very simple refactoring this time, in next post, I will add a GUI library as addon, and create a very basic menu.


Code: https://github.com/yjpark/FSharpGameTutorial/tree/posts/game-abstraction

comments powered by Disqus