Prefab

LevelsView

Hierarchy层级

界面

白色的框表示Levels的Rect大小,height为500,width为400.

LevelItem

LevelItem

其中的Enter和Mask都是Button,如果此关卡没有激活,Mask代表的Button会挡住Enter,表示不能选关;反之Mask会关闭,让紫色的Enter按钮露出来,点击Enter就能进入关卡。

LevelItem的锚点对齐方式为top-left,初始的Pos X和Pos Y都为0。

开发准备

Pathes

Pathes中添加预制体路径

1
2
3
4
public const string LEVELS_VIEW = PREFAB_FOLDER + "LevelsView";
public const string LEVEL_ITEM = PREFAB_FOLDER + "LevelItem";
//...
public static readonly string LEVEL_CONFIG = CONFIG_FOLDER + "/LevelConfig.json";

SelectedHeroController

添加进入关卡选择界面的逻辑

1
2
3
4
5
6
7
8
9
10
11
12
public class SelectedHeroController : ControllerBase
{
protected override void InitChild()
{
//...
transform.ButtonAction("OK/Start",() =>
{
UIMgr.Instance.Show(Pathes.LEVELS_VIEW);
});
//...
}
}

自动排列选关按钮

自动排列按钮

LevelCount

在StreamingAssets——Config文件夹内新建LevelConfig.json文件

1
2
3
4
{
"levelCount": 24,
"eachRow": 4
}

这只是个临时配置文件,在后期选关的逻辑会有调整。

LevelsView

LevelsViewLevelsController挂载在上面的Hierarchy根节点“LevelsView”上

1
2
3
4
5
6
7
8
[BindPrefab(Pathes.LEVELS_VIEW, Consts.BIND_PREFAB_PRIORITY_VIEW)]
public class LevelsView : ViewBase
{
protected override void InitChild()
{
UIUtil.Get("Levels").Go.AddComponent<LevelRoot>();
}
}

LevelsController

1
2
3
4
5
6
7
8
9
[BindPrefab(Pathes.LEVELS_VIEW, Consts.BIND_PREFAB_PRIORITY_CONTROLLER)]
public class LevelsController : ControllerBase
{
protected override void InitChild()
{
transform.ButtonAction("Back", UIMgr.Instance.Back);
transform.Find("Levels").gameObject.AddComponent<LevelRootController>();
}
}

LevelRoot

LevelRoot属于View层脚本,LevelRootController属于Controller脚本,挂载在“Levels”节点上。

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
using UnityEngine;

public class LevelRoot : ViewBase
{
private enum Name
{
levelCount = 0,
COUNT
}

protected override void InitChild()
{
var reader = ReaderMgr.Instance.GetReader(Pathes.LEVEL_CONFIG);

for (Name i = 0; i < Name.COUNT; i++)
{
//先创建一个临时变量缓存枚举值,否则直接调用i.ToString()会只访问到遍历的最后一个i的值,也就是COUNT
//因为() => reader[i.ToString()]是延时调用的。
Name temp = i;
TaskQueueMgr<int>.Instance.AddReaderQueue(() => reader[temp.ToString()]);
}

TaskQueueMgr<int>.Instance.Execute(SpawnLevelItem);
}

private void SpawnLevelItem(int[] values)
{
if (values.Length != (int)Name.COUNT)
{
Debug.LogErrorFormat("返回的Value和Key的数量不匹配:Key的正常数量:{0},Value的返回数量:{1}", (int)Name.COUNT, values.Length);
return;
}
int levelCount = values[(int)Name.levelCount];
SpawnItem(levelCount);
}

private void SpawnItem(int count)
{
GameObject itemObj;//重复切换引用的对象,提取出公共变量
for (int i = 0; i < count; i++)
{
itemObj = LoadMgr.Instance.LoadPrefab(Pathes.LEVEL_ITEM, transform);
itemObj.AddComponent<LevelItem>();
}
Reacquire();//重新初始化
}
}

关于TaskQueueMgr的内容在下一节

通过枚举获取值只适合json配置文件只有一层Key的情况。

注意这里的InitChild,它开启了一个异步任务队列,只有异步任务完成之后才将子物体挂载完毕,这时ViewBaseInit生命周期方法早就执行过了,所以我们需要执行Reacquire方法重新初始化子物体。关于ViewBaseReacquire方法在下一节介绍。

