用ECS做HexMap:不规则化

基于Unity2019最新ECS架构开发MMO游戏笔记23

准备工作

如果大佬对ECS版的海克斯无限地图感兴趣,不妨参与进来,欢迎Star/Folk源码
0下载Unity编辑器(2019.1.12f1 or 更新的版本),if(已经下载了)continue;
1克隆:git clone https://github.com/cloudhu/HexMapMadeInUnity2019ECS.git --recurse下载Zip压缩包
2如果下载的是压缩包,需要先将压缩包解压。然后将HexMapMadeInUnity2019ECS添加到Unity Hub项目中;
3用Unity Hub打开的开源项目:HexMapMadeInUnity2019ECS,等待Unity进行编译工作;
4打开项目后,启动场景在Scenes目录下,打开AdvancedHexMap场景(优化重构的版本)。

失败品

昨天度过了非常纠结的一天,先展示下失败的作品吧:
用ECS做HexMap:不规则化
失败在哪里呢?理想中的效果应该是这样的:
用ECS做HexMap:不规则化
需求是要实现不规则化,同时应该保留一定的规律性,比如六边形单元不再是规则的六条边,而是不规则的多边形,但是整个表面是相对平整的。失败的本质原因是ECS无法使用Texture2D相关方法,例如Texture2D.GetPixelBilinear(),大概是因为Texture2D是OOP架构下的对象,或者是Texture2D只能在主线程上运行,总之是无法使用。

这使得原本应该在CellSystem中进行的噪声干扰,不得不延迟到OOP世界中来做,这样的后果就是无法准确的进行特定干扰。
虽然是失败的案例,还是从头进行代码Review吧,首先需要噪声,这里原版教程已经提供,直接使用:
用ECS做HexMap:不规则化
图的设置也必须按照原版教程的设置:
用ECS做HexMap:不规则化
然后通过代码来进行噪声采样,写在HexMetrics脚本里:

    #region 噪声干扰

    /// <summary>
    /// 海拔干扰度
    /// </summary>
    public const float elevationPerturbStrength = 1.5f;

    /// <summary>
    /// 噪源
    /// </summary>
    public static Texture2D noiseSource;

    /// <summary>
    /// 噪声缩放
    /// </summary>
    public const float noiseScale = 0.003f;

    /// <summary>
    /// 噪声采样
    /// </summary>
    /// <param name="position">顶点位置</param>
    /// <returns>双线性过滤</returns>
    public static Vector4 SampleNoise(Vector3 position)
    {
        return noiseSource.GetPixelBilinear(position.x * noiseScale, position.z * noiseScale);
    }

    public const float cellPerturbStrength = 3f;

    #endregion

这样我们就可以在OOP中调用了,直接对每一个顶点进行干扰,而不是像原作者那样只对特定区域的顶点进行干扰。
这是很尴尬的事情,但是先这样做吧,代码写在MainWorld脚本里

    /// <summary>
    /// 噪声干扰
    /// </summary>
    /// <param name="position">顶点位置</param>
    /// <returns>被干扰的位置</returns>
    Vector3 Perturb(Vector3 position)
    {
        Vector4 sample = HexMetrics.SampleNoise(position);
        position.x += (sample.x * 2f - 1f) * HexMetrics.cellPerturbStrength;
        position.y += (sample.y * 2f - 1f) * HexMetrics.cellPerturbStrength;
        position.z += (sample.z * 2f - 1f) * HexMetrics.cellPerturbStrength;
        return position;
    }

接下来对获取到的每一个顶点进行暴力的、统一的干扰,生成怪异的、完全不规则的地图:

Vector3 vertex = Perturb(vertexBuffer[j]);

