United Kingdom: +44 (0)208 088 8978

A look at mingine: a WIP game engine for the web

Dragos takes at mingine, a WIP physics engine and game engine for the web

We're hiring Software Developers

Click here to find out more

Game development and F#? Not a combination you often hear, but as someone with an interest in both areas I am always on the lookout, so when I came across this library I knew I wanted to highlight it.

Mingine is a mini general-purpose physics engine and game engine for the web and, like the creator puts it, 'very WIP' but given the scarcity of game engines developed with F#, I believe it is a commendable effort, and I hope the creator plans to expand on it.

Let's take a closer look at the code to understand how it was developed with F#.

In the Mingine.Physics project, we can see how objects that will have physics simulated on them are defined in Types.fs:

namespace Mingine.Types

open System
open System.Collections.Generic
open FSharp.Data.UnitSystems.SI.UnitSymbols
open Mingine.Units

PhysicsObj =
    {pos: Vec2<m>
     mass: float<kg>
     velocity: Vec2<m / s>
     accel: Vec2<m / s^2>

     forces: ForceCalculator[]

     momentOfInertia: float<kg m^2>
     angle: float<rad> // theta
     angVelocity: float<rad / s> // omega
     angAccel: float<rad / s^2> // alpha

     restitutionCoeff: float}

The functions for calculating the force and torque along with the 2D Vector are also represented here

In Simulator.fs we can see how they are being used:

let calcForcesAndTorques (gObj: PhysicsObj) (timeStep: float<s>) =
    let forcesAndTorques = gObj.forces |> Array.map (fun f -> f (gObj, timeStep))

    if forcesAndTorques.Length = 0 then
        Vec2.origin, 0.<_>
    else
        forcesAndTorques
        // remove NaN and +-Infinity forces and torques
        |> Array.map guardForceTorqueInstability
        // sum all forces and torques
        |> Array.reduce (fun (f1, t1) (f2, t2) -> (f1 + f2, t1 + t2))

let updateObjectPos pObj timeStep =
    // update transform from last tick info
    let newPos =
        pObj.pos
        + (pObj.velocity * timeStep)
        + (pObj.accel * 0.5 * (timeStep * timeStep))

    let newAngle =
        pObj.angle
        + (pObj.angVelocity * timeStep)
        + (pObj.angAccel * 0.5 * (timeStep * timeStep))

    // calculate forces and torques
    let force, torque =
        calcForcesAndTorques
            {pObj with
                pos = newPos
                angle = newAngle}
            timeStep

    // update this tick acceleration
    // radians are dimensionless so are not part of the torque nor moment of inertia units, so add them in with *1rad
    let newAccel = force / pObj.mass

    let newAngAccel = torque / pObj.momentOfInertia * 1.<rad>

    let avgAccel = (pObj.accel + newAccel) / 2.

    let avgAngAccel = (pObj.angAccel + newAngAccel) / 2.

    // update this tick velocity
    let newVelocity = pObj.velocity + (avgAccel * timeStep)

    let newAngVelocity = pObj.angVelocity + (avgAngAccel * timeStep)

    {pObj with
        pos = newPos
        accel = newAccel
        velocity = newVelocity

        angle = newAngle
        angAccel = newAngAccel
        angVelocity = newAngVelocity}

Now let's look at the Mingine.Engine project

Starting with Types.fs we define the game object that will be rendered in the UI:

namespace Mingine.Types.Engine

open Browser.Types
open Mingine.Types
open Mingine.Units
open System.Collections.Generic
open FSharp.Data.UnitSystems.SI.UnitSymbols

/// Represents a renderable object in the game
type GameObj =
    {id: string
     physicsObj: PhysicsObj
     layer: int
     /// the vector of center subtract bottom left - this is used to position objects accurately
     blOffset: Vec2<m>
     styles: obj
     collider: Collider
     eventHandlers: (ResolvedEvent -> unit)[]}

Notice how PhysicsObj is being used from Mingine.Physics

We also define the Scene type which holds all the game objects:

/// Holds all the game objects in the game and controls the root render div
type Scene =
    {scale: float<px / m> // scale of 1 means 1px=1m, scale of 10 means 10px=1m, etc
     rootStyles: obj
     objects: WrappedGObj HashSet // hashset = es set, unlike F# map
     worldColliders: Collider[]
     /// think of this as the position of the camera
     renderOffset: Vec2<m>
     canvasSize: Vec2<m>
     postTickHooks: (Scene * float<s> * ResolvedEvent list -> unit)[]
     postFrameHooks: (Scene * ResolvedEvent list -> unit)[]
     eventHandlers: (ResolvedEvent -> unit)[]}

In Engine.fs we can see their usage:

