Sunday, March 12, 2017

Some Gotchas Writing Unity Apps in F#

If you're writing your Unity code in F#, here are some useful tips that may not be immediately obvious:

Nullable Fields in Monobehaviours

A lot of Unity scripts involve classes that inherit from MonoBehaviour, some of which will have fields that become settable properties in the Unity editor. Because of to the way Unity does serialization, the values are not passed to your class' constructor. (Unity wants you to use a no-argument constructor, if any; you shouldn't be calling any Unity API methods from it.)

As a result, you need fields that initialize to null, which Unity will then in turn set. Since F# is designed to interoperate with .NET, it foresees this eventuality (while screaming at you that it's generally a bad design decision):
namespace My.Unity.FSharp

open UnityEngine

type MyBehaviour() =
    inherit MonoBehaviour()

    [<SerializeField>] [<DefaultValue>] val mutable MyGameObject: GameObject

Using val mutable with the [<DefaultValue>] attribute allows for the field to take on a "zero" value when it is initialized; for reference objects this is null.

Mutable Struct Fields

Many Unity data structures—Quaternion, for example—are implemented as .NET structs. You may find yourself bewildered at why this code doesn't work:

    let toQuat: Quaternion = Camera.main.transform.localRotation
    toQuat.x <- 0.0f

If you hover your mouse over the x in toQuat.x, the helpful hover tip will tell you val mutable x: float32. Yet you still get the error "Error: A value must be mutable in order to mutate the contents or take the address of a value type, e.g., let mutable x = ..." If you read this too quickly, it looks it's telling you x has to be mutable, but we've already seen that it is a mutable field.

The answer is that for a value type's fields to be mutable, it itself has to be mutable. So the correct code is:

    let mutable toQuat: Quaternion = Camera.main.transform.localRotation
    toQuat.x <- 0.0f 

Dependencies on Scripts in Prefabs

If you happen to be working your way through the HoloLens tutorials, as I have been, you'll reach Chapter 6 of Holograms 101, where you're given code referencing SpatialMapping.Instance.  If you write this in F#, you'll likely see Error: The namespace or module 'SpatialMapping' is not defined.

The problem is that the SpatialMapping class is part of the SpatialMapping prefab asset that you're given in the exercise; the script is built into the prefab. But writing F# code for Unity requires compiling to a plugin, so we need the SpatialMapping class to be compiled in order to be able to refer to it.

The solution is actually not difficult. If you build your Unity project, Unity will create an Assembly-CSharp.csproj file at the root of your Unity project. This project file corresponds to a .NET project that you can reference as a project reference in your F# project. Once that dependent project's DLL is built, your F# code will be able to pick up the necessary references.

Out Parameters for Complex Method Signatures

Another issue I encountered was that in some cases, Unity's APIs take an out parameter but don't make it the last parameter in the method signature. While in general F# is smart with .NET out parameters—you call the method without the out parameter, and you get back a tuple pairing a boolean with the returned value—I wasn't able to make this work with a long method signature having the out parameter in the middle, e.g.

    public static bool Raycast(Vector3 origin, Vector3 direction, out RaycastHit hitInfo, float maxDistance = Mathf.Infinity, int layerMask = DefaultRaycastLayers, QueryTriggerInteraction queryTriggerInteraction = QueryTriggerInteraction.UseGlobal);

It's possible that the reason F# can't return a tuple is that the method signature without the out parameter collides with a different method signature of Physics.Raycast. In any event, here you have to handle the out parameter explicitly, so the F# code would look like this:

    let mutable hitInfo = Unchecked.defaultof<RaycastHit>

    if Physics.Raycast(headPosition, gazeDirection, &hitInfo, maxDistance, SpatialMapping.PhysicsRaycastMask) then
        this.transform.parent.position <- hitinfo.point 

Note how Unchecked.defaultof<'T> is used in F# to assign a null value to a binding for a reference object. The binding has to be mutable, and gets passed as an out parameter using the & prefix.

As I come up with more gotchas, I'll keep posting...

No comments:

Post a Comment