UIFrameWork

思考并回答以下问题:

  • Resources.Load后都需要使用Dictionary缓存吗?都需要写在管理类中吗?管理类需要设置成单例吗?
  • 为什么Asset文件夹和Hierarchy视图中的都需要管理?如何管理?
  • 为什么单例模式要分成可以继承MonoBehavior和不继承MonoBehavior的?
  • 为什么要使用单例继承?
  • UIFramework原来是3个类,后来多加了4个类。共7个类。为什么这样做?
  • 将多个UI管理起来成一个系统。怎么理解?
  • 当你不知道要传什么类时,就使用反射。是什么意思?
  • 为什么要自动挂载脚本到预制体上?
  • AddComponet(typeof(A))和AddComponet\()的区别是什么?
  • 枚举只有一种类型的成员:命名的整数值常量。定义枚举,ENUMName,声明EnumName ConstName = EnumName.ConcreteConstant。
  • 按钮点不了两种情况:1.被其他面板挡住了。2.EventSystem被删除掉了。怎么理解?
  • 窗体之间进行解耦合怎么理解?怎么做到?

普通切换方式的弊端

三个UI界面进行切换。

MainPanel.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class MainPanel : MonoBehaviour
{
private Button btnToPackUI;
public GameObject packPanel;

// Use this for initialization
void Start ()
{
btnToPackUI = this.transform.Find("BtnToPackUI").GetComponent<Button>();
btnToPackUI.onClick.AddListener(ShowPackPanel);
}

void ShowPackPanel()
{
packPanel.SetActive(true);
this.gameObject.SetActive(false);
}
}

PackPanel.cs

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

public class PackPanel : MonoBehaviour
{
private Button btnToMainUI;
private Button btnToLevelUI;

public GameObject mainPanel;
public GameObject levelPanel;

// Use this for initialization
void Start ()
{
btnToMainUI = this.transform.Find("BtnToMainUI").GetComponent<Button>();
btnToMainUI.onClick.AddListener(ShowMainPanel);

btnToLevelUI = this.transform.Find("BtnToLevelUI").GetComponent<Button>();
btnToLevelUI.onClick.AddListener(ShowLevelPanel);
}

void ShowMainPanel()
{
mainPanel.SetActive(true);
this.gameObject.SetActive(false);
}

void ShowLevelPanel()
{
levelPanel.SetActive(true);
this.gameObject.SetActive(false);
}
}

LevelPanel.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using UnityEngine;
using System.Collections;
using UnityEngine.UI;

public class LevelPanel : MonoBehaviour
{
private Button btnToPackUI;
public GameObject packPanel;

// Use this for initialization
void Start()
{
btnToPackUI = this.transform.Find("BtnToPackUI").GetComponent<Button>();
btnToPackUI.onClick.AddListener(ShowPackPanel);
}

void ShowPackPanel()
{
packPanel.SetActive(true);
this.gameObject.SetActive(false);
}
}

这种组织方式存在的问题:

  • 1.商业项目中UI窗体会非常多,现在这种处理方式非常杂乱,需要进行一个总体上的管理;
  • 2.窗体之间互相进行引用(在每个窗体内都引用了其他窗体,public GameObject packPanel;),耦合度太高,一旦修改起来很麻烦;
  • 3.脚本代码重复很多;
  • 4.所有的面板直接在场景中会导致包体变大。

解决方案:

  • 1.需要把窗体管理起来,需要一个窗体的管理类;
  • 2.窗体有很多属性是重复的,比如显示隐藏,需要一个基类把相同的行为特征封装起来,不同的窗体继承这个基类进行扩展,当然每个窗体的继承基类的子类脚本还是都要写;
  • 3.把UI变成预制体(Prefab),进行动态加载后缓存。

搭建框架

框架很多,代码各不相同,但是思想是一致的。

关键点:

  • 在Hierarchy视图中的UI游戏对象是可以变成Prefab的,Hierarchy中的一切都可以变成预制体(Hierarchy视图中的任何显示都是对象,都可以变成Prefab,无论是UI、空游戏对象还是模型。)。
  • 组件都是类型,可以直接理解为是类,组件就是一个个类。GetComponent\,T指的是类型,类是类型的一种。类名是确定的,对象名是不确定的,每次new类的时候都可以取新的对象名。
  • 缓存就是使用Dictionary\这个数据结构。缓存什么呢?缓存从Resources文件夹中加载的对象,Resources.Load是加载到Hierarchy视图中的,自然就变成游戏对象(GameObject)了。但是TValue不能写一个GameObject吧?A、B游戏对象的组件的脚本的类型是各异的,而且脚本可以满足里氏替换原则。
  • 脚本自动加载。即脚本写好放在那,然后动态附加到游戏对象上。

工具类

通用函数工具类 GameTool.cs

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

// 游戏工具类,把常用的一些(通用)方法放在这个类里面
public class GameTool : MonoBehaviour
{
// 清理内存的方法(一般在切换场景的时候调用)
public static void ClaerMemory()
{
// 手动调用垃圾回收,不能频繁的去调用,应该在适当的情况下去调用
// 因为垃圾回收会消耗很大的性能
GC.Collect();
// 卸载内存中没用的资源
Resources.UnloadUnusedAssets();
}

// 对PlayerPrefs也进行封装
// Windows下是放在注册表中的

// 操作内存,数据持久化
// 判断系统内存中是否存在某个键
public static bool HasKey(string key)
{
return PlayerPrefs.HasKey(key);
}

// 删除内存中所有的数据
public static void DeleteAll()
{
PlayerPrefs.DeleteAll();
}

// 删除内存中指定的数据
public static void DeleteKey(string key)
{
PlayerPrefs.DeleteKey(key);
}

// 获取值
public static int GetInt(string key)
{
return PlayerPrefs.GetInt(key);
}

public static float GetFloat(string key)
{
return PlayerPrefs.GetFloat(key);
}

public static string GetString(string key)
{
return PlayerPrefs.GetString(key);
}

// 设置值(存值)
public static void SetInt(string key,int value)
{
PlayerPrefs.SetInt(key, value);
}

public static void SetFloat(string key, float value)
{
PlayerPrefs.SetFloat(key, value);
}

public static void SetString(string key, string value)
{
PlayerPrefs.SetString(key, value);
}

// 查找子物体的方法
public static Transform FindTheChild(GameObject goParent, string childName)
{
Transform searchTrans = goParent.transform.Find(childName);

if (searchTrans==null)
{
foreach (Transform trans in goParent.transform)
{
// 递归
searchTrans = FindTheChild(trans.gameObject, hildName);
if (searchTrans!=null)
{
return searchTrans;
}
}
}
return searchTrans;
}

// 获取子物体上面的组件
public static T GetTheChildComponent<T>(GameObject goParent, string childName) where T : Component
{
Transform searchTrans = FindTheChild(goParent, childName);

if (searchTrans != null)
{
return searchTrans.GetComponent<T>();
}
else
{
return null;
}
}

// 给子物体添加组件
public static T AddTheChildComponent<T>(GameObject goParent, string childName) where T : Component
{
Transform searchTrans = FindTheChild(goParent, childName);
if (searchTrans != null)
{
T[] arr = searchTrans.GetComponents<T>();
for (int i = 0; i < arr.Length; i++)
{
// Destroy(arr[i]);// 销毁,但是不会立刻销毁,在当前帧结束前销毁
DestroyImmediate(arr[i],true); // 立刻销毁
}
return searchTrans.gameObject.AddComponent<T>();
}
else
{
return null;
}
}

// 添加子物体
public static void AddChildToParent(Transform parentTrans, Transform childTrans, bool isResetPs = true)
{
childTrans.parent = parentTrans;

if (isResetPs)
{
childTrans.localPosition = Vector3.zero;
}
childTrans.localScale = Vector3.one;
}
}

GameDefine定义一些枚举。枚举只有一种类型的成员:命名的整数值常量。

GameDefine.cs

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

// 物品的类型
public enum E_GoodsType
{
Default, // 全部0
Equipment, // 装备1
Potins, // 药水2
Rune, // 符文3
Material // 材料4
}

// 消息类型
public enum E_MessageType
{
sellGoods, // 出售物体
chooseGoods
}

// 窗体的ID
public enum E_UiId // 枚举写在外面
{
NullUI = 0,
MainUI,
LevelUI,
InforUI,
PackUI,
LoadingUI,
PlayUI,
ExitUI
}

// 窗体的显示方式
public enum E_ShowUIMode
{
// 界面显示出来的时候,不需要去隐藏其他窗体(比如主界面)
DoNothing,
// 界面显示出来的时候,需要隐藏其他窗体,但是不会隐藏保持在最前方的窗体(比如关卡界面不会隐藏信息界面)
HideOther,
// 界面显示出来的时候,需要隐藏所有的窗体,包括保持在最前方的窗体也要去隐藏它(比如PlayUI显示出来,要隐藏所有界面)
HideAll
}

