docs.unity3d.com
    Show / Hide Table of Contents

    Ghost component types and variants

    Ghost components and ghost field types are all handled a certain way during conversion and code generation to produce the right code when building players. It's possible to define the desired behavior in code and on a per ghost prefab basis.

    Inside the package we have default templates for how to generate serializers for a limited set of types:

    • bool
    • Entity
    • FixedString32Bytes
    • FixedString64Bytes
    • FixedString128Bytes
    • FixedString512Bytes
    • FixedString4096Bytes
    • float
    • float2
    • float3
    • float4
    • byte
    • sbyte
    • short
    • ushort
    • int
    • uint
    • enums (only for int/uint underlying type)
    • Quaternion

    Each template can also define different ways to serialize (see also ghost-snapshots.md#Authoring-component-serialization)

    • Quantized or unquantized. Where quantized means a float value is sent as an int with a certain multiplication factor which sets the precision (12.456789 can be sent as 12345 with a quantization factor of 1000).
    • Smoothing method as clamp or interpolate/extrapolate. Meaning the value can be applied from a snapshot as interpolated/extrapolated or unmodified directly (clamped).

    Since each of these can change how the source value is serialized and then unserialized and applied on the target these might new templates or a region inside defined to handle certain cases (like how to interpolate the value).

    There are two ways for customizing how these are handled. Ghost component variants and subtypes. Variants can allow you to define another way for example to synchronize a float, define a different way to quantize it for example.

    Defining additional templates

    It's possible to register other types which are not supported and the default templates either don't cover at all or need separate handling.

    Templates are added to the project by implementing a partial class, UserDefinedTemplates, and injecting it into the Unity.NetCode package by using an AssemblyDefinitionReference. The partial implementation must define the method RegisterTemplates and add new TypeRegistry entries.

    namespace Unity.NetCode.Generators
    {
        public static partial class UserDefinedTemplates
        {
            static partial void RegisterTemplates(System.Collections.Generic.List<TypeRegistryEntry> templates, string defaultRootPath)
            {
                templates.AddRange(new[]{
                    new TypeRegistryEntry
                    {
                        Type = "MySpecialType",
                        Quantized = true,
                        Smoothing = SmoothingAction.InterpolateAndExtrapolate,
                        SupportCommand = false,
                        Composite = false,
                        Template = "Assets/Samples/NetCodeGen/Templates/MySpecialTypeTemplate.cs",
                        TemplateOverride = "",
                    },
                });
            }
        }
    }
    

    The template MySpecialTypeTemplate.cs needs to be set up similar to default types, here is the default Float template (where the float is quantized and stored in an int):

    #region __GHOST_IMPORTS__
    #endregion
    namespace Generated
    {
        public struct GhostSnapshotData
        {
            struct Snapshot
            {
                #region __GHOST_FIELD__
                public int __GHOST_FIELD_NAME__;
                #endregion
            }
    
            public void PredictDelta(uint tick, ref GhostSnapshotData baseline1, ref GhostSnapshotData baseline2)
            {
                var predictor = new GhostDeltaPredictor(tick, this.tick, baseline1.tick, baseline2.tick);
                #region __GHOST_PREDICT__
                snapshot.__GHOST_FIELD_NAME__ = predictor.PredictInt(snapshot.__GHOST_FIELD_NAME__, baseline1.__GHOST_FIELD_NAME__, baseline2.__GHOST_FIELD_NAME__);
                #endregion
            }
    
            public void Serialize(int networkId, ref GhostSnapshotData baseline, ref DataStreamWriter writer, NetworkCompressionModel compressionModel)
            {
                #region __GHOST_WRITE__
                if ((changeMask__GHOST_MASK_BATCH__ & (1 << __GHOST_MASK_INDEX__)) != 0)
                    writer.WritePackedIntDelta(snapshot.__GHOST_FIELD_NAME__, baseline.__GHOST_FIELD_NAME__, compressionModel);
                #endregion
            }
    
            public void Deserialize(uint tick, ref GhostSnapshotData baseline, ref DataStreamReader reader,
                NetworkCompressionModel compressionModel)
            {
                #region __GHOST_READ__
                if ((changeMask__GHOST_MASK_BATCH__ & (1 << __GHOST_MASK_INDEX__)) != 0)
                    snapshot.__GHOST_FIELD_NAME__ = reader.ReadPackedIntDelta(baseline.__GHOST_FIELD_NAME__, compressionModel);
                else
                    snapshot.__GHOST_FIELD_NAME__ = baseline.__GHOST_FIELD_NAME__;
                #endregion
            }
    
            public unsafe void CopyToSnapshot(ref Snapshot snapshot, ref IComponentData component)
            {
                if (true)
                {
                    #region __GHOST_COPY_TO_SNAPSHOT__
                    snapshot.__GHOST_FIELD_NAME__ = (int) math.round(component.__GHOST_FIELD_REFERENCE__ * __GHOST_QUANTIZE_SCALE__);
                    #endregion
                }
            }
            public unsafe void CopyFromSnapshot(ref Snapshot snapshot, ref IComponentData component)
            {
                if (true)
                {
                    #region __GHOST_COPY_FROM_SNAPSHOT__
                    component.__GHOST_FIELD_REFERENCE__ = snapshotBefore.__GHOST_FIELD_NAME__ * __GHOST_DEQUANTIZE_SCALE__;
                    #endregion
    
                    #region __GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE_SETUP__
                    var __GHOST_FIELD_NAME___Before = snapshotBefore.__GHOST_FIELD_NAME__ * __GHOST_DEQUANTIZE_SCALE__;
                    var __GHOST_FIELD_NAME___After = snapshotAfter.__GHOST_FIELD_NAME__ * __GHOST_DEQUANTIZE_SCALE__;
                    #endregion
                    #region __GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE_DISTSQ__
                    var __GHOST_FIELD_NAME___DistSq = math.distancesq(__GHOST_FIELD_NAME___Before, __GHOST_FIELD_NAME___After);
                    #endregion
                    #region __GHOST_COPY_FROM_SNAPSHOT_INTERPOLATE__
                    component.__GHOST_FIELD_REFERENCE__ = math.lerp(__GHOST_FIELD_NAME___Before, __GHOST_FIELD_NAME___After, snapshotInterpolationFactor);
                    #endregion
                }
            }
            public unsafe void RestoreFromBackup(ref IComponentData component, in IComponentData backup)
            {
                #region __GHOST_RESTORE_FROM_BACKUP__
                component.__GHOST_FIELD_REFERENCE__ = backup.__GHOST_FIELD_REFERENCE__;
                #endregion
            }
            public void CalculateChangeMask(ref Snapshot snapshot, ref Snapshot baseline, uint changeMask)
            {
                #region __GHOST_CALCULATE_CHANGE_MASK_ZERO__
                changeMask = (snapshot.__GHOST_FIELD_NAME__ != baseline.__GHOST_FIELD_NAME__) ? 1u : 0;
                #endregion
                #region __GHOST_CALCULATE_CHANGE_MASK__
                changeMask |= (snapshot.__GHOST_FIELD_NAME__ != baseline.__GHOST_FIELD_NAME__) ? (1u<<__GHOST_MASK_INDEX__) : 0;
                #endregion
            }
            #if UNITY_EDITOR || DEVELOPMENT_BUILD
            private static void ReportPredictionErrors(ref IComponentData component, in IComponentData backup, ref UnsafeList<float> errors, ref int errorIndex)
            {
                #region __GHOST_REPORT_PREDICTION_ERROR__
                errors[errorIndex] = math.max(errors[errorIndex], math.abs(component.__GHOST_FIELD_REFERENCE__ - backup.__GHOST_FIELD_REFERENCE__));
                ++errorIndex;
                #endregion
            }
            private static int GetPredictionErrorNames(ref FixedString512Bytes names, ref int nameCount)
            {
                #region __GHOST_GET_PREDICTION_ERROR_NAME__
                if (nameCount != 0)
                    names.Append(new FixedString32Bytes(","));
                names.Append(new FixedString64Bytes("__GHOST_FIELD_REFERENCE__"));
                ++nameCount;
                #endregion
            }
            #endif
        }
    }
    

    When Quantized is set to true the __GHOST_QUANTIZE_SCALE__ variable must be present in the template, and also the quantized scale must be specified when using the type in a GhostField

    Smoothing is also important as it changes how serialization is done in the CopyFromSnapshot function, all sections must be filled in.

    TemplateOverride is used when you want to re-use an existing template but only override a specific section of it. This works well when using Composite types as you'll point Template to the basic type (like float template) and the TemplateOverride to only the sections which need to be customized. For example float2 only defines CopyFromSnapshot, ReportPredictionErrors and GetPredictionErrorNames, the rest uses the basic float template as a composite of the 2 values float2 contains.


    NOTE: It's important that the templates (when using .cs extension) are in a folder with an .asmdef effectively disabling compilation on it, since this isn't real code we want compiled. It can be done by adding an invalid conditional define on the .asmdef (we use NETCODE_CODEGEN_TEMPLATES define in the samples). It's possible though to just store them with any extension (like .txt) and then the compiler won't consider them.


    NOTE: When making changes to the templates you need to use the Multiplayer->Force Code Generation menu to force a new code compilation which will use the updated templates.


    Defining SubTypes and templates

    Subtypes are a way to define a different way of serializing a specific type. You use them by specifying them in the GhostField attribute.

    using Unity.NetCode;
    
    public struct MyComponent : Unity.Entities.IComponentData
    {
        [GhostField(SubType=GhostFieldSubType.MySubType)]
        public float value;
    }
    

    SubTypes are added to projects by implementing a partial class, GhostFieldSubTypes, and injecting it into the Unity.NetCode package by using an AssemblyDefinitionReference. The implementation should just need to add new constant literals to that class (at your own discretion) and they will be available to all your packages which already reference the Unity.NetCode assembly.

    namespace Unity.NetCode
    {
        static public partial class GhostFieldSubType
        {
            public const int MySubType = 1;
        }
    }
    

    Templates for the subtypes are handled like normal user defined templates but need to set the subtype index. So they are added to the project by implementing the partial class, UserDefinedTemplates, and injecting it into the Unity.NetCode package by using an AssemblyDefinitionReference. The partial implementation must define the method RegisterTemplates and add newTypeRegistry entries.

    namespace Unity.NetCode.Generators
    {
        public static partial class UserDefinedTemplates
        {
            static partial void RegisterTemplates(System.Collections.Generic.List<TypeRegistryEntry> templates, string defaultRootPath)
            {
                templates.AddRange(new[]{
                    new TypeRegistryEntry
                    {
                        Type = "System.Single",
                        SubType = GhostFieldSubType.MySubType,
                        Quantized = false,
                        Smoothing = SmoothingAction.InterpolateAndExtrapolate,
                        SupportCommand = false,
                        Composite = false,
                        Template = "Assets/Samples/NetCodeGen/Templates/MyCustomTemplate.cs",
                        TemplateOverride = "",
                    },
                });
            }
        }
    }
    

    As when using any template registration like this, you need to be careful to specify the correct parameters when defining the GhostField to exactly match it. The important properties are SubType of course, in addition to Quantized and Smoothing as these can affect how the serializer code is generated from the template.


    IMPORTANT: The Composite parameter should always be false with subtypes as it is assumed the template given is the one to use for the whole type.


    Ghost Component Variants

    To add networking serialization capability to a type that does not have ghostfields and for which you don't have access to (or cannot be modified), you must create a ghost component variant using the [GhostComponentVariation] attribute.

    The attribute constructor take as argument the type of component you want to specify the variant for (ex: Rotation). Then for each field in the original struct you would like to serialize you should add a GhostField attribute like you usually do. Only members that are present in the component type are allowed. Validation and exceptions are thrown at compile time in case the rule is not respected. At the moment it is mandatory to add and annotate all the fields for a variant.

    An example of a variant would be:

        [GhostComponentVariation(typeof(Transforms.Translation))]
        [GhostComponent(PrefabType=GhostPrefabType.All, OwnerPredictedSendType=GhostSendType.All, SendDataForChildEntity = false)]
        public struct TranslationVariant
        {
            [GhostField(Composite=true, Quantization=100, Interpolate=true)] public float3 Value;
        }
    

    In this case the TranslationVariant will generate serialization code for Transforms.Translation, using the properties and the attribute present in the variant declaration.

    A GhostComponentAttribute attribute can be added to the variant to further specify the component serialization properties.

    It is possible to prevent a component from supporting variation by using the DontSupportVariation attribute. When present, if a GhostComponentVariation is defined for that type, an exception is triggered.

    Ghost components variants for IBufferElementData is not fully supported.

    How to specify what variant to use

    Using GhostAuthoringComponentEditor it is then possible to select for each prefab what serialization variants to for each individual components.

    Ghost Authoring Variants

    Handling multiple ghost variants

    It is possible to configure different serialization variant for a component which already has a variant (ex: a 2D rotation that just serialize the angle instead of a full quaternion).

    In these cases where multiple variant are present for a type that does not have a "default" serialization (that it, the type we are specifying the variation for does not have any ghost fields) is considered a conflict. We can't in fact discern any more what it is the correct serialization to use for that type. To solve the conflicts, netcode use the first serializer in hash order. It is the users responsibility to indicate in this case what type should be the default.

    By using a singleton GhostVariantAssignmentCollection component you can control what variant to use by default for any type. The singleton must be created for both client and server worlds. Some utility methods are provided to make the assignment easier.

    The method of registering which variants are the defaults for each type is by declaring the RegisterDefaultVariants function in an implementation of the DefaultVariantSystemBase class.

    using System.Collections.Generic;
    using Unity.Entities;
    using Unity.Transforms;
    
    namespace Unity.NetCode.Samples
    {
        sealed class DefaultVariantSystem : DefaultVariantSystemBase
        {
            protected override void RegisterDefaultVariants(Dictionary<ComponentType, System.Type> defaultVariants)
            {
                defaultVariants.Add(new ComponentType(typeof(Rotation)), typeof(RotationDefault));
                defaultVariants.Add(new ComponentType(typeof(Translation)), typeof(TranslationDefault));
            }
        }
    }
    

    This class would make sure the default Translation and Rotation variants which come with the package are set as the defaults.

    Special variant types

    There might be cases where you need the variant to remove functionality instead of doing things differently and there are two cases covered. This saves the typing involved with creating a full variant registration which essentially is just turning it off.

    When you want a component to be stripped on clients so you don't see it at all there you can use the ClientOnlyVariant type when registering the default variant for a particular type.

    When you don't want any synchronization to be done with a variant type, so no serialization happens, you can use the DontSerializeVariant type when registering.

    using System.Collections.Generic;
    using Unity.Entities;
    using Unity.Transforms;
    
    namespace Unity.NetCode.Samples
    {
        sealed class DefaultVariantSystem : DefaultVariantSystemBase
        {
            protected override void RegisterDefaultVariants(Dictionary<ComponentType, System.Type> defaultVariants)
            {
                defaultVariants.Add(new ComponentType(typeof(SomeClientOnlyThing)), typeof(ClientOnlyVariant));
                defaultVariants.Add(new ComponentType(typeof(NoNeedToSyncThis)), typeof(DontSerializeVariant));
            }
        }
    }
    

    You can also pick the DontSerializeVariant in the ghost component on prefabs.

    DontSerializeVariant

    Back to top
    Terms of use
    Copyright © 2023 Unity Technologies — Terms of use
    • Legal
    • Privacy Policy
    • Cookies
    • Do Not Sell or Share My Personal Information
    • Your Privacy Choices (Cookie Settings)
    "Unity", Unity logos, and other Unity trademarks are trademarks or registered trademarks of Unity Technologies or its affiliates in the U.S. and elsewhere (more info here). Other names or brands are trademarks of their respective owners.
    Generated by DocFX on 18 October 2023