Unity的2.5D效果分析
发表于2018-12-21
2.5D可能并不是一个很精准的词,这里所指的是DNF这种可以在3个轴向上运动的2D精灵类型的效果。
以DNF为例,精灵实际上没有Z轴坐标的,当按↑,人物向屏幕里(远离玩家)方向移动时(z轴),在渲染上,我们看到人物是在向上移动。而我们按c,人物跳起(y轴),我们仍然看到人物在向上移动。这就是DNF类游戏看起来有点3d效果的奥秘:它营造一种我们在斜向下45度(或者其它角度)观看地面的感觉,因此当人物坐标在Z轴移动时,在屏幕投影位置上我们看到人物向上或向下移动。
所以,这种效果可以通过3维坐标在渲染时的一个变换来完成。
设点A(x1,y1,z1),那么它渲染时的世界坐标会变换为:
x2 = x1,
y2 =y1*cos(a)+z1*cos(90-a)
z2 = 0
其中a代表视线与xz平面的夹角,不过一般Y轴不计算投影,因为要保持精灵的正常显示,不能让它因斜视而变矮。
因此,y2 = y1 + z1*cos(90-a)
最开始,我打算了unity中为一个物体建立两个gameobject,一个物体赋值3维坐标,因为它处于真实位置,因此可直接使用物理系统,另一个物体用上面的映射方法,计算出坐标并使用它来表示精灵的位置,SpriteRenderer附加在第二个物体上。我确实这样实现了,也运行正常。
不过后面我想到,可以用shader来更改显示位置,这样只用一个gameobject,用普通Sprite即可,比较符合直觉,也便于使用。下面先看下效果:
45度角:
这样好处在于,物体的transform和boxcollider仍然是在正常的3D空间中的,所以碰撞检测可以完全以我们熟悉的3D物理组件来完成,而不需要自己实现。
另外,对于要做小游戏的程序员来说,2D游戏对素材要求会小一些。用此方式,表现力会比用单纯的2d精灵和2d物理好很多。
附上shader,用精灵的默认shader改的:
Shader "Sprites/YX2.5D" { Properties { [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {} _Color ("Tint", Color) = (1,1,1,1) [MaterialToggle] PixelSnap ("Pixel snap", Float) = 0 [HideInInspector] _RendererColor ("RendererColor", Color) = (1,1,1,1) [HideInInspector] _Flip ("Flip", Vector) = (1,1,1,1) [PerRendererData] _AlphaTex ("External Alpha", 2D) = "white" {} [PerRendererData] _EnableExternalAlpha ("Enable External Alpha", Float) = 0 [HideInInspector] ZFactor ("Z-Factor",float) = 0.707 [HideInInspector] WOrigin ("Origin",Vector) = (0,0,0,0) } SubShader { Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" "PreviewType"="Plane" "CanUseSpriteAtlas"="True" } Cull Off Lighting Off ZWrite Off Blend One OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex SpriteVert #pragma fragment SpriteFrag #pragma target 2.0 #pragma multi_compile_instancing #pragma multi_compile _ PIXELSNAP_ON #pragma multi_compile _ ETC1_EXTERNAL_ALPHA #include "UnityCG.cginc" #ifdef UNITY_INSTANCING_ENABLED UNITY_INSTANCING_CBUFFER_START(PerDrawSprite) // SpriteRenderer.Color while Non-Batched/Instanced. fixed4 unity_SpriteRendererColorArray[UNITY_INSTANCED_ARRAY_SIZE]; // this could be smaller but that's how bit each entry is regardless of type float4 unity_SpriteFlipArray[UNITY_INSTANCED_ARRAY_SIZE]; UNITY_INSTANCING_CBUFFER_END #define _RendererColor unity_SpriteRendererColorArray[unity_InstanceID] #define _Flip unity_SpriteFlipArray[unity_InstanceID] #endif // instancing CBUFFER_START(UnityPerDrawSprite) #ifndef UNITY_INSTANCING_ENABLED fixed4 _RendererColor; float4 _Flip; #endif float _EnableExternalAlpha; CBUFFER_END // Material Color. fixed4 _Color; float4 WOrigin; float ZFactor; float YFactor; 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; UNITY_VERTEX_OUTPUT_STEREO }; v2f SpriteVert(appdata_t IN) { v2f OUT; UNITY_SETUP_INSTANCE_ID (IN); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT); #ifdef UNITY_INSTANCING_ENABLED IN.vertex.xy *= _Flip.xy; #endif float4 wpos = mul(unity_ObjectToWorld, IN.vertex)-WOrigin; float4 wposMap = float4(wpos.x,wpos.y + wpos.z*ZFactor,0,wpos.w); OUT.vertex = mul(UNITY_MATRIX_VP,wposMap); OUT.texcoord = IN.texcoord; OUT.color = IN.color * _Color * _RendererColor; #ifdef PIXELSNAP_ON OUT.vertex = UnityPixelSnap (OUT.vertex); #endif return OUT; } sampler2D _MainTex; sampler2D _AlphaTex; fixed4 SampleSpriteTexture (float2 uv) { fixed4 color = tex2D (_MainTex, uv); #if ETC1_EXTERNAL_ALPHA fixed4 alpha = tex2D (_AlphaTex, uv); color.a = lerp (color.a, alpha.r, _EnableExternalAlpha); #endif return color; } fixed4 SpriteFrag(v2f IN) : SV_Target { fixed4 c = SampleSpriteTexture (IN.texcoord) * IN.color; c.rgb *= c.a; return c; } ENDCG } } }
使用方法为,创建一个材质,使用上面提供的shader,默认就是45度角的效果。然后SpriteRender使用该材质。
为了方便起见,还写了一个c#脚本,方便视角、原点等参数设置,但它不是必须的:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class Sprite25D : MonoBehaviour { public Vector3 Origin; public float Angle = 45; private new SpriteRenderer renderer; private void OnEnable() { Apply(); } public void Apply() { if (renderer == null) { renderer = GetComponent<SpriteRenderer>(); } Material mat; if (Application.isPlaying) mat = renderer.material; else mat = renderer.sharedMaterial; mat.SetVector("WOrigin", Origin); mat.SetFloat("ZFactor", Mathf.Sin(Angle * Mathf.Deg2Rad)); } }
using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEditor; [CustomEditor(typeof(Sprite25D))] public class Sprite25DInspector : Editor { public override void OnInspectorGUI() { base.OnInspectorGUI(); GUILayout.Label("以下操作会修改sharedMaterial"); if (GUILayout.Button("设置当前位置为映射原点")) { var inst = (target as Sprite25D); var pos = inst.transform.position; inst.Origin = new Vector3(pos.x, pos.y, pos.z); inst.Apply(); } if (GUILayout.Button("重置原点为zero")) { var inst = (target as Sprite25D); inst.Origin = Vector3.zero; inst.Apply(); } } }
就是这样了。