Prefab
LevelsView


白色的框表示Levels的Rect大小,height为500,width为400.
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
LevelsView
和LevelsController
挂载在上面的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++) { 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
,它开启了一个异步任务队列,只有异步任务完成之后才将子物体挂载完毕,这时ViewBase
的Init
生命周期方法早就执行过了,所以我们需要执行Reacquire
方法重新初始化子物体。关于ViewBase
的Reacquire
方法在下一节介绍。
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; 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; 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方法,否则会在后期整理阅读代码时产生混乱。
在此方法中,id
和eachRow
都没有声明成全局变量,而是全部以方法的参数的形式传入。在实际开发中要注意,没有必要的全局变量就不要声明。单个方法尽量闭包(单个方法内不引用全局变量)。
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();
transform.ButtonAction("Mask", () => { UIMgr.Instance.ShowDialog("当前关卡未开放"); }); } }
|