Minimal ImNui Example (Jump Box) - bryanedds/Nu GitHub Wiki

For this example, we'll intersperse our explanation directly in the code. The code is taken from the ImNui tutorial project here - https://github.com/bryanedds/Nu/tree/master/Projects/Jump%20Box

Quick screenshot of said tutorial project here -

image

namespace JumpBox
open System
open System.Numerics
open Prime
open Nu

// this extends the Game API to expose user-defined properties.
[<AutoOpen>]
module JumpBoxExtensions =
    type Game with
        member this.GetCollisions world : int = this.Get (nameof Game.Collisions) world
        member this.SetCollisions (value : int) world = this.Set (nameof Game.Collisions) value world
        member this.Collisions = lens (nameof Game.Collisions) this this.GetCollisions this.SetCollisions

Here we expose a property lens called Collisions which will count the number of rigid body collisions we're interested in. Notice that other than when defining our game behavior, we're just using the Classic Nu API as such.

// this is the dispatcher that customizes the top-level behavior of our game.
type JumpBoxDispatcher () =
    inherit GameDispatcherImNui ()

Here we use the ImNui-specific game dispatcher called 'GameDispatcherImNui'. Unlike MMCC, ImNui use doesn't expose a model.

    // here we define default property values
    static member Properties =
        [define Game.Collisions 0]

Here we give our user-defined property lens a default value of 0.

    // here we define the game's behavior
    override this.Process (jumpBox, world) =

Instead of defining our game behavior by overriding the Update method (or Message and Command methods like in MMCC), we override the Process method as such.

        // declare screen and group
        let (_, world) = World.beginScreen "Screen" true Vanilla [] world
        let world = World.beginGroup "Group" [] world

If you're familiar with ImGui, you'll recognize the begin and end function pairs where the begin functions bring you into a new identity scope. Here we declare the opening of a screen's scope and inside of it, we declare the opening of a group's scope. You have to be inside a group scope before you can declare entities like so -

        // declare a block
        let (_, _, world) = World.doBlock2d "Block2d" [Entity.Position .= v3 128.0f -64.0f 0.0f] world

Here is a 2D block and like with MMCC, we see how we have an equality operator used to specify its property (in this case, we declare the the 2D block has a position where X = 128, Y = -64, and Z = 0. The .= operator is used to specify a static equality; that is, an initial value for a property.

        // declare a box and then handle its body interactions for the frame
        let (boxBodyId, results, world) = World.doBox2d "Box" [Entity.Position .= v3 128.0f 64.0f 0.0f; Entity.Observable .= true] world
        let world =
            FQueue.fold (fun world result ->
                match result with
                | BodyPenetration _ -> jumpBox.Collisions.Map inc world
                | _ -> world)
                world results

Here we declare a box that can be propelled off of the block. A box is a dynamic rigid body whereas a block is a static rigid body. Additionally, we mark the box as Observable so that it can generate the collision information that we place in the results binding. We then do a fold over the results to see if its body has penetrated anything since the last frame and if so, increment the Collisions count in the model.

        // declare a control panel
        let world = World.beginPanel "Panel" [Entity.Position .= v3 -128.0f 0.0f 0.0f; Entity.Layout .= Flow (FlowDownward, FlowUnlimited)] world
        let world = World.doText "Collisions" [Entity.Text @= "Collisions: " + string (jumpBox.GetCollisions world)] world
        let (clicked, world) = World.doButton "Jump!" [Entity.EnabledLocal @= World.getBodyGrounded boxBodyId world; Entity.Text .= "Jump!"] world
        let world = if clicked then World.applyBodyLinearImpulse (v3Up * 256.0f) None boxBodyId world else world
        let world = World.doFillBar "FillBar" [Entity.Fill @= single (jumpBox.GetCollisions world) / 10.0f] world
        let world = if jumpBox.GetCollisions world >= 10 then World.doText "Full!" [Entity.Text .= "Full!"] world else world
        let world = World.endPanel world

Within the same group, we then declare a little user interface inside of a panel that automatically positions its children with a downward flow algorithm. It contains a text entity the shows the number of collisions, a button that when clicked applies a linear impulse to the box, a fill bar that fills based on the number of collisions (up to 10), and some text that displays "Full!" when the fill bar is full. Here we see the use of the other ImNui dynamic equality operator, @=. I call it the plug operator because it plugs the value into the given property for the lifetime of the simulant rather than just when initializing like the .= operator.

        // finish declaring group and screen
        let world = World.endGroup world
        let world = World.endScreen world

Now it's time to close the group and screen scope using their respective end functions.

        // handle Alt+F4 when not in editor
        if world.Unaccompanied && World.isKeyboardAltDown world && World.isKeyboardKeyDown KeyboardKey.F4 world
        then World.exit world
        else world

We then handle exiting by detecting Alt+F4 while outside of the editor (world.Unaccompanied).

⚠️ **GitHub.com Fallback** ⚠️