// 窗体的层级类型(父节点的类型)
public enum E_UIRootType
{
KeepAbove, // 保持在最前方的窗体(DoNothing)
Normal // 普通窗体(1、HideOther 2、HideAll)
}

public class GameDefine
{
// Resources文件下
// 窗体的加载路径<窗体ID, 加载路径>
public static Dictionary<E_UiId, string> dicPath = new Dictionary<E_UiId, string>
{
{E_UiId.MainUI, "UIPrefab/"+"MainUI"},
{E_UiId.InforUI, "UIPrefab/"+"InforUI"},
{E_UiId.PackUI, "UIPrefab/"+"PackUI"},
{E_UiId.LevelUI, "UIPrefab/"+"LevelUI"},
{E_UiId.LoadingUI, "UIPrefab/"+"LoadingUI"},
{E_UiId.PlayUI, "UIPrefab/"+"PlayUI"},
{E_UiId.ExitUI, "UIPrefab/"+"ExitUI"}
};

// 通过窗体的ID,得到对应的UI脚本,返回元数据类
// 具有通用性
public static Type GetUIScriptType(E_UiId uiId)
{
Type scriptType = null;

switch (uiId)
{
case E_UiId.NullUI:
Debug.LogError("添加脚本的时候,传入的窗体ID为NullUI");
break;

case E_UiId.MainUI:
scriptType=typeof(MainUI);
break;

case E_UiId.LevelUI:
scriptType = typeof(LevelUI);
break;

case E_UiId.InforUI:
scriptType = typeof(InforUI);
break;

case E_UiId.PackUI:
scriptType = typeof(PackUI);
break;

case E_UiId.LoadingUI:
scriptType = typeof(LoadingUI);
break;

case E_UiId.PlayUI:
scriptType = typeof(PlayUI);
break;

case E_UiId.ExitUI:
scriptType = typeof(ExitUI);
break;

default:
break;
}
return scriptType;
}
}

把日志也封装起来,这样可以总体控制日志的开关。然后转换成dll的形式,放进Plugins文件夹下。

GameDebugger.cs

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

public class GameDebugger: MonoBehaviour
{
// 输出信息的开关
public static bool isOpen = true;

// 普通的提示输出
public static void Log(object message)
{
Log(message, null);
}

public static void Log(object message, Object context)
{
if (isOpen)
{
Debug.Log(message, context);
}
}

// 警告的提示输出
public static void LogWarning(object message)
{
LogWarning(message, null);
}

public static void LogWarning(object message, Object context)
{
if (isOpen)
{
Debug.LogWarning(message, context);
}
}

// 错误的提示输出
public static void LogError(object message)
{
LogError(message, null);
}

public static void LogError(object message, Object context)
{
if (isOpen)
{
Debug.LogError(message, context);
}
}
}

Singleton.cs

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
// 单例模式(两种)
// 1、不继承于MonoBehaviour
// 2、有继承于MonoBehaviour
// 区别:有继承于MonoBehaviour的可以使用脚本生命周期函数
using UnityEngine;
using System.Collections;

namespace UICore
{
// 不继承于Mono的单例模式

// 写成泛型就是为了被继承

// 有一个类需要单例,就写一堆单例代码。需要10个单例类,每个类都要写这么多重复的代码。

// 所以把这些重复的代码提取出来,想变成单例的继承这个类就好了。

// 本来构造函数都要写成private,但是因为需要被继承,如果写成private会直接报错。`Singleton<InforData>.Singleton()' is inaccessible due to its protection level。这是因为C#实例化子类对象(有new的,需要new一次)的时候也会实例化父类对象,但是父类对象私有化了,实例化不了,会报错。需要把父类的构造函数写成protected。

// 就是因为要被多个类继承,所以Instance里不知道new哪个,才写成泛型的。

// 到时传哪个就new哪个就OK了。
public class Singleton<T> where T : new()
{
// 一个静态的字段
protected static T _instance;

public static T Instance
{
get
{
if (_instance==null)
{
_instance = new T();
}
return _instance;
}
}
protected Singleton()
{}
}

// 继承于Mono的单例模式

// 有3种方式,但是使用第2种,因为会有一个游戏初始化脚本,在那个类里把所有需要的单例管理类通过调用AddComponent<T>添加上去,方便管理。

// 第一种方式有一个单例管理类就需要新建一个空对象。

// 第3种方式各个单例管理类可能在随意的代码位置被添加上去,要寻找到在哪个代码位置添加上去的可能会很麻烦,不方便统一管理。

// 泛型约束为Component的子类,即是一个脚本组件,必须继承MonoBehaviour。
public class UnitySingleton<T> : MonoBehaviour where T : Component
{
// 第2个和第3个版本需要定义这个
private static GameObject unitySingletonObj;

protected static T _instance;

public static T Instance
{
// 第1个版本
/* get
{
if (_instance==null)
{
GameObject go = GameObject.Find(typeof(T).Name);
if (go == null)
{
Debug.LogError("场景里面找不到名称为"+ typeof(T).Name+"的物体");
}
_instance = go.GetComponent<T>();
}
return _instance;
} */

// 第2个版本
get
{
if (_instance == null)
{
if (unitySingletonObj == null)
{
unitySingletonObj = GameObject.Find("UnitySingletonObj");

if (unitySingletonObj == null)
{
GameDebuger.LogError("场景里面找不到UnitySingletonObj这个物体,请添加");
}
// 场景切换的时候,不被销毁
DontDestroyOnLoad(unitySingletonObj);
}

_instance = unitySingletonObj.GetComponent<T>();

if (_instance == null)
{
_instance = unitySingletonObj.AddComponent<T>();
}
}

return _instance;
}

// 第3个版本
/*get
{
if (_instance == null)
{
if (unitySingletonObj == null)
{
unitySingletonObj = GameObject.Find("UnitySingletonObj");

if (unitySingletonObj == null)
{
// GameDebuger.LogError("场景里面找不到UnitySingletonObj这个物体,请添加");
// 新建
unitySingletonObj = new GameObject("UnitySingletonObj");
}
// 场景切换的时候,不被销毁
DontDestroyOnLoad(unitySingletonObj);
}
}

_instance = unitySingletonObj.GetComponent<T>();
if(_instance == null)
{
_instance = unitySingletonObj.AddComponent<T>();
}
return _instance;
}*/
}

protected UnitySingleton()
{}
}
}

上面不继承于Mono的单例模式继承存在问题,因为子类还是可以new,可以使用反射来实现真正的单例继承,查看单例的模板与最佳实践

问:

  • 为什么要区分继承和不继承MonoBehavior的单例?
  • 单例的类不是直接调用吗?A类写成单例,就A.Instance使用,为什么还有单例继承这个东西?
  • UIManager为什么使用单例模式?所有的Manager类都要吗?

答:

  • 我所了解的使用单例模式有两个目的,一个是类似计算用,保持一份不会导致计算错误;二是节省内存。UIManager使用单例模式的原因是这个类负责管理游戏内的功能,它判断说这个没在加载,那就需要加载才能进行下一步,如果是多个对象同时运行,则可能会出现顺序错误。就像公司的总经理只有一个人一样,做具体工作的办公人员有很多,但最终调配决定的人只有总经理,不能两个总经理在发号施令,很容易造成工作指令的错发,漏发,重复发。把Manager类比成总经理。一般来说,做调配工作的,汇总工作的Manager需要设置成单例。

脚本管理

把Asset中的几个脚本统一管理,明显需要一个基类。这个基类最重要的是把窗体进行编号,因为编号后有每个窗体的id,可以知道是从哪一个窗体过来的。

问:

  • 让你提取UI的共同元素,你能提取出来吗?
  • 窗体之间如何识别彼此?

界面的显示类型有两种:

  • 1.一直保持在最前面显示的界面;
  • 2.会隐藏其他窗体的窗体,但是不会隐藏保持在最前面显示的界面。

隐藏方式有三种:

  • 1.HideOther。显示的时候隐藏其他窗体。
  • 2.HideAll。界面显示出来的时候,隐藏所有窗体。
  • 3.DoNothing。不去隐藏其他任何窗体。

UI面板脚本基类 BaseUI.cs

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

