个人最近Unity2D项目总结
前言
最近我为做公司了个2D的项目,因为项目本身是一个体量不大的小游戏,所以就由我一个人来独自完成,工期一个月。这算是我第一次做2D项目,而且公司这边在2D方面也没啥积累。这就导致了,我必须造一些轮子。为了方便以后的自己好找这些轮子,所以我想写篇文章记录一下。除此之外,我也会记录自己在项目中遇到的问题。
环境
Windows10
Unity2022.3.8f1c1
骨骼动画转序列帧
如果你的公司有钱,你可以直接买Spine这个软件,用Spine做骨骼动画的话可以直接导出序列帧。(我这里并没有在打广告,因为我自己也没有用这个软件。毕竟小公司是真的买不起什么正版授权,胆子小也不敢用盗版。我这里是在找资料的时候听说过,所以分享一下。)项目一开始,我们本来是确定要用Unity自带的2D骨骼动画去做处理。骨骼动画的毛病就是耗性能,因为其不能满足项目的性能要求,所以我就想到了序列帧。可是Unity并没有提供这个功能,所以这就很难受。幸好我找到了一篇文章给了解决方案(我本来是想挂文章链接的,但是我找不到文章)。
其主要是思想就是将骨骼动画的每一帧动作进行截图处理。这就好像游戏中的拍照一样。当然我们希望截屏的时候,除了需要被截图的部分其他都是透明的。还为了我们不需要手动移帧,我们必须要做一些额外的设置。
首先我们要保证场景中只存在需要被截图的物体和相机。然后我们选择相机,并将相机的Clear Flags 设定为Solid Color ,且将其BackGround 的A通道设定为0。当然这些设定也不绝对,看你项目的需求。这时候Game的背景画面虽然还存在颜色,但是我们截图后背景画面的颜色就不存在了。接下来,我们调整一下Game的屏幕比,我们要将其设定为我们所需的单张截图的大小(这个大小尽量是2的幂,否则会导致些问题。这这些问题后面说)。之后我们摆放好需要截图的物体并将下面的脚本挂载上去。而对于播放的动画类型,我们也需要一些特殊的设置。我们需要将这个动画设置为Legacy 。否则下面的脚本会出现报错。最后点击运行就可以了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 using System.Collections;using System.IO;using System.Linq;using UnityEngine;namespace ToolEditor { public class SpineTool : MonoBehaviour { [Header("需要转换序列帧的对象,记得它要挂载Animation组件,且动画要是Legacy" ) ] public GameObject obj; [Header("资源生成的文件位置" ) ] public string path = "" ; [Header("需要生成几帧的动画" ) ] [Min(1) ] public int frameCount = 1 ; [Header("是否需要生成图集" ) ] public bool needAtla = false ; [Header("翻转图集的y轴" ) ] public bool filpY = true ; private Animation m_animation; private void Start () { if (path == "" ) { path = Path.Combine(Application.dataPath, "OtherResources" , "SquenceAnimation" ); } StartCoroutine(SpineToSquence()); } public IEnumerator SpineToSquence () { if (Screen.width != Screen.height) { Debug.LogError($"确保屏幕宽高一致,现在的屏幕比为 {Screen.width} : {Screen.height} " ); yield break ; } else if (frameCount <= 0 ) { Debug.LogError("帧数至少设置为1帧" ); yield break ; } else if (obj == null ) { Debug.LogError("先设置一下Obj" ); yield break ; } m_animation = obj.GetComponent<Animation>(); if (m_animation != null ) { m_animation.playAutomatically = false ; var animaState = m_animation.Cast<AnimationState>(); if (!Directory.Exists(path)) { Directory.CreateDirectory(path); } foreach (var anima in animaState) { float perFrameTime = anima.length / frameCount; int curFrameCount = 0 ; int texWidth = Screen.width; float sampleTime = 0 ; m_animation.Play(anima.name); anima.time = 0 ; anima.enabled = false ; yield return new WaitForEndOfFrame () ; Vector2Int ratio = Vector2Int.one; if (needAtla) { ratio = GetAlatWH(frameCount); } Texture2D tex = new Texture2D(texWidth * ratio.x, texWidth * ratio.y, TextureFormat.ARGB32, false ); while (sampleTime <= anima.length && frameCount > curFrameCount) { anima.enabled = true ; anima.time = sampleTime; m_animation.Sample(); anima.enabled = false ; yield return new WaitForEndOfFrame () ; if (needAtla) { tex.ReadPixels(new Rect(0 , 0 , texWidth, texWidth), (curFrameCount % ratio.x) * texWidth, (filpY ? ratio.y - curFrameCount / ratio.x - 1 : curFrameCount / ratio.x) * texWidth); tex.Apply(); } else { tex.ReadPixels(new Rect(0 , 0 , texWidth, texWidth), 0 , 0 ); tex.Apply(); byte [] bytes = tex.EncodeToPNG(); string savePath = Path.Combine(path, anima.name + "_" + curFrameCount + ".png" ); if (!File.Exists(savePath)) { File.Create(savePath).Dispose(); } yield return File.WriteAllBytesAsync(savePath, bytes); } yield return new WaitForEndOfFrame () ; sampleTime += perFrameTime; curFrameCount++; } if (frameCount > curFrameCount + 1 ) { curFrameCount++; anima.time = anima.length; m_animation.Sample(); yield return new WaitForEndOfFrame () ; if (needAtla) { tex.ReadPixels(new Rect(0 , 0 , texWidth, texWidth), (curFrameCount % ratio.x) * texWidth, (filpY ? ratio.y - curFrameCount / ratio.x - 1 : curFrameCount / ratio.x) * texWidth); tex.Apply(); } else { tex.ReadPixels(new Rect(0 , 0 , texWidth, texWidth), 0 , 0 ); tex.Apply(); byte [] bytes = tex.EncodeToPNG(); string savePath = Path.Combine(path, anima.name + "_" + curFrameCount + ".png" ); if (!File.Exists(savePath)) { File.Create(savePath).Dispose(); } yield return File.WriteAllBytesAsync(savePath, bytes); } yield return new WaitForEndOfFrame () ; } if (needAtla) { byte [] bytes = tex.EncodeToPNG(); string savePath = Path.Combine(path, anima.name + "_Alats" + ".png" ); if (!File.Exists(savePath)) { File.Create(savePath).Dispose(); } yield return File.WriteAllBytesAsync(savePath, bytes); yield return new WaitForEndOfFrame () ; Debug.Log($"图集比为 {ratio.x} :{ratio.y} " ); } } Debug.Log("结束" ); } else Debug.LogError("无animation" ); } private int GetClosestBigerPowerOfTwo (float curVal ) { int val = 1 ; while (val > curVal) val *= 2 ; return val; } private Vector2Int GetAlatWH (int frameCount ) { int sq = Mathf.CeilToInt(Mathf.Sqrt(frameCount)); for (int i = sq; i > 0 ; --i) { if (frameCount % i == 0 ) { return new Vector2Int(i, frameCount / i); } } return Vector2Int.one; } } }
显然这份代码还有改进的空间,比如将摄像机的设置放到这个脚本中(因为是在编辑器中运行,屏幕比的设置我自己也没有什么好方法去设定)。还有一些就要看你们具体的项目需求而定了。
序列帧播放的方法
我们得到序列帧后可以使用粒子特效来播放这序列帧。粒子中播放序列帧有两种方法,一种是使用Sprite
+ Sprite
Alats,一种是使用图集。我们也可以使用Animation来实现这个功能。但是Animation是用一个Sprite组件然后不断替换其中的图片来实现这个效果的。最后我们也可以自己去写Shader实现。此时我们只需要一个Quad就可以实现了。下面是我自己写的Shader:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 Shader "Self/SelFrameAnima" { Properties { _Color ("Color Tint", Color) = (1 ,1 ,1 ,1 ) _MainTex ("Texture", 2 D) = "white" {} _HAmount ("Horizontal Amount",Int) = 0 _VAmount ("Vertical Amount",Int) = 0 _Speed ("Speed",Range(0 ,100 )) = 30 _ClipAmount ("Clip Amount",Range(0 ,1 )) = 0.5 _Offset ("Offset Value" ,Float) = 0 [Enum(StartBottom,0 ,StartTop,1 )] _StartNode ("StartNode",Int) = 0 } SubShader { Tags {"IgnoreProjector" = "True" "RenderType"="Transparent" "LightMode"="ForwardBase" "Queue" = "Transparent"} Pass { ZWrite On ColorMask 0 } Pass { Cull Off Blend SrcAlpha OneMinusSrcAlpha Lighting Off ZWrite Off ZTest Always CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma multi_compile_instancing #include "UnityCG.cginc" float4 _MainTex_ST; sampler2D _MainTex; int _StartNode; 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 }; UNITY_INSTANCING_BUFFER_START(Props) UNITY_DEFINE_INSTANCED_PROP(fixed4, _Color) UNITY_DEFINE_INSTANCED_PROP(int , _HAmount) UNITY_DEFINE_INSTANCED_PROP(int , _VAmount) UNITY_DEFINE_INSTANCED_PROP(float , _Speed) UNITY_DEFINE_INSTANCED_PROP(half, _ClipAmount) UNITY_DEFINE_INSTANCED_PROP(float , _Offset) UNITY_INSTANCING_BUFFER_END(Props) v2f vert (appdata v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_TRANSFER_INSTANCE_ID(v, o); o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { UNITY_SETUP_INSTANCE_ID(i); float curFrame = _Time.y - UNITY_ACCESS_INSTANCED_PROP(Props, _Offset); int hAmount = UNITY_ACCESS_INSTANCED_PROP(Props, _HAmount); int vAmount = UNITY_ACCESS_INSTANCED_PROP(Props, _VAmount); curFrame = curFrame / (hAmount * vAmount * UNITY_ACCESS_INSTANCED_PROP(Props, _Speed)); curFrame -= floor (curFrame); curFrame = curFrame * (hAmount * vAmount - 1 ); curFrame = floor (curFrame); float row = floor (curFrame / hAmount); float col = floor (curFrame - row * hAmount); half2 uv = i.uv + half2(col,lerp(row,vAmount - row - 1 ,_StartNode)); uv.x /= hAmount; uv.y /= vAmount; fixed4 finalColor = tex2D(_MainTex, uv); finalColor *= UNITY_ACCESS_INSTANCED_PROP(Props, _Color); finalColor.a *= UNITY_ACCESS_INSTANCED_PROP(Props, _ClipAmount); return finalColor; } ENDCG } } }
这里我为了照顾到序列帧开始方向的不同所以设定了一个_StartNode变量。实际开发的时候,大家应该和美术对好序列帧开始的顺序。这样也就不用_StartNode变量了。
方法之间的区别
性能消耗上,自写Shader + Quad(Unity自带创建的3D物体) <
粒子 + 图集 < 粒子 + Sprite <
Animation。因为是自己写Shader,所以可以使用一些方法来节约性能。且现代GPU拥有强大的实力使得这样的渲染不在话下。在我测试下,粒子的消耗会大一些。而Animation的消耗是最大的。我认为是因为Sprite原本就有较大的消耗,再加上Animation组件的自身的消耗导致了使用Animation消耗大。
在使用方面,如果你看过上面的Shader,你会发现你很难去做帧事件。因为Shader是按照渲染的时间跑的。你只能自己去算在什么时间动画会到这个帧。当然你也可以对这个Shader进行改造,让其变成按照你传输的帧数来播放动画的Shader。但这个你要多写脚本去实现这个功能,并且性能方面的消耗也变大了。除此之外,因为游戏中的每帧的间隔时间是不是固定时间,所以可能会导致在下一帧本是动画中的第12帧的动画,而Shader却给了第20帧。而粒子和Animation本身就支持事件调用,所以我们只要简单的调用Unity的API就好了。但是如果你是使用了粒子+图集的方法,那这个事件调用就更难受了。对其我是没有什么好办法去做事件回调的。个人感觉在使用方面上Animation
> 粒子 + Sprite > Shader + Quad > 粒子 + 图集。
在表现方面,相同情况下Quad +
Shader会存在较为明显的锯齿。在使用Sprite的情况,粒子和Animation是一样的。因为它们都是用sprite组件来实现的。而粒子
+ 图集的情况则和Quad +
Shader的情况差不多。在我自己的测试下,总体而言Quad + Shader = 粒子 +
图集 < 粒子 + Sprite = Animation。
具体用什么方法来制作项目,取决于你个人的项目需求。这个也可以多方法混用。我个人项目因为对性能上的追求,所以我都是用Shader
+
Quad方式。但是我也体会到了一堆的痛苦,比如帧事件,材质数量的增加导致项目资源管理麻烦,透明物体层级问题等。
补充
使用UGUI中的Image播放序列帧的shader。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 Shader "Self/SequenceImager" { Properties { [PerRendererData] _MainTex ("Sprite Texture", 2 D) = "white" {} _Color ("Tint", Color) = (1 ,1 ,1 ,1 ) _StencilComp ("Stencil Comparison", Float) = 8 _Stencil ("Stencil ID", Float) = 0 _StencilOp ("Stencil Operation", Float) = 0 _StencilWriteMask ("Stencil Write Mask", Float) = 255 _StencilReadMask ("Stencil Read Mask", Float) = 255 _ColorMask ("Color Mask", Float) = 15 _AnimAmout ("AnimAmout",Vector) = (1 ,1 ,1 ,0 ) [Enum(StartBottom,0 ,StartTop,1 )] _StartNode ("StartNode",Int) = 0 [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0 } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Stencil { Ref [_Stencil] Comp [_StencilComp] Pass [_StencilOp] ReadMask [_StencilReadMask] WriteMask [_StencilWriteMask] } Cull Off Lighting Off ZWrite Off ZTest [unity_GUIZTestMode] Blend One OneMinusSrcAlpha ColorMask [_ColorMask] Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 2.0 #include "UnityCG.cginc" #include "UnityUI.cginc" #pragma multi_compile_local _ UNITY_UI_CLIP_RECT #pragma multi_compile_local _ UNITY_UI_ALPHACLIP struct appdata_t { float4 vertex : POSITION; float4 color : COLOR; float2 texcoord : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 vertex : SV_POSITION; fixed4 color : COLOR; float2 texcoord : TEXCOORD0; float4 worldPosition : TEXCOORD1; float4 mask : TEXCOORD2; UNITY_VERTEX_OUTPUT_STEREO }; sampler2D _MainTex; fixed4 _Color; fixed4 _TextureSampleAdd; float4 _ClipRect; float4 _MainTex_ST; float _UIMaskSoftnessX; float _UIMaskSoftnessY; float4 _AnimAmout; int _StartNode; v2f vert(appdata_t v) { v2f OUT; UNITY_SETUP_INSTANCE_ID(v); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT); float4 vPosition = UnityObjectToClipPos(v.vertex); OUT.worldPosition = v.vertex; OUT.vertex = vPosition; float2 pixelSize = vPosition.w; pixelSize /= float2(1 , 1 ) * abs (mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy)); float4 clampedRect = clamp (_ClipRect, -2e10 , 2e10 ); float2 maskUV = (v.vertex.xy - clampedRect.xy) / (clampedRect.zw - clampedRect.xy); OUT.texcoord = TRANSFORM_TEX(v.texcoord.xy, _MainTex); OUT.mask = float4(v.vertex.xy * 2 - clampedRect.xy - clampedRect.zw, 0.25 / (0.25 * half2(_UIMaskSoftnessX, _UIMaskSoftnessY) + abs (pixelSize.xy))); OUT.color = v.color * _Color; return OUT; } fixed4 frag(v2f IN) : SV_Target { const half alphaPrecision = half(0xff ); const half invAlphaPrecision = half(1.0 /alphaPrecision); IN.color.a = round (IN.color.a * alphaPrecision)*invAlphaPrecision; float curFrame = _Time.y - _AnimAmout.w; int hAmount = floor (_AnimAmout.x); int vAmount = floor (_AnimAmout.y); curFrame = curFrame / (hAmount * vAmount * _AnimAmout.z); curFrame -= floor (curFrame); curFrame = curFrame * (hAmount * vAmount - 1 ); curFrame = floor (curFrame); float row = floor (curFrame / hAmount); float col = floor (curFrame - row * hAmount); half2 uv = IN.texcoord + half2(col,lerp(row,vAmount - row - 1 ,_StartNode)); uv.x /= hAmount; uv.y /= vAmount; half4 color = IN.color * (tex2D(_MainTex, uv) + _TextureSampleAdd); #ifdef UNITY_UI_CLIP_RECT half2 m = saturate((_ClipRect.zw - _ClipRect.xy - abs (IN.mask.xy)) * IN.mask.zw); color.a *= m.x * m.y; #endif #ifdef UNITY_UI_ALPHACLIP clip (color.a - 0.001 ); #endif color.rgb *= color.a; return color; } ENDCG } } }
关于UI
世界坐标系下的UI表现
我们时常有这样的需求:在世界坐标系下显示名字或者一些图片。以前我只知道使用Canvas组件然后将其Render
Mode属性变为World
Space,然后再使用UI来代替。这样当然是简单的方法。不过在我项目推进的过程中,因为我项目中需要在世界坐标下显示UI的需求都较为简单,所以我觉得可以不用这样做。因为各种原因,我并没有去对比性能上的消耗。而且之后有点复杂的UI效果就要写Shader
+
代码去实现。所以我觉得这个也没多好。但我觉得这样的想法不错就记录一下。
对于一些UI上简单的组件比如Image和Text。我们可以使用Quad(sprite也可以)和TextMeshPro来代替。TextMeshPro是Unity官方的插件,具体的教程大家可以去网上搜索一下。而Quad代替的Image的方法是使用Shader来完成Image的功能。一般而言,我们需要在世界坐标系下显示的图片没有那么复杂,单纯Quad+Unity自带的Unlit/Transparent
Shader 或是Unlit/Transparent Cutout
Shader就可以满足这些要求。个人建议如果效果差不多还是使用Transparent
Cutout
Shader。因为Unity自带的透明Shader并没有多做深度写入,这会导致层级问题的出现。如果你嫌Quad很麻烦,其实你也可以使用Sprite组件来代替。因为这个是Unity官方的组件,所以它几乎可以平替Image。不过你要注意它自身的性能消耗。在我个人的使用中,我发现Sprite
+ Sprite
Mask所带来的性能消耗不能达到项目中对性能的要求。所以我才使用了Quad +
Shader,而Sprite + Sprite
Mask我也用一个Shader来代替。这样我只需要一个Quad就可以了。其Shader如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 Shader "self/IconShader" { Properties { _MainTex ("IconTex", 2 D) = "white" {} _MaskTex ("MaskTex", 2 D) = "white" {} _Color("Color", Color) = (1 ,1 ,1 ,1 ) _ClipAmout("ClipAmout",Range(0 ,1 )) = 0 } SubShader { Tags { "RenderType"="Transparent" "IgnoreProjector" = "True" "Queue" = "Transparent"} LOD 100 Pass { ZWrite On ColorMask 0 } Pass { Blend SrcAlpha OneMinusSrcAlpha Lighting Off ZWrite Off ZTest LEqual HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; float4 _MainTex_ST; sampler2D _MaskTex; float4 _MaskTex_ST; float4 _Color; half _ClipAmount; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = TRANSFORM_TEX(v.uv, _MainTex); UNITY_TRANSFER_FOG(o,o.vertex); return o; } half4 frag (v2f i) : SV_Target { half4 col = tex2D(_MainTex, i.uv); col.a *= tex2D(_MaskTex, i.uv).a; return col; } ENDHLSL } } }
在游戏中血条的显示也是很重要的,这个如果我们使用世界坐标系下的Image就很容易就实现了。但是因为我决定不用这个,所以我只好自己写一个Shader出来去做实现了。而且这个也要用代码去控制,我个人感觉这个确实有点麻烦了。其Shader如下:(关于改材质信息的代码,大家去网上找找就可以找到了。所以我就不写了。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 Shader "Self/SpriteFill" { Properties { [NoScaleOffset] _MainTex ("Texture", 2 D) = "white" {} _Ratio ("Ratio",Range(0 ,1 )) = 1 _CutOff("CutOff",Range(0 ,1 )) = 0.5 [Enum(circle,0 ,square,1 )] _HideType ("HideType",Float) = 0 } SubShader { Tags { "IgnoreProjector"="True" "RenderType"="Opaque" "Queue" = "AlphaTest" } Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma target 2.0 #pragma multi_compile_instancing #pragma multi_compile_local _ PIXELSNAP_ON #pragma multi_compile _ ETC1_EXTERNAL_ALPHA #include "UnityCG.cginc" #ifdef UNITY_INSTANCING_ENABLED UNITY_INSTANCING_BUFFER_START(PerDrawSprite) UNITY_DEFINE_INSTANCED_PROP(float , _HideType) UNITY_DEFINE_INSTANCED_PROP(half, _Ratio) UNITY_INSTANCING_BUFFER_END(PerDrawSprite) #define _HideType UNITY_ACCESS_INSTANCED_PROP(PerDrawSprite,_HideType) #define _Ratio UNITY_ACCESS_INSTANCED_PROP(PerDrawSprite,_Ratio) #endif // instancing CBUFFER_START(UnityPerDrawSprite) #ifndef UNITY_INSTANCING_ENABLED float _HideType; half _Ratio; #endif CBUFFER_END struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; struct v2f { float4 uv : TEXCOORD0; float4 vertex : SV_POSITION; UNITY_VERTEX_INPUT_INSTANCE_ID }; sampler2D _MainTex; float4 _MainTex_ST; fixed _CutOff; v2f vert (appdata v) { v2f o; UNITY_SETUP_INSTANCE_ID(v); UNITY_TRANSFER_INSTANCE_ID(v, o); o.vertex = UnityObjectToClipPos(v.vertex); o.uv.xy = TRANSFORM_TEX(v.uv, _MainTex); o.uv.zw = v.uv; return o; } fixed4 frag (v2f i) : SV_Target { UNITY_SETUP_INSTANCE_ID(i); fixed4 col = tex2D(_MainTex, i.uv); half ratio = 0 ; if (_HideType == 0 ) { half2 cuv = i.uv.zw - half2(0.5 ,0.5 ); float curRatio = atan2(cuv.y, cuv.x) / 6.2831852 + 0.5 ; clip(_Ratio - curRatio); } else if (_HideType == 1 ) clip(_Ratio - i.uv.z); clip(col.a - _CutOff); return col; } ENDCG } } }
PS:其实这里可以不使用TRANSFORM_TEX,因为我已经使用NoScaleOffset来确保贴图不会进行变化了。但是我想到你的项目可能有这样的需求,所以我都加了。你如果要放到你的项目中,记得优化一下这个Shader。
总结
这次的项目不大,整体的开发时间也就一个月左右。我觉得可以讲的大概就只有这些了。这算是我第一次去完全由我个人去做的2D项目。希望我分享的这些能给你带来一点帮助。
碎碎念
本来这个项目只是试水,所以项目由我一个来开发也可以。但后面迫于其他厂商的压力公司的决策就变了,这个项目突然就重要起来了。然后我就只能不停加班以应对策划需求的改动。而开发的时间还是不变。后来因为平台审核等其他原因,项目延迟了才给了我休息的时间。所以我就把这篇文章赶出来了。其实项目中能说还有一些,只是我觉得这些东西在其他项目上也能用上,所以想新开一个文章记录一下。最近因为不停的加班和个人生活出现了一些状况,导致了我没时间更新文章。之后如果这个项目没起来,那我有时间更新了。但是我心里还是想它起来的,至少也要半死不活啊。不然我的工作又要没了。接下来,如果有时间我一定会去努力更新文章了(我个人计划现在还有两篇文章可以写)。