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 -
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).