namespace UICore
{
// 窗体的类型
public class UIType
{
// 显示类型,默认是隐藏其他窗体
public E_ShowUIMode showMode = E_ShowUIMode.HideOther;
// 层级类型(父节点类型),默认是Normal。即隐藏其他窗体。
public E_UIRootType uiRootType = E_UIRootType.Normal;
}

// UI窗体基类,相当于视图层,主要是封装窗体共有的属性以及行为特征
public class BaseUI : MonoBehaviour
{
// 窗体类型
public UIType uiType;

// 窗体的RectTransform
protected RectTransform thisTransform;
// 当前窗体的ID
protected E_UiId uiId = E_UiId.NullUI;
// 上一个跳转过来的窗体ID,默认没有上一个窗体
protected E_UiId beforeUiId = E_UiId.NullUI;

// 供外界访问的,获取当前窗体的ID
public E_UiId GetUiId
{
get
{
return uiId;
}
// 为什么没有set?因为每一个窗体的ID是固定的,只能让外界去获取,而不能让外界随意修改。
}

// 供外界访问的,获取前一个窗体的ID
public E_UiId GetBeforeUiId
{
get
{
return beforeUiId;
}
// 外部可以设置前一个窗体
set
{
beforeUiId = value;
}
}

// 对外提供一个属性,用来判断此窗体显示出来时是否要处理其他窗体的隐藏
public bool IsNeedDealWithUI
{
get
{
// 放在最前方的窗体无需隐藏其他窗体
if (this.uiType.uiRootType == E_UIRootType.KeepAbove)
{
return false;
}
else
{
// 显示出来的时候,需要隐藏其他窗体
return true;
}
}
}

protected virtual void Awake()
{
if(uiType == null)
{
uiType = new UIType();
}

thisTransform = this.GetComponent<RectTransform>();
// 初始化界面元素,如按钮,图片,Text等
InitUiOnAwake();
// 初始化界面数据,如界面的ID,界面的显示类型,界面的层级类型等
InitDataOnAwake();
}

protected virtual void Start()
{}

protected virtual void Update()
{}

// 初始化界面元素
protected virtual void InitUiOnAwake()
{
// 比如查找按钮,给按钮添加监听
}

// 初始化界面数据
protected virtual void InitDataOnAwake()
{
// 比如界面的ID,窗体类型
}

protected virtual void OnEnable()
{
PlayAudio();
}

protected virtual void OnDestroy()
{}

// 显示窗体
public virtual void ShowUI()
{
this.gameObject.SetActive(true);
}

// 隐藏窗体
// 委托作为参数是因为隐藏的时候可能需要执行某些操作
public virtual void HideUI(Action del=null)
{
this.gameObject.SetActive(false);

if (del != null)
{
del();
}
// 保存数据
Save();
}

// 保存数据
protected virtual void Save()
{}

// 播放音效
protected virtual void PlayAudio()
{}
}
}

窗体管理

把Hierarchy层次视图中的几个UI Panel面板对象(GameObject)也进行管理,需要一个管理类。

需要做的事情有:

  • 动态加载后存储。Resource.Load()加载之后会得到一个对象,加载的代码处用到了,别的类,别的地方也需要用到,难道用一次加载一次?所以需要存储起来。对象要一处加载,多处使用的话就需要放进Dictionary数据结构中,在游戏的运行过程中这个字典会在内存中一直存在。
  • 这个Manager是在游戏开始的就要执行的。所以需要直接展示主窗体(或欢迎窗体)。
  • 之前做单例继承的时候提到了,这个脚本因为继承了单例,所以要手动绑定在UnitySingletonObject对象上。UnitySingletonObject放在Canvas下。组织关系如下图所示,注意KeepAboveUIRoot在NormalUIRoot的下方。

UIManager.cs

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

namespace UICore
{
public class UIManager :UnitySingleton<UIManager>
{
// 缓存所有打开过的窗体,可能有100个窗体,最后一关10个没被打开过,前面90个打开过。
private Dictionary<E_UiId, BaseUI> dicAllUI;
// 缓存正在显示的窗体(可能有多个窗体同时正在显示)
private Dictionary<E_UiId, BaseUI> dicShowUI;

// 缓存最近一个显示的窗体
private BaseUI currentUI = null;

// 缓存上一个跳转过来的窗体
private BaseUI beforeUI = null;
private E_UiId beforeUiId = E_UiId.NullUI;

// 缓存画布(整个项目共用一个画布)
private Transform canvas;

// 缓存保持在最前方窗体的父节点
private Transform keepAboveUIRoot;

// 缓存普通窗体的父节点
private Transform normalUIRoot;

void Awake()
{
dicAllUI = new Dictionary<E_UiId, BaseUI>();
dicShowUI = new Dictionary<E_UiId, BaseUI>();
InitUIManager();
}

// 游戏开始的时候UI需要做这几件事
// 1.UIManager切换场景的时候不能被销毁。因为每个场景都需要这个UI管理类来管理。
// 2.寻找到画布和两个UIRoot。
// 3.显示主窗体
private void InitUIManager()
{
// 查找到画布和两个UIRoot
canvas = this.transform.parent;
keepAboveUIRoot = GameTool.FindTheChild(canvas.gameObject, "KeepAboveUIRoot");
normalUIRoot = GameTool.FindTheChild(canvas.gameObject, "NormalUIRoot");

// 切换场景的时候不让画布销毁
DontDestroyOnLoad(canvas);

if (dicAllUI!=null)
{
dicAllUI.Clear();
}
if (dicShowUI!=null)
{
dicShowUI.Clear();
}

//开始显示UI界面
ShowUI(E_UiId.MainUI);
ShowUI(E_UiId.InforUI);
}

// 对外提供的,显示窗体的方法(isNeedSaveBeforeUiId代表是否要保存上一个切换过来的窗体ID,一般情况下是需要保存的,除了窗体的反向切换)
public void ShowUI(E_UiId uiId, bool isNeedSaveBeforeUiId = true)
{
if (uiId == E_UiId.NullUI)
{
uiId = E_UiId.MainUI;
}
// 1、判断窗体是否有显示过
// 如果有显示过,把窗体再次显示出来,隐藏当前窗体
// 从未显示过,先把窗体动态加载出来,然后再显示,接着隐藏当前窗体
BaseUI baseUI = JudgeShowUI(uiId);

if (baseUI != null)
{
baseUI.ShowUI();

if (isNeedSaveBeforeUiId)
{
// 存储
baseUI.GetBeforeUiId = beforeUiId;
}
}
}

// 对外提供的,窗体反向切换的方法(点击界面返回按钮)
// 传入上一个窗体的ID
public void ReturnUI(E_UiId beforeUiId)
{
ShowUI(beforeUiId, false);
}

// 根据枚举uiId判断窗体是否有加载显示过
public BaseUI JudgeShowUI(E_UiId uiId)
{
// 判断窗体是否正在显示
if (dicShowUI.ContainsKey(uiId))
{
return null;
}
// 判断窗体是否有加载显示过
BaseUI baseUI = GetBaseUI(uiId);

if (baseUI == null) // 说明将要显示的窗体从未加载显示过
{
// 去动态加载出来
if (GameDefine.dicPath.ContainsKey(uiId))
{
// 得到窗体的加载路径,去加载
string path = GameDefine.dicPath[uiId];
GameObject theUI = Resources.Load<GameObject>(path);
// 是否加载到
if (theUI != null)
{
GameObject willShowUI = Instantiate(theUI);

// 判断显示出来的窗体上面是否有挂载UI脚本
baseUI = willShowUI.GetComponent<BaseUI>();

if (baseUI == null)// 窗体上面没有挂载UI脚本
{

// 自动挂载
// 为什么要自动挂载?
// 不自动挂载会怎么样?
// 不自动挂载就需要手动先挂载上去再生成预制体。
// 缺点:
// 1.每次修改脚本需要重新生成预制体。和预制体绑定了。
// 2.资源变大。即使不自动挂载脚本还是存在的。挂载之后预制体变大了。
Type type = GameDefine.GetUIScriptType(uiId);
// AddComponent支持两种调用方式
/* public Component AddComponent(Type componentType);
SphereCollider sc = gameObject.AddComponent(typeof(SphereCollider)) as SphereCollider;*/

/* public T AddComponent();
SphereCollider sc = gameObject.AddComponent<SphereCollider>() as SphereCollider; */

// 还有一个遗弃的版本
// Obsolete public Component AddComponent(string className);
// 难道以后不能使用string类型了吗?不是的

// 不能挂载BaseUI.cs,因为它是基类,只能挂载它的一个子类。
// 使用AddComponent<>()泛型版本无法传入变量。
//willShowUI.AddComponent<A>();那个A是个类型,不能是变量
// 使用泛型版本需要再写判断语句,如果把这些判断语句提出为函数的话,没有通用性。
// AddComponent(typeof(MainUI))。反射也要写判断语句,但是具有通用性。
// 当你不知道要传一个变量时,就使用反射。
baseUI = willShowUI.AddComponent(type) as BaseUI;

}
// 把生成出来的窗体放在对应的UiRoot下面
Transform uiRoot = GetUIRoot(baseUI);
GameTool.AddChildToParent(uiRoot, willShowUI.transform);

willShowUI.GetComponent<RectTransform>().sizeDelta = Vector2.zero;
willShowUI.GetComponent<RectTransform>().anchoredPosition3D = Vector3.zero;
dicAllUI.Add(uiId,baseUI);

}
else
{
Debug.LogError("在路径"+path+"下面找不到预制体"+ uiId+",请检查路径下面是否有该预制体");
}

}
else
{
Debug.LogError("GameDefine下面没有窗体ID为"+ uiId+"的加载路径");
}
}

return baseUI;
}

// 判断窗体的UIRoot类型
public Transform GetUIRoot(BaseUI baseUI)
{
if(baseUI.uiType.uiRootType == E_UIRootType.KeepAbove)
{
return keepAboveUIRoot;
}
return normalUIRoot;
}

private void UpdateDicShowUI(BaseUI baseUI)
{
//if (dicShowUI.Count>0)
//{
// foreach (KeyValuePair<E_UiId, BaseUI> item in dicShowUI)
// {
// //隐藏正在显示的窗体
// item.Value.HideUI(null);
// //缓存上一个窗体的ID
// beforeHidE_UiId = item.Key;
// }
// dicShowUI.Clear();
//}
HideAllUI();
dicShowUI.Add(baseUI.UiId,baseUI);
}

// 判断窗体是否有加载显示过
private BaseUI GetBaseUI(E_UiId uiId)
{
// 缓存里包含这个窗体id,说明已经加载显示过
if (dicAllUI.ContainsKey(uiId))
{
return dicAllUI[uiId];
}
else
{
return null;
}
}

// 隐藏单个窗体
public void HideSingleUI(E_UiId uiId,DelAfterHideUI del)
{
if (!dicShowUI.ContainsKey(uiId))
{
return;
}
if (del!=null)
{
dicShowUI[uiId].HideUI(del);
}
else
{
dicShowUI[uiId].HideUI(null);
}
dicShowUI.Remove(uiId);
}

// 隐藏所有窗体
public void HideAllUI()
{
if (dicShowUI.Count > 0)
{
foreach (KeyValuePair<E_UiId, BaseUI> item in dicShowUI)
{
// 隐藏正在显示的窗体
item.Value.HideUI(null);
// 缓存上一个窗体的ID
beforeHidE_UiId = item.Key;
}
dicShowUI.Clear();
}
}
}
}

