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