提示部分
本文所提到的内容会在整理后上传到阿茶的OSS仓库中,可以通过下面的链接点击下载,如果没有那就是阿茶还没有整理好。
前提
这次要实现一个可以拖动的大地图,大地图由若干的格子组成,而每个格子是由固定数量的3D物体之一进行显示。
假设3D物体足够复杂,运行设备的内存和显存又不足以处理无限量的物体的数据。
写到这里突然冒出一个疑问,既然3D物体是复用的,那为什么不能够直接生成整个地图的数据呢。抱着这个疑问,我去问了师父,得到的答案也让阿茶恍然大悟。就算3D物体是复用的,但是就单单一个Transform的数据,如果是100*100的地图,那就会有10000份,再加上各种各样其他的数据,这样来看数据量也会很大。看样子阿茶真的是个马鹿。
实现思路
为了表述清楚,这里进行一些专用名称的定义,并且在下文会高亮显示这些特定称呼,与正文其他内容以示区分。
名称: screen
/ Screen
含义: 一个屏幕(包括但不限于一个屏幕上的所有格子和其对应的数据)
名称: 屏幕坐标
含义: 将大地图对应的所有screen
完全排列,每个screen
对应的具体二维坐标
本次功能实现的核心逻辑被亲切的称为九宫格法。即指定每个Screen
上应当显示多少个block
。然后显示当前block
对应的Screen
和当前Screen
周边可以显示的一圈Screen
。
每次移动后检测当前Screen
上的block
是否为正中心的Screen
,如果不是则将当前Screen
当做中心位置重复上一步的操作。并检测除此之外的所有Screen
并且销毁其对应的所有block
。
实现方法(思路)
为了简化流程,消除数据与3D物体的关联性,不用去关心每一个block
对应的Transform.position,也不用关心每一个block
对应的数据。这里会使用池
的概念来管理3D物体与Script。
下面是池的概念,如果您已经了解,可以直接跳过。或者可以展开下面的内容来了解池的概念。
池的感念
温馨提示:这里的概念是阿茶自己理解的概念,不保证完全正确,请悉知。
池的概念简单来说就是有一个容器,容器中保存了若干相同类型的数据,池子内部处理一些诸如销毁、创建、显示、隐藏数据等。
用户(这里只使用数据的程序员)不需要关心池是如何处理这些数据,只需要在需要时伸手找池子要,不需要的时候扔回去就好。
如果不用池子进行管理,可能会出现频繁的GameObject的创建和销毁。这种操作是十分消耗性能的(虽然阿茶也不知道究竟会消耗多少性能,不过提前考虑这种事情总之不会有问题)。
您可能会想到,我可以直接调用SetActive()方法来切换显示和隐藏。
诚然,这样的做法确实可以解决消耗过多性能的问题,但是这样每次需要显示一个物体的时候都需要在节点树中遍历没有显示的物体进行操作。
或许看到这里你会嘲笑阿茶,直接建立一个类似于List或其他类型的变量来保存,每次从里面查找就好了。
没有错呢,您的想法非常正确!
但是,池子也是这样实现的哦!
为了实现代码的复用性(懒),不妨稍微多花一点心思,将这种代码单独写成一个可以继承的类,每次需要池子的时候直接调用就会变得非常省心啦。
然后建立一个父节点用于控制移动,一个管理类用于管理所有Screen
。
大致思路很简单,所以就介绍到这里。
具体实现的过程中可能会存在各种各样的细节,会在实现的时候具体描述。
那么让我们愉快的进入下一章节吧~
实现
对象池
首先,阿茶要介绍一下花心思写出来的(虽然是非常简陋的)对象池部分啦。
思考一下对象池所做的工作,我将对象池需要完成的部分分解成为下述步骤
- 有一个数据结构来存储所有放在池中的对象
- 负责在别人要对象的时候及时给出一个对象
- 负责接收别人不要的对象并妥善处理
不管您现在是单身,还是有对象,或者已经结婚,请都不要因为上面的几条内容中有对象就因此PTSD了哦,当然更不要做出危害公俗良序的事情呀。阿茶可不因此负责哦!
仔细想一想,因为这个需求中不仅需要使用对象池存储3D物体(即GameObject),还需要存储用于保存数据的Scripts。而这两类数据在加载和隐藏时拥有不同的方式,需要单独处理的。所以这里应该创建两类不同的池,根据情况分别调用(其实阿茶也有尝试将这两种方式合一,可是奈何能力有限,还是写成了两个)。
不过这两类的池仅在生成和销毁的时候存在不同的方式,也就是说会有相同(或相似)的部分,因此可以提出公共的部分变成基类,其他只需要继承基类就可以了。
基类
那么公共的部分都有什么呢?
- 有一个数据结构来存储所有放在池中的对象
- 一个数据结构来存放每个对象和其对应的使用状态标记
- 还有一个移除方式
因为这里的移除
对于GameObject来说是切换显示状态,而对于Script来说,本来就不是挂在GameObject上的,所以不存在切换隐藏,只需要将其使用状态标记为未使用,等待下次使用即可。
根据这些内容,基类就可以写成下面这样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| /// <summary> /// 对象池的基类 /// </summary> /// <typeparam name="T">对象的类型</typeparam> public class PoolBase<T> { /// <summary> /// 用于存储每一个T和对应状态的类 /// </summary> public class PoolStruct { public T t; private bool isUsing; /// <summary> /// 当前块是否正在使用,如果不再使用将会进行隐藏 /// </summary> public bool IsUsing { get => this.isUsing; set { this.isUsing = value; // 显示或隐藏当前块(当且仅当T为GameObject类型) if (typeof(T) == typeof(GameObject)) { (t as GameObject).SetActive(value); } } } public PoolStruct(T a, bool b) { this.t = a; this.IsUsing = b; } }
/// <summary> /// 对象池中所有的对象 /// </summary> protected List<PoolStruct> objects; protected PoolBase() { objects = new(); } /// <summary> /// 移除指定的内容 /// 这里会进行隐藏显示 /// </summary> /// <param name="g">指定的内容</param> /// <returns>是否隐藏成功</returns> public bool Remove(T item) { for (int i = 0; i < objects.Count; i++) { if (objects[i].t.Equals(item)) { objects[i].IsUsing = false; return true; } } return false; } }
|
为了简化代码(看起来很厉害),这里对于现实和隐藏(主要是针对于GameObject类型的变量)的操作都放在了名为IsUsing
的属性中进行操作,这样能够让代码逻辑更为清晰(便于理解)。
GameObject类型的池
初始化方法
针对GameObject类型,当需要动态加载(实例化)一个物体的时候,大概率会用到MonoBehaviour中的Instantiate方法。
根据参考资料中的描述,不能使用new()方法来实例化继承于MonoBehaviour的类,必须使用AddComponent或Instantiate函数来创建,因此该类型的池也同样适用于集成于MonoBehaviour的类(后者阿茶没有进行过实验,这里只是推测)。
不过要想使用池的概念,比如能够让池掌握实例化GameObject的方法,也就是Instantiate,但是要使用这个方法需要继承于MonoBehaviour。但是,为了这个去继承MonoBehaviour的话便不再被允许继承其基类(类“GameObjectPool”不能具有多个基类:“MonoBehaviour”和“PoolBase”),这就陷入了死循环。
不过聪明的阿茶(脸皮有够厚的说)很快就想到了,可以讲Instantiate方法作为参数传递进来,这样就能够让池掌握实例化的方法了。如果阿茶没有记错的话,之前或多或少用过一个名叫委托的概念,当时是写事件中心的时用到的。但是实现方法和阿茶想要的不太一样。
在写其他语言的时候(已经忘记是什么语言了),可以直接将方法名称当做参数传递过去而不用delegate这种去实现,在网路上检索了很多资讯后找到了这样一个内容,那就是使用Action
和Func
两者去实现。前者是不含返回值,后者含有返回值。其使用方法相较于delegate更加贴近于阿茶的理想形态,遂使用。
1 2 3 4
|
Func<GameObject, Transform, GameObject> Instantiate;
|
代码如上,其中尖括号中的最后一个类型是该方法的返回值,最后一个参数之前的所有参数是该方法的参数。即该方法为
1 2 3 4
| GameObject Instantiate(GameObject g, Transform t) { // 方法具体实现 }
|
根据需求,阿茶这里选择的实例化方法是可以设置父级节点Transform的实例化方法,如果需要其他方法可能需要重载,这里不做过多陈述。
或者这里还可以修改成一个类的静态变量,这样当有多个池的时候可以减少保存数据的开销,不过针对不同项目有着不同的初始化方法,如果没有统一的游戏入口,每次实例化一个池的时候都会传入这个方法比较保险,不会出现初始化位于调用之前或者忘记初始化Instantiate方法的问题。这里需要具体项目具体分析。
其他额外的变量
由于是GameObject,必然需要知道每次实例化的物体的模板,也就是预制体。
所以这里需要一个变量来保存每次实例化对应的预制体的引用。
即
1 2 3 4
|
GameObject prefab = null;
|
实现代码
综上所述,针对GameObject的池子的实现代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| /// <summary> /// GameObject的对象池类 /// </summary> public class GameObjectPool : PoolBase<GameObject> { /// <summary> /// 初始化对象用的方法,这里Monobehavior与new方法冲突,因此需要外部传入 /// </summary> readonly Func<GameObject, Transform, GameObject> Instantiate; /// <summary> /// 每次实例化的预制体 /// </summary> GameObject prefab = null; /// <summary> /// 初始化对象池 /// </summary> /// <param name="prefab">用于管理的对象的预制体</param> /// <param name="Instantiate">Instantiate方法,因为new()和Monobehavior继承冲突</param> public GameObjectPool(GameObject prefab, Func<GameObject, Transform, GameObject> Instantiate) : base() { this.prefab = prefab; this.Instantiate = Instantiate; } /// <summary> /// 取得一个可以用的GameObject /// </summary> /// <param name="parent">父节点</param> /// <param name="position">对象的位置</param> /// <returns></returns> public GameObject Get(Transform parent, Vector3 position) { foreach (PoolStruct item in objects) { if (!item.IsUsing) { item.IsUsing = true; item.t.transform.SetParent(parent); item.t.transform.position = position; return item.t; } } PoolStruct s = new(Instantiate(prefab, parent), true); s.t.transform.position = position; objects.Add(s); return s.t; } }
|
非GameObject类型的池
对于非GameObject类型的对象池(也不能够继承MonoBehaviour,具体原因和不能使用new()进行创建有关,上文已经有所提及),可以简单的使用new方法进行实例化,因此这里就变得很简单了,其对应的代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| /// <summary> /// 非GameObject对象的对象池 /// </summary> /// <typeparam name="T">对象类型,需要满足new()约束条件</typeparam> public class UnGameObjectPool<T> : PoolBase<T> where T : new() { public UnGameObjectPool() : base() { } /// <summary> /// 获取一个新的 /// </summary> /// <returns>获取到的T</returns> public T Get() { foreach (PoolStruct item in objects) { if (!item.IsUsing) { item.IsUsing = true; return item.t; } } PoolStruct s = new(new(), true); objects.Add(s); return s.t; } }
|
配置类
这里为了方便后期修改简单,将所有的关键数值提成一个配置类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| /// <summary> /// 大地图的各种参数 /// </summary> public static class BigMapConfigs { /// <summary> /// 一个屏幕中显示的行数(有多少行格子) /// </summary> public static int OneScreenRow { get; private set; } = 10; /// <summary> /// 一个屏幕中显示的列数(每行有多少个格子) /// </summary> public static int OneScreenCol { get; private set; } = 5; /// <summary> /// 大地图共有多少行 /// </summary> public static int MapRow { get; private set; } = 1000; /// <summary> /// 大地图共有多少列 /// </summary> public static int MapCol { get; private set; } = 1000; /// <summary> /// 大地图共有多少行屏幕大小 /// </summary> public static int MapScreenRow { get; private set; } = MapRow / OneScreenRow; /// <summary> /// 大地图共有多少列屏幕大小 /// </summary> public static int MapScreenCol { get; private set; } = MapCol / OneScreenCol; /// <summary> /// 最大3D物体半径 /// </summary> public static float StarRadius { get; private set; } = 0.5f; /// <summary> /// 箭头路径的最大偏移量 /// </summary> public static float ArrowOffset { get; private set; } = 0.5f; /// <summary> /// 每个地图格子(3D物体的背景)的宽度 /// 3D物体直径的二倍 /// </summary> public static float BlockWidth { get; private set; } = 4 * StarRadius; /// <summary> /// 每个地图格子的长度 /// 3D物体直径的二倍 /// </summary> public static float BlockHeight { get; private set; } = 4 * StarRadius;
|
单一屏幕的管理类
首先来分析一下该类需要做的事情
- 管理该屏幕下所有
block
(物体和对应的数据) - 记录该
屏幕坐标
- 管理
block
和屏幕坐标
之间的关系
该类的具体实现如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
| /// <summary> /// 保存一个屏幕下的所有block /// 包括数据对应的BigMapBLockData /// 和每个格子对应的预制体的引用 /// </summary> public class BigMapOneScreenBlocks { /// <summary> /// 保存该区域的所有物体的引用 /// </summary> public List<List<GameObject>> blocks; /// <summary> /// 保存该区域所有物体的数据 /// </summary> public List<List<BigMapBlockData>> datas; /// <summary> /// 当前屏幕属于地图的第X行屏幕 /// </summary> public int X { get; set; } /// <summary> /// 当前屏幕属于地图的第Y列屏幕 /// </summary> public int Y { get; set; } /// <summary> /// 是否已经初始化过 /// </summary> private bool isInited { get; set; } = false; /// <summary> /// [对象池调用]构造方法(初始化两个list) /// </summary> public BigMapOneScreenBlocks() { // 没有进行初始化才进行初始化,否则是重复使用,已经在Clear方法中清零过了。 if (!isInited) { blocks = new(); datas = new(); for (int i = 0; i < BigMapConfigs.OneScreenRow; i++) { List<GameObject> tmpb = new(); List<BigMapBlockData> tmpd = new(); for (int j = 0; j < BigMapConfigs.OneScreenCol; j++) { tmpb.Add(null); tmpd.Add(null); } blocks.Add(tmpb); datas.Add(tmpd); } isInited = true; } } /// <summary> /// [外部调用]构造方法(初始化两个list) /// </summary> /// <param name="x">当前屏幕内容属于第x行</param> /// <param name="y">当前屏幕坐标属于第y列</param> public BigMapOneScreenBlocks SetPosition(int x, int y) { this.X = x; this.Y = y; return this; } /// <summary> /// 清空所有List(归零) /// </summary> /// <param name="blockPool">物体的池</param> /// <param name="blockDataPool">物体数据的池</param> public void Clear(GameObjectPool blockPool, UnGameObjectPool<BigMapBlockData> blockDataPool) { for (int i = 0; i < BigMapConfigs.OneScreenRow; i++) { for (int j = 0; j < BigMapConfigs.OneScreenCol; j++) { blockPool.Remove(blocks[i][j]); blocks[i][j] = null; blockDataPool.Remove(datas[i][j]); datas[i][j] = null; } } }
#region 坐标换算相关 /// <summary> /// 获取某个block对应的大地图的真实坐标 /// </summary> /// <param name="x">当前屏幕块中的第x行</param> /// <param name="y">当前屏幕块中的第y列</param> /// <returns>指定block对应大地图的真实坐标</returns> public BigMapScreensManager.RectangularCoordinates GetBigMapMapCoordinates(int x, int y) { return new(x + X * BigMapConfigs.OneScreenRow, y + Y * BigMapConfigs.OneScreenCol); } /// <summary> /// 左上角对应大地图的真实坐标 /// </summary> public BigMapScreensManager.RectangularCoordinates LeftUpTrueCoordinates { get { return GetBigMapMapCoordinates(0, 0); } } /// <summary> /// 右下角对应大地图的真实坐标 /// </summary> public BigMapScreensManager.RectangularCoordinates RightDownTrueCoordinates { get { return GetBigMapMapCoordinates(BigMapConfigs.OneScreenRow - 1, BigMapConfigs.OneScreenCol - 1); } } /// <summary> /// 左上角对应大地图的真实坐标 /// </summary> public BigMapScreensManager.RectangularCoordinates FirstTrueCoordinates { get { return GetBigMapMapCoordinates(0, 0); } } /// <summary> /// 右下角对应大地图的真实坐标 /// </summary> public BigMapScreensManager.RectangularCoordinates LastTrueCoordinates { get { return GetBigMapMapCoordinates(BigMapConfigs.OneScreenRow - 1, BigMapConfigs.OneScreenCol - 1); } } #endregion 坐标换算相关 }
|
所有屏幕的管理类
这个类需要做的事情如下
- 生成单个
screen
- 生成步骤1中
screen
周围其他可以生成的screen
- 移除步骤1和步骤2之外剩余的所有
screen
注意:这里为了表达简洁采用了1->2->3的顺序,但是为了节省内存空间,实际执行的顺序是3->1->2。
生成单个屏幕
最初开始写代码的时候,阿茶采取了最符合直觉的逻辑进行编写的,为了方便理解,这里还保留了这种方式,如果尝试简化,可能能够减少一些计算的步骤(这里具体指是究竟采取screen
的中心点还是采取每个screen
的第一个block
(规定为左上角)的中心点进行保存和计算,如果这句话您看的一头雾水,请继续向下看,后面会对其进行解释)。
生成每一个屏幕的时候,需要给定一个screen
的中心点,即将单一屏幕中所有的block
看做一个整体,整体的中心点即为该中心点。而生成的方法会根据该中心点计算出screen
的第一个block
的中心点进行一次生成。
其方法如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| /// <summary> /// 生成指定位置所在的区域的所有block,从左上角开始生成 /// </summary> /// <param name="centerPosition">需要生成的区域的中心点</param> /// <param name="X">当前区域所在的行数</param> /// <param name="Y">当前区域所在的列数</param> /// <returns>返回一个屏幕的数据</returns> BigMapOneScreenBlocks InitGenerateBlocks(Vector3 centerPosition, int X, int Y) { // 计算左上角的位置 float x, y, z; // unity中的坐标,x为向右,z为向上 x = centerPosition.x - ((BigMapConfigs.OneScreenCol - 1) / 2.0f * BigMapConfigs.BlockWidth); y = centerPosition.y; z = centerPosition.z + ((BigMapConfigs.OneScreenRow - 1) / 2.0f * BigMapConfigs.BlockHeight); // 生成一个屏幕的数据 BigMapOneScreenBlocks oneScreenBlocks = this.screenBlocksPool.Get().SetPosition(X, Y); for (int i = 0; i < BigMapConfigs.OneScreenRow; i++) { for (int j = 0; j < BigMapConfigs.OneScreenCol; j++) { oneScreenBlocks.blocks[i][j] = blockPool.Get(this.parent, new(x + j * BigMapConfigs.BlockWidth, y, z - i * BigMapConfigs.BlockHeight)); var pos = oneScreenBlocks.GetBigMapMapCoordinates(i, j); // TODO 设置资源行为 oneScreenBlocks.blocks[i][j].GetComponent<BigMapBlock>().SetText($"{pos.x}, {pos.y}"); // TODO 设置资源数据 oneScreenBlocks.datas[i][j] = blockDataPool.Get(); } } // 记录该屏幕的数据 this.Screens[X][Y] = oneScreenBlocks; return oneScreenBlocks; }
|
生成周围屏幕
根据给定的屏幕坐标
生成其对应的周围的所有screen
,其要做的工作也很简单。
- 遍历当前
屏幕坐标
周围的8个位置(九宫格除去最中心的位置),找到可以生成(没有超出范围,如i=−1这类位置)且还没有生成过的地方 - 对这些位置进行生成。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| /// <summary> /// [内部]生成当前块周围的所有块 /// </summary> /// <param name="currentBlocks">当前屏幕的块</param> void GenerateArround(BigMapOneScreenBlocks currentBlocks) { /* 计算需要生成周围的哪些快 * 1. 可能存在边界位置 * 2. 可能存在已经生成的位置 */ // 计算周围需要生成的块 List<Coordinate> aroundPositions = GetAroundScreenCoordinate(currentBlocks); // 根据当前区域中的第一个(左上角)进行计算 // 计算出该区域周围其他区域的位置 // 调用InitGenerateBlocks()方法生成每个区域 foreach (var item in aroundPositions) { InitGenerateBlocks(item.center, item.x, item.y); } }
|
全部代码
该类的全部代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463
| /// <summary> /// 九宫格的管理类,单例 /// </summary> public class BigMapScreensManager : SingletonBase<BigMapScreensManager> { /// <summary> /// 块的对象池(GameObject) /// </summary> GameObjectPool blockPool; /// <summary> /// 块的数据对象池(BigMapBlockData) /// </summary> UnGameObjectPool<BigMapBlockData> blockDataPool; /// <summary> /// 九宫格中每个屏幕的数据管理类 /// </summary> UnGameObjectPool<BigMapOneScreenBlocks> screenBlocksPool; /// <summary> /// 所有格子的父级节点 /// </summary> Transform parent; /// <summary> /// 起始坐标第X行的屏幕 /// </summary> public int X { get; private set; } = 0; /// <summary> /// 起始坐标第Y列的屏幕 /// </summary> public int Y { get; private set; } = 0; /// <summary> /// 起始位置的第一个块的坐标 /// </summary> Vector3 StartScreenFirstBlockPosition { get { return new(this.parent.position.x - (BigMapConfigs.OneScreenCol - 1) / 2.0f * BigMapConfigs.BlockWidth, 0 , this.parent.position.z + (BigMapConfigs.OneScreenRow - 1) / 2.0f * BigMapConfigs.BlockHeight); } } /// <summary> /// [结构体]屏幕坐标(x,y)和中心点position /// </summary> struct Coordinate { public readonly int x; public readonly int y; public readonly Vector3 center; public Coordinate(int x, int y, Vector3 center) : this() { this.x = x; this.y = y; this.center = center; } } /// <summary> /// Int类型的二维坐标 /// </summary> public struct RectangularCoordinates { public readonly int x, y; public RectangularCoordinates(int x, int y) { this.x = x; this.y = y; } // 重载判断运算 public static bool operator ==(RectangularCoordinates lhs, RectangularCoordinates rhs) { bool status = false; if (lhs.x == rhs.x && lhs.y == rhs.y) { status = true; } return status; } // 重载判断运算 public static bool operator !=(RectangularCoordinates lhs, RectangularCoordinates rhs) { bool status = false; if (lhs.x != rhs.x || lhs.y != rhs.y) { status = true; } return status; } public override bool Equals(object obj) { return base.Equals(obj); } public override int GetHashCode() { return base.GetHashCode(); } } /// <summary> /// [属性]大地图地图中的所有屏幕数据 /// </summary> public List<List<BigMapOneScreenBlocks>> Screens { get { if(null == this.screens) { this.Init(); } return this.screens; } set { this.screens = value; } } /// <summary> /// [变量]大地图地图中的所有屏幕数据 /// </summary> private List<List<BigMapOneScreenBlocks>> screens;
/// <summary> /// 内部初始化方法 /// </summary> private void Init() { // 保存地图所有屏幕的List this.Screens = new(); for (int i = 0; i < BigMapConfigs.MapScreenRow; i++) { List<BigMapOneScreenBlocks> tmp = new(); for (int j = 0; j < BigMapConfigs.MapScreenCol; j++) { tmp.Add(null); } this.Screens.Add(tmp); } } /// <summary> /// 设置起始坐标 /// </summary> /// <param name="x">起始坐标x</param> /// <param name="y">起始坐标Y</param> /// <returns>脚本自身</returns> public BigMapScreensManager SetStart(int x, int y) { this.X = x; this.Y = y; return this; } /// <summary> /// 外部初始化方法 /// </summary> /// <param name="blockPool">格子的对象池</param> /// <param name="blockDataPool">格子数据的对象池</param> /// <param name="parent">所有格子的父级节点</param> /// <param name="startPosition">初始位置</param> /// <returns>脚本自身</returns> public BigMapScreensManager Init(GameObjectPool blockPool, UnGameObjectPool<BigMapBlockData> blockDataPool, Transform parent, Vector3 startPosition) { // 初始化管理单个屏幕数据的对象池 this.screenBlocksPool = new(); // 设置所有块的对象池 this.blockPool = blockPool; // 设置所有块数据的对象池 this.blockDataPool = blockDataPool; // 设置父级节点 this.parent = parent; #region 初始化的方法,后续可能会在其他地方调用 InitGenerateBlocks(startPosition, X, Y); // 生成周边 GenerateArround(this.Screens[X][Y]); #endregion 初始化的方法 return this; } /// <summary> /// 生成指定位置所在的区域的所有block,从左上角开始生成 /// </summary> /// <param name="centerPosition">需要生成的区域的中心点</param> /// <param name="X">当前区域所在的行数</param> /// <param name="Y">当前区域所在的列数</param> /// <returns>返回一个屏幕的数据</returns> BigMapOneScreenBlocks InitGenerateBlocks(Vector3 centerPosition, int X, int Y) { // 计算左上角的位置 float x, y, z; // unity中的坐标,x为向右,z为向上 x = centerPosition.x - ((BigMapConfigs.OneScreenCol - 1) / 2.0f * BigMapConfigs.BlockWidth); y = 0; z = centerPosition.z + ((BigMapConfigs.OneScreenRow - 1) / 2.0f * BigMapConfigs.BlockHeight); // 生成一个屏幕的数据 BigMapOneScreenBlocks oneScreenBlocks = this.screenBlocksPool.Get().SetPosition(X, Y); for (int i = 0; i < BigMapConfigs.OneScreenRow; i++) { for (int j = 0; j < BigMapConfigs.OneScreenCol; j++) { oneScreenBlocks.blocks[i][j] = blockPool.Get(this.parent, new(x + j * BigMapConfigs.BlockWidth, y, z - i * BigMapConfigs.BlockHeight)); var pos = oneScreenBlocks.GetBigMapMapCoordinates(i, j); // TODO 设置资源行为 oneScreenBlocks.blocks[i][j].GetComponent<BigMapBlock>().SetText($"{pos.x}, {pos.y}"); // TODO 设置资源数据 oneScreenBlocks.datas[i][j] = blockDataPool.Get(); } } this.Screens[X][Y] = oneScreenBlocks; return oneScreenBlocks; } /// <summary> /// 判断RectangularCoordinates数组中是否存在某个指定的RectangularCoordinates /// </summary> /// <param name="a">RectangularCoordinates的List</param> /// <param name="x">待判断的RectangularCoordinates的x值</param> /// <param name="y">待判断的RectangularCoordinates的y值</param> /// <returns>是否存在</returns> bool InNeedShowScreen(List<RectangularCoordinates> a, int x, int y) { foreach (RectangularCoordinates item in a) { if (x == item.x && y == item.y) { return true; } } return false; } /// <summary> /// [外部]根据当前移动的格子数生成其所在的屏幕和周围的屏幕 /// </summary> /// <param name="x">水平方向移动了多少格子</param> /// <param name="y">垂直方向移动了多少格子</param> public void GenerateNineScreenAtPosition(int x, int y) { // 检测格子所在的屏幕 var needLoadScreen = GetNeedLoadScreenCoordinate(x, y); x = needLoadScreen.x; y = needLoadScreen.y; // 取得当前应该显示的屏幕和其周围一圈的屏幕对应坐标(X, Y) List<RectangularCoordinates> needShowScreens = GetAroundScreenCoordinate(x, y); // 计算哪些需要进行移除并且移除对应的格子 for (int i = 0; i < BigMapConfigs.MapScreenRow; i++) { for (int j = 0; j < BigMapConfigs.MapScreenCol; j++) { // 是本次需要显示的 保留 if(InNeedShowScreen(needShowScreens, i, j)) { continue; } // 不是本次需要显示的 应当清除 else { if(null != this.Screens[i][j]) { this.Screens[i][j].Clear(blockPool, blockDataPool); this.screenBlocksPool.Remove(this.Screens[i][j]); this.Screens[i][j] = null; } } } } // 当前位置有则生成周围, 没有则先生成当前位置, 然后再生成周围 GenerateArround(this.Screens[x][y] ?? InitGenerateBlocks(GetScreenPosition(x, y), x, y)); } /// <summary> /// 获取指定屏幕对应的位置 /// </summary> /// <param name="row">指定屏幕的x行</param> /// <param name="col">指定屏幕的y列</param> /// <returns></returns> Vector3 GetScreenPosition(int row, int col) { float x, y, z; x = this.StartScreenFirstBlockPosition.x + (col - Y) * BigMapConfigs.OneScreenCol * BigMapConfigs.BlockWidth + (BigMapConfigs.OneScreenCol - 1) / 2.0f * BigMapConfigs.BlockWidth; y = this.StartScreenFirstBlockPosition.y; z = this.StartScreenFirstBlockPosition.z - (row - X) * BigMapConfigs.OneScreenRow * BigMapConfigs.BlockHeight - (BigMapConfigs.OneScreenRow - 1) / 2.0f * BigMapConfigs.BlockHeight; return new(x, y, z); } /// <summary> /// [内部]生成当前块周围的所有块 /// </summary> /// <param name="currentBlocks">当前屏幕的块</param> void GenerateArround(BigMapOneScreenBlocks currentBlocks) { /* 计算需要生成周围的哪些快 * 1. 可能存在边界位置 * 2. 可能存在已经生成的位置 */ // 计算周围需要生成的块 List<Coordinate> aroundPositions = GetAroundScreenCoordinate(currentBlocks); // 根据当前区域中的第一个(左上角)进行计算 // 计算出该区域周围其他区域的位置 // 调用InitGenerateBlocks()方法生成每个区域 foreach (var item in aroundPositions) { InitGenerateBlocks(item.center, item.x, item.y); } } /// <summary> /// 查找指定位置的周围是否有可以创建的位置 /// </summary> /// <param name="block">指定的块</param> /// <returns></returns> List<Coordinate> GetAroundScreenCoordinate(BigMapOneScreenBlocks block) { int x = block.X, y = block.Y; List<Coordinate> result = new(); // 上一行 if (x - 1 >= 0) { // 左边 if (y - 1 >= 0 && null == this.Screens[x - 1][y - 1]) { result.Add(new(x - 1, y - 1, GetScreenPosition(x - 1, y - 1))); } // 中间 if (null == this.Screens[x - 1][y]) { result.Add(new(x - 1, y, GetScreenPosition(x - 1, y))); } // 右边 if (y + 1 < BigMapConfigs.MapScreenCol && null == this.Screens[x - 1][y + 1]) { result.Add(new(x - 1, y + 1, GetScreenPosition(x - 1, y + 1))); } } // 本行 { // 左边 if (y - 1 >= 0 && null == this.Screens[x][y - 1]) { result.Add(new(x, y - 1, GetScreenPosition(x, y - 1))); } // 右边 if (y + 1 < BigMapConfigs.MapScreenCol && null == this.Screens[x][y + 1]) { result.Add(new(x, y + 1, GetScreenPosition(x, y + 1))); } } // 下一行 if (x + 1 < BigMapConfigs.MapScreenRow) { // 左边 if (y - 1 >= 0 && null == this.Screens[x + 1][y - 1]) { result.Add(new(x + 1, y - 1, GetScreenPosition(x + 1, y - 1))); } // 中间 if (null == this.Screens[x + 1][y]) { result.Add(new(x + 1, y, GetScreenPosition(x + 1, y))); } // 右边 if (y + 1 < BigMapConfigs.MapScreenCol && null == this.Screens[x + 1][y + 1]) { result.Add(new(x + 1, y + 1, GetScreenPosition(x + 1, y + 1))); } } return result; } /// <summary> /// 获取指定屏幕和周围的一圈的屏幕坐标的列表 /// </summary> /// <param name="x">指定屏幕的行</param> /// <param name="y">指定屏幕的列</param> /// <returns>一个存有坐标的List</returns> List<RectangularCoordinates> GetAroundScreenCoordinate(int x, int y) { List<RectangularCoordinates> result = new(); // 上一行 if (x - 1 >= 0) { // 左边 if (y - 1 >= 0) { result.Add(new(x - 1, y - 1)); } // 中间 result.Add(new(x - 1, y)); // 右边 if (y + 1 < BigMapConfigs.MapScreenCol) { result.Add(new(x - 1, y + 1)); } } // 本行 { // 左边 if (y - 1 >= 0) { result.Add(new(x, y - 1)); } // 自己 result.Add(new(x, y)); // 右边 if (y + 1 < BigMapConfigs.MapScreenCol) { result.Add(new(x, y + 1)); } } // 下一行 if (x + 1 < BigMapConfigs.MapScreenRow) { // 左边 if (y - 1 >= 0) { result.Add(new(x + 1, y - 1)); } // 中间 result.Add(new(x + 1, y)); // 右边 if (y + 1 < BigMapConfigs.MapScreenCol) { result.Add(new(x + 1, y + 1)); } } return new(); } /// <summary> /// 根据当前移动的格子数获取其对应的九宫格屏幕坐标 /// </summary> /// <param name="x">水平方向移动了多少格子</param> /// <param name="y">垂直方向移动了多少格子</param> /// <returns>当前显示的屏幕坐标x和y</returns> private RectangularCoordinates GetNeedLoadScreenCoordinate(int x, int y) { // 初始化为最开始的地方 int resY = Y, resX = X; // 父级向左移动(显示更加右边的内容,屏幕数增大) if(x < 0) { resY += Mathf.Abs(x) / BigMapConfigs.OneScreenCol + (Mathf.Abs(x) % BigMapConfigs.OneScreenCol > 0 ? 1 : 0); if (resY >= BigMapConfigs.MapScreenCol) { resY = BigMapConfigs.MapScreenCol - 1; } } // 父级向右移动(显示更小的内容) else if (x > 0) { resY -= Mathf.Abs(x) / BigMapConfigs.OneScreenCol + (Mathf.Abs(x) % BigMapConfigs.OneScreenCol > 0 ? 1 : 0); if (resY < 0) { resY = 0; } } // 父级向上移动(显示更大的内容) if(y > 0) { resX += Mathf.Abs(y) / BigMapConfigs.OneScreenRow + (Mathf.Abs(x) % BigMapConfigs.OneScreenRow > 0 ? 1 : 0); if(resX >= BigMapConfigs.MapScreenRow) { resX = BigMapConfigs.MapScreenRow - 1; } } // 父级向下移动(显示更小的内容) else if (y < 0) { resX -= Mathf.Abs(y) / BigMapConfigs.OneScreenRow + (Mathf.Abs(x) % BigMapConfigs.OneScreenRow > 0 ? 1 : 0); if(resX < 0) { resX = 0; } } return new(resX, resY); } }
|
上面的代码中存在SingletonBase
这样一个类,该类为单例模式的泛型基类,不针对U3D对象。具体实现内容在阿茶之前的文章游戏Traveller的开发记录中,这就不再占用篇幅进行描述了。
大地图的主体部分
该类所负责的内容也很简单
- 控制移动
- 负责告知屏幕管理的类来切换显示应当操作的
screen
这里阿茶觉得不需要做过多的解释,因此决定直接放上对应的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104
| /// <summary> /// 所有块的父节点 /// 1. 控制移动 /// 2. 控制动态生成和隐藏 /// </summary> public class BigMapBlocksParent : MonoBehaviour { #region 测试用代码部分 /// <summary> /// 每一个块的预制体 /// </summary> public GameObject blockPrefab; /// <summary> /// 相机 /// </summary> public Camera Camera; #endregion 测试用代码部分
#region 正式代码部分 /// <summary> /// 块的对象池(GameObject) /// </summary> GameObjectPool blockPool; /// <summary> /// 块的数据对象池(BigMapBlockData) /// </summary> UnGameObjectPool<BigMapBlockData> blockDataPool;
#region 移动相关 /// <summary> /// 是否正在移动地图 /// </summary> bool startMove = false; /// <summary> /// 点击的点和地图中心点的差值 /// </summary> Vector3 clickedDeltaPosition; /// <summary> /// 当前位置和起始点的差值 /// </summary> Vector3 deltaWithStartPosition; /// <summary> /// 地图的起始点 /// </summary> Vector3 startPosition; #endregion 移动相关
# endregion 正式代码部分
void Start() { // 初始化块的对象池 blockPool = new(blockPrefab, Instantiate); // 初始化块数据的对象池 blockDataPool = new(); // 记录初始位置 startPosition = this.transform.position; // 九宫格管理器初始化, 初始化第一个屏幕 BigMapScreensManager.Instance.SetStart(0, 0).Init(blockPool, blockDataPool, this.transform, startPosition); } // Update is called once per frame void Update() { MoveMap(); this.deltaWithStartPosition = this.transform.position - this.startPosition; }
/// <summary> /// 移动相关代码 /// </summary> void MoveMap() { Vector3 Mouseposition = Camera.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, 17)); Mouseposition.y = 0; if (!startMove && Input.GetMouseButton(0)) { startMove = true; clickedDeltaPosition = this.transform.position - Mouseposition; } if (Input.GetMouseButtonUp(0)) { startMove = false; // 检测是否需要加载 NeedLoadCheck(); } if (startMove) { // TODO 限制移动的范围 this.transform.position = Mouseposition + clickedDeltaPosition; } } /// <summary> /// 检查是否需要加载周围的格子 /// </summary> void NeedLoadCheck() { // 水平方向、竖直方向分别需要移动多少格子 int x, y; x = (int)(deltaWithStartPosition.x / BigMapConfigs.BlockWidth); y = (int)(deltaWithStartPosition.z / BigMapConfigs.BlockHeight); // 通知该屏幕加载周围剩下的屏幕 BigMapScreensManager.Instance.GenerateNineScreenAtPosition(x, y); } }
|
总结
本次的需求从概念上来看非常简单,但是在实际实现的过程中,遇到了各种各样的细节上的问题。其中最让阿茶头痛的就是在编写坐标换算的地方,看似非常简单的几行在这里耗费了阿茶大量的脑细胞来思考怎样能够找到一个规律将其简化。
为了记录一下阿茶逝去的这些脑细胞,阿茶决定在这里贴上优化之前和优化之后的代码
删除的内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
| /// <summary> /// 左上的中心点位置 /// </summary> public Vector3 LeftUpPosition { get { return new Vector3( -(BigMapConfigs.OneScreenCol + 1) / 2.0f * BigMapConfigs.BlockWidth + blocks[0][0].transform.position.x, 0 , (BigMapConfigs.OneScreenRow + 1) / 2.0f * BigMapConfigs.BlockHeight + blocks[0][0].transform.position.z ); } }
/// <summary> /// 上边的中心点位置 /// </summary> public Vector3 UpPosition { get { return new Vector3((BigMapConfigs.OneScreenCol - 1) / 2.0f * BigMapConfigs.BlockWidth + blocks[0][0].transform.position.x, 0 , (BigMapConfigs.OneScreenRow + 1) / 2.0f * BigMapConfigs.BlockHeight + blocks[0][0].transform.position.z ); } } /// <summary> /// 右上的中心点位置 /// </summary> public Vector3 RightUpPosition { get { return new Vector3( (BigMapConfigs.OneScreenCol + (BigMapConfigs.OneScreenCol - 1) / 2.0f) * BigMapConfigs.BlockWidth + blocks[0][0].transform.position.x, 0 , (BigMapConfigs.OneScreenRow + 1) / 2.0f * BigMapConfigs.BlockHeight + blocks[0][0].transform.position.z ); } } /// <summary> /// 左边中心点位置 /// </summary> public Vector3 LeftPosition { get { return new Vector3( -(BigMapConfigs.OneScreenCol + 1) / 2.0f * BigMapConfigs.BlockWidth + blocks[0][0].transform.position.x, 0 , -((BigMapConfigs.OneScreenRow - 1) / 2.0f * BigMapConfigs.BlockHeight) + blocks[0][0].transform.position.z ); } } /// <summary> /// 右边中心点位置 /// </summary> public Vector3 RightPosition { get { return new Vector3( (BigMapConfigs.OneScreenCol + (BigMapConfigs.OneScreenCol - 1) / 2.0f) * BigMapConfigs.BlockWidth + blocks[0][0].transform.position.x, 0 , -((BigMapConfigs.OneScreenRow - 1) / 2.0f * BigMapConfigs.BlockHeight) + blocks[0][0].transform.position.z ); } } /// <summary> /// 左下中心点位置 /// </summary> public Vector3 LeftDownPosition { get { return new Vector3( -(BigMapConfigs.OneScreenCol + 1) / 2.0f * BigMapConfigs.BlockWidth + blocks[0][0].transform.position.x, 0 , -(BigMapConfigs.OneScreenRow + (BigMapConfigs.OneScreenRow - 1) / 2.0f) * BigMapConfigs.BlockHeight + blocks[0][0].transform.position.z ); } } /// <summary> /// 下边中心点位置 /// </summary> public Vector3 DownPosition { get { return new Vector3( (BigMapConfigs.OneScreenCol - 1) / 2.0f * BigMapConfigs.BlockWidth + blocks[0][0].transform.position.x, 0 , -(BigMapConfigs.OneScreenRow + (BigMapConfigs.OneScreenRow - 1) / 2.0f) * BigMapConfigs.BlockHeight + blocks[0][0].transform.position.z ); } } /// <summary> /// 右下角中心点位置 /// </summary> public Vector3 RightDownPosition { get { return new Vector3( (BigMapConfigs.OneScreenCol + (BigMapConfigs.OneScreenCol - 1) / 2.0f) * BigMapConfigs.BlockWidth + blocks[0][0].transform.position.x, 0 , -(BigMapConfigs.OneScreenRow + (BigMapConfigs.OneScreenRow - 1) / 2.0f) * BigMapConfigs.BlockHeight + blocks[0][0].transform.position.z ); } } #endregion
|
修改成为的内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| /// <summary> /// 获取指定屏幕对应的位置 /// </summary> /// <param name="row">指定屏幕的x行</param> /// <param name="col">指定屏幕的y列</param> /// <returns>指定屏幕对应的(Unity的Transform)坐标</returns> Vector3 GetScreenPosition(int row, int col) { float x, y, z; x = Screens[X][Y].blocks[0][0].transform.position.x + (col - Y) * BigMapConfigs.OneScreenCol * BigMapConfigs.BlockWidth + (BigMapConfigs.OneScreenCol - 1) / 2.0f * BigMapConfigs.BlockWidth; y = Screens[X][Y].blocks[0][0].transform.position.y; z = Screens[X][Y].blocks[0][0].transform.position.z - (row - X) * BigMapConfigs.OneScreenRow * BigMapConfigs.BlockHeight - (BigMapConfigs.OneScreenRow - 1) / 2.0f * BigMapConfigs.BlockHeight; return new(x, y, z); }
|
大概就是这样,没有太多技术含量的东西,不过可能会有很多可以拿来复用的东西,所以就写了这篇文章,如果将来不再从事游戏行业的工作,自己在写一些玩具的时候也可以拿来直接复用一下,也不是不可以,就是这样。
参考资料
Unity的坑——不要用New来创建继承于MonoBehaviour脚本的对象: https://www.cnblogs.com/hwx0000/p/14146852.html
C#方法作参数——关于Action和Func的使用: https://blog.csdn.net/wf824284257/article/details/83661843