fastest way to get health flashing

We have that in our code so each time the character gets hit, it flashes.
With all these strings it just seems to be so inefficient so I was wondering if there is a better way to do this?

code

internal void UpdateHealthMaterial()
    {
        float damagePulseDuration = 0.25f;
        float f = health / maxHealth;
        float commandVal = 0;
        if (lastCommandTime != 0 && (NetworkManager.time - lastCommandTime < damagePulseDuration))
        {
            commandVal = Mathf.Sin(NetworkManager.time - lastCommandTime) * Mathf.PI / (2 * damagePulseDuration);
        }
        float damagePulseVal = 0.3f * (1 - f);
        float healPulseVal = 0;
        /*    if(lastDamageTime!=0 && f<1 && NetworkManager.time-lastDamageTime<damagePulseDuration){
                damagePulseVal =  0.25f+0.7f*(1-f)*Mathf.Sin(NetworkManager.time-lastDamageTime)*Mathf.PI/(2*damagePulseDuration);
            }
    */
        float damageFlashDuration = 0.1f;
        float damageFlashVal = 0;
        if (lastDamageTime != 0 && (NetworkManager.time - lastDamageTime) < damageFlashDuration)
        {
            damageFlashVal = 1;
        }
        if (lastHealTime != 0 && f < 1 && NetworkManager.time - lastHealTime < damagePulseDuration)
        {
            healPulseVal = 0.25f + 0.5f * (1 - f) * Mathf.Sin(NetworkManager.time - lastHealTime) * Mathf.PI / (2 * damagePulseDuration);
        }
        if (healPulseVal < 0) healPulseVal = 0;
        if (damagePulseVal < 0) damagePulseVal = 0;
        if (commandVal != lastCommandVal || healPulseVal != lastHealVal || damagePulseVal != lastDamageVal || damageFlashVal != lastFlashVal)
        {
            if (materialBlock == null) materialBlock = new MaterialPropertyBlock();
            materialBlock.Clear();
            materialBlock.SetFloat("_CommandPulseVal", commandVal);
            materialBlock.SetFloat("_DamagePulseVal", damagePulseVal);
            materialBlock.SetFloat("_HealPulseVal", healPulseVal);
            materialBlock.SetFloat("_DamageFlashVal", damageFlashVal);
            for (int i = 0; i < renderers.Length; i++)
            {
                Renderer r = renderers[i];
                if (r) r.SetPropertyBlock(materialBlock);
            }
            lastCommandVal = commandVal;
            lastHealVal = healPulseVal;
            lastDamageVal = damagePulseVal;
            lastFlashVal = damageFlashVal;
        }
    }

You could store an int instead, with

public static class ShaderProperties {

    public static readonly int _CommandPulseVal;
    static ShaderProperties() {
        _CommandPulseVal= Shader.PropertyToID(nameof(_CommandPulseVal));
    }
}

Then you use

materialBlock.SetFloat(ShaderProperties._CommandPulseVal, commandVal);

instead of using the string itself.

So two approaches I’ve used:

  1. In URP, MaterialPropertyBlock breaks SRP Batcher, so our solution is to write a custom pass to draw a specific render layer objects with a pure color hit material. This way you can draw characters in just two batches, one for normal characters, another for the hit characters.

When character gets hit, set its skinnedmeshrenderer’s render layer, also add a simple struct into a renderer tracking system, this system only do one thing: change render layer back to default after 0.1 second.

But this approach only works if your hit glow effect is pure color, since the replacement material does not know your characters’ textures.

  1. In default render pipeline(Or if you don’t need to get benefits from SRP Batcher), we add a _HitTime parameter and a global _CurrentGameTime to the shader, lerp between hit and normal state using saturate(_CurrentGameTime - _HitTime). So all you have to do is set _HitTime using materialpropertyblock when character gets hit. Also remember to set _CurrentGameTime in an singleton monobehavior.

Both are efficient enough, we used it in diablo-like game that many hits can happen in one frame.

1 Like

