前言
对应游戏中的UI部分,可以使用分层式管理,将2D组件按照功能关系按层次摆放,是比较容易了解、设计和修改的。
分层式管理一般也称树状结构,常用于软件实现和应用中的一种结构。
GOF对于组合模式Composite的定义是:
“将对象以树状结构组合,用以表现部分-全体的层次关系。组合模式让客户端在操作 各个对象或组合和组合对象是一致的”
树状结构来组合各个对象,所以实现上包含了根结点和叶结点的概念。而”根节点”中会包含叶节点的对象,所以当根节点被删除时,叶节点也会被一起删除,并且希望对于”根节点”和“叶节点”在操作方法上能够一致。
这表示,这两种节点都是继承自同一个操作界面,能够对根节点调用的操作,同样能在叶节点上使用。
游戏对象中的分层管理
在Unity中每个GameObject对象都有一个Transform组件,这个组件提供了几个和游戏对象分层操作有关的方法和变量。
变量:
childCount:代表子组件数量
parent:代表父组件中的Transform对象引用
方法:
DetachChildren:解除所有子组件与本身的关联
Find: 寻找子组件
GetChild: 使用Index的方式取回子组件
IsChildOf: 判断某个Transform对象是否为其子组件
SetParent:设置某个Transform对象为其父组件
再仔细分析,则可以将Unity3D的Transform类当成是一个通用类,因为它并不明显得可以察觉出其下又被再分成“目录节点”或是单纯的“单的终端节点”
其实应该说,Transform类完全符合组合模式的需求:“让客户端在操作各个对象或组件时是一致的”。
因此对于场景上所有的游戏对象GameObject,可以不管它们最终代表的什么,对于所有操作都能正确反应。
游戏用户界面
在《P级阵地》中,每一个主要游戏功能属于IGameSystem的子类,这些子类负责实现《P级阵地》中不同的游戏需求和功能,而它们每一个都会利用组合的方式,成为PBaseDefenseGame类的成员。
public abstract class IUserInterface{
protected PBaseDefenseGame m_PBDGame = null;
protected GameObject m_RootUI = null;
private bool m_bActive = true;
public IUserInterface(PBaseDefenseGame PBDGame){
m_PBDGame = PBDGame;
}
public bool IsVisible(){
return m_bActive;
}
public virtual void Show(){
m_RootUI.SetActive(true);
m_bActive = true;
}
public virtual void Hide(){
m_RootUI.SetActive(false);
m_bActive = false;
}
public virtual void Initalize(){}
public virtual void Release(){}
public virtual void Update(){}
}
游戏中使用的4个界面偶是IUseInterface的子类,并且使用组合的方式成为PBaseDefenseGame类的成员。
类结构图如上= = 就如同游戏系统那样,对内可以通过PBaseDefenseGame类中的中介者模式来通知其他系统或界面,对外也可以通过PBaseDenfenseGame类的外观模式让客户端存取和更新用户界面相关的功能
兵营界面的实现
public class CampInfoUI:IUserInterface{
//...界面组件
private Text m_AliveCountTxt = null;
private Text m_CampLvTxt = null;
private Text m_WeaponLvTxt = null;
private Image m_CampImage = null;
public CampInfoUI(PBaseDefenseGame PBDGame):base(PBDGame){
Initialize();
}
public override void Initialize(){
m_RootUI = UITool.FindUIGameObject("CampInfoUI");
//显示的信息
//兵营名称
m_CampNameTxt = UITool.GetUIComponent<Text>(m_RootUI,"CampNameText");
//兵营图
m_CampImage = UITool.GetUIComponent<Image>(m_RootUI,"CampIcon");
}
public void ShowInfo(Icamp Camp){
Show();
m_Camp = Camp;
//名称
m_CampNameTxt.text = m_Camp.GetName();
//...以下都为显示UI信息
}
}
这里的UI脚本并有继承MonoBehaviour 而是继承UI接口,通过构造函数来初始化UI脚本,
然后定义一个公有Show方法来做显示和更新
这里还自定义了UI有关的查找工具类,以便查找UI物体脚本。
public static class UITool{
//场景上的2D画布对象
private static GameObject m_CanvasObj = null;
public static GameObject FindUIGameObject(string UIName){
if(m_CanvasObj == null)
m_CanvasObj = UnityTool.FindGameObject("Canvas");
if(m_CanvasObj == null)
return null;
return UnityTool.FindChildGameObject(m_CanvasObj,UIName);
}
public static T GetUIComponent<T>(GameObject Container,string UIName) where T : UnityEngine.Component {
//找出子对象
GameObject childGameObject = UnityTool.FindChildGameObject(Container,UIName);
if(ChildGameObject == null)
return null;
T tempObj = ChildGameObj.GetComponent<T>();
if(tempObj == null){
Debug.LogWarning("组件["+UIName+"]不是["+typeof(T)+"]");
}
return tempObj;
}
}
以上是UI工具类
UI工具类用到了自定义的UnityTool工具类
public static class UnityTool{
//找到场景上的对象
public static GameObject FindGameObject(string GameObjectName){
//查找场景上的对象
GameObject pTmpGameObj = GameObject.Find(GameObjectName);
if(pTmpGameObj == null) {
Debug.LogWarning("场景中找不到GameObject["+GameObjectName+"]对象");
return null;
}
return pTmpGameObj;
}
public static GameObject FindChildGameObject(GameObject Container,string gameObjectName){
if(Container == null){
Debug.LogError("无父类对象");
return null;
}
Transform pGameObjectTF = null;
//是不是Container本身
if(Container.name == gameobjectName)
pGameObjectTF = Container.transform;
else{
Transfrom [] allChildren = Container.transform.GetComponentsInChildren<Transform>();
foreach(Transform child in allChildren){
if(child.name== gameObjectName)
if(pGameObject == null)
pGameObjectTF = child;
else
Debug.LogWarning("Container["+Container.name+"]下找出重子组件名称["+gameObjectName+"]");
}
if(GameObjectTF == null){
Debug.LogError("组件["+Container.name+"]找不到子组件["+gameObjectName+"]");
return null;
}
return pGameObjectTF.gameObject;
}
}
UnityTool工具类中,FindChildGameObject 方法是用来搜索某游戏对象下的子对象。
对重复命名的问题加以防呆(防止出错的处理)以便在开发时查找调试。
并且因为是这查找中使用,虽然需要遍历一遍,但是组件可以只获取一次,在后期游戏运行状态下并不会一直使用搜索界面组件的功能。
这也是大家常说的:不要频繁调用GetComponent方法获得组件,一般在Awake Start函数获取一遍。
m_CampNameTxt = UIToo.GetUIComponent<Text>(m_RootUI,"CampNameText");
接下来 CampInfoUI有一个公有ShowInfo方法,根据功能需求来控制显示UI信息
public void ShowInfo(ICamp camp){
Show();
m_Camp = camp;
m_CampNameTxt.text = m_Camp.GetName();
m_TrainConstText.text = string.Format("AP:{0}",m_Camp.GetTrainCost());
//训练中信息
ShowOnTranInfo();
//Icon
IAssetFactory Factory = PBDFactory.GetAssetFactory();
m_CampImage.sprite = Factory.LoadSprite(m_Camp.GetIconSpriteName());
if(m_Camp.GetLevel() <= 0) {
EnableLevelInfo(false);
}else{
m_CampLevelInfo(true);
m_CampLvTxt.text = string.Format("等级:"+m_Camp.GetLevel());
m_WeaponLvTxt.text = string.Format("武器等级:"+m_Camp.GetWeaponLevel());
}
}
总结
使用组合模式的优点:
界面与功能分离,更具移植性
工作切分更容易,当脚本移除,就可以让UI设计交由美术和企划组装
界面更改不影响项目:只要维持组件名称不变,界面的更改就不容易影响到游戏现有程序功能的运行。
缺点:
组件名称重复,如果没有将层级切分好,就容易出现该问题,在工具名称添加警告可以解决。
组件更名不易:组件名需要通过字符串来查找,界面组件一旦不能获取,则会出现null值,和不正确的场景,应对的方法同样是在UnityTool中添加查找失败的警告。