顶点是从动态缓存中获取的,这个在之前的ECS博文中讲过了。
我也想过使用柏林噪声,然后在Job中使用,这样就没有Texture2D的限制了,而且还利用了多线程的优势。
所以在原作者那里直接Copy了一份柏林噪声的算法:

    #region Perlin Noise
    /// <summary>
    /// Perlin哈希表
    /// </summary>
    private readonly static int[] hash = {
        151,160,137, 91, 90, 15,131, 13,201, 95, 96, 53,194,233,  7,225,
        140, 36,103, 30, 69,142,  8, 99, 37,240, 21, 10, 23,190,  6,148,
        247,120,234, 75,  0, 26,197, 62, 94,252,219,203,117, 35, 11, 32,
        57,177, 33, 88,237,149, 56, 87,174, 20,125,136,171,168, 68,175,
        74,165, 71,134,139, 48, 27,166, 77,146,158,231, 83,111,229,122,
        60,211,133,230,220,105, 92, 41, 55, 46,245, 40,244,102,143, 54,
        65, 25, 63,161,  1,216, 80, 73,209, 76,132,187,208, 89, 18,169,
        200,196,135,130,116,188,159, 86,164,100,109,198,173,186,  3, 64,
        52,217,226,250,124,123,  5,202, 38,147,118,126,255, 82, 85,212,
        207,206, 59,227, 47, 16, 58, 17,182,189, 28, 42,223,183,170,213,
        119,248,152,  2, 44,154,163, 70,221,153,101,155,167, 43,172,  9,
        129, 22, 39,253, 19, 98,108,110, 79,113,224,232,178,185,112,104,
        218,246, 97,228,251, 34,242,193,238,210,144, 12,191,179,162,241,
        81, 51,145,235,249, 14,239,107, 49,192,214, 31,181,199,106,157,
        184, 84,204,176,115,121, 50, 45,127,  4,150,254,138,236,205, 93,
        222,114, 67, 29, 24, 72,243,141,128,195, 78, 66,215, 61,156,180
    };

    /// <summary>
    /// 哈希遮罩
    /// </summary>
    private const int hashMask = 255;

    #region Perlin1D

    public static float Perlin1D(Vector3 point, float frequency)
    {
        point *= frequency;
        int i0 = Mathf.FloorToInt(point.x);
        float t0 = point.x - i0;
        float t1 = t0 - 1f;
        i0 &= hashMask;
        int i1 = i0 + 1;

        float g0 = gradients1D[hash[i0] & gradientsMask1D];
        float g1 = gradients1D[hash[i1] & gradientsMask1D];

        float v0 = g0 * t0;
        float v1 = g1 * t1;

        float t = Smooth(t0);
        return Mathf.Lerp(v0, v1, t) * 2f;
    }

    private static float[] gradients1D = {
        1f, -1f
    };

    private const int gradientsMask1D = 1;

    #endregion

    #region Perlin2D

    public static float Perlin2D(Vector3 point, float frequency)
    {
        point *= frequency;
        int ix0 = Mathf.FloorToInt(point.x);
        int iy0 = Mathf.FloorToInt(point.y);
        float tx0 = point.x - ix0;
        float ty0 = point.y - iy0;
        float tx1 = tx0 - 1f;
        float ty1 = ty0 - 1f;
        ix0 &= hashMask;
        iy0 &= hashMask;
        int ix1 = ix0 + 1;
        int iy1 = iy0 + 1;

        int h0 = hash[ix0];
        int h1 = hash[ix1];
        Vector2 g00 = gradients2D[hash[h0 + iy0] & gradientsMask2D];
        Vector2 g10 = gradients2D[hash[h1 + iy0] & gradientsMask2D];
        Vector2 g01 = gradients2D[hash[h0 + iy1] & gradientsMask2D];
        Vector2 g11 = gradients2D[hash[h1 + iy1] & gradientsMask2D];

        float v00 = Dot(g00, tx0, ty0);
        float v10 = Dot(g10, tx1, ty0);
        float v01 = Dot(g01, tx0, ty1);
        float v11 = Dot(g11, tx1, ty1);

        float tx = Smooth(tx0);
        float ty = Smooth(ty0);
        return Mathf.Lerp(
                   Mathf.Lerp(v00, v10, tx),
                   Mathf.Lerp(v01, v11, tx),
                   ty) * sqr2;
    }

    private readonly static Vector2[] gradients2D = {
        new Vector2( 1f, 0f),
        new Vector2(-1f, 0f),
        new Vector2( 0f, 1f),
        new Vector2( 0f,-1f),
        new Vector2( 1f, 1f).normalized,
        new Vector2(-1f, 1f).normalized,
        new Vector2( 1f,-1f).normalized,
        new Vector2(-1f,-1f).normalized
    };

    private const int gradientsMask2D = 7;
    private static float sqr2 = Mathf.Sqrt(2f);

    #endregion

    #region Perlin3D

    private static Vector3[] gradients3D = {
        new Vector3( 1f, 1f, 0f),
        new Vector3(-1f, 1f, 0f),
        new Vector3( 1f,-1f, 0f),
        new Vector3(-1f,-1f, 0f),
        new Vector3( 1f, 0f, 1f),
        new Vector3(-1f, 0f, 1f),
        new Vector3( 1f, 0f,-1f),
        new Vector3(-1f, 0f,-1f),
        new Vector3( 0f, 1f, 1f),
        new Vector3( 0f,-1f, 1f),
        new Vector3( 0f, 1f,-1f),
        new Vector3( 0f,-1f,-1f),

        new Vector3( 1f, 1f, 0f),
        new Vector3(-1f, 1f, 0f),
        new Vector3( 0f,-1f, 1f),
        new Vector3( 0f,-1f,-1f)
    };

    private const int gradientsMask3D = 15;

    public static float Perlin3D(Vector3 point, float frequency)
    {
        point *= frequency;
        int ix0 = Mathf.FloorToInt(point.x);
        int iy0 = Mathf.FloorToInt(point.y);
        int iz0 = Mathf.FloorToInt(point.z);
        float tx0 = point.x - ix0;
        float ty0 = point.y - iy0;
        float tz0 = point.z - iz0;
        float tx1 = tx0 - 1f;
        float ty1 = ty0 - 1f;
        float tz1 = tz0 - 1f;
        ix0 &= hashMask;
        iy0 &= hashMask;
        iz0 &= hashMask;
        int ix1 = ix0 + 1;
        int iy1 = iy0 + 1;
        int iz1 = iz0 + 1;

        int h0 = hash[ix0];
        int h1 = hash[ix1];
        int h00 = hash[h0 + iy0];
        int h10 = hash[h1 + iy0];
        int h01 = hash[h0 + iy1];
        int h11 = hash[h1 + iy1];
        Vector3 g000 = gradients3D[hash[h00 + iz0] & gradientsMask3D];
        Vector3 g100 = gradients3D[hash[h10 + iz0] & gradientsMask3D];
        Vector3 g010 = gradients3D[hash[h01 + iz0] & gradientsMask3D];
        Vector3 g110 = gradients3D[hash[h11 + iz0] & gradientsMask3D];
        Vector3 g001 = gradients3D[hash[h00 + iz1] & gradientsMask3D];
        Vector3 g101 = gradients3D[hash[h10 + iz1] & gradientsMask3D];
        Vector3 g011 = gradients3D[hash[h01 + iz1] & gradientsMask3D];
        Vector3 g111 = gradients3D[hash[h11 + iz1] & gradientsMask3D];

        float v000 = Dot(g000, tx0, ty0, tz0);
        float v100 = Dot(g100, tx1, ty0, tz0);
        float v010 = Dot(g010, tx0, ty1, tz0);
        float v110 = Dot(g110, tx1, ty1, tz0);
        float v001 = Dot(g001, tx0, ty0, tz1);
        float v101 = Dot(g101, tx1, ty0, tz1);
        float v011 = Dot(g011, tx0, ty1, tz1);
        float v111 = Dot(g111, tx1, ty1, tz1);

        float tx = Smooth(tx0);
        float ty = Smooth(ty0);
        float tz = Smooth(tz0);
        return Mathf.Lerp(
            Mathf.Lerp(Mathf.Lerp(v000, v100, tx), Mathf.Lerp(v010, v110, tx), ty),
            Mathf.Lerp(Mathf.Lerp(v001, v101, tx), Mathf.Lerp(v011, v111, tx), ty),
            tz);
    }

    #endregion

    private static float Dot(Vector3 g, float x, float y, float z)
    {
        return g.x * x + g.y * y + g.z * z;
    }

    private static float Dot(Vector2 g, float x, float y)
    {
        return g.x * x + g.y * y;
    }

    /// <summary>
    /// 平滑
    /// </summary>
    /// <param name="t">参数</param>
    /// <returns></returns>
    private static float Smooth(float t)
    {
        return t * t * t * (t * (t * 6f - 15f) + 10f);
    }
    #endregion

