Skip to content

Minimal ImSim Example (Jump Box)

Bryan Edds edited this page Nov 4, 2025 · 7 revisions

For this example, we'll intersperse our explanation directly in the code. The code is taken from the ImSim 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 GameDispatcherImSim ()

Here we use the ImSim-specific game dispatcher called 'GameDispatcherImSim'.

    // 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 (game, world) =

We define our game behavior by overriding the Process method as such.

        // declare screen and group
        World.beginScreen "Screen" true Vanilla [] world |> ignore
        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
        World.doBlock2d "Block" [Entity.Position .= v3 128.0f -64.0f 0.0f] world |> ignore

Here is a 2D block declaration where we see how an equality operator is used to specify its position 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 said property.

        // declare a box and then handle its body interactions for the frame
        let (boxBodyId, results) = World.doBox2d "Box" [Entity.Position |= v3 128.0f 64.0f 0.0f] world
        for result in results do
            match result with
            | BodyPenetrationData _ -> game.Collisions.Map inc world
            | _ -> ()

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. Here we use a special static equality |= operator that skips synchronization on code reload. Use this operator for properties that you don't want to be reset on code reload, like player position and rotation.

Here, collision event information is placed 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 with a flow layout
        let layout = Flow (FlowDownward, FlowUnlimited)
        World.beginPanel "Panel" [Entity.Position .= v3 -128.0f 0.0f 0.0f; Entity.Layout .= layout] world

        // declare a collision counter
        let collisions = game.GetCollisions world
        World.doText "Collisions" [Entity.Text @= "Collisions: " + string collisions] world

        // declare a jump button
        let canJump = World.getBodyGrounded boxBodyId world
        if World.doButton "Jump!" [Entity.EnabledLocal @= canJump; Entity.Text .= "Jump!"] world then
            World.jumpBody false 8.0f boxBodyId world

        // declare a bar that fills based on up to 10 collisions and a text that displays when the bar is full
        World.doFillBar "FillBar" [Entity.Fill @= single collisions / 10.0f] world
        if collisions >= 10 then
            World.doText "Full!" [Entity.Text .= "Full!"] 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 ImSim 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 the control panel, group, and screen
        World.endPanel world
        World.endGroup 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
        if  World.isKeyboardAltDown world &&
            World.isKeyboardKeyDown KeyboardKey.F4 world &&
            world.Unaccompanied then
            World.exit world

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

Clone this wiki locally