基于Unity2019最新ECS架构开发MMO游戏笔记19
- 颜色混合
- 更新计划
- ECS系列目录
- ECS官方示例1:ForEach
- ECS官方案例2:IJobForEach
- ECS官方案例3:IJobChunk
- ECS官方案例4:SubScene
- ECS官方案例5:SpawnFromMonoBehaviour
- ECS官方案例6:SpawnFromEntity
- ECS官方案例7:SpawnAndRemove
- ECS进阶:FixedTimestepWorkaround
- ECS进阶:Boids
- ECS进阶:场景切换器
- ECS进阶:MegaCity0
- ECS进阶:MegaCity1
- UnityMMO资源整合&服务器部署
- UnityMMO选人流程
- UnityMMO主世界
- UnityMMO网络同步
- 用ECS做HexMap:自动生成地图系统
- 用ECS做HexMap:利用RenderMesh绘制六边形
- 用ECS做HexMap:利用RenderMesh为六边形涂色
- 用ECS做HexMap:六边形单元的颜色混合
- 用ECS做HexMap:重构地图系统
- 用ECS做HexMap:鼠标点击六边形单元涂色
颜色混合
在开始之前先纠正上一篇的错误,首先逐个打印颜色:
//打印六个方向的颜色:NE, E, SE, SW, W, NW
if (i == 0)
{
Debug.Log("自身颜色:" + color + " | NE方向颜色:" + neighbor);
}
然后分别得到第一个六边形单元的六个方向的颜色值:
HexCellColorRGBA(0.098, 0.902, 0.934, 1.000) | NE Direction ColorRGBA(0.638, 0.136, 0.585, 1.000)
HexCellColorRGBA(0.098, 0.902, 0.934, 1.000) | E Direction ColorRGBA(0.239, 0.273, 0.753, 1.000)
HexCellColorRGBA(0.098, 0.902, 0.934, 1.000) | SE Direction ColorRGBA(0.098, 0.902, 0.934, 1.000)
HexCellColorRGBA(0.098, 0.902, 0.934, 1.000) | SW Direction ColorRGBA(0.098, 0.902, 0.934, 1.000)
HexCellColorRGBA(0.098, 0.902, 0.934, 1.000) | W Direction ColorRGBA(0.098, 0.902, 0.934, 1.000)
HexCellColorRGBA(0.098, 0.902, 0.934, 1.000) | NW Direction ColorRGBA(0.098, 0.902, 0.934, 1.000)
如下图所示,第0个六边形单元的SE、SW、W、NW这四个方向的颜色应该与自身颜色一致才对。实际上打印出来的颜色也是如此,那为什么还会渲染出其他颜色来呢?
我调整矩阵的宽和高,使其只产生4个单元,终于发现了真相:
把最后一个放到第0个,然后依照顺序递推,就能得到正确的结果。为什么会产生这种问题的本质原因不明,但是逻辑上已经找到了蛛丝马迹,愚蠢的解决办法是按照其逻辑反递推,把最后一个插入到第0个,代码很简单:
//把最后一个放到第0顺位,后面的依此类推
Vector3 center;
if (i+1== vertices.Length)
{
center = vertices[0];
}
else
{
center = vertices[i+1];
}
本质原因是实体在Job中的执行顺序颠倒了,正常顺位应该是:0->1->2->3,但实际上变成了:3->1->2->0。
虽然不知道底层为什么要这样做,但是困扰我一夜的bug总算搞定了。
如上图所示,问题修复了!之前做了减法(单元数量4),现在做加法看看有没有问题:
好了,问题解决了,代码已推送Github!
颜色平均化
其实只需改动少量的代码即可完成这个功能,我们在HexCellData六边形单元数据中已经有字段来保存颜色数据了:
neighbor = (color + neighbor) * 0.5f;
可以看到,和最终效果还有一定距离,每3个相邻的单元就有四种颜色:
接下来就进行三个方向的颜色混合:
//保存需要混合的颜色
Color[] blendColors = new Color[6];
//把六个相邻单元的颜色添加到这个数组中,代码省略
………………
//三向混合
for (int j = 0; j < 6; j++)
{
int prev = (j - 1)< 0 ? 5 : j;
int next = (j + 1) > 5 ? 0 : j;
Colors.Add(color);
Colors.Add((color+blendColors[prev]+blendColors[j])/3F);
Colors.Add((color + blendColors[next] + blendColors[j]) / 3F);
}
效果如下,边缘的地方开始变得模糊,但是仍然明显。
区域混合
把每个六边形单元进行区域划分,中心区域保持本色,外围区域进行颜色混合,如下图所示:
在常量脚本HexMetrics中新增两个常量,用来计算中心本色区域和外围混合区域:
/// <summary>
/// 六边形单元中心本色区域占比
/// </summary>
public const float solidFactor = 0.75f;
/// <summary>
/// 六边形单元外围混合区域占比
/// </summary>
public const float blendFactor = 1f - solidFactor;
六边形的六个角因此受影响,与其相乘得出中心区域的顶点
/// <summary>
/// 六边形的六个角组成的数组
/// </summary>
public readonly static Vector3[] corners = {
new Vector3(0f, 0f, outerRadius)*solidFactor,
new Vector3(innerRadius, 0f, 0.5f * outerRadius)*solidFactor,
new Vector3(innerRadius, 0f, -0.5f * outerRadius)*solidFactor,
new Vector3(0f, 0f, -outerRadius)*solidFactor,
new Vector3(-innerRadius, 0f, -0.5f * outerRadius)*solidFactor,
new Vector3(-innerRadius, 0f, 0.5f * outerRadius)*solidFactor,
new Vector3(0f, 0f, outerRadius)*solidFactor
};
其他代码不用修改,我们就可以得到中心区域,如下图所示:
做了减法之后,我们需要做加法,把周边区域画出来。和我们画六边形一样,我们也要从顶点到三角,一步一步来。
三角化混合区域
画六边形的时候我们将其拆分成了六个三角,现在我们再来拆分这六个三角,将其分为中心区域和外围区域。
中心区域已经有了,外围是一个等腰梯形,它有四个顶点(Vertex),接下来我们就先计算它们:
//为每个六边形单元添加顶点、三角和颜色
for (int j = 0; j < 6; j++)
{
Vector3 V1 = (center + HexMetrics.SolidCorners[j]);
Vector3 V2 = (center + HexMetrics.SolidCorners[j + 1]);
int vertexIndex = Vertices.Length;
//添加中心区域的3个顶点
Vertices.Add(center);
Vertices.Add(V1);
Vertices.Add(V2);
Colors.Add(color);
Colors.Add(color);
Colors.Add(color);
//添加中心区域的三角
Triangles.Add(vertexIndex);
Triangles.Add(vertexIndex + 1);
Triangles.Add(vertexIndex + 2);
vertexIndex += 3;
//添加外围梯形区域的4个顶点
Vector3 V3 = (center + HexMetrics.Corners[j]);
Vector3 V4 = (center + HexMetrics.Corners[j + 1]);
Vertices.Add(V1);
Vertices.Add(V2);
Vertices.Add(V3);
Vertices.Add(V4);
//添加外围区域的三角
Triangles.Add(vertexIndex);
Triangles.Add(vertexIndex + 2);
Triangles.Add(vertexIndex + 1);
Triangles.Add(vertexIndex + 1);
Triangles.Add(vertexIndex + 2);
Triangles.Add(vertexIndex + 3);
//添加外圈区域三向颜色混合
int prev = (j - 1) < 0 ? 5 : j;
int next = (j + 1) > 5 ? 0 : j;
Colors.Add(color);
Colors.Add(color);
Colors.Add((color + blendColors[prev] + blendColors[j]) / 3F);
Colors.Add((color + blendColors[next] + blendColors[j]) / 3F);
}
效果如下图所示:
稍微提示下外围区域的代码,把等腰梯形拆分成两个三角,如下图所示:
这时每个六边形单元都像模像样了,只是有些细节还不是很好看,需要进一步平滑处理。
边界连接桥
为了使边界的颜色看起来更自然,我们把等腰梯形裁剪成矩形,作为连接单元之间的桥,如下图所示:
利用桥重新计算V3和V4:
//添加外围梯形区域的4个顶点
Vector3 bridge=(HexMetrics.GetBridge(j));
Vector3 V3 = (V1 + bridge);
Vector3 V4 = (V2 + bridge);
填充间隙
如上图所示,在六边形之间还有一些三角空隙,是我们裁剪掉的。这个空隙叫做桥洞好了,先填充左边桥洞:
/// <summary>
/// 画个三角补丁来填补桥洞
/// </summary>
//填充桥左边三角
vertexIndex += 4;
//添加桥三角的3个顶点
Vertices.Add(V1);
Vertices.Add(center+ HexMetrics.Corners[j]);
Vertices.Add(V3);
//添加桥洞区域的三角
Triangles.Add(vertexIndex);
Triangles.Add(vertexIndex + 1);
Triangles.Add(vertexIndex + 2);
Colors.Add(color);
Colors.Add((color + blendColors[prev] + blendColors[j]) / 3F);
Colors.Add(bridgeColor);
Ok,左边的桥洞已经填充了,再画一个三角补丁来填充右边的桥洞:
//填充桥右边三角
vertexIndex += 3;
//添加桥洞三角的3个顶点
Vertices.Add(V2);
Vertices.Add(V4);
Vertices.Add(center + HexMetrics.Corners[j+1]);
//添加桥洞区域的三角
Triangles.Add(vertexIndex);
Triangles.Add(vertexIndex + 1);
Triangles.Add(vertexIndex + 2);
Colors.Add(color);
Colors.Add(bridgeColor);
Colors.Add((color + blendColors[next] + blendColors[j]) / 3F);
依样画葫芦,桥洞都被三角补丁填充好了!
原作者说这已经是颜色混合的极限了,看起来不是很平滑,边界模糊。后面的教程会有改善,知乎大佬沈琰翻译了一些。有兴趣可以去看看详细的教程,和原作者几乎一致,我们这里用ECS实现会比较麻烦一些,后面再做优化吧。
边界合并
现在六边形单元之间的边界结构如下图所示:
这个图画得很明白了,两个六边形单元是通过两个矩形的桥连在一起的,三个单元之间又通过三个小三角形链接。
放到线框中观察,会发现三角其实挺多的,三角和顶点的数量会直接影响游戏的性能,因此我们来做一些优化。
首先优化六边形单元之间的桥,原来是两个矩形,现在合并成一个矩形:
/// <summary>
/// 获取相邻六边形单元之间的矩形桥
/// </summary>
/// <param name="cellIndex">六边形单元索引</param>
/// <returns>矩形桥</returns>
public static Vector3 GetBridge(int cellIndex)
{
//return (Corners[cellIndex] + Corners[cellIndex + 1]) * 0.5f * BlendFactor;
//这里把原来的0.5去掉即可
return (Corners[cellIndex] + Corners[cellIndex + 1]) * BlendFactor;
}
现在得到的桥是原来的两倍,所以单元之间就会出现重叠的桥,因此我们限制桥生成的数量:
if (j<=2)
{
vertexIndex += 3;
//添加外围梯形区域的4个顶点
Vector3 bridge = (HexMetrics.GetBridge(j));
Vector3 V3 = (V1 + bridge);
Vector3 V4 = (V2 + bridge);
Vertices.Add(V1);
Vertices.Add(V2);
Vertices.Add(V3);
Vertices.Add(V4);
//添加外围区域的三角
Triangles.Add(vertexIndex);
Triangles.Add(vertexIndex + 2);
Triangles.Add(vertexIndex + 1);
Triangles.Add(vertexIndex + 1);
Triangles.Add(vertexIndex + 2);
Triangles.Add(vertexIndex + 3);
}
于是我们得到了下图所示的限量版的桥,我们发现桥尽管限制了数量,但是边缘部分还是多出来了。
接下来裁剪掉这些多余的桥,只需加一个判断即可:
//如果没有相邻的单元,则跳过循环
if (blendColors[j]==color)
{//自身颜色和混合颜色相同,说明不需要混合,进一步说明没有相邻的单元
continue;
}
裁剪后如下图所示:
再次填充桥之间的大三角形,这次我们直接打一个大补丁,由于三个单元之间只需要打一个三角补丁,我们还限制了补丁的数量,从而避免三角补丁的重合:
int next = (j + 1) > 5 ? 0 : (j + 1);
if (j<=1 && blendColors[next] != color)
{
//填充桥三角
vertexIndex += 4;
//添加桥三角的3个顶点
Vertices.Add(V2);
Vertices.Add(V4);
Vector3 V5 = (V2 + HexMetrics.GetBridge(next));
Vertices.Add(V5);
//添加桥洞区域的三角
Triangles.Add(vertexIndex);
Triangles.Add(vertexIndex + 1);
Triangles.Add(vertexIndex + 2);
Colors.Add(color);
Colors.Add((color + blendColors[next] + blendColors[j]) / 3F);
Colors.Add(bridgeColor);
}
渲染效果如下图所示:
再通过线框图,我们发现减少了许多顶点,大概是36*47个顶点。
三角形也大量减少了,但是我们的图也看起来不完整了,下一篇将填补周边缺少的边缘区域。
更新计划
作者的话
如果喜欢我的文章可以点赞支持一下,谢谢鼓励!如果有什么疑问可以给我留言,有错漏的地方请批评指证!
如果有技术难题需要讨论,可以加入开发者联盟:566189328(付费群)为您提供有限的技术支持,以及,心灵鸡汤!
当然,不需要技术支持也欢迎加入进来,随时可以请我喝咖啡、茶和果汁!( ̄┰ ̄*)