u/Tunaxor

Announcing Mibo Framework 4.3.0

Hey there, first time posting here.

>Just in case: my name is Angel Munoz; I'm one of the 12 F# devs in the world and I dedicate my hobby time entirely to F#

Mibo is an F# code-first micro framework on top of MonoGame and Raylib.

Mibo offers abstractions to architect your games as MVU (elmish, elm architecture) programs. and now with version 4.3.0, you can opt in for an Adaptive model with my boringly coined SPU (State, Projection, Update) which is based on Adaptive Data for incremental computations of derived state.

>If you have some frontend background, you may have heard of Signals as a way to manage state in a reactive way

While v4.3.0 has a bunch of fixes and the main item is the Adaptive Model release A minimal game I can come up with in a short snippet could be like this:

Declaring the state of the game, what is composed of and what is going to be part of the adaptive graph

type State = {
  PaddleX: cval<float32>
  Ball: cval<Vector2>; Velocity: cval<Vector2>
  IsHit: aval<bool>; PaddleColor: aval<Color>
}
    
[<Struct>]
type Snapshot = { PaddleX: float32; Ball: Vector2; PaddleColor: Color }
    
let toSnapshot (s: State) () : Snapshot = {
  PaddleX = s.PaddleX |> AVal.getValue
  Ball = s.Ball |> AVal.getValue
  PaddleColor = s.PaddleColor |> AVal.getValue
}

>aval: Adaptive value, read only
cval: changeable value, read and write

Please note that not everything has to be adaptive or derived state, you can store any kind of values, you own that.

Some setup functions, our main game logic and the rendering view function

let init (state: State) (ctx: AdaptiveFrameContext) : AdaptiveInit<Frame> =
  AdaptiveInit.ofFrameBuilder(toSnapshot world)
      
let update (state: State) (_: AdaptiveContext) (gameTime: GameTime) =
  let dt = float32 gameTime.ElapsedGameTime.TotalSeconds
      
  if Raylib.IsKeyDown KeyboardKey.Left then s.PaddleX.Set(s.PaddleX.Value - 450f * dt)
  if Raylib.IsKeyDown KeyboardKey.Right then s.PaddleX.Set(s.PaddleX.Value + 450f * dt)
    
  let velocity = s.Velocity |> AVal.getValue
  let ball = s.Ball |> AVal.getValue
      
  let pos = ball + velocity  * dt
    
  let xVel =
    if pos.X < 0f || pos.X > 780f then -velocity.X else velocity.X
  let yVel = 
    if pos.Y < 0f || (s.IsHit |> AVal.getValue) then -velocity.Y else velocity.Y
    
  s.Ball.Set pos
  s.Velocity.Set(Vector2(xVel, yVel))
    
let view (_: GameContext) (snapshot: Snapshot) (buffer: RenderBuffer2D) =
  buf
    .fillRect(sn.PaddleX, 520f, 80f, 16f, sn.PaddleColor)
    .fillRect(sn.Ball.X, sn.Ball.Y, 16f, 16f, Color.Red)
    .drop()

Our state should be created once, the derived state will change and be tracked automatically from the adaptive state via transformations (linq style)

let state =
  let px = CVal.create 360f
  let ball = CVal.create (Vector2(400f, 100f))
  let vel = CVal.create (Vector2(250f, 250f))
    
  // Projection 1: Position collision predicate
  let isHit =
    AVal.map2
      (fun x b -> b.Y >= 500f && b.X >= x && b.X <= x + 80f)
      px
      ball
    
  // Projection 2: Visual feedback derived from collision state
  let color =
    isHit
    |> AVal.map (fun hit ->
      if hit then Color.Green else Color.White
    )
    
  { 
    PaddleX = px
    Ball = ball
    Velocity = vel
    IsHit = isHit
    PaddleColor = color
  }

bring them all together into the entry point

[<EntryPoint>]
let main _ =
  let program =
    AdaptiveProgram.mkProgram (init world) (update world)
    |> AdaptiveProgram.withConfig(GameConfig.withTitle "Mibo Game")
    |> AdaptiveProgram.withRenderer(fun () -> Renderer2D.create view)
    
  let game = new AdaptiveRaylibGame<Frame>(program)
  game.Run()
  0

The video in the post is a sample made using adaptive state

You can find the source code for that sample here: https://github.com/AngelMunoz/Mibo.Samples/tree/master/Defli3D

If you're a numbers person you can find some numbers I tracked via the dotnet trace tool when on very busy moments of the game.

The library (based on FSharp.Data.Adaptive) is built for tight-loop work:

  • Steady state allocates nothing. Once your graph has settled, reads, writes, and delta propagation don't allocate. The exceptions are the deliberate materializations (forcetoSettoMap).
  • A value recomputes at most once per change. Ten writes between two reads cost one recompute. A read when nothing changed is a cheap O(1) check.

So... in summary this release opens up a different functional approach to mutable state which is often friendlier to high performance shaped code (rather than the traditional functional-ish looking F# code)

If you're interested to see some particular kind of genere or approach to all of this (or the more functional version MVU) feel free to let me know. I tried to make sure to open the path for F# high-performance code with some friendly APIs to ease up game development

u/Tunaxor — 3 days ago