LevelRootController

LevelRootController

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
using System.Collections;
using UnityEngine;

public class LevelRootController : ControllerBase
{
protected override void InitChild()
{
var reader = ReaderMgr.Instance.GetReader(Pathes.LEVEL_CONFIG);
reader["levelCount"].Get<int>(data =>
{
CoroutineMgr.Instance.ExecuteOnce(Wait(data));
});
}
private IEnumerator Wait(int count)
{
yield return new WaitUntil(() => transform.childCount >= count);
AddComponent();
Reacquire();
}
private void AddComponent()
{
foreach(Transform trans in transform)
{
trans.gameObject.AddComponent<LevelItemController>();
}
}
}

由于LevelRootController的初始化需要在LevelRoot初始化之后执行,而LevelRoot那里是延时初始化的,所以这里的InitChild需要使用CoroutineMgr等待所有的子物体加载完毕后再执行,并且在最后调用ControllerBase.Reacquire,重新初始化Controller。

关于ControllerBase.Reacquire,在下一节介绍。

在协程中我们使用了WaitUntil(Func<bool>),顾名思义它需要传入一个返回bool的方法,只有方法为true时才会继续执行。

LevelItem

LevelItem属于View层

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
using UnityEngine;

public class LevelItem : ViewBase
{
private readonly int _leftOffset = 50;//偶数行左侧缩进的值
private readonly int _lineSpacing = 10;//行间距
private readonly int _offset = 10;//列间距

protected override void InitChild()
{
int id = transform.GetSiblingIndex();
SetLevelId(id);
bool isOpen = JudgeOpenState(id);
SetMaskState(isOpen);
InitPos(id);
}
private void InitPos(int id)
{
var reader = ReaderMgr.Instance.GetReader(Pathes.LEVEL_CONFIG);
reader["eachRow"].Get<int>(data =>
{
int eachRow = data;
var grid = GetGrid(id, eachRow);
SetPos(grid);
});
}
private void SetLevelId(int id)
{
UIUtil.Get("Enter/Text").SetText(id + 1);
}
private bool JudgeOpenState(int id)
{
int passed = -1;//passed = -1表示从头开始,passed = 0表示已经通过了第一关,以此类推
if (DataMgr.Instance.Contains(DataKeys.LEVEL_PASSED))
{
passed = DataMgr.Instance.Get<int>(DataKeys.LEVEL_PASSED);
}
return id <= passed + 1;
}
private void SetMaskState(bool isOpen)
{
UIUtil.Get("Mask").Go.SetActive(!isOpen);
}

private Vector2 GetGrid(int id, int eachRow)
{
int y = id / eachRow;
int x = id % eachRow;
return new Vector2(x, y);
}

private void SetPos(Vector2 grid)
{
float width = transform.GetRect().rect.width * transform.localScale.x;//注意localScale要相乘,否则会出现问题
float height = transform.GetRect().rect.height * transform.localScale.y;

int indention = grid.y % 2 == 0 ? _leftOffset : 0;

float x = indention + width * 0.5f +(_offset + width) * grid.x;
float y = height * 0.5f + (_lineSpacing + height) * grid.y;
transform.GetRect().anchoredPosition = new Vector2(x, -y);
}
}

在Item中,对外暴露的Init接口只有UI框架内的InitChild,这样做是正确的,在实际开发中,尽量不要在子UI中开放其他的Init方法,否则会在后期整理阅读代码时产生混乱。

在此方法中,ideachRow都没有声明成全局变量,而是全部以方法的参数的形式传入。在实际开发中要注意,没有必要的全局变量就不要声明。单个方法尽量闭包(单个方法内不引用全局变量)。

DataKeys.LEVEL_PASSED在下下节“关卡加载部分”提到,它是当前已经通过的关卡的Key。

LevelItemController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LevelItemController : ControllerBase
{
private int _id;
protected override void InitChild()
{
_id = transform.GetSiblingIndex();
//TODO: 跳转场景
//transform.ButtonAction("Enter", () =>
//{

//});
transform.ButtonAction("Mask", () =>
{
UIMgr.Instance.ShowDialog("当前关卡未开放");
});
}

}