Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
## [1.2.0-pre.4] - 2023-11-28

### Added

* You can now disable the automatic Entities `ICustomBootstrap` bootstrapping (which calls NetCode's own `ClientServerBootstrap`) by either; a) disabling it in the ProjectSettings (default value is enabled), or b) adding the new `OverrideAutomaticNetcodeBootstrap` MonoBehaviour to your first build scene (i.e. your Active scene). Thus, there is no longer any need to write a custom bootstrap just to support a Frontend scene vs a Gameplay scene.
* A `NetCodeConfig` ScriptableObject, containing most NetCode configuration variables, allowing customization without needing to modify code. Most variables are live-tweakable.
* A 'Snapshot Sequence Id' (SSId), which is used to accurately measure packet loss. It adds 1 byte of header to each snapshot, but enables us to measure Netcode-caused causes of PL (i.e. out of order snapshots being discarded, and discarding a snapshot if another arrives on the same frame). Access statistics via a new struct on the client's `NetworkSnapshotAck`.
* `RpcCollection.GetRpcHeaderLength` and `NetworkStreamDriver.GetMaximumHeaderSize` to allow users to determine max safe payload sizes.

### Fixed

* Esoteric exception in `MultiplayerPlaymodeWindow` in server-only cases.
* Interpolated ghosts now support `IInputComponentData` and `AutoCommandTarget`.
* Improved `UpdateGhostOwnerIsLocal` to make it reactive to `GhostOwner` changes, thus it no longer needs to poll.
* NetDbg `ArgumentException` when a predicted ghost contains a replicated enableable flag component.
* Display-only issue where the variants for additional entities (created via baking) were calculated as if they were 'root' entities. They are - in fact - child entities, thus the variants automatically selected for them should default to child defaults.
* QoL issue; we now allow users to opt-out of auto-baking `GhostAuthoringInspectionComponent`s when selecting their GameObject, reducing stalls when clicking around the Hierarchy or Project.
* QoL issue where `GhostAuthoringInspectionComponent` was not always modifiable in areas of the Editor where it is valid to modify them.
* Issue where `GhostAuthoringComponent` was disallowed in nested prefab setups (where the root prefab is NOT a ghost).
* Log verbiage when creating a driver in DefaultDriverConstructor read like a 'call to action'. It's not.
  • Loading branch information
Unity Technologies committed Nov 28, 2023
1 parent 35c9c7d commit b63a410
Show file tree
Hide file tree
Showing 48 changed files with 1,412 additions and 406 deletions.
22 changes: 22 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,28 @@ uid: changelog

# Changelog

## [1.2.0-pre.4] - 2023-11-28

### Added

* You can now disable the automatic Entities `ICustomBootstrap` bootstrapping (which calls NetCode's own `ClientServerBootstrap`) by either; a) disabling it in the ProjectSettings (default value is enabled), or b) adding the new `OverrideAutomaticNetcodeBootstrap` MonoBehaviour to your first build scene (i.e. your Active scene). Thus, there is no longer any need to write a custom bootstrap just to support a Frontend scene vs a Gameplay scene.
* A `NetCodeConfig` ScriptableObject, containing most NetCode configuration variables, allowing customization without needing to modify code. Most variables are live-tweakable.
* A 'Snapshot Sequence Id' (SSId), which is used to accurately measure packet loss. It adds 1 byte of header to each snapshot, but enables us to measure Netcode-caused causes of PL (i.e. out of order snapshots being discarded, and discarding a snapshot if another arrives on the same frame). Access statistics via a new struct on the client's `NetworkSnapshotAck`.
* `RpcCollection.GetRpcHeaderLength` and `NetworkStreamDriver.GetMaximumHeaderSize` to allow users to determine max safe payload sizes.

### Fixed

* Esoteric exception in `MultiplayerPlaymodeWindow` in server-only cases.
* Interpolated ghosts now support `IInputComponentData` and `AutoCommandTarget`.
* Improved `UpdateGhostOwnerIsLocal` to make it reactive to `GhostOwner` changes, thus it no longer needs to poll.
* NetDbg `ArgumentException` when a predicted ghost contains a replicated enableable flag component.
* Display-only issue where the variants for additional entities (created via baking) were calculated as if they were 'root' entities. They are - in fact - child entities, thus the variants automatically selected for them should default to child defaults.
* QoL issue; we now allow users to opt-out of auto-baking `GhostAuthoringInspectionComponent`s when selecting their GameObject, reducing stalls when clicking around the Hierarchy or Project.
* QoL issue where `GhostAuthoringInspectionComponent` was not always modifiable in areas of the Editor where it is valid to modify them.
* Issue where `GhostAuthoringComponent` was disallowed in nested prefab setups (where the root prefab is NOT a ghost).
* Log verbiage when creating a driver in DefaultDriverConstructor read like a 'call to action'. It's not.


## [1.2.0-exp.3] - 2023-11-09

### Added
Expand Down
1 change: 0 additions & 1 deletion Documentation~/networked-cube.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,6 @@ using Unity.Entities;
using Unity.NetCode;
using UnityEngine;

[GhostComponent(PrefabType=GhostPrefabType.AllPredicted)]
public struct CubeInput : IInputComponentData
{
public int Horizontal;
Expand Down
32 changes: 25 additions & 7 deletions Editor/Authoring/BakedNetCodeComponents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,36 @@

namespace Unity.NetCode.Editor
{
/// <summary>Internal class used by the GhostComponentInspector to store post-conversion (i.e. Baked) data.</summary>
struct BakedGameObjectResult
/// <summary>Internal structs used by the GhostComponentInspector to store post-conversion (i.e. Baked) data.</summary>
class BakedResult
{
public Dictionary<GameObject, BakedGameObjectResult> GameObjectResults;
public GhostAuthoringComponent GhostAuthoring;

public BakedGameObjectResult GetInspectionResult(GhostAuthoringInspectionComponent inspection)
{
foreach (var kvp in GameObjectResults)
{
if (kvp.Value.SourceInspection == inspection)
return kvp.Value;
}
return null;
}
}

class BakedGameObjectResult
{
public BakedResult AuthoringRoot;
public GameObject SourceGameObject;
[CanBeNull] public GhostAuthoringInspectionComponent SourceInspection => SourceGameObject.GetComponent<GhostAuthoringInspectionComponent>();
public GhostAuthoringComponent RootAuthoring;
[CanBeNull] public GhostAuthoringInspectionComponent SourceInspection;
public GhostAuthoringComponent RootAuthoring => AuthoringRoot.GhostAuthoring;
public string SourcePrefabPath;
public List<BakedEntityResult> BakedEntities;
public int NumComponents;
}

/// <inheritdoc cref="BakedGameObjectResult"/>
struct BakedEntityResult
class BakedEntityResult
{
public BakedGameObjectResult GoParent;
public Entity Entity;
Expand All @@ -29,7 +47,7 @@ struct BakedEntityResult
public bool IsPrimaryEntity => EntityIndex == 0;
public List<BakedComponentItem> BakedComponents;
public bool IsLinkedEntity;
public bool IsRoot;
public bool IsRoot => !IsLinkedEntity && GoParent.SourceGameObject == GoParent.RootAuthoring.gameObject && IsPrimaryEntity;
}

/// <inheritdoc cref="BakedGameObjectResult"/>
Expand Down Expand Up @@ -100,7 +118,7 @@ public ref GhostAuthoringInspectionComponent.ComponentOverride GetPrefabOverride
{
if (EntityParent.GoParent.SourceInspection.TryFindExistingOverrideIndex(managedType, entityGuid, out var index))
return ref EntityParent.GoParent.SourceInspection.ComponentOverrides[index];
throw new InvalidOperationException("No override declared.");
throw new InvalidOperationException($"No override created for '{fullname}'! '{serializationStrategy.ToFixedString()}', EntityGuid: {entityGuid.ToString()}!");
}

/// <summary>Returns true if this Inspection Component has a prefab override for this Baked Component Type.</summary>
Expand Down
145 changes: 98 additions & 47 deletions Editor/Authoring/EntityPrefabComponentsPreview.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Unity.Collections;
using Unity.Collections.NotBurstCompatible;
using Unity.Entities;
using Unity.Entities.Conversion;
using UnityEditor;
using UnityEngine;

Expand All @@ -20,113 +18,166 @@ class EntityPrefabComponentsPreview
struct ComponentNameComparer : IComparer<ComponentType>
{
public int Compare(ComponentType x, ComponentType y) =>
x.GetManagedType().FullName.CompareTo(y.GetManagedType().FullName);
string.Compare(x.GetManagedType().FullName, y.GetManagedType().FullName, StringComparison.Ordinal);
}

/// <summary>Triggers the baking conversion process on the 'authoringComponent' and appends all resulting baked entities and components to the 'bakedDataMap'.</summary>
public void BakeEntireNetcodePrefab(GhostAuthoringComponent authoringComponent, Dictionary<GameObject, BakedGameObjectResult> bakedDataMap)
public void BakeEntireNetcodePrefab(GhostAuthoringComponent ghostAuthoring, GhostAuthoringInspectionComponent inspectionComponent, Dictionary<GhostAuthoringInspectionComponent, BakedResult> cachedBakedResults)
{
GhostAuthoringInspectionComponent.forceBake = false;
if (ghostAuthoring == null)
{
Debug.LogError($"Attempting to bake `GhostAuthoringInspectionComponent` '{inspectionComponent.name}', but no root `GhostAuthoringComponent` found!");
return;
}

try
{
EditorUtility.DisplayProgressBar($"Baking '{authoringComponent}'...", "Baking triggered by the GhostAuthoringInspectionComponent.", .9f);
GhostAuthoringInspectionComponent.forceBake = false;
GhostAuthoringInspectionComponent.forceSave = true;
EditorUtility.DisplayProgressBar($"Baking '{ghostAuthoring}'...", "Baking triggered by the GhostAuthoringInspectionComponent.", .9f);

// TODO - Handle exceptions due to invalid prefab setup. E.g.
// "InvalidOperationException: OwnerPrediction mode can only be used on prefabs which have a GhostOwner"
using(var world = new World(nameof(EntityPrefabComponentsPreview)))
using var world = new World(nameof(EntityPrefabComponentsPreview));
using var blobAssetStore = new BlobAssetStore(128);
ghostAuthoring.ForcePrefabConversion = true;

var bakeResult = new BakedResult
{
using var blobAssetStore = new BlobAssetStore(128);
authoringComponent.ForcePrefabConversion = true;
GhostAuthoring = ghostAuthoring,
GameObjectResults = new (32),
};

var bakingSettings = new BakingSettings(BakingUtility.BakingFlags.AddEntityGUID, blobAssetStore);
BakingUtility.BakeGameObjects(world, new[] {authoringComponent.gameObject}, bakingSettings);
var bakingSystem = world.GetExistingSystemManaged<BakingSystem>();
var primaryEntitiesMap = new HashSet<Entity>(16);
var bakingSettings = new BakingSettings(BakingUtility.BakingFlags.AddEntityGUID, blobAssetStore);
BakingUtility.BakeGameObjects(world, new[] {ghostAuthoring.gameObject}, bakingSettings);
var bakingSystem = world.GetExistingSystemManaged<BakingSystem>();
var primaryEntitiesMap = new HashSet<Entity>(16);

var primaryEntity = bakingSystem.GetEntity(authoringComponent.gameObject);
var ghostBlobAsset = world.EntityManager.GetComponentData<GhostPrefabMetaData>(primaryEntity).Value;
var primaryEntity = bakingSystem.GetEntity(ghostAuthoring.gameObject);
var ghostBlobAsset = world.EntityManager.GetComponentData<GhostPrefabMetaData>(primaryEntity).Value;

CreatedBakedResultForPrimaryEntities(world, bakedDataMap, authoringComponent, bakingSystem, primaryEntitiesMap, ghostBlobAsset);
CreatedBakedResultForLinkedEntities(world, bakedDataMap, primaryEntitiesMap, ghostBlobAsset);
}
CreatedBakedResultForPrimaryEntities(bakeResult, world, bakingSystem, primaryEntitiesMap, ghostBlobAsset, cachedBakedResults);
CreatedBakedResultForAdditionalEntities(bakeResult, world, primaryEntitiesMap, ghostBlobAsset, bakingSystem);
}
finally
{
EditorUtility.ClearProgressBar();
authoringComponent.ForcePrefabConversion = false;
GhostAuthoringInspectionComponent.forceRebuildInspector = true;
ghostAuthoring.ForcePrefabConversion = false;
}
}

void CreatedBakedResultForPrimaryEntities(World world, Dictionary<GameObject, BakedGameObjectResult> bakedDataMap, GhostAuthoringComponent authoringComponent, BakingSystem bakingSystem, HashSet<Entity> primaryEntitiesMap, BlobAssetReference<GhostPrefabBlobMetaData> blobAssetReference)

internal static int CountComponents(GameObject go)
{
return go.GetComponents<Component>().Length;
}

static void CreatedBakedResultForPrimaryEntities(BakedResult bakedResult, World world, BakingSystem bakingSystem, HashSet<Entity> primaryEntitiesMap, BlobAssetReference<GhostPrefabBlobMetaData> blobAssetReference, Dictionary<GhostAuthoringInspectionComponent, BakedResult> cachedBakedResults)
{
foreach (var t in authoringComponent.GetComponentsInChildren<Transform>())
foreach (var t in bakedResult.GhostAuthoring.GetComponentsInChildren<Transform>())
{
var go = t.gameObject;

// I'd like to skip children that DONT have an Inspection component, but not possible as they may add one.

var sourcePrefabPath = AssetDatabase.GetAssetPath(go);
var result = new BakedGameObjectResult
var goResult = new BakedGameObjectResult
{
AuthoringRoot = bakedResult,
SourceGameObject = go,
SourceInspection = go.GetComponent<GhostAuthoringInspectionComponent>(),
SourcePrefabPath = sourcePrefabPath,
RootAuthoring = authoringComponent,
BakedEntities = new List<BakedEntityResult>(1)
BakedEntities = new List<BakedEntityResult>(2),
NumComponents = CountComponents(go),
};
var discoveredInspectionComponent = goResult.SourceInspection;
if (discoveredInspectionComponent != null)
cachedBakedResults[discoveredInspectionComponent] = bakedResult;

var primaryEntity = bakingSystem.GetEntity(go);
if (bakingSystem.EntityManager.Exists(primaryEntity))
{
result.BakedEntities.Add(CreateBakedEntityResult(result, 0, world, primaryEntity, false, blobAssetReference));
goResult.BakedEntities.Add(CreateBakedEntityResult(goResult, 0, world, bakingSystem, primaryEntity, false, blobAssetReference));
primaryEntitiesMap.Add(primaryEntity);
}
bakedDataMap[go] = result;
bakedResult.GameObjectResults[go] = goResult;
}
}

void CreatedBakedResultForLinkedEntities(World world, Dictionary<GameObject, BakedGameObjectResult> bakedDataMap, HashSet<Entity> primaryEntitiesMap, BlobAssetReference<GhostPrefabBlobMetaData> blobAssetReference)
static void CreatedBakedResultForAdditionalEntities(BakedResult bakedResult, World world, HashSet<Entity> primaryEntitiesMap, BlobAssetReference<GhostPrefabBlobMetaData> blobAssetReference, BakingSystem bakingSystem)
{
foreach (var kvp in bakedDataMap)
// Note: We only expect the ROOT entity to have a LinkedEntityGroup,
// but checking EVERY baked GameObject as this is not an assumption we control.
foreach (var kvp in bakedResult.GameObjectResults)
{
// TODO - Test-case to ensure the root entity does not contain ALL linked entities (even for children + additional).
for (int index = 0, max = kvp.Value.BakedEntities.Count; index < max; index++)
{
var bakedEntityResult = kvp.Value.BakedEntities[index];
var primaryEntity = bakedEntityResult.Entity;
if (world.EntityManager.HasComponent<LinkedEntityGroup>(primaryEntity))
if (!world.EntityManager.HasComponent<LinkedEntityGroup>(primaryEntity))
continue;

var linkedEntityGroup = world.EntityManager.GetBuffer<LinkedEntityGroup>(primaryEntity);
for (int i = 1; i < linkedEntityGroup.Length; ++i)
{
var linkedEntityGroup = world.EntityManager.GetBuffer<LinkedEntityGroup>(primaryEntity);
for (int i = 1; i < linkedEntityGroup.Length; ++i)
var linkedEntity = linkedEntityGroup[i].Value;

// Child entities are considered 'primary' entities. Thus, ignore them.
// I.e. During Baking, if users call `CreateAdditionalEntity`, it won't be 'primary'.
if (primaryEntitiesMap.Contains(linkedEntity))
continue;

// Find the actual authoring GameObject for this linked entity. It might be one of our children.
var foundActualAuthoring = TryGetAuthoringForAdditionalEntity(linkedEntity, bakingSystem, bakedResult.GameObjectResults.Values, out var actualAuthoring);
if (!foundActualAuthoring)
{
var linkedEntity = linkedEntityGroup[i].Value;

// Only show linked entities if they're not primary entities of child GameObjects.
// I.e. Only possible if, during Baking, users call `CreateAdditionalEntity`.
if (!primaryEntitiesMap.Contains(linkedEntity))
{
kvp.Value.BakedEntities.Add(CreateBakedEntityResult(kvp.Value, i, world, linkedEntity, true, blobAssetReference));
}
Debug.LogWarning($"Expected to find the source BakedGameObjectResult for Additional Entity '{linkedEntity.ToFixedString()}' ('{bakingSystem.EntityManager.GetName(linkedEntity)}') (via EntityGuid search), but did not! Assuming the authoring GameObject is '{kvp.Value.SourceGameObject.name}'! Please file a bug report if this assumption is false.", kvp.Value.SourceGameObject);

actualAuthoring = kvp.Value;
}
var entityResult = CreateBakedEntityResult(actualAuthoring, i, world, bakingSystem, linkedEntity, true, blobAssetReference);
actualAuthoring.BakedEntities.Add(entityResult);
}
}
}
}

static bool TryGetAuthoringForAdditionalEntity(Entity additionalEntity, BakingSystem bakingSystem, Dictionary<GameObject, BakedGameObjectResult>.ValueCollection results, out BakedGameObjectResult found)
{
found = default;
if (!bakingSystem.EntityManager.HasComponent<EntityGuid>(additionalEntity))
{
Debug.LogError($"Additional entity '{additionalEntity.ToFixedString()}' did not have an EntityGuid! Thus, cannot find Authoring for it!");
return false;
}
var additionalEntitiesEntityGuid = bakingSystem.EntityManager.GetComponentData<EntityGuid>(additionalEntity);

foreach (var result in results)
{
foreach (var x in result.BakedEntities)
{
if (x.Guid.OriginatingId == additionalEntitiesEntityGuid.OriginatingId)
{
found = result;
return true;
}
}
}

return false;
}

BakedEntityResult CreateBakedEntityResult(BakedGameObjectResult parent, int entityIndex, World world, Entity convertedEntity, bool isLinkedEntity, BlobAssetReference<GhostPrefabBlobMetaData> blobAssetReference)
static BakedEntityResult CreateBakedEntityResult(BakedGameObjectResult authoring, int entityIndex, World world, BakingSystem bakingSystem, Entity convertedEntity, bool isLinkedEntity, BlobAssetReference<GhostPrefabBlobMetaData> blobAssetReference)
{
var isRoot = parent.SourceGameObject == parent.RootAuthoring.gameObject;
var guid = world.EntityManager.GetComponentData<EntityGuid>(convertedEntity);
var result = new BakedEntityResult
{
GoParent = parent,
GoParent = authoring,
Entity = convertedEntity,
Guid = guid,
EntityName = world.EntityManager.GetName(convertedEntity),
EntityIndex = entityIndex,
BakedComponents = new List<BakedComponentItem>(16),
IsLinkedEntity = isLinkedEntity,
IsRoot = isRoot,
};

using var query = world.EntityManager.CreateEntityQuery(ComponentType.ReadOnly<GhostComponentSerializerCollectionData>());
Expand All @@ -144,7 +195,7 @@ BakedEntityResult CreateBakedEntityResult(BakedGameObjectResult parent, int enti
{
variantTypesList.Add(compItem.availableSerializationStrategies[i]);
}
compItem.serializationStrategy = collectionData.SelectSerializationStrategyForComponentWithHash(ComponentType.ReadWrite(compItem.managedType), searchHash, variantTypesList, isRoot);
compItem.serializationStrategy = collectionData.SelectSerializationStrategyForComponentWithHash(ComponentType.ReadWrite(compItem.managedType), searchHash, variantTypesList, result.IsRoot);
compItem.sendToOwnerType = compItem.serializationStrategy.IsSerialized != 0 ? collectionData.Serializers[compItem.serializationStrategy.SerializerIndex].SendToOwner : SendToOwnerType.None;

if (compItem.anyVariantIsSerialized)
Expand Down
Loading

0 comments on commit b63a410

Please sign in to comment.