消息中心

使用观察者模式的消息中心。这边没有新建观察者的接口并实现子类,而是通过C#语言的委托来代替观察者。

观察者模式最重要的一点是建立一个消息中心类,维护一个消息枚举和观察者的字典。

简单版本的观察者。缺点在于:1.对参数的装箱拆箱影响性能;2.参数只能传1个。

MessageCenter.cs

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

namespace UICore
{
public class MessageCenter : MonoBehaviour
{
// 监听到消息后所要处理的逻辑
public delegate void DelCallBack(object param);

// 定义一个字典用来存放所有的监听者<消息类型,监听到该消息所要处理的逻辑>
public static Dictionary<E_MessageType, DelCallBack> dicMessageType = new Dictionary<E_MessageType, DelCallBack>();

/// <summary>
/// 对外提供的,添加监听者
/// </summary>
/// <param name="messageType">事先定义好的枚举消息类型</param>
/// <param name="handler">该消息触发时,要执行的监听者委托。一个消息可以有多个监听者。委托作为参数时,实参只能传一个方法</param>
public static void AddMessageListener(E_MessageType messageType, DelCallBack handler)
{
if (!dicMessageType.ContainsKey(messageType))
{
dicMessageType.Add(messageType, null);
}
dicMessageType[messageType] += handler;
}

/// <summary>
/// 对外提供的,取消指定的监听者
/// </summary>
/// <param name="messageType">事先定义好的枚举消息类型</param>
/// <param name="handler">该消息触发时,要执行的监听者委托。一个消息可以有多个监听者。</param>
public static void RemoveMessageListener(E_MessageType messageType, DelCallBack handler)
{
if (dicMessageType.ContainsKey(messageType))
{
dicMessageType[messageType] -= handler;
}
}

/// <summary>
/// 对外提供的,移除所有的监听者
/// </summary>
public static void RemoveAllMessageListener()
{
dicMessageType.Clear();
}

/// <summary>
/// 广播消息(分发消息)
/// </summary>
/// <param name="messageType"></param>
/// <param name="del"></param>
public static void SendMessage(E_MessageType messageType, object param=null)
{
DelCallBack del;
if (dicMessageType.TryGetValue(messageType, out del))
{
del(param);
}
}
}

}
**新版本的事件监听**

EventController是观察者模式必备的维护事件与观察者列表的类。
Dispatch是发送的意思。

EventController.cs

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

