用ECS做HexMap:自动生成地图系统

31 篇文章 2 订阅
31 篇文章 3 订阅

自动生成地图系统

ECS的世界由许许多多的系统来操控,在进入主世界的时候会创建这些系统,如下图所示:

CreateSystem
CreateSystem
CreateSystem
CreateSystem
MainWorld
PlayerInputSystem
CreateTargetPosFromUserInputSystem
MovementUpdateSystem
etc等等System

上一篇中PlayerInputSystem负责处理玩家的操作,与之对应的组件有UserCommand(用户命令),TargetPosition(目标位置)和MoveSpeed(移动速度)。原本想一起看看源码,加一点注释进去,算是走马观花,画蛇添足。不过,这样做实在没有太多营养价值,如果大家有兴趣,自行看下源码吧。这一篇想写一点创造性的东西,例如生动生成地图系统。

AutoCreateMapSystem

灵感来源于Unity Hex Map Tutorial,我觉得自动生成地图这件事情太适合ECS了,为什么?

  • 自动生成的地图涉及到大量的实体;
  • ECS的性能是为大世界而生,在其性能加持下,我们可以生成无限世界;
  • 逻辑解耦,分工明确。

不管怎样,都值得尝试一下。
说下我的大概需求:

  1. 自动生成地图,利用各种System来制定地图的规则,使其尽量贴近自然;
  2. 无限地图,玩家离地图边缘一定距离后,预判玩家行走线路并在其方向上动态扩展;
  3. 将地图数据保存到服务器,与其他玩家进行同步;
  4. 动态加载和动态裁剪,以最小的资源做出最大的地图;
  5. 地图与玩家互动,可破坏,可创建,所有操作进行网络同步。

神奇的六边形

我觉得像MegaCity那样的大地图,太吃资源,如果把地图的所有一切都转换成数据。然后再通过数据来驱动无限地图,这样也许很有意思,但是也不是随机生成所有一切,要利用算法来尽量还原大自然的规则。
大概就是这样,我们先从最简单的开始,一步一步实现我们的需求。就先从六边形开始吧!
国外的大佬解释了六边形有多么神奇和好用,蜜蜂选择六边形来筑巢,足以说明这个东西道法自然,详情点上面的链接了解。

using UnityEngine;
/// <summary>
/// 六边形常量
/// </summary>
public static class HexMetrics {

    /// <summary>
    /// 总的顶点数,一个六边形有18个顶点
    /// </summary>
    public static int totalVertices = 18;
    
    /// <summary>
    /// 六边形外半径=六边形边长
    /// </summary>
    public const float outerRadius = 10f;

    /// <summary>
    /// 六边形内半径=0.8*外半径
    /// </summary>
    public const float innerRadius = outerRadius * 0.866025404f;

    /// <summary>
    /// 六边形的六个角组成的数组
    /// </summary>
	public readonly static Vector3[] corners = {
		new Vector3(0f, 0f, outerRadius),//最顶上那个角作为起点,顺时针画线
		new Vector3(innerRadius, 0f, 0.5f * outerRadius),//顺数第二个
		new Vector3(innerRadius, 0f, -0.5f * outerRadius),//顺数第三个
		new Vector3(0f, 0f, -outerRadius),//依次类推,坐标如下图所示
		new Vector3(-innerRadius, 0f, -0.5f * outerRadius),
		new Vector3(-innerRadius, 0f, 0.5f * outerRadius),
		new Vector3(0f, 0f, outerRadius)
	};
}

在这里插入图片描述
如图,红色虚线代表内半径,蓝色实线代表外半径,而其数值都是相对固定的常量,因此这里直接定义出来。
根据这些常量,设定圆心坐标为(0,0,0),我们以最上角最为起点,就可以得出六个角的顶点坐标了。

六边形实体

接下来创建六边形实体,如下图所示:
在这里插入图片描述
实际上就是个空对象,我本来要通过ConvertToEntity将其转化成实体的,但是出了一个红色警报,只好移除,保留E脚本:

/// <summary>
/// E:六边形单元
/// </summary>
[RequiresEntityConversion]
public class HexCellEntity : MonoBehaviour,IConvertGameObjectToEntity {

    /// <summary>
    /// 三维坐标
    /// </summary>
    public int X;
    public int Y;
    public int Z;

    /// <summary>
    /// 颜色
    /// </summary>
    public Color Color;
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        //数据交给C保存
        dstManager.AddComponentData(entity, new HexCellData
        {
            X=this.X,
            Y=this.Y,
            Z=this.Z,
            color=Color,
            RadiansPerSecond= math.radians(DegreesPerSecond)
        });
        //添加父组件
        dstManager.AddComponent(entity, typeof(Parent));
        //添加相对父类的本地位置组件
        dstManager.AddComponent(entity, typeof(LocalToParent));
    }

}

对应的C组件:

/// <summary>
/// C:保存六边形的坐标和颜色数据
/// </summary>
[Serializable]
public struct HexCellData : IComponentData
{
    public int X;
    public int Y;
    public int Z;
    public Color color;
    public float RadiansPerSecond;
}