这个计算相当复杂,我数学不太好,抱着能用就行的心态这么做了,把方法直接放到CellSystem中:

    //噪声频率
	private float frequency = 5f;
	//高度
	private const float height = 0.2f;

    /// <summary>
    /// 噪声干扰
    /// </summary>
    /// <param name="position">顶点位置</param>
    /// <returns>被干扰的位置</returns>
    Vector3 Perturb(Vector3 position)
    {
        position.x += (height * HexMetrics.Perlin3D(position, frequency)* 2f - 1f) * HexMetrics.cellPerturbStrength;
        position.y += (height * HexMetrics.Perlin3D(position, frequency)* 2f - 1f) * HexMetrics.cellPerturbStrength;
        position.z += (height * HexMetrics.Perlin3D(position, frequency)* 2f - 1f) * HexMetrics.cellPerturbStrength;
        return position;
    }

然后再在对应的位置进行干扰,而不是全局干扰,结果仍然不理想。这是因为这个算法得出的是散列的噪点,而不像那张噪声图一样,虽然都是Perlin的算法,却成为一个整体。可以利用UV去找到对应的点,从而进行干扰。
这里对每一个点单独进行干扰,必然会造成混乱的结果,并非我们的需求。
那么就还原一张噪声贴图,再利用贴图上的点来进行干扰?
这是可行的,但是同样也是复杂的,考虑到自己的数学水平,还是放弃这么做了。如果有数学比较好的大佬,可以对这部分进行优化,这个噪声的bug先保留在项目中,我也新建了一个Issue,等待诸君来解决。
用ECS做HexMap:不规则化
同样也在官方论坛发帖,希望官方有对应的解决方案,这里留一个官方DOTS论坛的超链接给大家,有问题可以去发帖。
这一篇暂时先这样,后续如果有改进方案,再来更新,下面是分界线:
——————————————————二〇一九年八月三十日 10:39:33——————————————————————

ECS专题目录

ECS更新计划

作者的话

用ECS做HexMap:不规则化

如果喜欢可以点赞支持一下,谢谢鼓励!如果有什么疑问可以给我留言,有错漏的地方请批评指证!
技术难题?加入开发者联盟:566189328(QQ付费群)提供有限技术探讨,以及,心灵鸡汤Orz!
当然,不需要技术探讨也欢迎加入进来,在这里劈柴、遛狗、聊天、撸猫!( ̄┰ ̄*)

上一篇:tableau制作某教育平台看板


下一篇:GSS系列中线段树部分的学习笔记