#Drawing World Space Healthbars with ECS/DOTS

1 messages · Page 1 of 1 (latest)

celest stag
#

I've tried to ask about this in the DOTS General Discussion but I think it might be better to have a dedicated topic for this as I'm sure it's a common issue that many games that use this technology will have to tackle eventually.

I hope it's okay that I ping you @silver citrus as I was told you have some experience tackling this sort of issue! Basically, as I've been shown here: #1064581837055348857 message I need to do basically the same thing. But I have no idea how to do that as I have no experience with drawing world space graphics using Unity's Graphics class.

Essentially I want to just make a simple bar above every monster in my game that represents Health. I tried a very naive approach at first, to just get something on screen but it's not working. See below:

#
public partial struct DrawHealthbarSystem : ISystem
{
    [BurstCompile]
    public readonly void OnCreate(ref SystemState state)
    {
        state.RequireForUpdate<MaterialConfig>();
        state.RequireForUpdate<Health>();
    }

    public void OnUpdate(ref SystemState state)
    {
        var materialConfig = SystemAPI.GetSingleton<MaterialConfig>();

        foreach (var (health, transform) in
            SystemAPI.Query<Health, LocalTransform>().WithAbsent<Destroy>())
        {
            Rect screenRect = new(new(transform.Position.x, transform.Position.z), Vector2.one);
            Texture texture = new Texture2D(1, 1);
            Rect sourceRect = new();
            Material matInstance = new(materialConfig.HealthbarMaterial.Value);
            matInstance.SetFloat("_Fill", math.clamp(health.Value / health.MaxValue, 0f, 1f));
            Graphics.DrawTexture(
                screenRect, texture, sourceRect,
                leftBorder: 0, rightBorder: 0, topBorder: 0, bottomBorder: 0,
                mat: matInstance);
        }
    }
}
silver citrus
#

Ok. As mentioned in linked post there is 2 solutions I would suggest. Based on your experience and laziness.
Firs one and it's simplest one. Instead of material have a prefab entity with simple quad mesh (even default one will be fine), and have this entity as child to your objects that should show health bar (as mentioned earlier transform hierarchy cost) or have them in world space and just have "reference" to their owner entity (unit for example) and just "follow" them (cost of this additional system but a bit less than transform system hierarchy cost). In this case what you need is simple component for controlling your health bar:

[MaterialProperty("_HealthBarData"), Serializable]
public struct HealthBarData: IComponentData
{
   /// This can be float float2 float3 float4, depends on usecase
   public float4 Value;
}