暂时设定六边形的功能是旋转,后面再更改成变色:


/// <summary>
/// S:这里暂时只做旋转,后面会变色等
/// </summary>
public class HexCellSystem : JobComponentSystem {
    EntityQuery m_Group;//查询到特定组件的实体,将其放入这个组中

    /// <summary>
    /// 这里根据类型来查询到特定的实体
    /// </summary>
    protected override void OnCreate()
    {
        ///typeof(Rotation)=带有Rotation组件的;ComponentType=对应HexCellData组件类型的
        /// ReadOnly=只读会加快获取实体的速度,ReadWrite=读写 则相对较慢
        m_Group = GetEntityQuery(typeof(Rotation), ComponentType.ReadOnly<HexCellData>());
    }

    [BurstCompile]//同样使用Burst编译器来加速,区别是使用了块接口:IJobChunk
    struct RotationSpeedJob : IJobChunk {
        /// <summary>
        /// 时间
        /// </summary>
        public float DeltaTime;

        /// <summary>
        /// 原型块组件类型=Rotation
        /// </summary>
        public ArchetypeChunkComponentType<Rotation> RotationType;

        /// <summary>
        /// 只读 原型块组件类型=HexCellData
        /// </summary>
        [ReadOnly]
        public ArchetypeChunkComponentType<HexCellData> RotationSpeedType;

        /// <summary>
        /// 找出满足条件的实体来执行
        /// </summary>
        /// <param name="chunk"><原型块/param>
        /// <param name="chunkIndex">块索引</param>
        /// <param name="firstEntityIndex">第一个实体索引</param>
        public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
        {
            var chunkRotations = chunk.GetNativeArray(RotationType);
            var chunkRotationSpeeds = chunk.GetNativeArray(RotationSpeedType);
            for (var i = 0; i < chunk.Count; i++)
            {
                var rotation = chunkRotations[i];
                var rotationSpeed = chunkRotationSpeeds[i];

                chunkRotations[i] = new Rotation
                {
                    Value = math.mul(math.normalize(rotation.Value),
                        quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * DeltaTime))
                };
            }
        }
    }

    /// <summary>
    /// 这个方法在主线程上每帧运行
    /// </summary>
    /// <param name="inputDependencies">输入依赖</param>
    /// <returns></returns>
    protected override JobHandle OnUpdate(JobHandle inputDependencies)
    {
        // Explicitly declare: 声明
        // - Read-Write access to Rotation 读写的方式访问旋转
        // - Read-Only access to HexCellData 只读的方式访问旋转速度
        var rotationType = GetArchetypeChunkComponentType<Rotation>();
        var rotationSpeedType = GetArchetypeChunkComponentType<HexCellData>(true);

        var job = new RotationSpeedJob()
        {
            RotationType = rotationType,
            RotationSpeedType = rotationSpeedType,
            DeltaTime = Time.deltaTime
        };

        return job.Schedule(m_Group, inputDependencies);
    }
}

如上代码是六边形单元的基本ECS写法,都是最基础的:

ECS
HexCellEntityHexCellDataHexCellSystem

在游戏对象上添加上一个Mesh显示相应的组件就可以让其旋转起来了,其实很简单。
在这里插入图片描述
接下来我们把它做成一个预设,然后再大量生成,以后的大地图就建立在这个六边形单元的基础上。

创建者和创建六边形单元系统

接下来我们新建一个空游戏对象,命名为:MapCreater。为其添加ConvertToEntity脚本组件,使其转化为实体,新建一个C#脚本来描述这个实体,命名为CreaterEntity:

/// <summary>
/// E:创建者实体
/// </summary>
[RequiresEntityConversion]
public class CreaterEntity : MonoBehaviour, IDeclareReferencedPrefabs, IConvertGameObjectToEntity
{
    /// <summary>
    /// 六边形单元预设
    /// </summary>
    public GameObject HexCellPrefab;

    /// <summary>
    /// 地图宽度(以六边形为基本单位)
    /// </summary>
    public int MapWidth=6;

    /// <summary>
    /// 地图长度(以六边形为基本单位)
    /// </summary>
    public int MapHeight=6;

    /// <summary>
    /// 地图颜色
    /// </summary>
    public Color defaultColor = Color.white;

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        HexMetrics.totalVertices = MapWidth * MapHeight * 18;
        dstManager.AddComponentData(entity, new MapData
        {
            Width=MapWidth,
            Height=MapHeight,
            Prefab = conversionSystem.GetPrimaryEntity(HexCellPrefab),
            Color=defaultColor,
            bIsNewMap=bCreatNewMap
        });
    }

    public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
    {
        referencedPrefabs.Add(HexCellPrefab);
    }
}

数据交给C保存起来:

/// <summary>
/// C:保存创建者数据
/// </summary>
[Serializable]
public struct CreaterData : IComponentData {
    public int Width;
    public int Height;
    public Entity Prefab;
    public Color Color;
}