namespace UICore
{
/// <summary>
/// 消息控制类(事件处理类)。
/// </summary>
public class EventController
{
// Delegate是所有委托类型的父类
// 简单版维护的字典的value值是一个具体的委托类型。
// 此处的value是委托的父类,及可以是各种委托类型。
// 比如简单版是无参无返回委托,则字典里的value只能传无参无返回值的方法。
// 这个版本就不是了,字典的value是委托类型,往里添加的时候基本可以写所有方法。
private Dictionary<E_MessageType, Delegate> m_theRouter = new Dictionary<E_MessageType, Delegate>();

public Dictionary<E_MessageType, Delegate> TheRouter
{
get { return m_theRouter; }
}

/// <summary>
/// 判断是否已经包含事件
/// </summary>
/// <param name="eventType"></param>
/// <returns></returns>
public bool ContainsEvent(E_MessageType eventType)
{
return m_theRouter.ContainsKey(eventType);
}
//清除所有监听
public void ClearAllListener()
{
m_theRouter.Clear();
}

/// <summary>
/// 处理增加监听器前的事项, 检查 参数等
/// </summary>
/// <param name="eventType"></param>
/// <param name="listenerBeingAdded"></param>
private void OnListenerAdding(E_MessageType eventType, Delegate listenerBeingAdded)
{

if (!m_theRouter.ContainsKey(eventType))
{
m_theRouter.Add(eventType, null);
}

// Delegate d = m_theRouter[eventType];
//if (d != null && d.GetType() != listenerBeingAdded.GetType())
//{
// throw new Exception(string.Format(
// "Try to add not correct event {0}. Current type is {1}, adding type is {2}.",
// eventType, d.GetType().Name, listenerBeingAdded.GetType().Name));
//}
}

/// <summary>
/// 移除监听器之前的检查
/// </summary>
/// <param name="eventType"></param>
/// <param name="listenerBeingRemoved"></param>
private bool OnListenerRemoving(E_MessageType eventType, Delegate listenerBeingRemoved)
{

if (!m_theRouter.ContainsKey(eventType))
{
return false;
}

//Delegate d = m_theRouter[eventType];
//if ((d != null) && (d.GetType() != listenerBeingRemoved.GetType()))
//{
// throw new Exception(string.Format(
// "Remove listener {0}\" failed, Current type is {1}, adding type is {2}.",
// eventType, d.GetType(), listenerBeingRemoved.GetType()));
//}
//else
return true;
}

/// <summary>
/// 移除监听器之后的处理。删掉事件
/// </summary>
/// <param name="eventType"></param>
private void OnListenerRemoved(E_MessageType eventType)
{
if (m_theRouter.ContainsKey(eventType) && m_theRouter[eventType] == null)
{
m_theRouter.Remove(eventType);
}
}

#region 增加监听器
/// <summary>
/// 增加监听器, 不带参数
/// </summary>
/// <param name="eventType"></param>
/// <param name="handler">此处的handler和简单版的不同</param>
public void AddListener(E_MessageType eventType, Action handler)
{
OnListenerAdding(eventType, handler);
m_theRouter[eventType] = (Action)m_theRouter[eventType] + handler;
}

/// <summary>
/// 增加监听器, 1个参数
/// </summary>
/// <param name="eventType"></param>
/// <param name="handler"></param>
public void AddListener<T>(E_MessageType eventType, Action<T> handler)
{
OnListenerAdding(eventType, handler);
m_theRouter[eventType] = (Action<T>)m_theRouter[eventType] + handler;
}

/// <summary>
/// 增加监听器, 2个参数
/// </summary>
/// <param name="eventType"></param>
/// <param name="handler"></param>
public void AddListener<T, U>(E_MessageType eventType, Action<T, U> handler)
{
OnListenerAdding(eventType, handler);
m_theRouter[eventType] = (Action<T, U>)m_theRouter[eventType] + handler;
}

/// <summary>
/// 增加监听器, 3个参数
/// </summary>
/// <param name="eventType"></param>
/// <param name="handler"></param>
public void AddListener<T, U, V>(E_MessageType eventType, Action<T, U, V> handler)
{
OnListenerAdding(eventType, handler);
m_theRouter[eventType] = (Action<T, U, V>)m_theRouter[eventType] + handler;
}

/// <summary>
/// 增加监听器, 4个参数
/// </summary>
/// <param name="eventType"></param>
/// <param name="handler"></param>
public void AddListener<T, U, V, W>(E_MessageType eventType, Action<T, U, V, W> handler)
{
OnListenerAdding(eventType, handler);
m_theRouter[eventType] = (Action<T, U, V, W>)m_theRouter[eventType] + handler;
}
#endregion

#region 移除监听器

/// <summary>
/// 移除监听器, 不带参数
/// </summary>
/// <param name="eventType"></param>
/// <param name="handler"></param>
public void RemoveListener(E_MessageType eventType, Action handler)
{
if (OnListenerRemoving(eventType, handler))
{
m_theRouter[eventType] = (Action)m_theRouter[eventType] - handler;
OnListenerRemoved(eventType);
}
}

/// <summary>
/// 移除监听器, 1个参数
/// </summary>
/// <param name="eventType"></param>
/// <param name="handler"></param>
public void RemoveListener<T>(E_MessageType eventType, Action<T> handler)
{
if (OnListenerRemoving(eventType, handler))
{
m_theRouter[eventType] = (Action<T>)m_theRouter[eventType] - handler;
OnListenerRemoved(eventType);
}
}

/// <summary>
/// 移除监听器, 2个参数
/// </summary>
/// <param name="eventType"></param>
/// <param name="handler"></param>
public void RemoveListener<T, U>(E_MessageType eventType, Action<T, U> handler)
{
if (OnListenerRemoving(eventType, handler))
{
m_theRouter[eventType] = (Action<T, U>)m_theRouter[eventType] - handler;
OnListenerRemoved(eventType);
}
}

/// <summary>
/// 移除监听器, 3个参数
/// </summary>
/// <param name="eventType"></param>
/// <param name="handler"></param>
public void RemoveListener<T, U, V>(E_MessageType eventType, Action<T, U, V> handler)
{
if (OnListenerRemoving(eventType, handler))
{
m_theRouter[eventType] = (Action<T, U, V>)m_theRouter[eventType] - handler;
OnListenerRemoved(eventType);
}
}

/// <summary>
/// 移除监听器, 4个参数
/// </summary>
/// <param name="eventType"></param>
/// <param name="handler"></param>
public void RemoveListener<T, U, V, W>(E_MessageType eventType, Action<T, U, V, W> handler)
{
if (OnListenerRemoving(eventType, handler))
{
m_theRouter[eventType] = (Action<T, U, V, W>)m_theRouter[eventType] - handler;
OnListenerRemoved(eventType);
}
}
#endregion

#region 触发事件
/// <summary>
/// 触发事件, 不带参数触发
/// </summary>
/// <param name="eventType"></param>
/// <param name="handler"></param>
public void TriggerEvent(E_MessageType eventType)
{
Delegate d;
if (!m_theRouter.TryGetValue(eventType, out d))
{
return;
}

var callbacks = d.GetInvocationList();
for (int i = 0; i < callbacks.Length; i++)
{
Action callback = callbacks[i] as Action;

if (callback == null)
{
throw new Exception(string.Format("TriggerEvent {0} error: types of parameters are not match.", eventType));
}

try
{

callback();
}
catch (Exception ex)
{

}
}
}

/// <summary>
/// 触发事件, 带1个参数触发
/// </summary>
/// <param name="eventType"></param>
/// <param name="handler"></param>
public void TriggerEvent<T>(E_MessageType eventType, T arg1)
{


Delegate d;
if (!m_theRouter.TryGetValue(eventType, out d))
{
return;
}

var callbacks = d.GetInvocationList();
for (int i = 0; i < callbacks.Length; i++)
{
Action<T> callback = callbacks[i] as Action<T>;

if (callback == null)
{
throw new Exception(string.Format("TriggerEvent {0} error: types of parameters are not match.", eventType));
}

try
{
callback(arg1);
}
catch (Exception ex)
{

}
}
}

/// <summary>
/// 触发事件, 带2个参数触发
/// </summary>
/// <param name="eventType"></param>
/// <param name="handler"></param>
public void TriggerEvent<T, U>(E_MessageType eventType, T arg1, U arg2)
{
Delegate d;
if (!m_theRouter.TryGetValue(eventType, out d))
{
return;
}
var callbacks = d.GetInvocationList();
for (int i = 0; i < callbacks.Length; i++)
{
Action<T, U> callback = callbacks[i] as Action<T, U>;

if (callback == null)
{
throw new Exception(string.Format("TriggerEvent {0} error: types of parameters are not match.", eventType));
}

try
{
callback(arg1, arg2);
}
catch (Exception ex)
{

}
}
}

/// <summary>
/// 触发事件, 带3个参数触发
/// </summary>
/// <param name="eventType"></param>
/// <param name="handler"></param>
public void TriggerEvent<T, U, V>(E_MessageType eventType, T arg1, U arg2, V arg3)
{
Delegate d;
if (!m_theRouter.TryGetValue(eventType, out d))
{
return;
}
var callbacks = d.GetInvocationList();
for (int i = 0; i < callbacks.Length; i++)
{
Action<T, U, V> callback = callbacks[i] as Action<T, U, V>;

if (callback == null)
{
throw new Exception(string.Format("TriggerEvent {0} error: types of parameters are not match.", eventType));
}
try
{
callback(arg1, arg2, arg3);
}
catch (Exception ex)
{

}
}
}

/// <summary>
/// 触发事件, 带4个参数触发
/// </summary>
/// <param name="eventType"></param>
/// <param name="handler"></param>
public void TriggerEvent<T, U, V, W>(E_MessageType eventType, T arg1, U arg2, V arg3, W arg4)
{
Delegate d;
if (!m_theRouter.TryGetValue(eventType, out d))
{
return;
}
var callbacks = d.GetInvocationList();
for (int i = 0; i < callbacks.Length; i++)
{
Action<T, U, V, W> callback = callbacks[i] as Action<T, U, V, W>;

if (callback == null)
{
throw new Exception(string.Format("TriggerEvent {0} error: types of parameters are not match.", eventType));
}
try
{
callback(arg1, arg2, arg3, arg4);
}
catch (Exception ex)
{
//DebugUtil.Except(ex);
}
}
}

#endregion
}

}

EventDispatcher.cs

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

namespace UICore
{
/// <summary>
/// 事件分发函数。
/// 提供事件注册, 反注册, 事件触发
/// 采用 delegate, dictionary 实现
/// 支持自定义事件。 事件采用字符串方式标识
/// 支持 0,1,2,3 等4种不同参数个数的回调函数
/// </summary>
public class EventDispatcher
{

private static EventController _eventController = new EventController();

public static Dictionary<E_MessageType, Delegate> TheRouter
{
get { return _eventController.TheRouter; }
}

/// <summary>
/// 取消所有事件
/// </summary>
public static void Cleanup()
{
_eventController.ClearAllListener();
}

#region 增加监听器
/// <summary>
/// 增加监听器, 不带参数
/// </summary>
static public void AddListener(E_MessageType eventType, Action handler)
{
_eventController.AddListener(eventType, handler);
}

/// <summary>
/// 增加监听器, 1个参数
/// </summary>
static public void AddListener<T>(E_MessageType eventType, Action<T> handler)
{
_eventController.AddListener(eventType, handler);
}

/// <summary>
/// 增加监听器, 2个参数
/// </summary>
static public void AddListener<T, U>(E_MessageType eventType, Action<T, U> handler)
{
_eventController.AddListener(eventType, handler);
}

/// <summary>
/// 增加监听器, 3个参数
/// </summary>
static public void AddListener<T, U, V>(E_MessageType eventType, Action<T, U, V> handler)
{
_eventController.AddListener(eventType, handler);
}

/// <summary>
/// 增加监听器, 4个参数
/// </summary>
static public void AddListener<T, U, V, W>(E_MessageType eventType, Action<T, U, V, W> handler)
{
_eventController.AddListener(eventType, handler);
}
#endregion

#region 移除监听器
/// <summary>
/// 移除监听器, 不带参数
/// </summary>
static public void RemoveListener(E_MessageType eventType, Action handler)
{
_eventController.RemoveListener(eventType, handler);
}

/// <summary>
/// 移除监听器, 1个参数
/// </summary>
static public void RemoveListener<T>(E_MessageType eventType, Action<T> handler)
{
_eventController.RemoveListener(eventType, handler);
}

/// <summary>
/// 移除监听器, 2个参数
/// </summary>
static public void RemoveListener<T, U>(E_MessageType eventType, Action<T, U> handler)
{
_eventController.RemoveListener(eventType, handler);
}

/// <summary>
/// 移除监听器, 3个参数
/// </summary>
static public void RemoveListener<T, U, V>(E_MessageType eventType, Action<T, U, V> handler)
{
_eventController.RemoveListener(eventType, handler);
}

/// <summary>
/// 移除监听器, 4个参数
/// </summary>
static public void RemoveListener<T, U, V, W>(E_MessageType eventType, Action<T, U, V, W> handler)
{
_eventController.RemoveListener(eventType, handler);
}
#endregion

#region 触发事件
/// <summary>
/// 触发事件, 不带参数触发
/// </summary>
static public void TriggerEvent(E_MessageType eventType)
{
_eventController.TriggerEvent(eventType);
}

/// <summary>
/// 触发事件, 带1个参数触发
/// </summary>
static public void TriggerEvent<T>(E_MessageType eventType, T arg1)
{
_eventController.TriggerEvent(eventType, arg1);
}

/// <summary>
/// 触发事件, 带2个参数触发
/// </summary>
static public void TriggerEvent<T, U>(E_MessageType eventType, T arg1, U arg2)
{
_eventController.TriggerEvent(eventType, arg1, arg2);
}

/// <summary>
/// 触发事件, 带3个参数触发
/// </summary>
static public void TriggerEvent<T, U, V>(E_MessageType eventType, T arg1, U arg2, V arg3)
{
_eventController.TriggerEvent(eventType, arg1, arg2, arg3);
}

/// <summary>
/// 触发事件, 带4个参数触发
/// </summary>
static public void TriggerEvent<T, U, V, W>(E_MessageType eventType, T arg1, U arg2, V arg3, W arg4)
{
_eventController.TriggerEvent(eventType, arg1, arg2, arg3, arg4);
}

#endregion
}
}

