前段时间开发的动态加载ui界面的功能,在程序面版不太多的情况下还是减少了不少工作量,但由于开发的程序日渐复杂,才终于理解了那些可视化编程插件的意义。毕竟程序再怎么精练也有太多的重复功能,少量的变化就造成了开发过程中无止境的劳动。为此目前也在研究相关的程序化开发程序的插件功能,这两天遇到的面板开发数量已经达到RunTimeUIPanel-ugui这个模块工作的极限 ---- ---- 面板分解粒度过细。
这几日,想在一个面板组合中实现界面之间的切换,但由于切换过来切换过去比较乱,也都是些重复的东西,写了些就放弃了。但功能还是需要实现的,所以就想到把界面切换这种功能抽象出来,和界面启动时动画播放播放一样,写在一个单独的脚本中。由于unity3d中最为常用的就是利用button和toggle这两个控件来实现界面的打开和关闭,所以这个模块目前只支持这种方式的界面切换,因为每个面板都可以打开一组子面板,这样像极了xml中的节点的效果,于是我将这个模块取名叫NodePanels.
下面介绍一个这个 简单 却实用的 NodePanels
一、使用详情
1.Demo背景
假设有一个面板集合,包含三个面板,程序运行时就显示其中一个主要的面板,这个主要的面板上面有一个Button和一个Toggle,如果图一所示:
[图一]
当点击这个面板上的Red这个Button时需要隐藏自身并打开一个红色面板,如图二所示
[图二]
当点击图一面板中的green这个Toggle时需要打开一个弹窗,如图三所示
[图三]
2.脚本挂载
这样的切换,其实是无限重复的一种状态,写程序一定会遇到,NodePanels的功能就是要将这样重复的功能封装起来,下次遇到直接使用就可。要使用这个模块要进行如下的操作:首先在三个面板身上都挂上一个NodePanel这个脚本,在面板一上挂好后设置如图四所示。
[图四]
Start表示一开始就打开,程序中不会进行任何操作,hideSelf选中后将会在打开目标面板后隐藏自身,这个功能暂时只是目标面板的打开方式为Button时生效。Red面板设置如图五所示。
[图五]
这里只需要设置自身的打开方式为Button就可以了,closeButton在自己打开的方式为Button时生效,用于关闭自身并打开父级窗口。
Green面板的设置如图六所示。
[图六]
如果打开方式为Toggle,则无需进行其他选择,当然作为一个nodePanel,都可以自己作为父级再次包含子面板,而达到无限扩展的效果。但目的仅是在高度关联的一组面板间切换,而没有考虑复用的情况。
二、代码说明
1.功能主体
以下这个脚本就是上面那个个NodePanel脚本,内部记录了相关的控件和面板信息,并在Awake中启动时进行注册相关的事件,关闭和开启后,在其他脚本的OnEnable和OnDisable中可以处理对应的事件
public sealed class NodePanel : MonoBehaviour
{
public Button closeBtn;
public OpenType openType;
public List<NodePanelPair> relatedPanels;
private UnityAction onClose;
private void Awake()
{
if (openType == OpenType.ByButton){
if(closeBtn) closeBtn.onClick.AddListener(Close);
}
for (int i = 0; i < relatedPanels.Count; i++)
{
NodePanelPair pair = relatedPanels[i];
if (pair.nodePanel.openType == OpenType.ByButton)
{
if (pair.openBtn != null) pair.openBtn.onClick.AddListener(() =>
{
if (pair.hideSelf)
{
if (pair.nodePanel.Open(() => { UnHide();}))
{
Hide();
}
}
else
{
pair.nodePanel.UnHide();
}
});
}
if ((pair.nodePanel.openType & OpenType.ByToggle) == OpenType.ByToggle)
{
if (pair.openTog != null) pair.openTog.onValueChanged.AddListener((x) =>
{
if (x)
{
pair.nodePanel.UnHide();
}
else
{
pair.nodePanel.Close();
}
});
}
}
}
public void Hide()
{
gameObject.SetActive(false);
}
public void UnHide()
{
gameObject.SetActive(true);
}
public bool Open(UnityAction onClose)
{
IOpenAble panel = gameObject.GetComponent<IOpenAble>();
if ((panel != null && panel.OpenAble()) || panel == null)
{
UnHide();
this.onClose = onClose;
return true;
}
else
{
return false;
}
}
public void Close()
{
ICloseAble panel = gameObject.GetComponent<ICloseAble>();
if ((panel != null && panel.CloseAble()) || panel == null)
{
for (int i = 0; i < relatedPanels.Count; i++){
relatedPanels[i].nodePanel.Hide();
}
if (onClose != null) onClose.Invoke();
Hide();
}
}
}
2.外用接口
下面这两个脚本,需要由目标面板上的脚本来继承,也就是说,目前脚本将有权力控制对应的面板是否能关闭和打开,可以防止一些问题还没有处理完的时候就触发了关闭,和防止一此问题还没有处理就被打开。
public interface IOpenAble
{
bool OpenAble();
}
public interface ICloseAble
{
bool CloseAble();
}
3.界面重绘
虽然这个面板的功能已经有了,但由于Button打开的面板和Toggle打开的面板需要的参数不相同,所以还需要将面板进行一定的重绘。这里依旧使用Rotorz这个列表插件。
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
using System.Collections.Generic;
using UnityEditor;
using Rotorz.ReorderableList;
using NodePanels;
[CustomPropertyDrawer(typeof(NodePanelPair)), CanEditMultipleObjects]
public class NodePanelPairDrawer : PropertyDrawer
{
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
float height = 0 ;
var nodePanelProp = property.FindPropertyRelative("nodePanel");
if (nodePanelProp == null||nodePanelProp.objectReferenceValue == null){
height += EditorGUIUtility.singleLineHeight;
}
else
{
height += 2 * EditorGUIUtility.singleLineHeight;
var obj = new SerializedObject(nodePanelProp.objectReferenceValue);
var typeProp = obj.FindProperty("openType");
OpenType type = (OpenType)typeProp.intValue;
if ((type & OpenType.ByButton) == OpenType.ByButton)
{
height += EditorGUIUtility.singleLineHeight;
}
}
return height;
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var rect = position;
rect.height = EditorGUIUtility.singleLineHeight;
var hideSelfProp = property.FindPropertyRelative("hideSelf");
var openBtnProp = property.FindPropertyRelative("openBtn");
var openTogProp = property.FindPropertyRelative("openTog");
var nodePanelProp = property.FindPropertyRelative("nodePanel");
if (nodePanelProp == null || nodePanelProp.objectReferenceValue == null) {
EditorGUI.PropertyField(rect, nodePanelProp);
return;
}
else
{
var obj = new SerializedObject(nodePanelProp.objectReferenceValue);
var typeProp = obj.FindProperty("openType");
OpenType type = (OpenType)typeProp.intValue;
if (type == OpenType.ByToggle)
{
EditorGUI.PropertyField(rect, openTogProp);
}
if (type == OpenType.ByButton)
{
EditorGUI.PropertyField(rect, hideSelfProp);
rect.y += EditorGUIUtility.singleLineHeight;
EditorGUI.PropertyField(rect, openBtnProp);
}
rect.y += EditorGUIUtility.singleLineHeight;
EditorGUI.PropertyField(rect, nodePanelProp);
}
}
}
[CustomEditor(typeof(NodePanel)), CanEditMultipleObjects]
public class NodePanelDrawer : Editor
{
SerializedProperty script;
SerializedProperty openTypeProp;
SerializedProperty relatedPanelsProp;
SerializedPropertyAdaptor adapt;
SerializedProperty closeBtnProp;
private void OnEnable()
{
script = serializedObject.FindProperty("m_Script");
openTypeProp = serializedObject.FindProperty("openType");
closeBtnProp = serializedObject.FindProperty("closeBtn");
relatedPanelsProp = serializedObject.FindProperty("relatedPanels");
adapt = new SerializedPropertyAdaptor(relatedPanelsProp);
}
public override void OnInspectorGUI()
{
serializedObject.Update();
EditorGUILayout.PropertyField(script);
EditorGUILayout.PropertyField(openTypeProp);
OpenType type = (OpenType)openTypeProp.intValue;
if (type == OpenType.ByButton){
EditorGUILayout.PropertyField(closeBtnProp);
}
ReorderableListGUI.Title("相关面板");
ReorderableListGUI.ListField(adapt);
serializedObject.ApplyModifiedProperties();
}
}
三、问题解释
1.如果需要在面板间传送数据
一开始我被局限在了数据传递这个过程,还写了一种不同于OpenType.ByButton和ByToggle的ByScript,通过事件注册的方式进行数据传递,但后来一样,既然是高度关联的一组面板,直接共享一个数据对象就可以了,有脚本中直接关联根目节点的那个脚本,于是就不存在数据传递的问题了,同时还实现了界面之间的解耦合。
由于将界面打开的功能分离出来就不容易很好的控制界面是否能够打开和关闭,所以还是需要在原脚本中去实现以上两个接口,当然了,不实现也不会错,默认就当成返回为true了。
3.复杂度不足以应付产品
的确,这个模块是非常简单的功能,不能够应付大量的面板切换,所以最好只用于少量的关联高的一组面板中。像那种可以重用和相对独立的面板之间的切换还是要另寻他法的,现在我的项目暂时就是和前面那个框架一起用了。
最后,源码的下载地址如下,本文的demo就在其中:
https://github.com/zouhunter/NodePanels