先看看最终效果
前言
关于使用TileMap生成随机2D地图,其实之前已经有做过类似的,感兴趣可以看看:
【unity小技巧】Unity2D TileMap+柏林噪声生成随机地图
但是随着学习深入,发现之前做的比较粗糙和不够全面,最近又在外网看到一个程序化生成2D地牢的视频,觉得不错,所以写了这一篇学习笔记,记录分享一下。
本项目可能比较长,会分几期来讲,感兴趣的可以关注一下,方便获取后续内容,本期主要是使用随机游走算法生成随机的地牢房间。
随机游走算法
新增
/// <summary> /// 静态类,包含用于二维环境的程序化生成算法。 /// </summary> public static class ProceduralGenerationAlgorithms { /// <summary> /// 在二维空间中生成简单的随机行走路径。 /// </summary> /// <param name="startPosition">行走的起始位置。</param> /// <param name="walkLength">行走步数。</param> /// <returns>表示所走路径的 Vector2Int 的 HashSet。</returns> public static HashSet<Vector2Int> SimpleRandomWalk(Vector2Int startPosition, int walkLength) { // 创建路径 HashSet HashSet<Vector2Int> path = new HashSet<Vector2Int>(); // 将起始位置添加到路径 HashSet 中 path.Add(startPosition); // 将当前位置设置为起始位置 var previousPosition = startPosition; // 沿着随机方向移动,将每个新位置添加到路径 HashSet 中 for (int i = 0; i < walkLength; i++) { var newPosition = previousPosition + Direction2D.GetRandomCardinalDirection(); path.Add(newPosition); previousPosition = newPosition; } // 返回路径 HashSet return path; } } /// <summary> /// 静态类,包含二维方向性工具。 /// </summary> public static class Direction2D { /// <summary> /// 二维空间中基本方向的列表。 /// </summary> public static List<Vector2Int> cardinalDirectionsList = new List<Vector2Int> { new Vector2Int(0,1), //上 new Vector2Int(1,0), //右 new Vector2Int(0, -1), // 下 new Vector2Int(-1, 0) //左 }; /// <summary> /// 从列表中返回一个随机的基本方向。 /// </summary> /// <returns>表示随机基本方向的 Vector2Int。</returns> public static Vector2Int GetRandomCardinalDirection() { return cardinalDirectionsList[UnityEngine.Random.Range(0, cardinalDirectionsList.Count)]; } }
使用随机游走算法
新增SimpleRandomWalkDungeonGenerator, 用于生成简单随机行走地牢的类
/// <summary> /// 用于生成简单随机行走地牢的类,继承自 MonoBehaviour。 /// </summary> public class SimpleRandomWalkDungeonGenerator : MonoBehaviour { [SerializeField, Header("地牢生成的起始位置")] protected Vector2Int startPosition = Vector2Int.zero; [SerializeField, Header("迭代次数")] private int iterations = 10; [SerializeField, Header("每次行走的步数")] public int walkLength = 10; [SerializeField, Header("每次迭代是否随机起始位置")] public bool startRandomlyEachIteration = true; /// <summary> /// 执行程序化生成地牢的方法。 /// </summary> public void RunProceduralGeneration() { HashSet<Vector2Int> floorPositions = RunRandomWalk(); // 获取地牢地板坐标集合 foreach (var position in floorPositions) { Debug.Log(position); // 输出地板坐标信息 } } /// <summary> /// 执行随机行走算法生成地牢地板坐标的方法。 /// </summary> /// <returns>地牢地板坐标的 HashSet。</returns> protected HashSet<Vector2Int> RunRandomWalk() { var currentPosition = startPosition; // 当前位置初始化为起始位置 HashSet<Vector2Int> floorPositions = new HashSet<Vector2Int>(); // 地板坐标的 HashSet for (int i = 0; i < iterations; i++) { var path = ProceduralGenerationAlgorithms.SimpleRandomWalk(currentPosition, walkLength); // 生成随机行走路径 floorPositions.UnionWith(path); // 将路径添加到地板坐标集合中 if (startRandomlyEachIteration) { currentPosition = floorPositions.ElementAt(Random.Range(0, floorPositions.Count)); // 如果需要每次迭代随机起始位置,则随机选择一个已生成的位置 } } return floorPositions; // 返回地板坐标集合 } }
挂载脚本
配置点击事件
效果
添加地板瓦片
1. 新增TilemapVisualizer,用于可视化地图
/// <summary> /// 用于可视化地图的 TilemapVisualizer 类,继承自 MonoBehaviour。 /// </summary> public class TilemapVisualizer : MonoBehaviour { [SerializeField] private Tilemap floorTilemap; // 地板瓦片地图 [SerializeField] private TileBase floorTile; // 地板瓦片 /// <summary> /// 绘制地板瓦片的方法。 /// </summary> /// <param name="floorPositions">地板位置的坐标集合。</param> public void PaintFloorTiles(IEnumerable<Vector2Int> floorPositions) { PaintTiles(floorPositions, floorTilemap, floorTile); } /// <summary> /// 绘制瓦片的方法。 /// </summary> /// <param name="positions">瓦片位置的坐标集合。</param> /// <param name="tilemap">瓦片地图。</param> /// <param name="tile">要绘制的瓦片。</param> private void PaintTiles(IEnumerable<Vector2Int> positions, Tilemap tilemap, TileBase tile) { foreach (var position in positions) { PaintSingleTile(tilemap, tile, position); } } /// <summary> /// 绘制单个瓦片的方法。 /// </summary> /// <param name="tilemap">瓦片地图。</param> /// <param name="tile">要绘制的瓦片。</param> /// <param name="position">瓦片的位置坐标。</param> private void PaintSingleTile(Tilemap tilemap, TileBase tile, Vector2Int position) { var tilePosition = tilemap.WorldToCell((Vector3Int)position); // 将位置坐标转换为瓦片地图上的单元格坐标 tilemap.SetTile(tilePosition, tile); // 在指定位置绘制瓦片 } // 清空瓦片地图 public void Clear() { floorTilemap.ClearAllTiles(); } }
修改SimpleRandomWalkDungeonGenerator,执行程序化生成地牢的方法
[SerializeField] private TilemapVisualizer tilemapVisualizer; /// <summary> /// 执行程序化生成地牢的方法。 /// </summary> public void RunProceduralGeneration() { HashSet<Vector2Int> floorPositions = RunRandomWalk(); // 获取地牢地板坐标集合 tilemapVisualizer.Clear();// 清空瓦片地图 tilemapVisualizer.PaintFloorTiles(floorPositions); }
2. 瓦片素材
https://pixel-poem.itch.io/dungeon-assetpuck
挂载脚本,配置参数
效果
不运行执行程序化生成地牢方法
每次测试都要运行程序再执行生成地图,非常的麻烦,我们可以实现不运行也可以执行程序化生成地牢的方法
1. 先简单重构代码
新增AbstractDungeonGenerator,定义抽象地牢生成器的基类
/// <summary> /// 抽象地牢生成器的基类,继承自 MonoBehaviour。 /// </summary> public abstract class AbstractDungeonGenerator : MonoBehaviour { [SerializeField, Header("瓦片可视化器")] protected TilemapVisualizer tilemapVisualizer = null; [SerializeField, Header("地牢生成的起始位置")] protected Vector2Int startPosition = Vector2Int.zero; /// <summary> /// 生成地牢的方法。 /// </summary> public void GenerateDungeon() { tilemapVisualizer.Clear(); // 清空瓦片可视化器 RunProceduralGeneration(); // 执行程序化生成 } /// <summary> /// 执行程序化生成地牢的抽象方法,需要在子类中实现具体逻辑。 /// </summary> protected abstract void RunProceduralGeneration(); }
修改SimpleRandomWalkDungeonGenerator
public class SimpleRandomWalkDungeonGenerator : AbstractDungeonGenerator { //。。。 // [SerializeField, Header("地牢生成的起始位置")] // protected Vector2Int startPosition = Vector2Int.zero; // [SerializeField] // private TilemapVisualizer tilemapVisualizer; /// <summary> /// 执行程序化生成地牢的方法。 /// </summary> protected override void RunProceduralGeneration() { HashSet<Vector2Int> floorPositions = RunRandomWalk(); // 获取地牢地板坐标集合 tilemapVisualizer.Clear(); tilemapVisualizer.PaintFloorTiles(floorPositions); } //。。。 }
修改点击事件
运行测试,一切正常
2. 新增Editor脚本RandomDungeonGeneratorEditor
/// <summary> /// 自定义编辑器类,用于 RandomDungeonGenerator。 /// </summary> [CustomEditor(typeof(AbstractDungeonGenerator), true)] public class RandomDungeonGeneratorEditor : Editor { private AbstractDungeonGenerator generator; // 地牢生成器对象 private void Awake() { generator = (AbstractDungeonGenerator)target; // 获取目标对象并转换为地牢生成器类型 } /// <summary> /// 在 Inspector 窗口中绘制自定义的 GUI。 /// </summary> public override void OnInspectorGUI() { base.OnInspectorGUI(); // 绘制基类的默认 Inspector 界面 if (GUILayout.Button("Create Dungeon")) // 创建地牢的按钮 { generator.GenerateDungeon(); // 调用地牢生成器的方法生成地牢 } } }
效果
将参数保存到可编辑脚本对象(ScriptableObject)
1. 定义简单随机行走数据的 ScriptableObject 类
新增SimpleRandomWalkSO
/// <summary> /// 简单随机行走数据的 ScriptableObject 类。 /// </summary> [CreateAssetMenu(fileName = "SimpleRandomWalkParameters_", menuName = "PCG/SimpleRandomWalkData")] public class SimpleRandomWalkSO : ScriptableObject { [Header("迭代次数")] public int iterations = 10; [Header("每次行走的步数")] public int walkLength = 10; [Header("每次迭代是否随机起点")] public bool startRandomlyEachIteration = true; }
2. 配置不同的地形参数
配置不同的参数
3. 调用
修改SimpleRandomWalkDungeonGenerator,调用前面定义的参数
[SerializeField] private SimpleRandomWalkSO randomWalkParameters;
4. 效果
大地牢
小地牢
岛
生成墙壁
新增WallGenerator,墙体生成器的静态类
/// <summary> /// 墙体生成器的静态类。 /// </summary> public static class WallGenerator { /// <summary> /// 创建墙体的方法。 /// </summary> /// <param name="floorPositions">地板位置的集合</param> /// <param name="tilemapVisualizer">瓦片可视化器</param> public static void CreateWalls(HashSet<Vector2Int> floorPositions, TilemapVisualizer tilemapVisualizer) { var basicWallPositions = FindWallsInDirections(floorPositions, Direction2D.cardinalDirectionsList); // 在每个墙体位置上绘制基本墙体 foreach (var position in basicWallPositions) { tilemapVisualizer.PaintSingleBasicWall(position); } } /// <summary> /// 在指定方向上查找墙体的方法。 /// </summary> /// <param name="floorPositions">地板位置的集合</param> /// <param name="directionList">方向列表</param> /// <returns>墙体位置的集合</returns> private static HashSet<Vector2Int> FindWallsInDirections(HashSet<Vector2Int> floorPositions, List<Vector2Int> directionList) { HashSet<Vector2Int> wallPositions = new HashSet<Vector2Int>(); foreach (var position in floorPositions) { foreach (var direction in directionList) { var neighbourPosition = position + direction; // 如果邻居位置不在地板位置集合中,则认为是墙体位置 if (!floorPositions.Contains(neighbourPosition)) { wallPositions.Add(neighbourPosition); } } } return wallPositions; } }
修改TilemapVisualizer
[SerializeField, Header("墙壁瓦片地图")] private Tilemap wallTilemap; [SerializeField, Header("墙壁瓦片")] private TileBase wallTop; //绘制墙壁瓦片的方法 internal void PaintSingleBasicWall(Vector2Int position) { PaintSingleTile(wallTilemap, wallTop, position); }
修改SimpleRandomWalkDungeonGenerator
/// <summary> /// 执行程序化生成地牢的方法。 /// </summary> protected override void RunProceduralGeneration() { HashSet<Vector2Int> floorPositions = RunRandomWalk(); // 获取地牢地板坐标集合 tilemapVisualizer.PaintFloorTiles(floorPositions);//绘制地板瓦片 WallGenerator.CreateWalls(floorPositions, tilemapVisualizer);//创建墙体 } // 清空瓦片地图 public void Clear() { floorTilemap.ClearAllTiles(); wallTilemap.ClearAllTiles(); }
配置参数
效果
补充
想要优化墙壁的显示,可以选择使用rule tile绘制墙壁内容,不懂得可以看我这篇文章,写的比较详细:【Unity小技巧】Unity2D TileMap的探究
还不懂的也可以看我后面的文章,后面会讲到