Unity的2.5D效果分析

发表于2018-12-21
评论2 1.1w浏览
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();
        }
    }
}
就是这样了。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引