Cesium for Unity 1.15.2
Loading...
Searching...
No Matches
Creating MonoBehaviours

Guidelines and tips for creating MonoBehaviours in Cesium for Unity.

Avoid implementing non-static methods in C++

If you don't need C++-specific state, static methods are much more efficient. Consider adding the staticOnly=true parameter to the [ReinteropNativeImplementation] attribute, so that you - and other developers in the future - can't accidentally create C++ state.

Lifecycle and Methods to Implement.

  • Do initialization in OnEnable. Avoid Start and Awake. The reason is that Start and Awake are not invoked on domain reload, such as when editing a script, but OnEnable is. So by using only OnEnable, we can essentially treat a domain reload the same as a normal reload of the scene.
  • OnEnable for different components may be called in arbitrary order. So if you're implementing an OnEnable that requires some other component to already be initialized, you must trigger this initialization manually. For example, if your OnEnable needs to use a CesiumGeoreference, call the georeference's Initialize method near the beginning of your OnEnable.
  • Implement ICesiumRestartable and its Restart method. This method is called by the UI when Unity has updated the serialized fields in unspecified ways, and so the state of the object should be recreated from the serialized fields.
  • In most cases, the implementation of OnEnable should simply call Restart.
  • Implement Reset, usually by simply calling Restart. This method is called when Unity has directly written to the serialized fields to reset them, and so the Restart method is the right way to synchronize the object's state.
  • What's the difference between Restart (e.g. on CesiumGlobeAnchor) and Initialize (e.g. on CesiumGeoreference)? Restart completely recreates the object's state from the current values of the serialized fields. The current state is assumed to be invalid and thrown away. Initialize, on the other hand, prepares the object for first use but does nothing if the object is already initialized.
  • OnEnable and OnDisable usually work as a pair, with initialization happening in OnEnable and cleanup happening in OnDisable. Unity guarantees that OnDisable is called before an enabled component is destroyed. But be careful if some of the initialization can happen elsewhere other than OnEnable, such as in Reset, Restart, or Initialize. If that happens for a disabled component, OnDisable will never be called and so the cleanup may never happen. The solution is to skip some or all of the initialization work if the component is not isActiveAndEnabled.

Serialization

Carefully consider every field that you add to the class. In general, only the essential fields necessary to reconstruct the state of the object should by marked [SerializeField]. Cached and derived fields should instead be marked [NonSerialized]. Fields without any attribute should be extremely rare.

Characteristic [SerializeField] No attribute [NonSerialized]
Saved / Loaded with the Scene
Preserved on script change / AppDomain reload
Transfers from Edit mode to Play mode

Backward compatibility

Unity's serialization system is extremely simplistic (presumably in the name of performance), and makes backward compatibility difficult. Our solution is powerful but a bit involved. Here's how it works.

Let's say we want to implement backward compatible loading for CesiumGlobeAnchor instances saved with Cesium for Unity v0.2.0. First, we add a new CesiumGlobeAnchorBackwardCompatibility0dot2dot0.cs file, but don't launch Unity yet! In that file, define a new class named CesiumGlobeAnchorBackwardCompatibility0dot2dot0, derived from the original class, CesiumGlobeAnchor and implementing the interface IBackwardCompatibilityComponent<CesiumGlobeAnchor>. Add the following attributes to the class declaration:

  • [ExecuteInEditMode] - So that the class's OnEnable (which we'll write shortly) is called in the Editor.
  • [AddComponentMenu("")] - So that this class will not show up in the "Add Component" panel in the Editor.
  • [DefaultExecutionOrder(-1000000)] - So that this class's OnEnable runs really early, before other classes.
[ExecuteInEditMode]
[AddComponentMenu("")]
[DefaultExecutionOrder(-1000000)]
internal class CesiumGlobeAnchorBackwardCompatibility0dot2dot0 : CesiumGlobeAnchor, IBackwardCompatibilityComponent<CesiumGlobeAnchor>
{
// ...
}

Define all of the properties that existed in the old version of the CesiumGlobeAnchor, but rename them by appending the version (0dot2dot0) to the end. Add a [FormerlySerializedAs] attribute with the previous name. For example:

[FormerlySerializedAs("_adjustOrientationForGlobeWhenMoving")]
public bool _adjustOrientationForGlobeWhenMoving0dot2dot0 = false;
[FormerlySerializedAs("_positionAuthority")]
public CesiumGlobeAnchorPositionAuthorityBackwardCompatibility0dot2dot0 _positionAuthority0dot2dot0 = CesiumGlobeAnchorPositionAuthorityBackwardCompatibility0dot2dot0.None;
[FormerlySerializedAs("_latitude")]
public double _latitude0dot2dot0 = 0.0;

If a field is an enum that has been eliminated entirely, or if its enum values were changed from the old version, declare the old enum type nested inside the backward compatibility class. Since CesiumGlobeAnchorPositionAuthority was removed from CesiumGlobeAnchor, a backwards-compatible enum is defined in CesiumGlobeAnchorBackwardCompatibility0dot2dot0:

