Remove spriteRenderer from Ghost Snapshot

Hello,

I’ve been getting started with the netcode package and have run into a problem almost identical to the one described in this forum post . I am using the new 0.50 ecs packages, specifically netcode 0.50-preview.29.

In the picture there are actually two green and two white entities. The white cylinder offset from the center of the screen is the player (in ClientWorld0), and the one in the center follows this object (in ServerWorld). The green entities are duplicates with one in ClientWorld0 and one in ServerWorld.

7979919--1024419--upload_2022-3-20_15-17-8.png

The Circle object is the green entity. When I manually change green entity values, they do not affect the corresponding entity but when I manually change white entity values they affect the other one.

I tried clicking the button to remove the spriteRenderer component:

7979919--1024425--upload_2022-3-20_15-26-5.png

but it doesn’t stop the ServerWorld player from rendering. It works for removing the translation for some reason though??

I thought the white entity in the ServerWorld was a ghost snapshot because it follows the ClientWorld0 entity (with a delay time related to send/recieve delay) but now I’m doubting myself. If someone could explain what exactly is going on that would be very appreciated. Particularly, I would like the player to only see its predicted white entity and also not a duplicated green entity (I think this means presenting only the client world by somehow removing rendering capabilities for the ServerWorld entirely?). Thanks in advance!

I don’t know what the correct approach to this is and I haven’t looked if there was a better way to handle it in 0.50 but I’ve got a system that strips certain Components and companion objects out of the server.

At some point I’ll investigate this again to see if there’s an alternative but been working for me.
Currently I’m stripping these 2 components

But you can add any number of strippers that you like.

// <copyright file="ServerStripSystem.cs" company="BovineLabs">
//     Copyright (c) BovineLabs. All rights reserved.
// </copyright>

namespace BovineLabs.Game.App
{
    using System.Collections.Generic;
    using BovineLabs.Core.Internal;
    using Unity.Collections;
    using Unity.Entities;
    using Unity.NetCode;
    using UnityEngine;
    using UnityEngine.Rendering.HighDefinition;

    /// <summary> System that strips managed components and their companion links from the server that are not required. </summary>
    [UpdateInWorld(TargetWorld.Server)]
    [UpdateInGroup(typeof(InitializationSystemGroup))]
    public partial class ServerStripSystem : SystemBase
    {
        private readonly List<IStripper> strippers = new List<IStripper>();
        private readonly HashSet<GameObject> removeGameObjects = new HashSet<GameObject>();
        private readonly HashSet<Entity> removeCompanionLinks = new HashSet<Entity>();

        private interface IStripper
        {
            /// <summary> Destroys all components objects then remove the component from the entity. </summary>
            /// <param name="removedGameObjects"> GameObjects to destroy. </param>
            /// <param name="removeCompanionLinks"> Companion links to remove. </param>
            void Strip(HashSet<GameObject> removedGameObjects, HashSet<Entity> removeCompanionLinks);
        }

        /// <inheritdoc/>
        protected override void OnCreate()
        {
            this.AddStripper<HDAdditionalLightData>(); // Must be before light due to dependencies
            this.AddStripper<Light>();
        }

        /// <inheritdoc/>
        protected override void OnUpdate()
        {
            foreach (var stripper in this.strippers)
            {
                stripper.Strip(this.removeGameObjects, this.removeCompanionLinks);
            }

            foreach (var go in this.removeGameObjects)
            {
                Object.Destroy(go);
            }

            foreach (var entity in this.removeCompanionLinks)
            {
                this.EntityManager.RemoveComponent(entity, TypesInternal.CompanionLink);
            }

            this.removeGameObjects.Clear();
            this.removeCompanionLinks.Clear();
        }

        private void AddStripper<T>()
            where T : Component
        {
            this.strippers.Add(new Stripper<T>(this));
        }

        private class Stripper<T> : IStripper
            where T : Component
        {
            private EntityQuery query;
            private EntityManager entityManager;

            public Stripper(ComponentSystemBase systemBase)
            {
                this.query = systemBase.GetEntityQuery(typeof(T));
                this.entityManager = systemBase.EntityManager;
            }

            public void Strip(HashSet<GameObject> removedGameObjects, HashSet<Entity> removeCompanionLinks)
            {
                if (this.query.IsEmpty)
                {
                    return;
                }

                this.query.CompleteDependency();
                var entities = this.query.ToEntityArray(Allocator.Temp);

                foreach (var entity in entities)
                {
                    var component = this.entityManager.GetComponentObject<T>(entity);
                    removeCompanionLinks.Add(entity);
                    removedGameObjects.Add(component.transform.root.gameObject);
                }

                this.entityManager.RemoveComponent<T>(this.query);
            }
        }
    }
}

It also requires 2 extension methods that you’ll also need to give yourself internal access to the entity package to use.

// <copyright file="TypesInternal.cs" company="BovineLabs">
//     Copyright (c) BovineLabs. All rights reserved.
// </copyright>

#if !UNITY_DISABLE_MANAGED_COMPONENTS && !DOTS_HYBRID_COMPONENTS_DEBUG
namespace BovineLabs.Core.Internal
{
    using Unity.Entities;