实现了解耦合,一个UI不再需要引用另一个UI。

配置表

一般有三种配置格式,Excel、JSON和XML。

ExcelData.cs

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

public class ExcelData
{
// 读取加载TXT配置文件
public static void LoadExcelFormCSV (string cfgName, out Dictionary<string, Dictionary<string, string>> DicContent)
{
// 创建一个TextAsset,通过Resources.Load来加载配置表
TextAsset tw = Resources.Load("Cfg/"+ cfgName) as TextAsset;

// 存放数据表里面的所有内容
string strLine = tw.text;
// 创建一个字典,用于存放数据表的内容(从配置表第四行读取)// <字段名,字段值(string数组)>
Dictionary<string, string[]> content = new Dictionary<string, string[]>();
// 开始解析字符串
ExplainString (strLine, content,out DicContent );
}

// 解析字符串
private static void ExplainString(string strLine, Dictionary<string, string[]> content, out Dictionary<string, Dictionary<string, string>> DicContent)
{
// 创建一个字典,用于存放默认值的//<字段名,默认值>
Dictionary<string, string> initNum = new Dictionary<string, string> ();
// 通过换行符来切割配置表里面的内容,使之成为一行一行的数据
string[] lineArray = strLine.Split (new char[]{'\n'});

// 获取行数
int rows = lineArray.Length - 1;
// 获取列数
int Columns = lineArray[0].Split(new char[] { ',' }).Length;
// 定义一个数组用于存放字段名
string[] ColumnName = new string[Columns];
for (int i = 0; i < rows; i++)
{
// 每一行数据都根据逗号进行分割,得到一个数组
string[] Array = lineArray[i].Split(new char[] { ',' });

for (int j = 0; j < Columns; j++)
{
// 获取Array的列的值
string nvalue = Array[j].Trim();
// 第一行字段名
if (i == 0)
{
// 存储字段名
ColumnName[j] = nvalue;
// 实例化字典,长度为rows - 3,因为是从第四行读取
content[ColumnName[j]] = new string[rows - 3];
}
// if (i == 1)// 此时是读到配置表的第二行(字段的注释),不做处理,配置表所填的值,类型都为string
//{
//}
else if (i == 2) // 第三行默认值
{
// 存储对应字段的默认值//<字段名,默认值>
initNum[ColumnName[j]] = nvalue;
}
else if (i >= 3)// 第三行开始是实际数据,存储实际数据
{
// <字段名,字段值(string数组)>
content[ColumnName[j]][i - 3] = nvalue;
if (nvalue == "")// 如果读到的值为空字符,则给予默认值(默认值就是配置表第三行所填的值)
{
content[ColumnName[j]][i - 3] = initNum[ColumnName[j]];
// <字段名,字段值(string数组)>
}
}

}
}
// 开始解析
ExplainDictionary(content, out DicContent);
}
// 解析字典中的数据
private static void ExplainDictionary(Dictionary<string,string[]> content, out Dictionary<string, Dictionary<string, string>> DicContent)
{
// 实例化字典//<字段名><<ID><字段值>>
DicContent = new Dictionary<string, Dictionary<string, string>>();
// 获取字典中所有的键(字段名)
Dictionary<string, string[]>.KeyCollection Allkeys = content.Keys;
// 遍历所有的字段名
foreach (string key in Allkeys)
{
// 实例化一个hasData的字典//<ID,字段值>
Dictionary<string, string> hasData = new Dictionary<string, string>();
// 获取当前遍历到的key(字段名)所对应的字段值,存到数组里面
string[] Data = content[key];
// 循环这个数组
for (int i = 0; i < Data.Length; i++)
{
// <ID><字段值>
hasData[content["ID"][i]] = Data[i];
}
// 最后给DicContent这个字典赋值
DicContent[key] = hasData; // <字段名><<ID><字段值>>
}
}
}

使用

1
2


初始化

InitGame.cs

1
2
3
4
5
6
void Init()
{
GameObject unitySingletonObj = GameObject.Find("UnitySingletonObj");

unitySingletonObj.AddComponent<UIManager>();
}

场景加载

Loading场景的作用:

  • 1.从场景A直接切换到场景B后,场景A的资源依然在内存里。所以一般新增一个中间场景,叫做Loading场景。此场景展示加载进度。此时内存中有Loading场景和A场景的资源。当加载到B场景时,内存中只有B场景和Loading场景的资源。Loading场景占用资源非常少,可以忽略。
  • 2.减少卡顿。

LoadSceneHelper.cs 需要先把此脚本拖曳到UnitySingletonObj对象下

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

namespace UICore
{
public class LoadSceneHelper : UnitySingleton<LoadSceneHelper>
{
// 用于显示加载进度的进度条
private Slider slider_Progress;

// 异步加载的对象
AsyncOperation asyn;

// 加载场景的实际进度
private int infactProgress;

// 用来显示的进度(在进度条上面显示)
private int showProgress = 0;

private Action asynDel = null;
//
public void LoadNextSceneAsyn(string sceneName, Action del = null)
{
asynDel = null;
// 重置两个进度值
infactProgress = 0;
showProgress = 0;

UIManager.Instance.ShowUI(E_UiId.LoadingUI);
LoadNextScene("LoadingScene");

if (slider_Progress == null)
{
slider_Progress = GameObject.Find("Slider_Progress").GetComponent<Slider>();
}
slider_Progress.value = 0;
// 开始异步加载(StartCoroutine开启协程)
StartCoroutine(LoadSceneAsyn(sceneName));
}

// IEnumerator迭代器
// 为什么迭代器可以用来做协程?
// 因为像foreach一样,一个一个来
// 协程也是这样,一个一个来 。
private IEnumerator LoadSceneAsyn(string sceneName)
{
asyn = SceneManager.LoadSceneAsync(sceneName);
// 场景开始加载的时候,先不显示
asyn.allowSceneActivation = false;
yield return asyn;
}

// 对外提供的,直接加载场景的方法
public void LoadNextScene(string sceneName, Action del=null)
{
SceneManager.LoadScene(sceneName);
if (del != null)
{
del();
}
}

void Update()
{
if (asyn == null)
{
return;
}

// asyn.progress的范围是0~1,但是要注意,这个值在代码里面顶多检测到0.9
if (asyn.progress < 0.9f) // 场景未加载完成
{
infactProgress = (int)asyn.progress * 100 ;
}
else // 默认场景加载完成了
{
infactProgress = 100;
}

if (showProgress < infactProgress)
{
showProgress++;
}
slider_Progress.value = showProgress / 100f;

if (showProgress == 100)
{
UIManager.Instance.ShowUI(E_UiId.PlayUI);
asyn.allowSceneActivation = true;
}

if (asyn.isDone) // 场景全部加载完成
{
// 加载完成后需要处理一些逻辑
if (asynDel != null)
{
asynDel();
}

asynDel = null;
GameTool.ClaerMemory();
}
}
}
}

新增窗体

框架完成后,发挥威力的地方在于新增窗体时。

新增窗体需要以下几个步骤:

写一个UI脚本的步骤:

  • 1.UI要继承于BaseUI;
  • 2.重写两个重要的方法:InitUiOnAwake()和InitDataOnAwake()(InitUIOnAwake主要是初始化UI元素,比如获取按钮,监听按钮单击等等;InitDataOnAwake主要是给该窗体的ID赋值及设置父节点与显示方式。);
  • 3.重写了以上两个方法后,在InitDataOnAwake()方法中一定要给该窗体的ID赋值。

MainUI.cs

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

