用户界面文字
这是最普遍和常见的文字使用场景,Unity引擎提供了包括UGUI在内的一系列工具用于开发游戏的用户操作界面,此外也有大量成熟的第三方框架和工具可用。
这部分稍微值得探讨的,除了各种第三方UI插件和工具之外,大概也就是UGUI自己的Text组件了,它提供了相对基础和易用的UI文字显示方案,而且开发者可以使用Shader对其进行一定的自定义拓展。
Text组件有一项Material属性,它可以用于配置自定义的材质,换言之它支持一些Shader效果,比如说描边。
对文字进行描边在部分情况下是很重要的一项功能,其最主要的功效是凸出文字本身,强调其试图展示的信息。
UGUI的Text并不直接支持描边效果(TextMeshPro则本身支持描边),因此需要使用自定义Shader来实现它。
Shader "Custom/TextOutline" { Properties{ _MainTex("Font Texture", 2D) = "white" {} _Color("Text Color", Color) = (1,1,1,1) _OutlineColor("Outline Color", Color) = (0,0,0,1) } SubShader{ Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" "PreviewType" = "Plane" } Lighting Off Cull Off ZTest Always ZWrite Off Blend SrcAlpha OneMinusSrcAlpha //第一个Pass,渲染Text内容背景颜色,并向外扩大形成描边 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile _ UNITY_SINGLE_PASS_STEREO STEREO_INSTANCING_ON STEREO_MULTIVIEW_ON #include "UnityCG.cginc" struct appdata_t { float4 vertex : POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; //UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; UNITY_VERTEX_OUTPUT_STEREO }; sampler2D _MainTex; uniform float4 _MainTex_ST; uniform float4 _MainTex_TexelSize; uniform fixed4 _Color; uniform fixed4 _OutlineColor; v2f vert(appdata_t v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); o.vertex = UnityObjectToClipPos(v.vertex); o.color = v.color * _Color; o.texcoord = TRANSFORM_TEX(v.texcoord,_MainTex); return o; } //确定每个像素周围8个像素的坐标。 static const float2 dirList[9] = { float2(-1,-1),float2(0,-1),float2(1,-1), float2(-1,0),float2(0,0),float2(1,0), float2(-1,1),float2(0,1),float2(1,1) }; //获取坐标列表中第dirIndex个偏移位置的透明度值。 float getDirPosAlpha(float index, float2 xy) { float2 curPos = xy; float2 dir = dirList[index]; float2 dirPos = curPos + dir * _MainTex_TexelSize.xy * 0.6; return tex2D(_MainTex, dirPos).a; }; //对于每个像素,传入片元参数v2f i ,获取次像素周围和自身的共9个像素进行透明度叠加。 //那么得出的结果就是非透明的区域被放大了,形成了黑边。 float getShadowAlpha(float2 xy) { float a = 0; float index = 0; a += getDirPosAlpha(index, xy); a += getDirPosAlpha(index++, xy); a += getDirPosAlpha(index++, xy); a += getDirPosAlpha(index++, xy); a += getDirPosAlpha(index++, xy); a += getDirPosAlpha(index++, xy); a += getDirPosAlpha(index++, xy); a += getDirPosAlpha(index++, xy); a += getDirPosAlpha(index++, xy); a = clamp(a,0,1); return a; } //由于渲染Text内容时,Text字上没有被渲染的区域是透明的,也就是透明度a值是0, //所以只要将有内容的区域往外透明度为0的区域扩展一些像素将就能够形成描边效果。 fixed4 frag(v2f i) : SV_Target { fixed4 col = _OutlineColor; float2 xy = i.texcoord.xy; col.a *= getShadowAlpha(xy); return col; } ENDCG } //第二个Pass,常规渲染Text内容。 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile _ UNITY_SINGLE_PASS_STEREO STEREO_INSTANCING_ON STEREO_MULTIVIEW_ON #include "UnityCG.cginc" struct appdata_t { float4 vertex : POSITION; fixed4 color : COLOR; float4 texcoord : TEXCOORD0; //UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; UNITY_VERTEX_OUTPUT_STEREO }; sampler2D _MainTex; uniform float4 _MainTex_ST; uniform float4 _MainTex_TexelSize; uniform fixed4 _Color; v2f vert(appdata_t v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(o); o.vertex = UnityObjectToClipPos(v.vertex); o.color = v.color * _Color; step(v.texcoord, v.vertex.xy); o.texcoord = TRANSFORM_TEX(v.texcoord.xy,_MainTex); return o; } fixed4 frag(v2f i) : SV_Target { fixed4 col = i.color; col.a = tex2D(_MainTex, i.texcoord).a; return col; } ENDCG } } }
新建一个Material并将Shader设置为TextOutline即可实现效果,该Shader参考他人代码改动而成,描边宽度是固定的,只能设置描边颜色和文字本体的叠加颜色。
而该Shader的描边原理也很简单,Text在绘制文字时会将文字转化为纹理绘制到画布上,而Shader中在实际绘制文字本体前先利用文字渲染时“没有文字的地方为透明”的特性,找到文字的边缘并将其向外进行拓展。
基于该原理也可以编写出能自由调整描边宽度的Shader,甚至可以拓展到Sprite或者Image等组件的描边效果上。
游戏内非界面文字
同样是相当常见的一类文字效果,例如敌人头顶的生命值,受到伤害时蹦出来还会飞走的伤害数值提示,贴在场景内物体上可以动态改变的文字提示等等。
这一类文字也有多种解决方案,如果不考虑第三方插件和工具的话,Unity自身提供了TextMesh组件可用于在场景中渲染文字,虽然该组件是为3D场景服务,但经过一定的配置后2D场景也可以使用它来创建场景动态文字效果。
此外也可以直接将Canvas组件挂载到场景物体之上并设置其渲染模式为WorldSpace即可在场景中显示UI元素,这种方式可以用于制作跟随角色的状态栏或者对话框等界面。
此外还有一种折中的方案,是在耗费一定运算时间的基础上,利用UI去模拟场景内文字的效果。
毕竟在不考虑第三方插件的情况下,为每个有文字显示需求的对象都挂在Canvas并显示需要经常更新的UI数据会有相当高的性能消耗,如果场景内挂载的Canvas数量较少还行,但当对象多起来的时候,这种做法或许会很难满足需求。
而折中方案就是只用一个普通的Canvas,将它的渲染模式设置为Camera并分配一个摄像机,然后编写脚本去跟踪每个有文字显示需求的对象并为它们显示所需的文字。
这种方案减少了Canvas的数量,但增加了一定的运算量,Update中需要处理的数据会变多,但如果加入了裁剪功能,保证只有可见的一部分对象有文字显示就能降低运算量。
public class DynamicUIPanel : MonoBehaviour { public Camera UICamera; public Camera GameCamera; private RectTransform viewTransform; private Dictionary<int, UIComponent> uiComponents; private void Awake() { viewTransform = GetComponent<RectTransform>(); uiComponents = new Dictionary<int, UIComponent>(); } private void Update() { foreach(int id in uiComponents.Keys) { UpdatePosition(uiComponents[id]); uiComponent[id].controller.UpdateContent(); } } private void UpdatePosition(UIComponent com) { Vector2 screenPosition = RectTransformUtility.WorldToScreenPoint(GameCamera, com.followTarget.position); Vector2 rectPosition; if (RectTransformUtility.ScreenPointToLocalPointInRectangle(viewTransform, screenPosition, UICamera, out rectPosition)) { com.controller.SetupAnchoredPosition(rectPosition); } } public class UIComponent { public int instanceID; public Transform followTarget; public UIControlller controller; } }
如上的伪代码就大概展示了如何注册自动跟踪指定目标对象的UI控制器并按照需求自动更新位置和刷新显示内容,这套逻辑可以用于制作在屏幕上跳动的伤害数字或者一些操作提示和反馈,例如走到某个物体范围内后在物体顶上显示操作提示,或者是施展某种技能后在被击中的物体和敌人头顶显示他们被施加的状态。
除了以上这些还在纠结着UI组件的方案,游戏内文字的最佳,也是最终解决方案是用图片替代文字。
这个方案虽然成本很高,但难度较低,对性能的影响也相对较小,而且可定制性极佳。(笑)
暂无关于此日志的评论。