Skip to content
Bryan Edds edited this page Oct 17, 2024 · 23 revisions

The World

This is the type that stores all of the program state and functionality, including the simulant state, the event system, the physics engines, the renderers, the audio player, and anything else you can think of. The World value along with its module is your main access point to Nu's complete API. Because Nu exposes a functional API (unless configured otherwise for speed), most operations on a world value produce a new, updated world value. This is what functional programmers often call a 'crank-the-world' style.

The Simulation Hierarchy

By looking at the Nelmish example, you might be able to make inferences of how Nu simulations are defined and structured. Let’s make sure you have a clear idea.

First and foremost, Nu's simulation engine was designed for games. This may seem an obvious statement, but it has some implications that vary it from other middleware technologies.

Nu comes with a family of simulation types out of the box, allowing you to house the various parts of your game inside of it. Here’s the overall structure of a game simulation as provided by Nu –

Game --> [Screen] --> [Group] --> [Entity]

In the above diagram, X --> [Y] denotes a one-to-many relationship, and [X] --> [Y] denotes that each X has a one-to-many relationship with Y. So for example, there is only one active Game in existence, but the Game can contain many Screens (such as a title screen and a credits screen), and each Screen may contain multiple Groups (such as a gui group and a scene group), where each Group may contain multiple Entities that have some user-determined relationship to each other (such as all entities that implement the containing screen's gui).

Let’s break down what each of Nu’s of these types represent in detail.

The Game

The Game is a one-of-a-kind simulant that is used to describe how your game's various screens hook together. In an MMCC context, by defining the game's DesiredScreen property based on the game's model value, you can direct the engine to transition among its various screens like so -

        // here we define the game's property values and event handling
        override this.Definitions (model, _) =
            [Game.DesiredScreen :=
                match model with
                | Splash -> Desire Simulants.Splash
                | Title -> Desire Simulants.Title
                | Credits -> Desire Simulants.Credits
                | Gameplay -> Desire Simulants.Gameplay
             ...]

In an ImNui context, you set whether the currently defined screen should be selected by passing a boolean to the second argument of the World.beginScreen function like so -

let (results, world) = World.beginScreen<GameplayDispatcher> Simulants.Gameplay.Name (myGame = Gameplay) (Dissolve (Constants.Dissolve.Default, None)) [] world

Screens

Screens are precisely what they sound like – a way to implement a single ‘screen’ of interaction in your game. In Nu’s conceptual model, a game is nothing more than a series of interactive screens to be traversed like a graph. The main simulation occurs within a given screen, just like everything else. Familiar uses of 'screens' in games include splash screens, title screens, credits screens, and sim or gameplay screens - the screens where the bulk of gameplay interactions take place.

Groups

Groups represent collections of entities that are logically related in some way. Individual groups can be saved to and loaded from a single .nugroup file. Oftentimes, a screen will have its gui entities in a single 'gui' group and its level entities in a single 'scene' group. If a level is large, you can create a group for each cell in a large grid if you like. Groups are very purpose-agnostic, so you can use groups to organize entities in any way your game needs!

In the editor, each group has a Visible property that can be used to hide or show all of its contained entities, and you can only click and drag entities that are in the editor's currently selected group.

Entities (and Facets)

And here we come down to brass tacks. Entities represent individual interactive ‘things’ in your game, such as user-interface buttons, tile maps, and physics boxes. What differentiates a button entity from a box entity, though? Well, each entity picks up its unique attributes from its dispatcher and facets. Let's understand in more detail first what dispatchers are, then facets.

Dispatchers in Detail

A Dispatcher is a stateless F# class that configures the base properties and behaviors of a simulant. You've already seen an example of a GameDispatcher. Let's look at an EntityDispatcher from the engine code, in particular, the LabelDispatcher -

[<AutoOpen>]
module LabelDispatcherModule =

    type Entity with
        member this.GetLabelImage world : Image AssetTag = this.Get (nameof this.LabelImage) world
        member this.SetLabelImage (value : Image AssetTag) world = this.Set (nameof this.LabelImage) value world
        member this.LabelImage = lens (nameof this.LabelImage) this this.GetLabelImage this.SetLabelImage

    /// Gives an entity the base behavior of a gui label.
    type LabelDispatcher () =
        inherit GuiDispatcher ()

        static member Properties =
            [define Entity.LabelImage Assets.Default.Label]

        override this.Render (entity, world) =
            let mutable transform = entity.GetTransform world
            let mutable spriteTransform = Transform.makePerimeter transform.Perimeter transform.Offset transform.Elevation transform.Absolute transform.Centered
            let spriteImage = entity.GetLabelImage world
            World.enqueueLayeredOperation2d
                { Elevation = spriteTransform.Elevation
                  Horizon = spriteTransform.Horizon
                  AssetTag = AssetTag.generalize spriteImage
                  RenderOperation2d =
                    RenderSprite
                        { Transform = spriteTransform
                          InsetOpt = ValueNone
                          Image = spriteImage
                          Color = if transform.Enabled then Color.One else entity.GetDisabledColor world
                          Blend = Transparent
                          Emission = Color.Zero
                          Flip = FlipNone }}
                world

        override this.GetQuickSize (entity, world) =
            match Metadata.tryGetTextureSizeF (entity.GetLabelImage world) with
            | Some size -> size.V3
            | None -> Constants.Engine.EntitySizeGuiDefault

Unlike the other dispatchers you've seen so far, this dispatcher is NOT MMCC-based, but rather a uses the Classic Nu API that existed before the MMCC API was added to the engine. All of Nu's pre-built dispatchers use the Classic Nu API. Classic-style entity dispatchers have a subset of the over-ridable functions available to their MMCC counterpart, which is as follows -

    /// Register an entity when adding it to a group.
    abstract Register : Entity * World -> World

    /// Unregister an entity when removing it from a group.
    abstract Unregister : Entity * World -> World

    /// Update an entity.
    abstract Update : Entity * World -> World

    /// Render an entity.
    abstract Render : Entity * World -> World

    /// Apply physics changes from a physics engine to an entity.
    abstract ApplyPhysics : Vector3 * Quaternion * Vector3 * Vector3 * Entity * World -> World

    /// Send a signal to an entity.
    abstract Signal : obj * Entity * World -> World

    /// Attempt to get the initial model value if the dispatcher defines one.
    abstract TryGetInitialModelValue<'a> : World -> 'a option

    /// Attempt to synchronize content of an entity.
    abstract TrySynchronize : bool * Entity * World -> World

    /// Get the default size of an entity.
    abstract GetQuickSize : Entity * World -> Vector3

    /// Attempt to pick an entity with a ray.
    abstract RayCast : Ray3 * Entity * World -> single array

    /// Attempt to get the highlight bounds of an entity.
    abstract TryGetHighlightBounds : Entity * World -> Box3 option

    /// Participate in defining additional editing behavior for an entity via the ImGui API.
    abstract Edit : EditOperation * Entity * World -> World

Additionally, there are special static members that you can define to further specify the dispatcher -

    /// Configure the facets intrinsically used by an entity dispatcher.
    static member Facets : Type list

    /// Configure which properties a dispatcher attaches to a simulant and the default value of each property.
    static member Properties : PropertyDefinition list

As seen in the example, you're able to use type extensions to define new property getters, property setters, and property lenses, like the label dispatcher does above -

    type Entity with
        member this.GetLabelImage world : Image AssetTag = this.Get (nameof this.LabelImage) world
        member this.SetLabelImage (value : Image AssetTag) world = this.Set (nameof this.LabelImage) value world
        member this.LabelImage = lens (nameof this.LabelImage) this this.GetLabelImage this.SetLabelImage

Nu's definition of a lens is just a value that can get and optionally set the property of a simulant.

User-defined lenses like the above LabelImage lens are what allow us to easily write new synchronizers in MMCC like so -

    LabelImage := someChangingLabelImage

...or access the property value from an entity handle like so -

    let oldLabelImage = someEntity.GetLabelImage world
    let world = someEntity.SetLabelImage newLabelImage world

Here, we use the new lens and the define function to indicate that an entity attaches the LabelImage property with the initial value of Assets.Default.Label -

        static member Properties =
            [define Entity.LabelImage Assets.Default.Label]

But there are also alternative ways to attach properties, such as nonPersistent, variable, and computed. A property defined via nonPersistent tells the engine to avoid serializing any property with that name. An example from the engine -

        static member Properties =
            [...
             nonPersistent Entity.TmxMap (TmxMap.makeDefault ())
             ...]

A property defined via variable is non-persistent and rather than having a pre-defined initial value, has its initial value computed via a callback when the property is attached to a specific entity. An example from the engine -

        static member Properties =
            [...
             variable Entity.EffectHistory (fun _ -> Deque<Effects.Slice> (inc Constants.Effects.EffectHistoryMaxDefault))
             ...]

A property defined via computed is non-persistent and also computed via a getter and setter rather than having a value backing store. An example from the engine -

        static member Properties =
            [...
             computed Entity.BodyId (fun (entity : Entity) _ -> { BodySource = entity; BodyIndex = 0 }) None
             ...]

You can find many examples of pre-defined entity dispatchers in WorldDispatchers.fs.

Facets in Detail

So what are facets? Well, as facet is very similar to a dispatcher, but instead of statically defining a base set of properties and behaviors for an entity, it dynamically augments an existing entity with additional properties and behaviors. These play a similar role as MonoBehaviors in Unity; they are added - sometimes at runtime - to an entity in order to give additional capabilities to an entity. However, unlike MonoBehaviors, only one instance of each type of facet can be added to a given entity at a time. You can't add multiple StaticSpriteFacets to a single entity, for example. However, because facets attach properties to an entity the same way a dispatcher does, you can simply and directly access facet-attaached properties using the normal property access API like so -

    someEntity.GetStaticSpriteImage world

...rather than having to inefficiently look-up the MonoBehavior component (or binding the mutable component reference in the editor to another MonoBehavior's field...) and then calling one of its getters. Dynamically attached properties in Nu have a constant-time look-up, so they're efficient to use in a convenient manner. In that way, they're a little more like using mixins or non-diamond multiple-inheritance.

To make things more concrete, let's look at a simple facet from the engine, the StaticSpriteFacet -

[<AutoOpen>]
module StaticSpriteFacetModule =

    type Entity with
        member this.GetStaticImage world : Image AssetTag = this.Get (nameof this.StaticImage) world
        member this.SetStaticImage (value : Image AssetTag) world = this.Set (nameof this.StaticImage) value world
        member this.StaticImage = lens (nameof this.StaticImage) this this.GetStaticImage this.SetStaticImage
        member this.GetInsetOpt world : Box2 option = this.Get (nameof this.InsetOpt) world
        member this.SetInsetOpt (value : Box2 option) world = this.Set (nameof this.InsetOpt) value world
        member this.InsetOpt = lens (nameof this.InsetOpt) this this.GetInsetOpt this.SetInsetOpt
        member this.GetColor world : Color = this.Get (nameof this.Color) world
        member this.SetColor (value : Color) world = this.Set (nameof this.Color) value world
        member this.Color = lens (nameof this.Color) this this.GetColor this.SetColor
        member this.GetBlend world : Blend = this.Get (nameof this.Blend) world
        member this.SetBlend (value : Blend) world = this.Set (nameof this.Blend) value world
        member this.Blend = lens (nameof this.Blend) this this.GetBlend this.SetBlend
        member this.GetEmission world : Color = this.Get (nameof this.Emission) world
        member this.SetEmission (value : Color) world = this.Set (nameof this.Emission) value world
        member this.Emission = lens (nameof this.Emission) this this.GetEmission this.SetEmission
        member this.GetFlip world : Flip = this.Get (nameof this.Flip) world
        member this.SetFlip (value : Flip) world = this.Set (nameof this.Flip) value world
        member this.Flip = lens (nameof this.Flip) this this.GetFlip this.SetFlip

    /// Augments an entity with a static sprite.
    type StaticSpriteFacet () =
        inherit Facet (false)

        static member Properties =
            [define Entity.StaticImage Assets.Default.Box
             define Entity.Color Color.One
             define Entity.Blend Transparent
             define Entity.Emission Color.Zero
             define Entity.InsetOpt None
             define Entity.Flip FlipNone]

        override this.Render (entity, world) =
            let mutable transform = entity.GetTransform world
            let staticImage = entity.GetStaticImage world
            let insetOpt = match entity.GetInsetOpt world with Some inset -> ValueSome inset | None -> ValueNone
            let color = entity.GetColor world
            let blend = entity.GetBlend world
            let emission = entity.GetEmission world
            let flip = entity.GetFlip world
            World.renderLayeredSpriteFast (transform.Elevation, transform.Horizon, AssetTag.generalize staticImage, &transform, &insetOpt, staticImage, &color, blend, &emission, flip, world)

        override this.GetQuickSize (entity, world) =
            match Metadata.tryGetTextureSizeF (entity.GetStaticImage world) with
            | Some size -> size.V3
            | None -> Constants.Engine.EntitySize2dDefault

Here we see that properties are defined in the same way, and even the over-ridden functions have the same signatures as the ones in the Classic EntityDispatcher. Here are all of the facet functions available for overriding -

    /// Register a facet when adding it to an entity.
    abstract Register : Entity * World -> World

    /// Unregister a facet when removing it from an entity.
    abstract Unregister : Entity * World -> World

    /// Participate in the registration of an entity's physics with the physics subsystem.
    abstract RegisterPhysics : Entity * World -> World

    /// Participate in the unregistration of an entity's physics from the physics subsystem.
    abstract UnregisterPhysics : Entity * World -> World

    /// Update a facet.
    abstract Update : Entity * World -> World

    /// Render a facet.
    abstract Render : Entity * World -> World

    /// Participate in attempting to pick an entity with a ray.
    abstract RayCast : Ray3 * Entity * World -> single array

    /// Attempt to get the highlight bounds of a facet.
    abstract TryGetHighlightBounds : Entity * World -> Box3 option

    /// Participate in getting the default size of an entity.
    abstract GetQuickSize : Entity * World -> Vector3

    /// Participate in defining additional editing behavior for an entity via the ImGui API.
    abstract Edit : EditOperation * Entity * World -> World

As you can see, the Facet API is very similar to the Classic EntityDispatcher API.

Facets are very convenient in that they allow you to statically compose entities by defining the Facets static member in a dispatcher, or programmatically by calling World.trySetEntityFacetNames, or dynamically in the editor. If you open up Nu's editor, Gaia, you can create an EntityDispatcher2d and attach a StaticSpriteFacet to it like so -

image

Try adding other facets like BasicStaticSpriteEmitterFacet and hitting F5...

Facets are a nice way to statically or dynamically compose entities while minimizing code duplication. You can find many examples of facets in WorldFacets.fs


Simulant Handles

By now, you've noticed that unlike in imperative game engines, we don't directly access or mutate simulant property values. As a functional engine, this makes intuitive sense, at least, the lack of mutation part. The actual backing store for property value for simulants is in one of two places - either in a (mostly encapsulated) pre-defined F# record (such as GameState, ScreenState, GroupState, or EntityState), or in an Xtension object contained by one of the aforementioned records. An Xtension object is just a property bag with constant-time look-up that allowed dispatchers and facets to dynamically attach properties to a simulant. And it's the above type extensions that defined property getters, setters, and lenses are what expose these user-defined properties via a type-safe interface.

So because the state records and their Xtension objects are what hold the actual property values, they way you as a user identify a particular simulant is via an opaque handle that contains an Address value that the engine uses to look up the underlying state record when it wants to access or update information from it. You're already familiar with Nu's 4 simulant handle types, Game, Screen, Group, and Entity, but there's also a parent interface from which they all inherit, Simulant.

Using handles to refer to simulants might be a little more indirection than you're used to, but it has massive benefits, such as keeping all values transformations in sync via a single world reference and automatically exposing ChangeEvents for every single simulant property, even the ones you define yourself with the syntax above. In Unity, you can't really even reliably get a property change event, and if you try to get at least a partial one, you have to define it yourself, and the subscription is coupled to the lifetime of the publishing object. Nu gives you a more complete and decoupled property change event model where the event publishing code is all implemented for you.

Addresses

So we know that simulant handles are constructed from addresses (most conveniently with the overloaded / operator), but what is an Address?

Simply, it's a series of names that uniquely identify either a simulant or an event in the simulation. Events have their own addresses, and those that involve an event originating from a simulant are appended with the simulant's address. Here are some examples of addresses -

Game = The address of the Game simulant, consisting of a single name, "Game". The address of the Game simulant is always the same and can never change. Constructing a Game simulant with any other address will fail.

Game/MyScreen = The address of the screen 'MyScreen'

Game/MyScreen/MyGroup/MyButton = The address of the entity 'MyButton' inside of a group 'MyGroup' inside of a screen 'MyScreen'

Mouse/Left/Down/Event = The address of the left mouse down event.

Click/Event/Game/MyScreen/MyGroup/MyButton = The address of the event when MyButton is clicked.

Because simulant events like Click use addresses, the subscriber does not need to care about whether the publishing simulant exists in order to subscribe to the event. If the publishing simulant goes away and comes back, the subscription will still be there until the subscriber unsubscribes.

Additionally, you can use wildcards and ellipsis to specify multiple event sources like so -

Click/Event/Game/MyScreen/MyGroup/* - specify any click event from any top-level entity in MyGroup.

Click/Event/Game/MyScreen/... - specify any click event from any entity in MyScreen.

Note that you cannot use both the wildcard and the ellipsis together since that would have to large of a performance impact. Additionally, you cannot use wildcard or ellipses in property change events for the same reason.

Clone this wiki locally