Unity大量伤害数字生成解决方案
伤害数字生成的需求分析
在Unity中实现大量伤害数字效果,通常需要解决性能优化、视觉效果多样性以及动态管理问题。以下是解决方案
使用GPU Instancing实现批量渲染
Unity GPU Instancing的优势
性能优化
GPU Instancing通过一次绘制调用(Draw Call)渲染多个相同或相似的网格,大幅减少CPU到GPU的通信开销。适合渲染大量重复对象(如植被、子弹、建筑群),性能提升显著,尤其在移动端和低端设备上表现突出。
内存效率
共享相同材质和网格的实例仅存储一份基础数据,仅通过变换矩阵(位置、旋转、缩放)区分不同实例。相比单独渲染每个对象,内存占用更低,尤其适用于大规模场景。
GPU Instancing伤害字制作
原理用一张包含0-9和符号位的图片来存储伤害数字的基本信息,通过一个Quad网格渲染每一位数字,同时可以利用shader制作一些顶点偏移动画来满足字体动画需求。
// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)' Shader "Custom/DamageNumberInstanced" { Properties { _MainTex ("Font Texture", 2D) = "white" {} _VerticalHeightFactor ("VerticalHeightFactor",Range(0,1)) = 0.5 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" } Blend SrcAlpha OneMinusSrcAlpha ZWrite Off Cull Off Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #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; float4 color : COLOR; float4 outlineColor : TEXCOORD1; float outlineWidth: TEXCOORD2; UNITY_VERTEX_INPUT_INSTANCE_ID }; sampler2D _MainTex; float4 _MainTex_ST; float4 _MainTex_TexelSize; float _VerticalHeightFactor; // 实例化数据 UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(float4, _InstanceColor) UNITY_DEFINE_INSTANCED_PROP(float4, _InstanceOutlineColor) UNITY_DEFINE_INSTANCED_PROP(float, _InstanceCreationTime) UNITY_DEFINE_INSTANCED_PROP(float, _InstanceDamageDuration) UNITY_DEFINE_INSTANCED_PROP(float, _InstanceRandomOffset) UNITY_DEFINE_INSTANCED_PROP(float, _InstanceIsCritical) UNITY_DEFINE_INSTANCED_PROP(float, _InstanceDamageValue) UNITY_DEFINE_INSTANCED_PROP(float, _InstanceOutlineWidth) UNITY_DEFINE_INSTANCED_PROP(float, _InstanceMaintainSizeTime) UNITY_DEFINE_INSTANCED_PROP(float, _InstancesizeScalePercent) UNITY_DEFINE_INSTANCED_PROP(float, _InstanceFadeInTime) UNITY_DEFINE_INSTANCED_PROP(float, _InstanceFadeOutTime) UNITY_DEFINE_INSTANCED_PROP(float, _InstanceSign) UNITY_INSTANCING_BUFFER_END(Props) // 数字纹理中每个数字的UV布局(假设是2x10网格布局0-9和符号) float2 GetDigitUV(float digit, float sign,float2 baseUV) { // 将数字转换为整数并限制在0-9范围内 int intDigit = (int)digit; intDigit = clamp(intDigit, 0, 10); float x = (intDigit % 10) * 0.1; float y = ((int)sign % 2) * 0.5; // 调整UV到正确的数字区域 float2 uv = baseUV; uv.x = uv.x * 0.1 + x; uv.y = uv.y * 0.5 + y; return uv; } v2f vert (appdata v, uint instanceID : SV_InstanceID) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_TRANSFER_INSTANCE_ID(v, o); // 获取实例数据 float creationTime = UNITY_ACCESS_INSTANCED_PROP(Props, _InstanceCreationTime); float duration = UNITY_ACCESS_INSTANCED_PROP(Props, _InstanceDamageDuration); float randomOffset = UNITY_ACCESS_INSTANCED_PROP(Props, _InstanceRandomOffset); float isCritical = UNITY_ACCESS_INSTANCED_PROP(Props, _InstanceIsCritical); float4 instanceColor = UNITY_ACCESS_INSTANCED_PROP(Props, _InstanceColor); float damageValue = UNITY_ACCESS_INSTANCED_PROP(Props, _InstanceDamageValue); float4 outlineColor = UNITY_ACCESS_INSTANCED_PROP(Props, _InstanceOutlineColor); float outlineWidth = UNITY_ACCESS_INSTANCED_PROP(Props, _InstanceOutlineWidth); float maintainSizeTime = UNITY_ACCESS_INSTANCED_PROP(Props, _InstanceMaintainSizeTime); // 维持大小的时间 float sizeScalePercent = UNITY_ACCESS_INSTANCED_PROP(Props, _InstancesizeScalePercent); //缩放比例 float fadeInTime = UNITY_ACCESS_INSTANCED_PROP(Props, _InstanceFadeInTime); //淡入时间 float fadeOutTime = UNITY_ACCESS_INSTANCED_PROP(Props, _InstanceFadeOutTime); //淡出时间 float sign = UNITY_ACCESS_INSTANCED_PROP(Props, _InstanceSign); //符号图像位 // 计算动画进度 float elapsed = _Time.y - creationTime; float progress = saturate(elapsed / duration); // 动画效果:上浮 float verticalOffset = progress * _VerticalHeightFactor; // 淡入淡出效果 float fadeInProgress = saturate(elapsed / fadeInTime); // 淡入进度 float fadeOutProgress = saturate(duration - elapsed / fadeOutTime); // 淡出进度 float alpha = fadeInProgress * fadeOutProgress; // 结合淡入淡出 // 随机水平偏移 float horizontalOffset = sin(progress * 10.0 + randomOffset * 10.0) * 0.3; // 字体大小变化效果 float sizeProgress = saturate((elapsed - maintainSizeTime) / (duration - maintainSizeTime)); float sizeScale = lerp(1.0, sizeScalePercent, sizeProgress); // 从原始大小缩小到50% // 应用变换 float4 worldPos = mul(UNITY_MATRIX_M,v.vertex); worldPos.xyz += float3(0, verticalOffset, 0); // 应用字体大小缩放 worldPos.xyz = worldPos.xyz * sizeScale; o.vertex = mul(UNITY_MATRIX_VP, worldPos); o.uv = TRANSFORM_TEX(v.uv, _MainTex); // 设置颜色和透明度 o.color = instanceColor; o.outlineColor = outlineColor; o.outlineWidth = outlineWidth; o.color.a *= alpha; // 根据数字值计算UV偏移 o.uv = GetDigitUV(damageValue, sign,o.uv); return o; } fixed4 frag (v2f i) : SV_Target { // 如果轮廓宽度为0,直接返回文字颜色 if (i.outlineWidth < 0.001) { fixed4 center = tex2D(_MainTex, i.uv); fixed4 col = i.color; col.a *= center.a; return col; } // 文字轮廓效果 fixed4 outline = i.outlineColor; fixed4 center = tex2D(_MainTex, i.uv); // 简单的轮廓检测 if (center.a < 0.5) { // 检查周围像素 float2 uvOffset = _MainTex_TexelSize.xy * i.outlineWidth; fixed a1 = tex2D(_MainTex, i.uv + float2(uvOffset.x, 0)).a; fixed a2 = tex2D(_MainTex, i.uv + float2(-uvOffset.x, 0)).a; fixed a3 = tex2D(_MainTex, i.uv + float2(0, uvOffset.y)).a; fixed a4 = tex2D(_MainTex, i.uv + float2(0, -uvOffset.y)).a; if (a1 + a2 + a3 + a4 > 0) { outline.a = i.color.a; return outline; } } fixed4 col = i.color; col.a *= center.a; return col; } ENDCG } } }贴图如下
预制体制作
每位数字SingleNum上需要挂载控制shader显示的脚本
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using UnityEngine; public class SingleNum : MonoBehaviour { private MaterialPropertyBlock propertyBlock; private MeshRenderer meshRenderer; private Material numberMaterial; public enum SignType { Crit = 0, Sub = 1, Add = 2 } private void Awake() { propertyBlock = new MaterialPropertyBlock(); numberMaterial = GetComponent<MeshRenderer>().sharedMaterial; meshRenderer = GetComponent<MeshRenderer>(); } /// <summary> /// 设置符号 /// </summary> public void SetSign( Color color, Color outline, float outlineWidth, float furation, float randomOffest, float maintainSizeTime, float scalePercent, float fadeInTime, float fadeOutTime, SignType signType) { // 获取Shader全局的_Time值 Vector4 globalTime = Shader.GetGlobalVector("_Time"); float timeY = globalTime.y; propertyBlock.SetVector("_InstanceColor", color); propertyBlock.SetVector("_InstanceOutlineColor", outline); propertyBlock.SetFloat("_InstanceCreationTime", timeY); propertyBlock.SetFloat("_InstanceDamageDuration", furation); propertyBlock.SetFloat("_InstanceRandomOffset", randomOffest); propertyBlock.SetFloat("_InstanceDamageValue", (float)((int)signType)); propertyBlock.SetFloat("_InstanceOutlineWidth", outlineWidth); propertyBlock.SetFloat("_InstanceMaintainSizeTime", maintainSizeTime); propertyBlock.SetFloat("_InstancesizeScalePercent", scalePercent); propertyBlock.SetFloat("_InstanceFadeInTime", fadeInTime); propertyBlock.SetFloat("_InstanceFadeOutTime", fadeOutTime); propertyBlock.SetFloat("_InstanceSign", 0f); meshRenderer.SetPropertyBlock(propertyBlock); } /// <summary> /// 设置数字 /// </summary> /// <param name="damage"></param> /// <param name="color"></param> /// <param name="outline"></param> /// <param name="outlineWidth"></param> /// <param name="furation"></param> /// <param name="randomOffest"></param> /// <param name="maintainSizeTime"></param> /// <param name="scalePercent"></param> /// <param name="fadeInTime"></param> /// <param name="fadeOutTime"></param> /// <param name="isCritical"></param> public void SetDamageNumber( float damage, Color color, Color outline, float outlineWidth, float furation, float randomOffest, float maintainSizeTime, float scalePercent, float fadeInTime, float fadeOutTime ) { // 获取Shader全局的_Time值 Vector4 globalTime = Shader.GetGlobalVector("_Time"); float timeY = globalTime.y; propertyBlock.SetVector("_InstanceColor", color); propertyBlock.SetVector("_InstanceOutlineColor", outline); propertyBlock.SetFloat("_InstanceCreationTime", timeY); propertyBlock.SetFloat("_InstanceDamageDuration", furation); propertyBlock.SetFloat("_InstanceRandomOffset", randomOffest); propertyBlock.SetFloat("_InstanceDamageValue", damage); propertyBlock.SetFloat("_InstanceOutlineWidth", outlineWidth); propertyBlock.SetFloat("_InstanceMaintainSizeTime", maintainSizeTime); propertyBlock.SetFloat("_InstancesizeScalePercent", scalePercent); propertyBlock.SetFloat("_InstanceFadeInTime", fadeInTime); propertyBlock.SetFloat("_InstanceFadeOutTime", fadeOutTime); propertyBlock.SetFloat("_InstanceSign", 1f); meshRenderer.SetPropertyBlock(propertyBlock); } }在DamageNums上需要挂载一个管理类控制数字的所有位的显示
using UnityEngine; using System.Collections.Generic; using UnityEngine.Rendering; using System; using UnityEngine.Events; public class DamageNumberRender : MonoBehaviour { public Color color; public float duration; public float randomOffset; public int damage; [Header("轮廓设置")] public Color OutlineColor; public float outlineWidth; [Header("缩放设置")] [Tooltip("维持大小时间")] public float maintainSizeTime; [Tooltip("缩放大小"),Range(0,1f)] public float scalePercent; [Header("淡入淡出")] public float fadeInTime; public float fadeOutTime; public UnityEvent completeEvent = new UnityEvent(); private SingleNum[] singleNums; private float durationElaspe = 0; private bool isOver = false; private int[] damageNum; private void Awake() { singleNums = GetComponentsInChildren<SingleNum>(); damageNum = SplitDigits(damage); } private void Update() { if (Input.GetKeyDown(KeyCode.V)) SetDamageNumber(damage); if (durationElaspe >= 0) durationElaspe -= Time.deltaTime; else { if (!isOver) { completeEvent?.Invoke(); isOver = true; } } } public int[] SplitDigits(int number) { if (number == 0) return new int[1] { 0 }; // 处理负数并转换为正数,使用long避免Math.Abs(int.MinValue)溢出 long absoluteValue = Math.Abs((long)number); // 计算数字的位数 int digitCount = CountDigits(absoluteValue); int[] digits = new int[digitCount]; // 从最低位到最高位依次提取 for (int i = digitCount - 1; i >= 0; i--) { digits[i] = (int)(absoluteValue % 10); // 取最后一位 absoluteValue /= 10; // 移除最后一位 } return digits; } private int CountDigits(long number) { if (number == 0) return 1; int count = 0; while (number > 0) { count++; number /= 10; } return count; } public void SetDamageNumber(int damage, bool isCritical = false) { isOver = false; durationElaspe = duration; if (damage != this.damage) { this.damage = damage; damageNum = SplitDigits(damage); } int index = 0; if (isCritical) { singleNums[index].SetSign( color, OutlineColor, outlineWidth, duration, randomOffset, maintainSizeTime, scalePercent, fadeInTime, fadeOutTime, SingleNum.SignType.Crit ); index++; } if (damage < 0) { singleNums[index].SetSign( color, OutlineColor, outlineWidth, duration, randomOffset, maintainSizeTime, scalePercent, fadeInTime, fadeOutTime, SingleNum.SignType.Sub ); index++; } if (damageNum.Length + index > singleNums.Length) Debug.LogError($"数据{damage}长度>显示长度,请扩充数字长度"); for (int i = 0; i < damageNum.Length; i++) { singleNums[i + index].SetDamageNumber( damageNum[i], color, OutlineColor, outlineWidth, duration, randomOffset, maintainSizeTime, scalePercent, fadeInTime, fadeOutTime ); } } }对象池技术(Object Pooling)
有了上面GPUInstance渲染方案后,再加入对象池技术就可以提升大量性能。
using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using UnityEngine; namespace DayZ { public class DamageNumberManager { private Queue<DamageNumberRender> damageNumQueue = new Queue<DamageNumberRender>(); private Transform parent; private string path = ""; public DamageNumberManager(string path) { GameObject damgObject = new GameObject("DamageNumberPool"); parent = damgObject.transform; this.path = path; } /// <summary> /// 预加载 /// </summary> /// <param name="num"></param> /// <returns></returns> public void PreLoadDamageNums(int num) { for (int i = 0; i < num; i++) { DamageNumberRender numRender = CreateDamageNumberRender(); damageNumQueue.Enqueue(numRender); } } public DamageNumberRender GetDamageNumber() { DamageNumberRender numRender; if (damageNumQueue.Count > 0) { numRender = damageNumQueue.Dequeue(); } else { numRender = CreateDamageNumberRender(); } return numRender; } private DamageNumberRender CreateDamageNumberRender() { //"Game/LastWar/Particles/DamageNums" GameObject gameObject = CCNodeCache.Instance.GetGameObject(this.path); gameObject.transform.SetParent(parent); DamageNumberRender numRender = gameObject.GetComponent<DamageNumberRender>(); numRender.completeEvent.AddListener(() => { damageNumQueue.Enqueue(numRender); }); return numRender; } public void Clear() { while(damageNumQueue.Count > 0) { DamageNumberRender numRender = damageNumQueue.Dequeue(); CCNodeCache.Instance.RecycleGameObject(numRender.gameObject, path); } GameObject.Destroy(parent.gameObject); } } }