-
-
Notifications
You must be signed in to change notification settings - Fork 187
Minimal ImSim Example (Jump Box)
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 -

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.SetCollisionsHere 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" [] worldIf 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 |> ignoreHere 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!"] worldWithin 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 worldNow 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 worldWe then handle exiting by detecting Alt+F4 while outside of the editor (world.Unaccompanied).