Skip to content

Minimal ImNui Example (Jump Box)

Bryan Edds edited this page Oct 19, 2024 · 17 revisions

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 MyGame
open System
open System.Numerics
open Prime
open Nu

// this extends the Game API to expose user-defined properties.
[<AutoOpen>]
module MyGameExtensions =
    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 MyGameDispatcher () =
    inherit GameDispatcher ()

Here we use the Classic Nu GameDispatcher you're already familiar with. Unlike MMCC, ImNui use doesn't require using a special set of dispatchers. Nor does ImNui expose a model like the MMCC API. Here's you just use normal property lenses.

    // here we handle running the game
    override this.Run (myGame, world) =

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

        // 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. Here we fold over the results of the box declaration which represent the events that have transpired with it over the past frame. In this fold, we increment the Collisions count for each BodyPenetration that occurred.

        // declare a box, store its handle and body id for reference, then handle its body interactions
        let (results, world) = World.doBox2d "Box" [Entity.Position .= v3 128.0f 64.0f 0.0f; Entity.Observable .= true] world
        let box = world.RecentEntity
        let boxBodyId = box.GetBodyId world
        let world =
            FQueue.fold (fun world result ->
                match result with
                | BodyPenetration _ -> myGame.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 myGame.Collisions] 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 myGame.Collisions / 10.0f] world
        let world = if myGame.Collisions >= 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 while unaccompanied
        let world =
            if World.isKeyboardAltDown world && World.isKeyboardKeyDown KeyboardKey.F4 world && world.Unaccompanied
            then World.exit world
            else world

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

        // fin
        world

Finally, we return the world value, and that's it! Couldn't be simpler!

Clone this wiki locally