S:创建六边形单元系统

/// <summary>
/// 创建六边形单元系统
/// </summary>
public class CreateHexCellSystem : JobComponentSystem {

    BeginInitializationEntityCommandBufferSystem m_EntityCommandBufferSystem;

    /// <summary>
    /// 是否是新地图
    /// </summary>
    public bool bIfNewMap = true;

    protected override void OnCreate()
    {
        m_EntityCommandBufferSystem = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();
    }

    /// <summary>
    /// 循环创建六边形单元,使其生成对应长宽的阵列
    /// </summary>
    struct SpawnJob : IJobForEachWithEntity<CreaterData> {
        public EntityCommandBuffer.Concurrent CommandBuffer;
        [BurstCompile]
        public void Execute(Entity entity, int index, [ReadOnly]ref CreaterData  createrData)
        {

            for (int z = 0; z < createrData.Height; z++)
            {
                for (int x = 0; x < createrData.Width; x++)
                {
                    //1.实例化
                    var instance = CommandBuffer.Instantiate(index, createrData.Prefab);
                    //2.计算阵列坐标
                    float _x = (x + z * 0.5f - z / 2) * (HexMetrics.innerRadius * 2f);
                    float _z = z * (HexMetrics.outerRadius * 1.5f);
                    //3.设置父组件
                    CommandBuffer.SetComponent(index, instance, new Parent
                    {
                        Value = entity

                    });
                    //4.设置每个单元的数据
                    CommandBuffer.SetComponent(index, instance, new HexCellData
                    {
                        X = x - z / 2,
                        Y = 0,
                        Z = z,
                        color = createrData.Color,

                    });
                    //5.设置位置
                    CommandBuffer.SetComponent(index, instance, new Translation
                    {
                        Value = new float3(_x, 0F, _z)

                    });
                }
            }

        }
    }

    /// <summary>
    /// 如果有新地图,则启动任务
    /// </summary>
    /// <param name="inputDeps">依赖</param>
    /// <returns>任务句柄</returns>
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {

        if (bIfNewMap)
        {
            var job = new SpawnJob
            {
                CommandBuffer = m_EntityCommandBufferSystem.CreateCommandBuffer().ToConcurrent(),

            }.Schedule(this, inputDeps);

            m_EntityCommandBufferSystem.AddJobHandleForProducer(job);
            job.Complete();
            var mapSystem = World.GetOrCreateSystem<CreateHexMapSystem>();
            mapSystem.bIfNewMap = true;
            //新地图创建完成,关闭创建
            bIfNewMap = false;
            return job;
        }

        return inputDeps;
    }
}

在这里插入图片描述
如上图所示,我们创建6*6的单元矩阵,但是它们并没有旋转。我们通过Entity Debugger窗口可以看到对应的实体。
我发现Rotation的数据一直都是0,并没有发生旋转,但是代码并没有问题。到官方论坛反馈时,发现是Rotation的API变了!
ECS还处于过渡时期,所以API会经常变动,开发起来非常尴尬。
我发现以前的写法,在做升级之后,就不起作用了。不仅如此,很多物理组件无法使用。
因此这一篇到这里搁浅了,后面找到正确的API继续写。
已经把项目上传到Github,有兴趣的朋友可以看看:HexMapMadeInUnity2019ECS

更新计划

Mon 12 Mon 19 Mon 26 1. ForEach 2. IJobForEach 3. IJobChunk 4. SubScene 5. SpawnFromMonoBehaviour 6. SpawnFromEntity 7. SpawnAndRemove 休息 修正更新计划 参加表哥婚礼 进阶:FixedTimestepWorkaround 进阶:BoidExample 初级:SceneSwitcher 我是休息时间 资源整合 部署服务器 启动流程 登录流程 MegaCity 选人流程 游戏主世界 UnityMMO网络同步 我是休息时间 待计划 待计划 待计划 待计划 我是休息时间 待计划 待计划 待计划 待计划 待计划 我是休息时间 读取Excel自动生成Entity 读取Excel自动生成Component 读取数据库自动生成Entity 读取数据库自动生成Component ESC LuaFrameWork Skynet DOTS 官方示例学习笔记 -----休息----- 基于ECS架构开发MMO学习笔记 休息----- LuaFrameWork学习笔记 -----休息----- 基于Skynet架构开发服务器学习笔记 制作代码自动生成工具 总结 基于Unity2019最新ECS架构开发MMO游戏笔记

作者的话

AltAlt

如果喜欢我的文章可以点赞支持一下,谢谢鼓励!如果有什么疑问可以给我留言,有错漏的地方请批评指证!
如果有技术难题需要讨论,可以加入开发者联盟:566189328(付费群)为您提供有限的技术支持,以及,心灵鸡汤!
当然,不需要技术支持也欢迎加入进来,随时可以请我喝咖啡、茶和果汁!( ̄┰ ̄*)

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:鼠标点击六边形单元涂色

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

CloudHu1989

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值