And in these 4 floats you can encode everything - current health (mostly percentage if you don't want to spent 2 values for current and max health), second float you can use for some effect (like if it is > 0, health bar should pulsate and flash some color, for example when damage taken you set cooldown to this float and reduce it for some seconds and all this time health bar flashing), etc. In simple case this component will be just one float with health percentage.
Then you should have your billboard shader where you draw your quad as actual health bar rectangle faced to the camera. And then you just access _HealthBarData in your code, it can be vertex function or in fragment.

// The rest of your code, helper functions and pass setup

CBUFFER_START(UnityPerMaterial)
// Rest of material properties
// ...
float4 _HealthBarData;
CBUFFER_END

#ifdef UNITY_DOTS_INSTANCING_ENABLED
    UNITY_DOTS_INSTANCING_START(MaterialPropertyMetadata)
        UNITY_DOTS_INSTANCED_PROP(float4, _HealthBarData)
    UNITY_DOTS_INSTANCING_END(MaterialPropertyMetadata)

    #define _Prop    UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(float4 , _HealthBarData)
#endif

// The rest of your code, vertex function etc.

half4 Fragment(FragmentInput input) : SV_TARGET
{
    half4 backColor = SAMPLE_TEXTURE2D_X(_BackTex, sampler_BackTex, uv) * _BackColor;
    half4 fillColor = SAMPLE_TEXTURE2D_X(_FillTex, sampler_FillTex, uv) * _FillColor;
    // Your quad UV usually is 0-1 range as result it can represent fill percent by X 
    // in simple case. We just read _HealthBarData.x (our first float as I mentioned 
    // above where we put health percentage)
    half4 result = IN.uv.x < _HealthBarData.x ? fillColor : backColor; // This simple bnranching is ok
    return result;
}

Second solution is a bit harder. You have system which gather entities health bar data, then you have ComputeBuffer/GraphicsBuffer which populated by data from entities, in the end this is similar to first approach where you populate health percent value, but in addition you need to populate world space position for your health bar and in the end call Graphics.DrawProcedural (Graphics.RenderPrimitives in latest Unity versions). In shader you do literally the same as in first step, except you need to use StructuredBuffer/ByteAddressBuffer for accessing your data populated on CPU side. And in vertex shader you should reconstruct your quad based on SV_VertexId semantic. (If you don't want to procedurally generate mesh in vertex function you can use Graphics.RenderMeshInstanced but that's has 1023 instances per call limitation and will introduce more useless code.)

celest stag
#

Thanks for your insights @silver citrus .
I'll give it a shot and come back with questions most likely.

celest stag
#

I found a shader online that doesn't really work for ECS. I'm not really good with shaders so I had thought I could use an existing one for this. But alas I get an error message about buffer usage since the shader was likely not written specifically for ECS usage.

#
Shader "WorldSpaceUI/HealthBar" {
    Properties {
        _MainTex ("Texture", 2D) = "white" {}
        _Fill ("Fill", float) = 0
    }
    SubShader {
        Tags { "Queue"="Overlay" }
        LOD 100

        Pass {
            ZTest Off

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #pragma multi_compile_instancing


            #include "UnityCG.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                // If you need instance data in the fragment shader, uncomment next line
                //UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            UNITY_INSTANCING_BUFFER_START(Props)
            UNITY_DEFINE_INSTANCED_PROP(float, _Fill)
            UNITY_INSTANCING_BUFFER_END(Props)

            v2f vert (appdata v) {
                v2f o;
                UNITY_SETUP_INSTANCE_ID(v);
                // If you need instance data in the fragment shader, uncomment next line
                //UNITY_TRANSFER_INSTANCE_ID(v, o);

                float fill = UNITY_ACCESS_INSTANCED_PROP(Props, _Fill);

                o.vertex = UnityObjectToClipPos(v.vertex);

                // generate UVs from fill level (assumed texture is clamped)
                o.uv = v.uv;
                o.uv.x += 0.5 - fill;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                return tex2D(_MainTex, i.uv);
            }
            ENDCG
        }
    }
}
#

A BatchDrawCommand is using a pass from the shader "WorldSpaceUI/HealthBar" that is not SRP Batcher compatible. Reason: "Material property is found in another cbuffer than "UnityPerMaterial"" (_Fill)
This is not supported when rendering with a BatchRendererGroup (or Entities Graphics). MaterialID: 8 ("HealthBar"), MeshID: 6 ("Quad"), BatchID: 2.

#

I understand that the Shader gods are upset, but I do not know how to have them answer my prayer easily.
Would I have to discard this shader and build a different one from scratch to get what I want?

#

Doing this made it stop complaining about buffers:

// UNITY_INSTANCING_BUFFER_START(Props)
// UNITY_DEFINE_INSTANCED_PROP(float, _Fill)
// UNITY_INSTANCING_BUFFER_END(Props)

CBUFFER_START(UnityPerMaterial)
float _Fill;
CBUFFER_END

Then I just got another error about DOTS_INSTANCING_ON though.

#

A BatchDrawCommand is using the pass "<Unnamed Pass 0>" from the shader "WorldSpaceUI/HealthBar" which does not define a DOTS_INSTANCING_ON variant.
This is not supported when rendering with a BatchRendererGroup (or Entities Graphics). MaterialID: 8 ("HealthBar"), MeshID: 6 ("Quad"), BatchID: 2.

silver citrus
celest stag
#
Shader "WorldSpaceUI/HealthBar" {
    Properties {
        _MainTex ("Texture", 2D) = "white" {}
        _Fill ("Fill", float) = 0.5
    }
    SubShader {
        Tags 
        { 
            "RenderPipeline"="UniversalRenderPipeline" "Queue"="Geometry"
        }
        LOD 100

        Pass {
            Name "ECS/DOTS"
            ZTest Off

            HLSLPROGRAM
            #pragma exclude_renderers gles gles3 glcore
            #pragma target 4.5
            #pragma vertex UnlitPassVertex
            #pragma fragment UnlitPassFragment
            #pragma multi_compile_instancing
            #pragma instancing_options renderinglayer
            #pragma multi_compile _ DOTS_INSTANCING_ON
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            #include "UnityCG.cginc"

            struct appdata {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f {
                float2 uv : TEXCOORD0;
                float4 vertex : SV_POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            CBUFFER_START(UnityPerMaterial)
            float _Fill;
            CBUFFER_END

            #if defined(UNITY_DOTS_INSTANCING_ENABLED)
                UNITY_DOTS_INSTANCING_START(MaterialPropertyMetadata)
                    UNITY_DOTS_INSTANCED_PROP(float, _Fill)
                UNITY_DOTS_INSTANCING_END(MaterialPropertyMetadata)
            #endif
#
            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v) {
                v2f o;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_TRANSFER_INSTANCE_ID(v, o);

                float fill = UNITY_ACCESS_INSTANCED_PROP(Props, _Fill);

                o.vertex = UnityObjectToClipPos(v.vertex);

                // generate UVs from fill level (assumed texture is clamped)
                o.uv = v.uv;
                o.uv.x += 0.5 - fill;
                return o;
            }

            fixed4 frag (v2f i) : SV_Target {
                return tex2D(_MainTex, i.uv);
            }
            ENDHLSL
        }
    }
}
silver citrus
celest stag
#

If it has any

silver citrus
#

Of course it has, I suggest you to start from learning how to write basic unlit hlsl based shaders, not randomly picking pieces of code here and there harold

#

Otherwise it will be not so easy to make such things which used often in dots based project if you want performance.

#

Because input I gave suppose you know how to write custom shaders for SRP

celest stag
#

I don't have unlimited time to sit down and learn shaders

#

Not that I haven't tried, mind you.

silver citrus
#

Probably you can start with shader graph

#

It will provide you basics for this exact case, where you can check generated code and see how to do things

celest stag
#

How do you tell Shadergraph your shader should be DOTS?

silver citrus
#

It’s not about shader is about properties.

celest stag
#

Project Properties?

#

Or Shader Properties?

celest stag
#

@silver citrus I fixed the existing shader

#

Now I just need to populate the data correctly and make the quad face the camera

silver citrus
#

Yep, good for you

celest stag
#

I appreciate sending me links on primers for shaders

#

It's just that I don't necessarily have the kind of time needed to start from scratch on those

#

I really wish I was better at shaders

#

They are just incomprehensibly complex to me still

silver citrus
#

Well making games requires wide range of knowing thingsharold and shaders is (imo) at the same level of requirement as general programming. In the end you don’t need to know every detail of graphic pipeline (well it preferable of course), but learn how to do it specifically in Unity for specific pipeline and specifically for dots - will help you a lot in the end.

celest stag
#

Taking a visual idea and expressing it purely in math for your shaders is an artform. It's not something every programmer can or should be doing.

silver citrus
# celest stag I disagree that Shaders fall under the same level of knowledge as general progra...

Which is often mixed in one personharold Is your team have guy for editor tools only, guy for navigation logic only, guy for physics only, guy for ui behaviour only, guy for programming inverse kinematic types of animated movements only, guy for audio behaviour programming only? Or it’s mixed across gameplay programmers? That’s the same as tech artist - completely different specialisations in every engineKEKLEO LurkMode

celest stag
silver citrus
# celest stag You can specialize in all of those if you wish to, however a Gameplay programmer...

Well thats incorrect. Tech artist not focus only on math equations in shaders, he is writing gameplay systems very often to interacting with graphics side, in 95% of cases do VFX too not only in code but in thighs like VFX Graph. They woking in 3d modelling soft, they animate things and many other things. And this list can continue. The same as gameplay programmer should know basics and write simple shaders and systems for communicating with GPU side, and tech artist will look at them later for better optimise themnot_bad_huiyce

celest stag
#

That's why it's a Technical Artist, and not a "General Programmer".

#

The "General Programmer" also reminds me of the "Fullstack Developer", the way you describe it. I don't really believe in that concept seeing as a "Fullstack Developer" can mean many things to many people.

#

Thanks for your help with this so far though.

celest stag
#

Do I have to make a system that manually updates every healthbar in the scene instead?

civic basin
#

Are you sure your shader is compatible with MaterialProperty?

celest stag
#

How would I check?

civic basin
#

I've really only used MaterialProperty with shader graphs, I'm not sure in your case

#

In shader graph you declare the variable as Hybrid Per Instance

celest stag
celest stag
#

I'm assuming it's a #pragma statement or something

civic basin
#

I'm not sure, the only reference is this:

Shader "Shader Graphs/New Shader Graph" {
Properties {
 _HealthbarData ("HealthbarData", Float) = 0.000000
[HideInInspector]  _CastShadows ("_CastShadows", Float) = 1.000000
[HideInInspector]  _Surface ("_Surface", Float) = 0.000000
...
}
#

Oh, I found some more

#

I don't know how to read this myself, but

#
// Graph Properties
        CBUFFER_START(UnityPerMaterial)
        float _HealthbarData;
        CBUFFER_END
        
        #if defined(DOTS_INSTANCING_ON)
        // DOTS instancing definitions
        UNITY_DOTS_INSTANCING_START(MaterialPropertyMetadata)
            UNITY_DOTS_INSTANCED_PROP_OVERRIDE_SUPPORTED(float, _HealthbarData)
        UNITY_DOTS_INSTANCING_END(MaterialPropertyMetadata)
        // DOTS instancing usage macros
        #define UNITY_ACCESS_HYBRID_INSTANCED_PROP(var, type) UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(type, var)
        #elif defined(UNITY_INSTANCING_ENABLED)
        // Unity instancing definitions
        UNITY_INSTANCING_BUFFER_START(SGPerInstanceData)
            UNITY_DEFINE_INSTANCED_PROP(float, _HealthbarData)
        UNITY_INSTANCING_BUFFER_END(SGPerInstanceData)
        // Unity instancing usage macros
        #define UNITY_ACCESS_HYBRID_INSTANCED_PROP(var, type) UNITY_ACCESS_INSTANCED_PROP(SGPerInstanceData, var)
        #else
        #define UNITY_ACCESS_HYBRID_INSTANCED_PROP(var, type) var
        #endif```
#

And that just repeats a few times

celest stag
#

I see

#

Hmm, that didn't get me any closer to getting it to work.

#

Like, I can make it compile but it doesn't seem to be reading the value from my components automagically.

#

Unless, as I said, that's a wrong understanding and I actually do need to make a system that updates the values of every material instance every frame.

#
#if defined(DOTS_INSTANCING_ON)
    UNITY_DOTS_INSTANCING_START(MaterialPropertyMetadata)
        UNITY_DOTS_INSTANCED_PROP_OVERRIDE_SUPPORTED(float4, _HealthbarData)
    UNITY_DOTS_INSTANCING_END(MaterialPropertyMetadata)
    #define UNITY_ACCESS_HYBRID_INSTANCED_PROP(var, type) UNITY_ACCESS_DOTS_INSTANCED_PROP_WITH_DEFAULT(type, var)
#elif defined(UNITY_INSTANCING_ENABLED)
    UNITY_INSTANCING_BUFFER_START(SGPerInstanceData)
        UNITY_DEFINE_INSTANCED_PROP(float4, _HealthbarData)
    UNITY_INSTANCING_BUFFER_END(SGPerInstanceData)
    #define UNITY_ACCESS_HYBRID_INSTANCED_PROP(var, type) UNITY_ACCESS_INSTANCED_PROP(SGPerInstanceData, var)
#else
    #define UNITY_ACCESS_HYBRID_INSTANCED_PROP(var, type) var
#endif
celest stag
#

Okay, I found an old Unity post from EIZENHORN himself about this very problem 😛
A guy in that thread graciously posted his Shadergraph setup so I copied that.

#

However, my healthbars still do not update as I would expect them to.

#

So what am I missing?

#

Far as I understood I shouldn't need a System dedicated to updating Material values as the MaterialProperty attribute on my struct should do that for me.

#

I have a separate system that updates the health values though

celest stag
#

I'm only missing the last part that makes the healthbars actually update..

#

Otherwise I have pretty much everything else.

#

I'm just not sure what I'm missing :/

celest stag
# celest stag

Just to underline that I did make a component for this too like EIZENHORN did.

/// <summary>
/// Add this to an <see cref="Entity"/> if it needs to have health.
/// </summary>
[MaterialProperty("_HealthbarData"), Serializable]
public struct  Health : IComponentData
{
    public float4 Value;
}
celest stag
#

Wait hold on

#

Does the above not work with Universal Render Pipeline??

celest stag
#

Does the component need to be attached to the entity that has the mesh with the material as well?

silver citrus
silver citrus
celest stag
celest stag
#

Thanks for all the help @silver citrus and @civic basin
I might not have a perfect solution, but I got something that works thansk to you two.