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();
}
}
}
就是这样了