public class MainUI : BaseUI
{
// 不需要引用其他窗体了
// public GameObject packPanel;

private Button btn_GotoPackUI;

// 滚动的字幕Text
private Transform txt_Notice;

// 滚动字幕的背景
private Transform sv_Bg;

// 滚动字幕的初始位置
private Vector3 startPs;

// 滚动字幕的结束位置
private Vector3 endPs;

// 字幕滚动的速度
private float speed = 50;

protected override void InitUiOnAwake()
{
base.InitUiOnAwake();

txt_Notice = GameTool.FindTheChild(this.gameObject, "Txt_Notice");
sv_Bg = GameTool.FindTheChild(this.gameObject, "Scroll View");
btn_GotoPackUI = GameTool.GetTheChildComponent<Button>(this.gameObject, "Btn_GotoPackUI");

btn_GotoPackUI.onClick.AddListener(GotoPackUI);
}

protected override void InitDataOnAwake()
{
base.InitDataOnAwake();
// 每个UI脚本都必须给窗体的ID赋值,必备操作!!
this.uiId = E_UiId.MainUI;

// 记录滚动字幕的初始位置
startPs = txt_Notice.localPosition;

// 获取字体的长度
float txtLength=txt_Notice.GetComponent<RectTransform>().sizeDelta.x;

// 获取滚动字幕背景的长度
float bgLength=sv_Bg.GetComponent<RectTransform>().sizeDelta.x;

// 记录滚动字幕的结束位置
endPs = new Vector3(startPs.x - txtLength - bgLength, startPs.y, startPs.z);
}

private void GotoPackUI()
{
// GameDebuger.Log("跳转到背包界面");
UIManager.Instance.ShowUI(E_UiId.PackUI);
}

protected override void Update()
{
if (txt_Notice.localPosition.x < endPs.x)
{
txt_Notice.localPosition = startPs;
}
else
{
txt_Notice.localPosition = new Vector3(txt_Notice.localPosition.x - speed*Time.deltaTime, startPs.y, startPs.z);
}
}
}

PackUI.cs

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

public class PackUI : BaseUI
{
private Button btn_Return;
private Button btn_GotoLevelUI;
// private Button btn_Sell;

// 背包物品的预制体
private GameObject goodsPrefab;

// 背包物体的父物体
private Transform goodsContent;

// 五个分类按钮
private Button btn_All;
private Button btn_Equitment;
private Button btn_Potion;
private Button btn_Rune;
private Button btn_Material;

// 以下是被选中物品的相关信息
// 用于显示被选中物品图片的Image
private Image img_Goods;
private Text txt_GoodsName;
private Text txt_HasCount;
private Text txt_Introduce;
private Text txt_Price;

protected override void InitUiOnAwake()
{
base.InitUiOnAwake();

goodsContent = GameTool.FindTheChild(this.gameObject, "GoodsContent");
btn_Return = GameTool.GetTheChildComponent<Button>(this.gameObject, "Btn_Return");
btn_GotoLevelUI = GameTool.GetTheChildComponent<Button>(this.gameObject, "Btn_GotoLevelUI");

btn_Return.onClick.AddListener(ReturnUI);
btn_GotoLevelUI.onClick.AddListener(GotoLevelUI);

// 查找五个分类按钮
btn_All = GameTool.GetTheChildComponent<Button>(this.gameObject, "Btn_All");
btn_Equitment = GameTool.GetTheChildComponent<Button>(this.gameObject, "Btn_Equitment");
btn_Potion = GameTool.GetTheChildComponent<Button>(this.gameObject, "Btn_Potion");
btn_Rune = GameTool.GetTheChildComponent<Button>(this.gameObject, "Btn_Rune");
btn_Material = GameTool.GetTheChildComponent<Button>(this.gameObject, "Btn_Material");

// 给五个分类按钮添加监听
UguiEventListener.Get(btn_All.gameObject).onClick = ChangeType;
UguiEventListener.Get(btn_Equitment.gameObject).onClick = ChangeType;
UguiEventListener.Get(btn_Potion.gameObject).onClick = ChangeType;
UguiEventListener.Get(btn_Rune.gameObject).onClick = ChangeType;
UguiEventListener.Get(btn_Material.gameObject).onClick = ChangeType;

img_Goods = GameTool.GetTheChildComponent<Image>(this.gameObject, "Img_Goods");
txt_GoodsName = GameTool.GetTheChildComponent<Text>(this.gameObject, "Txt_GoodsName");
txt_HasCount = GameTool.GetTheChildComponent<Text>(this.gameObject, "Txt_HasCount");
txt_Introduce = GameTool.GetTheChildComponent<Text>(this.gameObject, "Txt_Introduce");
txt_Price = GameTool.GetTheChildComponent<Text>(this.gameObject, "Txt_Price");
}

protected override void InitDataOnAwake()
{
base.InitDataOnAwake();
this.uiId = E_UiId.PackUI;
goodsPrefab = Resources.Load<GameObject>("Goods/Goods");
InitPack();
EventDispatcher.AddListener<Sprite, int>(E_MessageType.chooseGoods, ShowInfor);
}

private void ShowInfor(Sprite sprite, int beChooseId)
{
img_Goods.sprite = sprite;
// 获取被选中物品的名称
string name = DataController.Instance.ReadCfg("Name", beChooseId, DataController.Instance.dicPack);

txt_GoodsName.text = name;

int hasCount = PackData.Instance.GetCountById(beChooseId.ToString());
txt_HasCount.text = "拥有" + hasCount + "件";
string introduce = DataController.Instance.ReadCfg("Introduce", beChooseId, DataController.Instance.dicPack);
txt_Introduce.text = introduce;

string price = DataController.Instance.ReadCfg("Price", beChooseId, DataController.Instance.dicPack);
txt_Price.text = price;
}

private void InitPack()
{
// 读取背包配置表里面物品的数量
int count = DataController.Instance.ReadCfgCount(DataController.Instance.dicPack);

for (int i = 0; i < count; i++)
{
GameObject goods= Instantiate(goodsPrefab);
GameTool.AddChildToParent(goodsContent, goods.transform);
// 获取物品的数量
int goodsCount = PackData.Instance.GetCountById((i+1).ToString());
GameTool.GetTheChildComponent<Text>(goods, "Txt_GoodsCount").text = goodsCount.ToString();
// 给每一个物品添加实体类
GoodsEntity goodsEntity=goods.AddComponent<GoodsEntity>();
// 记录ID
goodsEntity.goodsId = i + 1;
// 记录物品类型
int typeIndex =int.Parse( DataController.Instance.ReadCfg("Type",i+1,DataController.Instance.dicPack));
goodsEntity.type = (E_GoodsType)typeIndex;
// 读取物品图片的名称
string iconName = DataController.Instance.ReadCfg("IconName",i+1,DataController.Instance.dicPack);
goods.GetComponent<Image>().sprite = Resources.Load<Sprite>("PackSprite/" + iconName);
}
}

private void ReturnUI()
{
UIManager.Instance.ReturnUI(this.beforeUiId);
}

private void GotoLevelUI()
{
UIManager.Instance.ShowUI(E_UiId.LevelUI);
}

private void Sell()
{
// GameDebuger.Log("出售了一个物品,获得了10个金币");
int getCoinCount = 100;
// MessageCenter.SendMessage(E_MessageType.sellGoods, getCoinCount);
EventDispatcher.TriggerEvent<int>(E_MessageType.sellGoods, getCoinCount);
}

// typeObj就是被点击的分类按钮
private void ChangeType(GameObject typeBtn)
{
switch (typeBtn.name)
{
case "Btn_Equitment":
// beChooseType = E_GoodsType.Equipment;
ShowGoodsByType(E_GoodsType.Equipment);
break;

case "Btn_Potion":
// beChooseType = E_GoodsType.Potins;
ShowGoodsByType(E_GoodsType.Potins);
break;

case "Btn_Rune":
//beChooseType = E_GoodsType.Rune;
ShowGoodsByType(E_GoodsType.Rune);
break;

case "Btn_Material":
// beChooseType = E_GoodsType.Material;
ShowGoodsByType(E_GoodsType.Material);
break;

default:
// beChooseType = E_GoodsType.Default;
ShowGoodsByType(E_GoodsType.Default);
break;
}
}

private void ShowGoodsByType(E_GoodsType type)
{
GameObject goods;

for (int i = 0; i < goodsContent.childCount; i++)
{
goods = goodsContent.GetChild(i).gameObject;
if (type == E_GoodsType.Default)
{
if (goods.activeInHierarchy == false)
{
goods.SetActive(true);
}
}
else
{
if (goods.GetComponent<GoodsEntity>().type == type)
{
goods.SetActive(true);
}
else
{
goods.SetActive(false);
}
}
}
}
}

LevelUI.cs

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