let createEngine scene =

    let mutable this = Unchecked.defaultof<_> // actually just `null`

    let eventHandler (e: Event) =
        let maybeGObj = this.gObjMountedCache.TryGet2 (e.target :?> HTMLElement)
        match maybeGObj with
        | None -> ()
        | Some(go) ->
            for h in go.o.eventHandlers do
                h (e.``type``, e, Some go)

        let resolvedEvent = e.``type``, e, maybeGObj

        this.tickEventCache <- resolvedEvent::this.tickEventCache
        this.frameEventCache <- resolvedEvent::this.frameEventCache

        for h in this.scene.eventHandlers do h resolvedEvent

    this <-
        {scene = scene
         running = false
         mounted = None
         lastTick = 0

         gObjMountedCache = DoubleDict()
         collisionCache = Dictionary()

         tickEventCache = []
         frameEventCache = []

         queryCollision = (fun o1 o2 ->
             (this.collisionCache.ContainsKey o1 && (this.collisionCache[o1] |> List.contains o2))
             || (this.collisionCache.ContainsKey o2 && (this.collisionCache[o2] |> List.contains o1)))

         mount = (fun elem ->
             for eventName in eventNames do
                 elem.addEventListener(eventName, eventHandler)

             this.mounted <- Some elem
             )

         unmount =
             (fun () ->
                 match this.mounted with
                 | Some m ->
                     m.remove ()
                     this.mounted <- None
                     m
                 | None -> failwith "Cannot unmount a non-mounted scene")

         stop = defaultStopFunc
         start =
            (Option.defaultValue
                {lockPhysicsToRender = None
                 physicsHz = None
                 tsCap = None})
            >> (fun sOpts ->
                let lockPhysicsToRender =
                    sOpts.lockPhysicsToRender
                    |> Option.defaultValue false

                let physicsHz =
                    sOpts.physicsHz |> Option.defaultValue 200

                let inline calcTStep tick =
                    match sOpts.tsCap with
                    | Some t when t > 0 -> min (tick - this.lastTick) t
                    | None -> min (tick - this.lastTick) 25 // 25ms default cap
                    | _ -> tick - this.lastTick

                let mutable cancel = false

                let rec renderLoop =
                    (fun tick ->
                        if lockPhysicsToRender then
                            let timeStep = calcTStep tick
                            this.lastTick <- tick
                            runPhysicsTick this (timeStep / 1000.<_>)

                        renderRoot this
                        renderGameObjects this

                        for h in this.scene.postFrameHooks do h (this.scene, this.frameEventCache)

                        this.frameEventCache <- []

                        if not cancel then
                            window.requestAnimationFrame renderLoop |> ignore)

                renderLoop (performance.now ())

                let intervalCode =
                    if lockPhysicsToRender then
                        None
                    else
                        Some(
                            setInterval
                                (fun () ->
                                    let tick = performance.now ()
                                    let timeStep = calcTStep tick
                                    this.lastTick <- tick

                                    runPhysicsTick this (timeStep / 1000.<_>))
                                (int (1000. / physicsHz))
                        )

                this.stop <-
                    (fun () ->
                        cancel <- true
                        this.running <- true
                        // clear the interval if intervalCode is Some(int), do nothing if None
                        Option.map clearInterval intervalCode |> ignore
                        this.stop <- defaultStopFunc)

                this.lastTick <- performance.now ()
                this.running <- true)}

    this

Usage

If you want to run it:

Clone the repository

git clone https://github.com/uwu/mingine.git

Make sure to install the packages

npm i

Then build with the following command

npm run build:fable

And looking closely on how this command is defined in package.json we can see that fable converts the Mingine F# code to JavaScript!

Which is then used in test.html

import * as mg from "./dist/JavaScript.js"

With VSCode you can make use of "Live Server" extension and just open the html file with "Open with Live Server"

In your browser you should now see a flying rectangle! Soon enough it should idle in a floating position!

Notes

There are two important observations to make, which you may have already noticed.

Firstly, in the last F# code example I provided, you can see that mutability is being used in many parts of the code. This is not in line with functional programming principles, which we prioritize here at CIT.

Secondly, JavaScript is used to call our converted F# code, such as in the following example:

const engine = mg.createEngine(mg.createScene({
        scale: renderScale,
        rootStyles: {border: "1px solid gray"},
        canvasSize: mg.v(2, 2),
        postTickHooks: [
            // box collision test
            ([s, _t]) => {
                const objects = s.getObjects();
                const bouncingBall = objects.find(o => o.id === "BOUNCING_BALL");
                const rotatingRect = objects.find(o => o.id === "ROTATING_RECT");

                collidedThisFrame ||= engine.queryCollision(bouncingBall, rotatingRect);
            }
        ],

        postFrameHooks: [
            ...

Although this code is written in F# and simply converted with Fable, it gives the impression of a JavaScript game engine that uses F# libraries, rather than an F# game engine.

However, as mentioned in the overview, this project is still a work in progress. These issues will likely be addressed in due course.

Conclusion

If you want to learn more about game development in F#, check out this more in-depth blog post I made about the possibilities of using F# for game development.

Otherwise, I hope you found this blog post enjoyable, and let's hope that we see more libraries and tools for game development with F# in the future! 😀