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! 😀