public class LevelUI : BaseUI
{
private Button btn_Return;
private Button btn_Level1;

// 没有其他窗体的引用了,达到了解耦合
// public GameObject packPanel;

protected override void InitUiOnAwake()
{
base.InitUiOnAwake();

btn_Return = GameTool.GetTheChildComponent<Button>(this.gameObject, "Btn_Return");
btn_Return.onClick.AddListener(ReturnUI);

btn_Level1 = GameTool.GetTheChildComponent<Button>(this.gameObject, "Btn_Level1");
btn_Level1.onClick.AddListener(EnterLevel);
}

protected override void InitDataOnAwake()
{
base.InitDataOnAwake();
// 需要指定自己的窗体ID
this.uiId = E_UiId.LevelUI;
}

private void EnterLevel()
{
// Debug.Log("进入关卡");
// SceneManager.LoadScene("LevelScene1");
// UIManager.Instance.HideSingleUI(E_UiId.LevelUI);
// LoadSceneHelper.Instance.LoadNextScene("LevelScene1", delegate
// {
// UIManager.Instance.HideSingleUI(E_UiId.LevelUI);
// });
// 通过专门的场景类进行处理
LoadSceneHelper.Instance.LoadNextSceneAsyn("LevelScene1", delegate
{
UIManager.Instance.ShowUI(E_UiId.PlayUI);
});
}

private void ReturnUI()
{
UIManager.Instance.ReturnUI(this.beforeUiId);
}
}

全局协程控制类

协程和Update函数一样,是会中断的,什么时候中断呢?当协程所在的脚本附加的对象SetActive设置为false时,协程就会中断(此时Update也会中断)。而有些时候,协程不能中断,必须执行完,所以定义一个全局协程类脚本,保证其所附加的对象不能被销毁,不会被设置为false。

1
2
3
4
5
// 这个脚本需要手动附加到UnitySingletonObj对象上
public class CoroutineCtrl : UnitySingletonObj<CoroutineCtrl>
{

}

调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Demo : MonoBehaviour
{
void Start()
{
// StartCoroutine(ShowLog());
CoroutineCtrl.Instance.StartCoroutine(ShowLog());
// StartCoroutine是MonoBehavior类的方法,只要是继承MonoBehavior的类就可以直接使用
}

IEnumerator ShowLog()
{
yield return new WaitForSeconds(3f);
for(i = 0; i < 500; i++)
{
Debug.Log(i);
}
}

}

场景管理

GameSceneManager.cs

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

namespace UICore
{
// 场景加载帮助类
public class GameSceneManager : UnitySingleton<GameSceneManager>
{
// 用于显示加载进度的进度条
Slider slider_Progress;
// 异步加载的对象
AsyncOperation asyn;

// 加载场景的实际进度
private int infactProgress=0;
// 用来显示的进度(在进度条上面显示)
private int showProgress=0;

private Action asynDel=null;
// 对外提供的,异步加载场景的方法
public void LoadNextSceneAsyn(string sceneName, Action del = null)
{
asynDel = del;
//重置两个进度值
infactProgress = 0;
showProgress = 0;
UIManager.instance.ShowUI(E_UiId.LoadingUI);
LoadNextScene("LoadingScene");
if (slider_Progress==null)
{
slider_Progress = GameObject.Find("Slider_Progress").GetComponent<Slider>();
}
slider_Progress.value = 0;
// 开始异步加载(StartCoroutine开启协程)
StartCoroutine(LoadSceneAsyn(sceneName));
}
// IEnumerator迭代器
private IEnumerator LoadSceneAsyn(string sceneName)
{
// 执行场景的异步加载
asyn = SceneManager.LoadSceneAsync(sceneName);
// 场景开始加载的时候,先不显示
asyn.allowSceneActivation = false;

yield return asyn;

}
// 对外提供的,直接加载场景的方法
public void LoadNextScene(string sceneName,Action del=null)
{
SceneManager.LoadScene(sceneName);
if (del!=null)
{
del();
}
}
void Update()
{
if (asyn==null)
{
return;
}
// asyn.progress的范围是0~1,但是要注意,这个值在代码里面顶多检测到0.9
if (asyn.progress<0.9f)
{
// 场景未加载完成
infactProgress = (int)asyn.progress*100;
}
else
{
// 默认场景加载完成了
infactProgress = 100;
}
if (showProgress<infactProgress)
{
showProgress++;
}
slider_Progress.value = showProgress / 100f;
if (showProgress==100)
{
// UIManager.Instance.ShowUI(E_UiId.PlayUI);
asyn.allowSceneActivation = true;

}
//if (asyn.isDone)//场景全部加载完成
//{

//}
if (asyn.isDone && showProgress==100)
{
////加载完成后需要处理一些逻辑
if (asynDel != null)
{
asynDel();
}
asynDel = null;

GameTool.ClaerMemory();
asyn = null;
}
}
}
}

AudioManager

AudioManager.cs 对Audio一个简单的封装

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
using UnityEngine;
using System.Collections;
using System.Collections.Generic;
// Unity3D游戏引擎一共支持4个音乐格式的文件
//.AIFF 适用于较短的音乐文件可用作游戏打斗音效
//.WAV 适用于较短的音乐文件可用作游戏打斗音效
//.MP3 适用于较长的音乐文件可用作游戏背景音乐
//.OGG 适用于较长的音乐文件可用作游戏背景音乐
public class AudioManager : UnitySingleton<AudioManager>
{
//背景音乐
private AudioSource audioSource_Music;
private AudioClip musicClip;
//音效
private AudioSource audioSource_MusicEffect;
//private AudioClip[] musicEffectClips;
private Dictionary<string, AudioClip> musicEffectClipDic = new Dictionary<string, AudioClip>();

//音乐音量的大小
private float musicVolume;

public float MusicVolume
{
get { return musicVolume; }
set { musicVolume = value; }
}
//音效音量的大小
private float musicEffectVolume;

public float MusicEffectVolume
{
get { return musicEffectVolume; }
set { musicEffectVolume = value; }
}
//是否静音(默认不关闭)
private bool isCloseAudio = false;

public bool IsCloseAudio
{
get { return isCloseAudio; }
set { isCloseAudio = value; }
}

void Awake()
{
InitAudio();
}
public void InitAudio()
{
audioSource_Music = GameTool.GetTheChildComponent<AudioSource>(this.transform.parent.gameObject, "AudioSource_Music");
musicClip = Resources.Load<AudioClip>("Audio/Music/BgMusic");
audioSource_Music.clip = musicClip;
audioSource_MusicEffect = GameTool.GetTheChildComponent<AudioSource>(this.transform.parent.gameObject, "AudioSource_MusicEffect");
AudioClip[] musicEffectClips = Resources.LoadAll<AudioClip>("Audio/MusicEffect");
for (int i = 0; i < musicEffectClips.Length; i++)
{
musicEffectClipDic.Add(musicEffectClips[i].name,musicEffectClips[i]);
}
//判断内存中是否有记录静音的值
if (GameTool.HasKey("isCloseAudio"))
{
isCloseAudio = bool.Parse(GameTool.GetString("isCloseAudio"));
}
else
{
GameTool.SetString("isCloseAudio","false");
isCloseAudio = false;
}
//判断内存中是否有音乐以及音效的值
if (GameTool.HasKey("MusicVolume"))
{
musicVolume = GameTool.GetFloat("MusicVolume");
}
else
{
//GameTool.SetFloat("MusicVolume",0.5f);
musicVolume = 0.5f;
}
SetMusicValue(musicVolume);
if (GameTool.HasKey("MusicEffectVolume"))
{
musicEffectVolume = GameTool.GetFloat("MusicEffectVolume");
}
else
{
//GameTool.SetFloat("MusicEffectVolume", 0.5f);
musicEffectVolume = 0.5f;
}
SetMusicEffectValue(musicEffectVolume);
//播放背景音乐
PlayOrPauseMusic(isCloseAudio);
}

//供外界调用,播放或者暂停音乐的方法(传入true代表静音,false代表不静音)
public void PlayOrPauseMusic(bool isClose)
{
if (audioSource_Music.clip==null)
{
return;
}
if (isClose)
{
//静音
audioSource_Music.Pause();
isCloseAudio = true;
}
else
{
//播放
audioSource_Music.Play();
isCloseAudio = false;
}
GameTool.SetString("isCloseAudio", isClose.ToString());
}
//供外界调用,播放音效的方法
public void PlayEffectMusic(string name)
{
//基础方法
//for (int i = 0; i < musicEffectClips.Length; i++)
//{
// if (musicEffectClips[i].name==name)
// {
// audioSource_MusicEffect.clip = musicEffectClips[i];
// audioSource_MusicEffect.Play();
// break;
// }
//}
if (!isCloseAudio)
{
if (musicEffectClipDic.ContainsKey(name))
{
audioSource_MusicEffect.clip = musicEffectClipDic[name];
audioSource_MusicEffect.Play();
}

}

}
//供外界调用,调节音量大小的方法
public void SetMusicValue(float value)
{
audioSource_Music.volume = value;
GameTool.SetFloat("MusicVolume", value);
}
public void SetMusicEffectValue(float value)
{
audioSource_MusicEffect.volume = value;
GameTool.SetFloat("MusicEffectVolume", value);
}
}

总结

0%