[EDIT] I just understood 1. It’s a hard pulse though, no fade from pulse to normal.
I just added 2. it pulses but doesn’t fade from pulse to normal. No idea why.

    MaterialPropertyBlock materialBlock;
    float _oldHealth = 0;
  
    internal void UpdateHealthMaterial()
    {
        materialBlock.Clear();
        if (health > _oldHealth)
            materialBlock.SetFloat(shaderHealTimeID,  NetworkManager.time + .25f);
        if (health < _oldHealth)
            materialBlock.SetFloat(shaderDamageTimeID, NetworkManager.time + .25f);
        _oldHealth = health;
        foreach (var r in renderers)
            if (r)
                r.SetPropertyBlock(materialBlock);
    }
Shader "SG/SG Opaque" {
    Properties {
        _Color ("Color",Color) = (1,1,1,1)
        _MainTex ("Texture", 2D) = "white" {}
        _DamageColor ("Damage Color", Color) = (1,0,0,1)
        _FlashColor ("Flash Color", Color) = (1,1,1,1)
        _HealColor ("Heal Color", Color) = (0,1,0,1)
        _BumpMap ("Bumpmap", 2D) = "bump" {}
        _RimColor ("Rim Color", Color) = (0.26,0.19,0.16,0.0)
        _RimPower ("Rim Power", float) = 3.0
        _OutlineColor ("Outline Color", Color) = (0,0,0,1)
        _Outline ("Outline width", float) = .005
    }
    SubShader {
        LOD 900
        LOD 800
        Tags { "RenderType" = "Opaque" }

        CGPROGRAM
        #pragma target 3.0
        #pragma surface surf WrapLambert approxview  vertex:vert nodirlightmap
        #pragma instancing_options assumeuniformscaling
        #pragma multi_compile_instancing
      
        float4 _Color;
        sampler2D _MainTex;
        sampler2D _BumpMap;
        uniform float4 _RimColor;    
        uniform float _RimPower;
        uniform float4 _DamageColor;
        uniform float4 _HealColor;
        uniform float4 _FlashColor;
        uniform float _currentHealTime;   
        uniform float _currentDamageTime;    
        uniform float _currentFlashTime;

        half4 LightingWrapLambert (SurfaceOutput s, half3 lightDir, half atten) {
            half NdotL = dot (s.Normal, lightDir);
            half diff = NdotL * 0.5 + 0.5;
            half4 c;
            c.rgb = s.Albedo * _LightColor0.rgb * (diff * atten * 2);
            c.a = s.Alpha;
            return c;
        }

        struct Input {
            float2 uv_MainTex;
            float2 uv_BumpMap;
            float3 viewDir;
            float3 vertexColor;
            float factor;
        };

        void vert (inout appdata_full v, out Input o) {
            UNITY_INITIALIZE_OUTPUT(Input,o);
            o.vertexColor = v.color;
        }

        void surf (Input IN, inout SurfaceOutput o) {
            half3 c = tex2D (_MainTex, IN.uv_MainTex).rgb;       
            c *= IN.vertexColor;
            c *= _Color;
            o.Albedo = c;
            o.Normal = UnpackNormal (tex2D (_BumpMap, IN.uv_BumpMap));
            half rim = 1.0 - saturate(dot (IN.viewDir, o.Normal));
            half3 e = _RimColor.rgb * pow (rim, _RimPower) + _DamageColor*saturate(_currentDamageTime  - _Time) +  _HealColor*saturate(_currentHealTime  - _Time);
            o.Emission = e + rim*c;
        }
        ENDCG

    UsePass "Toon/Basic Outline/OUTLINE"
    }

You cleared your materialpropertyblock every frame. So your _currentDamageTime and _currentHealTime lose their values if the character’s hp is unchanged. Set the properblock only when hit happens and don’t clear it unless you disable/destroy the character.

Also your NetworkManager.time may be different from the default shader _Time parameter. Try add a new script to pass NetworkManager.time as a global value to the shader every frame, use that value instead of _Time.

Thanks @Thermos it works now.