    public static class TypesInternal
    {
        public static ComponentType CompanionLink => typeof(Unity.Entities.CompanionLink);
    }
}
#endif
// <copyright file="ComponentSystemBaseInternals.cs" company="BovineLabs">
//     Copyright (c) BovineLabs. All rights reserved.
// </copyright>

namespace BovineLabs.Core.Internal
{
    using Unity.Entities;

    public static class ComponentSystemBaseInternals
    {
        public static EntityQuery GetEntityQuery(this ComponentSystemBase system, params ComponentType[] componentTypes)
        {
            return system.GetEntityQuery(componentTypes);
        }
    }
}

(I find my naming amusing)

1 Like

There are different way to handle that in 0.5:
1 - On the prefab, setting up the PrefabType (like you did). And that should add the component to the removeFromServer list in the ghost prefab metadata in the GhostConversionSystem.
2 - Setup a default variant for that component that use ClientOnlyVariant. This make the component by default client-only
3 - Create a renderer variant that is configured as client only like :

[GhostComponentVariation(typeof(SpriteRenderer))]
[GhostComponet(PrefabType=Clients)]
public class MySpriteRenderer
{
}

And assign the variant in the prefab inspector to the component.
4 - Prevent the component to be present on the server during conversion (that is usually the best approach). That requires to setup the the build setting by adding a ConversionSystemFilterSettings and add to the list the hybrid renderers (and other as necessary) dll. This is also requires to have in you game a system (or a custom boostrap, up to you) that set the desired SceneSystem.BuildConfigurationGUID, so that when conversion happen in the editor, the right configuration is used.

A mix and match of the 1/2/3 and 4 usually provides what you need.
Also, please check that you are seeing the runtime state and not the authoring state in by checking the Dots/LiveConversion menu setting.

By doing as you did, modifying the prefab, should add the SpriteRenderer to the list of component that should be removed. You can double check that in the GhostConversionSystem, where the prefab metadata is constructed, by inspecting the RemoveOnServer property of the GhostPrefabMetaData.
You may check that as well in the GhostCollectionSystem

...
                    for (int rm = 0; rm < ghostMetaData.RemoveOnServer.Length; ++rm)
                    {
                        var indexHashPair = ghostMetaData.RemoveOnServer[rm];
                        var compType = ComponentType.ReadWrite(TypeManager.GetTypeIndexFromStableTypeHash(indexHashPair.StableHash));
#if ENABLE_UNITY_COLLECTIONS_CHECKS
                        if (indexHashPair.EntityIndex >= entities.Length)
                            throw new InvalidOperationException($"Cannot remove server component from child entity {indexHashPair.EntityIndex} for ghost {ghostMetaData.Name.ToString()}. Child index out of bound");
#endif
                        var ent = entities[indexHashPair.EntityIndex].Value;
                        if (EntityManager.HasComponent(ent, compType))
                            EntityManager.RemoveComponent(ent, compType);
                    }
1 Like

This is exactly what I was looking for!! I forgot to mention in my original post that I was looking through the multiplayer samples and couldn’t figure out why it was working and mine wasn’t; this was it. Thank you so much!

For anyone else with the same problem: Take a look at the bootstrap folder of that sample. Also, if you make your own BuildConfiguration the GUID is in its metadata which you need to go into your file system to find.

I can’t tell you how much of a relief this is :slight_smile:

So I implemented the fix here in my bootstrap:

using System;
using UnityEditor;
using Unity.Scenes;
using Unity.Entities;
using Unity.NetCode;
using hash128 = Unity.Entities.Hash128;

[UnityEngine.Scripting.Preserve]
public class GameBootstrap : ClientServerBootstrap
{
    private hash128 serverBuildGUID => new hash128("2985b62464532d8488dc96e190982876");
    public override bool Initialize(string defaultWorldName)
    {
        AutoConnectPort = 7979; // Enabled auto connect
        World defaultWorld = CreateDefaultWorld(defaultWorldName);
        World hostClientWorld = CreateClientWorld(defaultWorld, "HostClientWorld");
        World ServerWorld = CreateServerWorld(defaultWorld, "ServerWorld");
        ServerWorld.GetOrCreateSystem<SceneSystem>().BuildConfigurationGUID = serverBuildGUID;

        return true;
    }
}

This fixed the server rendering privileges in the unity editor but doesn’t fix the problem in an actual build of the game.

Here is the serverBuild:
7985937--1025808--upload_2022-3-22_16-24-28.png

Here is the build config I am using to build the game:
7985937--1025811--upload_2022-3-22_16-25-4.png

I’ve tried to match some things to what I have seen used in the samples I referenced earlier, but none of it helped so I went back to the simplest stuff that works. Am I doing something wrong here? Any idea what I need to do to fix this?

Edit: Turns out the fix for this problem is less of a fix and more of a do the correct thing with ECS. SpriteRenderer seems to not be very well supported, therefore switching to mesh renderer and just using quad mesh filter with it is important. Entities will stop rendering entirely if you are not using the universal render pipeline and a scripting define in settings->player->scripting defines of ENABLE_HYBRID_RENDERER_V2. This bug should just be fixed, its now working for me :slight_smile: .