# 前言
使用 Unity 实现脚印,这种功能很简单,而且之前我也写过一篇相关 Blog 来描述: 链接
过去很久之后,这个功能终于又被 “旧事重提” 了,不过,新的要求是:根据玩家在地形不同的位置,检测地形相应点的贴图,再根据贴图来判断播放的特效、音效及其它处理。因为策划希望能在地形相应的区域,有着不同的效果,而最省力的实现方式,便莫过于如此了。
这个方式的最大难点大约在于:如何取得玩家当前点的地形贴图?包括几张贴图的混合区域等。
# 分析
为了获取到这个信息,我首先调试了整个地形数据,并分析其中 “可能” 有用的几项,主要位于 TerrainData 中:
// 摘要: | |
// /// | |
// Height of the alpha map. | |
// /// | |
public int alphamapHeight { get; } | |
// 摘要: | |
// /// | |
// Width of the alpha map. | |
// /// | |
public int alphamapWidth { get; } | |
// 摘要: | |
// /// | |
// The total size in world units of the terrain. | |
// /// | |
public Vector3 size { get; set; } | |
// 摘要: | |
// /// | |
// Splat texture used by the terrain. | |
// /// | |
public SplatPrototype[] splatPrototypes { get; set; } |
在上边几个参数中,只所以将 AlphaMap 当作重要参数,是因为在 Unity 中,不同的地形贴图,就是全靠这个贴图进行混合完成的,所以它也是获取各贴图的重要线索。
Size 作为地形的大小,用于判断 Alpha 贴图与地形实际大小的差距 (如果有的话)。比如我这儿地形大小是 4096,贴图大小是 2048。为此,在取样的时候,同样必须注意坐标的转换。
splatPrototypes 更不必说,这就是地形所用到的几张贴图了,到时候最终信息肯定就是从这儿取。
在 TerrainData 中,提供了一个 “GetAlphamaps” 的接口,这个接口便可以用于获取指定点指定宽度的 Alpha 贴图信息:
// 摘要: | |
// /// | |
// Returns the alpha map at a position x, y given a width and height. | |
// /// | |
// | |
// 参数: | |
// x: | |
// | |
// y: | |
// | |
// width: | |
// | |
// height: | |
[WrapperlessIcall] | |
public float[,,] GetAlphamaps(int x, int y, int width, int height); |
查询官方文档,可以明白这个接口返回值得意义:第一维即 X 坐标,第二维 Y 坐标,重要的是第三维:地形贴图的贡献值!
刚开始不是很清楚这个接口的使用,还花了些时间做调试。这个第三维,即地形贴图的贡献,大小与 splatPrototypes 的大小一致,简单来说,就是地形贴图有多少,第三维就有多大。并且值位于 0~1 之间。
既然如此,这就好办了,循环输出一次,就可以发现这一维与 splatPrototypes 一一对应,并以值 0~1 代表相应贴图在地形指定点的贡献。并且,所有的总和同样为 1。
# 实现
现在,所有的信息已经就位。改一下上一遍实现脚印的 Blog 中检测地面的代码,从检测地面的 Hit 中获取地形组件:
Terrain terrain = hit.transform.GetComponent<Terrain>(); |
然后将世界坐标转化为贴图坐标,并于 AlphaMap 中进行采样 (这儿的坐标转化方式因地而异,因为不同大小地形与贴图比例自然需要不同的转换方式,我连调试都是直接在大地形上做的,因为数据太大,还费了不少功夫。现在想来,完全可以新建一个小号的哩):
Vector3 inTerrainPos = (hit.point - terrain.GetPosition()) * 0.5f; | |
var maps = terrain.terrainData.GetAlphamaps((int)inTerrainPos.x, (int)inTerrainPos.z, 1, 1); |
最后,循环找出贴图中对当前点贡献最大的那张贴图,并返回名字。这样,就可以根据策划填写的数据库,索引相应的音效、特效等等:
int length = maps.GetLength(2); | |
int index = 0; | |
float alpha = 0; | |
for (int i = 0; i < length; i++) | |
{ | |
float a = maps[0, 0, i]; | |
if (a > alpha) | |
{ | |
alpha = a; | |
index = i; | |
} | |
if (alpha == 1) break; | |
} | |
return terrain.terrainData.splatPrototypes[index].texture.name; |
策划给出的表格如下:
序号 | 场景描述 | 跑(走)动音效 | 跳跃音效 | 落地音效 | 跳跃特效(白天) | 跳跃落地特效(白天) | 脚印(白天) | 跳跃特效(晚上) | 跳跃落地特效(晚上) | 脚印(晚上) |
---|---|---|---|---|---|---|---|---|---|---|
Terrain01 | 草地 | 42006 | 42007 | 42008 | 20002 | 20008 | 20014 | 20003 | 20009 | 20015 |
Terrain02 | 草地 | 42006 | 42007 | 42008 | 20002 | 20008 | 20014 | 20003 | 20009 | 20015 |
Terrain17 | 草地 | 42006 | 42007 | 42008 | 20002 | 20008 | 20014 | 20003 | 20009 | 20015 |
Terrain04 | 硬地 | 42000 | 42001 | 42002 | 20001 | 20007 | 20013 | 20004 | 20010 | 20016 |
Terrain06 | 硬地 | 42000 | 42001 | 42002 | 20001 | 20007 | 20013 | 20004 | 20010 | 20016 |
Terrain09 | 硬地 | 42000 | 42001 | 42002 | 20001 | 20007 | 20013 | 20004 | 20010 | 20016 |
Terrain14 | 硬地 | 42000 | 42001 | 42002 | 20001 | 20007 | 20013 | 20004 | 20010 | 20016 |
Terrain16 | 硬地 | 42000 | 42001 | 42002 | 20001 | 20007 | 20013 | 20004 | 20010 | 20016 |
Terrain19 | 硬地 | 42000 | 42001 | 42002 | 20001 | 20007 | 20013 | 20004 | 20010 | 20016 |
Terrain03 | 沙漠地形 | 42003 | 42004 | 42005 | 20005 | 20011 | 20017 | 20006 | 20012 | 20018 |
Terrain23 | 沙漠地形 | 42003 | 42004 | 42005 | 20005 | 20011 | 20017 | 20006 | 20012 | 20018 |
Terrain24 | 沙漠地形 | 42003 | 42004 | 42005 | 20005 | 20011 | 20017 | 20006 | 20012 | 20018 |
# 源码
源码就发主要的方法吧,至于获得了贴图信息,然后查表、处理各项功能,就是些简单重复的功能了,这儿就不多赘述。
public static string CheckToGround(Transform transform, GameObject footPrint) | |
{ | |
RaycastHit hit; | |
Ray ray = new Ray(transform.position + Vector3.up * 0.5f, Vector3.down); | |
if (Physics.Raycast(ray, out hit, 0.7f, m_groundLayer)) | |
{ | |
if (footPrint) | |
{ | |
footPrint.transform.position = hit.point; | |
footPrint.transform.LookAt(hit.point + hit.normal, transform.forward); | |
} | |
Terrain terrain = hit.transform.GetComponent<Terrain>(); | |
if (terrain) | |
{ | |
Vector3 inTerrainPos = (hit.point - terrain.GetPosition()) * 0.5f; | |
var maps = terrain.terrainData.GetAlphamaps((int)inTerrainPos.x, (int)inTerrainPos.z, 1, 1); | |
int length = maps.GetLength(2); | |
int index = 0; | |
float alpha = 0; | |
for (int i = 0; i < length; i++) | |
{ | |
float a = maps[0, 0, i]; | |
if (a > alpha) | |
{ | |
alpha = a; | |
index = i; | |
} | |
if (alpha == 1) break; | |
} | |
return terrain.terrainData.splatPrototypes[index].texture.name; | |
} | |
} | |
return ""; | |
} |