public enum CesiumGlobeAnchorPositionAuthorityBackwardCompatibility0dot2dot0
{
None,
LongitudeLatitudeHeight,
EarthCenteredEarthFixed,
UnityCoordinates
}

Next, declare an Editor class, nested inside CesiumGlobeAnchorBackwardCompatibility0dot2dot0, that merely provides an Upgrade button, and an OnEnable that automatically upgrades. Put both inside an ifdef for UNITY_EDITOR:

#if UNITY_EDITOR
[CustomEditor(typeof(CesiumGlobeAnchorBackwardCompatibility0dot2dot0))]
internal class CesiumGlobeAnchorBackwardCompatibility0dot2dot0Editor : Editor
{
public override void OnInspectorGUI()
{
if (GUILayout.Button("Upgrade"))
{
CesiumGlobeAnchorBackwardCompatibility0dot2dot0 o = (CesiumGlobeAnchorBackwardCompatibility0dot2dot0)this.target;
CesiumBackwardCompatibility<CesiumGlobeAnchor>.Upgrade(o);
}
}
}
void OnEnable()
{
CesiumBackwardCompatibility<CesiumGlobeAnchor>.Upgrade(this);
}
#endif

Finally, implement IBackwardCompatibilityComponent<CesiumGlobeAnchor> to write the upgrade logic itself:

public string VersionToBeUpgraded => "v0.2.0";
public void Upgrade(GameObject gameObject, CesiumGlobeAnchor upgraded)
{
// Temporarily disable orientation adjustment so that we can set the position without
// risking rotating the object.
upgraded.adjustOrientationForGlobeWhenMoving = false;
upgraded.detectTransformChanges = false;
switch (this._positionAuthority0dot2dot0)
{
case CesiumGlobeAnchorPositionAuthorityBackwardCompatibility0dot2dot0.None:
// This shouldn't happen, but if it does, just leave the position at the default.
break;
case CesiumGlobeAnchorPositionAuthorityBackwardCompatibility0dot2dot0.LongitudeLatitudeHeight:
upgraded.longitudeLatitudeHeight = new double3(this._longitude0dot2dot0, this._latitude0dot2dot0, this._height0dot2dot0);
break;
case CesiumGlobeAnchorPositionAuthorityBackwardCompatibility0dot2dot0.EarthCenteredEarthFixed:
upgraded.positionGlobeFixed = new double3(this._ecefX0dot2dot0, this._ecefY0dot2dot0, this._ecefZ0dot2dot0);
break;
case CesiumGlobeAnchorPositionAuthorityBackwardCompatibility0dot2dot0.UnityCoordinates:
// Any backward compatibility for CesiumGeoreference must have a more negative
// DefaultExecutionOrder so that the real CesiumGeoreference is created first.
// If this component is not nested inside a CesiumGeoreference, converting Unity coordinates
// to ECEF is impossible, so just keep the default position.
CesiumGeoreference georeference = this.GetComponentInParent<CesiumGeoreference>();
if (georeference != null)
{
georeference.Initialize();
double3 ecef = georeference.TransformUnityPositionToEarthCenteredEarthFixed(new double3(this._unityX0dot2dot0, this._unityY0dot2dot0, this._unityZ0dot2dot0));
upgraded.positionGlobeFixed = ecef;
}
break;
}
upgraded.adjustOrientationForGlobeWhenMoving = this._adjustOrientationForGlobeWhenMoving0dot2dot0;
upgraded.detectTransformChanges = this._detectTransformChanges0dot2dot0;
}

Finally, rename CesiumGlobeAnchor.cs.meta to CesiumGlobeAnchorBackwardCompatibility0dot2dot0.cs.meta. Now launch Unity. Unity will create a new CesiumGlobeAnchor.cs.meta with a new GUID. Any scenes saved with a CesiumGlobeAnchor using the old GUID will instead end up loading a CesiumGlobeAnchorBackwardCompatibility0dot2dot0 instead. When we invoke CesiumBackwardCompatibility<CesiumGlobeAnchor>.Upgrade() on the new backward compatibility component, either when this object is enabled in the Editor or when the user explicitly clicks the Upgrade button in a prefab, a few things happen:

  • A component of the new type (CesiumGlobeAnchor) is created.
  • The Upgrade method is called to initialize the new CesiumGlobeAnchor from the backward compatibility instance.
  • The mapping between the old (CesiumGlobeAnchorBackwardCompatibility0dot2dot0) and the new (CesiumGlobeAnchor) instances is added to a dictionary, and a call to UpdateScene is registered for the next Editor tick.

In the next Editor tick, after one or more backward compatible components have been upgraded, the following happens:

  • We walk through every component on every game object in all open scenes, looking for serialized fields contain a reference to any of the old, backward compatible instances.
  • For each we find, we replace it with a reference to the new instance.
  • The old backward compatible instances are destroyed.
  • The new components are moved up or down in their game object's component list so that they're in the same position as the backward compatible instance they replace.
  • Modified game objects are marked dirty, so the Editor will prompt the user to save the changes.