公司动态
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 { QueueTransparent IgnoreProjectorTrue RenderTypeTransparent } 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 GetComponentMeshRenderer().sharedMaterial; meshRenderer GetComponentMeshRenderer(); } /// 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 namedamage/param /// param namecolor/param /// param nameoutline/param /// param nameoutlineWidth/param /// param namefuration/param /// param namerandomOffest/param /// param namemaintainSizeTime/param /// param namescalePercent/param /// param namefadeInTime/param /// param namefadeOutTime/param /// param nameisCritical/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 GetComponentsInChildrenSingleNum(); 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 QueueDamageNumberRender damageNumQueue new QueueDamageNumberRender(); private Transform parent; private string path ; public DamageNumberManager(string path) { GameObject damgObject new GameObject(DamageNumberPool); parent damgObject.transform; this.path path; } /// summary /// 预加载 /// /summary /// param namenum/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.GetComponentDamageNumberRender(); 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); } } }