提示部分

本文所提到的内容会在整理后上传到阿茶的OSS仓库中,可以通过下面的链接点击下载,如果没有那就是阿茶还没有整理好。

前提

这次要实现一个可以拖动的大地图,大地图由若干的格子组成,而每个格子是由固定数量的3D物体之一进行显示。

假设3D物体足够复杂,运行设备的内存和显存又不足以处理无限量的物体的数据。

写到这里突然冒出一个疑问,既然3D物体是复用的,那为什么不能够直接生成整个地图的数据呢。抱着这个疑问,我去问了师父,得到的答案也让阿茶恍然大悟。就算3D物体是复用的,但是就单单一个Transform的数据,如果是100*100的地图,那就会有10000份,再加上各种各样其他的数据,这样来看数据量也会很大。看样子阿茶真的是个马鹿。

实现思路

为了表述清楚,这里进行一些专用名称的定义,并且在下文会高亮显示这些特定称呼,与正文其他内容以示区分。

名称: block
含义: 组成大地图的每一个格子

名称: 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

大致思路很简单,所以就介绍到这里。

具体实现的过程中可能会存在各种各样的细节,会在实现的时候具体描述。

那么让我们愉快的进入下一章节吧~

实现

对象池

首先,阿茶要介绍一下花心思写出来的(虽然是非常简陋的)对象池部分啦。

思考一下对象池所做的工作,我将对象池需要完成的部分分解成为下述步骤

  1. 有一个数据结构来存储所有放在池中的对象
  2. 负责在别人要对象的时候及时给出一个对象
  3. 负责接收别人不要的对象并妥善处理

不管您现在是单身,还是有对象,或者已经结婚,请都不要因为上面的几条内容中有对象就因此PTSD了哦,当然更不要做出危害公俗良序的事情呀。阿茶可不因此负责哦!

仔细想一想,因为这个需求中不仅需要使用对象池存储3D物体(即GameObject),还需要存储用于保存数据的Scripts。而这两类数据在加载和隐藏时拥有不同的方式,需要单独处理的。所以这里应该创建两类不同的池,根据情况分别调用(其实阿茶也有尝试将这两种方式合一,可是奈何能力有限,还是写成了两个)。

不过这两类的池仅在生成和销毁的时候存在不同的方式,也就是说会有相同(或相似)的部分,因此可以提出公共的部分变成基类,其他只需要继承基类就可以了。

基类

那么公共的部分都有什么呢?

  1. 有一个数据结构来存储所有放在池中的对象
  2. 一个数据结构来存放每个对象和其对应的使用状态标记
  3. 还有一个移除方式

因为这里的移除对于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这种去实现,在网路上检索了很多资讯后找到了这样一个内容,那就是使用ActionFunc两者去实现。前者是不含返回值,后者含有返回值。其使用方法相较于delegate更加贴近于阿茶的理想形态,遂使用。

1
2
3
4
/// <summary>
/// 初始化对象用的方法,这里Monobehavior与new方法冲突,因此需要外部传入
/// </summary>
Func<GameObject, Transform, GameObject> Instantiate;

代码如上,其中尖括号中的最后一个类型是该方法的返回值,最后一个参数之前的所有参数是该方法的参数。即该方法为

1
2
3
4
GameObject Instantiate(GameObject g, Transform t)
{
// 方法具体实现
}

根据需求,阿茶这里选择的实例化方法是可以设置父级节点Transform的实例化方法,如果需要其他方法可能需要重载,这里不做过多陈述。

或者这里还可以修改成一个类的静态变量,这样当有多个池的时候可以减少保存数据的开销,不过针对不同项目有着不同的初始化方法,如果没有统一的游戏入口,每次实例化一个池的时候都会传入这个方法比较保险,不会出现初始化位于调用之前或者忘记初始化Instantiate方法的问题。这里需要具体项目具体分析。

其他额外的变量

由于是GameObject,必然需要知道每次实例化的物体的模板,也就是预制体。

所以这里需要一个变量来保存每次实例化对应的预制体的引用。

1
2
3
4
/// <summary>
/// 每次实例化的预制体
/// </summary>
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;

单一屏幕的管理类

首先来分析一下该类需要做的事情

  1. 管理该屏幕下所有block(物体和对应的数据)
  2. 记录该屏幕坐标
  3. 管理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 坐标换算相关
}

所有屏幕的管理类

这个类需要做的事情如下

  1. 生成单个screen
  2. 生成步骤1中screen周围其他可以生成的screen
  3. 移除步骤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,其要做的工作也很简单。

  1. 遍历当前屏幕坐标周围的8个位置(九宫格除去最中心的位置),找到可以生成(没有超出范围,如i=1i = -1这类位置)且还没有生成过的地方
  2. 对这些位置进行生成。
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的开发记录中,这就不再占用篇幅进行描述了。

大地图的主体部分

该类所负责的内容也很简单

  1. 控制移动
  2. 负责告知屏幕管理的类来切换显示应当操作的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