Unity中的UI系统之UGUI_登陆面板实现

本次练习的资源可以在Unity中的UI系统------UGUI中找到

本文是基于 B 站 UP 主唐老狮的 UGUI 实战练习,记录了完整的设计思路与思考过程。本次练习脱离课程视频独立完成,内容上会略显零散,如有疏漏或错误,欢迎大家指正。


一、目标实现及需求分析

面板切换及对应功能的流程分析

二、实现

2.1 面板基类实现

在实现其他面板前,我们需要实现一个面板的基类,该类需要具有面板的初始化、显示隐藏、淡入淡出等功能,其中淡入淡出是动态变化的所以需要Update实时更新

面板基类设计思路

在一开始我的想法是,通过向显示与隐藏方法传入对应的面板,实现通过预制体动态创建UI面板,但是后面发现和老师的不一样,查询后发现,应该尽量保持单一职责的原则,让BasePanel只负责 "自己的显示、隐藏、淡入淡出、初始化",不负责创建自己

复制代码
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Events;


/// <summary>
/// 面板积类,需要实现面板的初始化、显示隐藏、淡入淡出
/// </summary>
public abstract class BasePanel : MonoBehaviour
{
    #region 面板积类的成员变量
    private CanvasGroup canvasGroup;    //面板的画布组 => 控制面板的淡入淡出效果
    private bool isShowing; 		//当前是否显示
    public float fadeSpeed = 1f;	//当前画布淡入淡出的速度

    private UnityAction hideCallBack;   //面板成功淡出时执行的方法
    //为什么这样设计?
    //答:通过了解这样UI面板只负责实现自己的显示隐藏,淡入淡出
    //具体的创建销毁则由UI管理器负责,
    #endregion

    #region 进行面板的初始化
    //初始化面版关联的UI控件
    public abstract void Init();
    #endregion

    #region 实现面版的显示与隐藏
    public virtual void ShowMe()
    {
        //面板的显示,显示时需要动态创建对应的UI面版
        isShowing = true;
    }
    public virtual void HideMe(UnityAction callBack)
    {
        //面板的隐藏,隐藏时需要销毁对应的UI面板
        isShowing = false;
        hideCallBack = callBack;
    }
    #endregion

    protected virtual void Awake()
    {
        //获取面板上的Canvas组件,用来实现淡入淡出的效果
        canvasGroup = GetComponent<CanvasGroup>();
        if(canvasGroup == null )
            this.AddComponent<CanvasGroup>();
    }
    protected virtual void Start()
    {
        //对面板进行初始化
        Init();
    }
    protected virtual void Update()
    {
        if (isShowing&&canvasGroup.alpha<1)
        {
            //这时候要实现面板的谈入,透明度设置为0-1之间
            canvasGroup.alpha = Mathf.Clamp01( canvasGroup.alpha + Time.deltaTime * fadeSpeed); 
        }
        else if(!isShowing&&canvasGroup.alpha>0) 
        {
            //这时候要实现面板的谈出,透明度设置为0-1之间
            canvasGroup.alpha = Mathf.Clamp01(canvasGroup.alpha - Time.deltaTime * fadeSpeed);
            hideCallBack?.Invoke();
        }
    }

}

2.2 UI管理器

UI管理器用来统一控制面板的显隐,以及一开始显示登录面板

实现思路:

面版字典:这里我一开始没有想到用字典存储当前显示得字典,直到我在写HidePanel逻辑时 发现用字典存储Panel面板,然后通过名字索引比较好,否则这里我使用列表需要查有没有这个面板,然后还要索引出来销毁

显示面板:这里我没有让他返回UI面版,因为我注意到后面有个获取面板得功能,这里我认为不需要返回,如果后面有需求,这里会改。此外,显示面板得实现思路为:获得面板得名字 ------> 拼接面板存储得路径 ------>通过路径加载相应得预设体------>实例化面板------>调用面板得淡入方法,并将其保存到字典中

隐藏面板: 检测是否打开该面板 ------> 面板执行淡出动画 ------> 透明度归 0 后触发回调 ------> 销毁面板对象 ------> 从字典移除

获取面板:检测是否打开该面板 ------> 打开则返回该面板,反之为null

复制代码
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 该脚本为UI管理器,需要实现UI面板的显示,隐藏与获取
/// </summary>
public class UIManager :MonoBehaviour
{
    //该管理器统一管理所有UI面板
    #region 单例模式实现
    private static UIManager instance;
    public static UIManager Instance => instance;
    private void Awake()
    {
        if(instance == null)
            instance = this;
        DontDestroyOnLoad(gameObject);
    }

    public Dictionary<string,BasePanel> nowPanels = new Dictionary<string, BasePanel>();  //该管理器现在显示的UI面板
    #endregion

    #region 面板的显示与隐藏

    //显示UI面板
    public void ShowPanel<T>() where T : BasePanel
    {
        //显示面板前我们需要现创建一个UI面板
        //首先,根据泛型的具体类型,来拼接对应UI面板的路径地址
        //这里不直接得地址了,先得名字,后面检测面板是否开启时使用
        string panelName = typeof(T).Name;
        Debug.Log(panelName);
        if (nowPanels.ContainsKey(panelName))
        {
            //这里检测是否该面板被打开,打开了就不执行后续逻辑
            return;
        }
        //然后,利用该地址创建UI面板
        GameObject panelObj = Resources.Load<GameObject>("UI/Panels/" + panelName);
        if (panelObj == null)
        {
            Debug.Log("加载UI面板预制体失败");
            return;
        }
        //实例化预制体
        GameObject panelInstance = Instantiate(panelObj);
        //从实例化的GameObject上获取脚本组件
        T panel = panelInstance.GetComponent<T>();
        //
        if (panel == null)
        {
            Debug.Log("UI面板实列化失败");
            return;
        }
        //接下来显示该UI面板,并将其添加到显示得字典中
        panel.ShowMe();
        nowPanels.Add(panelName,panel);

    }

    public void HidePanel<T>()
    {
        //首先得到面板得名字,用来索引
        string panelName = typeof(T).Name;
        //这里发现还是用字典存储Panel面板,然后通过名字索引比较好,否则这里我需要查有没有这个面板,然后还要索引出来销毁

        if (nowPanels.ContainsKey(panelName))
        {
            //存在,则进行隐藏
            nowPanels[panelName].HideMe(() =>
            {
                //这里淡出效果结束,需要销毁该对象
                Destroy(nowPanels[panelName].gameObject);
                //并且将字典移除
                nowPanels.Remove(panelName);
            });
        }

    }
    #endregion

    #region 面板获取
    public T GetPanel<T>() where T : BasePanel
    {
        string panelName = typeof(T).Name;
        //检测有没有该面板,有就返回
        if (nowPanels.ContainsKey(panelName))
            return nowPanels[panelName] as T;

        //没有,返回null
        return null;
    }
    #endregion
}

UI管理器问题

第一点,UI面板相关要保持过场景不销毁,这里可以让UI相关得摄像机和EventSystem作为Canvas得子对象

第二点,在测试提示面板得显隐时,发现面板可以被创建出来,但是不渲染,这是因为Canvas是UI控件渲染得核心,详见三大基础控件------Canvas相关,解决方式如下,将创建出的panel实例作为Canvas的子对象

2.3 登录面板

面板绘制

面板设计:包含登录信息、登录管理器和登录面板逻辑三个部分

  • 登录信息,是用来进行登录检测和登录面板更新的数据信息,包含当前输入的账户密码、是否记住帐户密码和是否自动登录,与登录管理器配合使用
  • 登录管理器,用来检测帐户密码是否匹配,用来注册新帐户,保存登录信息
  • 登录面板,登录面板脚本的实现思路如下
复制代码
/// <summary>
/// 登录信息,用来进行登录检测和登录面板更新的数据信息,包含当前输入的账户密码、是否记住帐户密码,是否自动登陆
/// </summary>
public class LoginData 
{
    public string account = null;  //账号
    public string password = null; //密码
    public bool isRemeberPassword = false;  //是否记住密码
    public bool isAutoLogin = false;    //是否自动登陆
}

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LoginManager
{
    #region 单例实现
    private static LoginManager instance = new();
    public static LoginManager Instance => instance;
    //存储的所有账号及对应密码
    private Dictionary<string,string> saveAccount = new();
    //存储的登陆数据
    public LoginData loginData = new();
    private LoginManager()
    {
        if (instance == null)
            instance = this;
        //加载持久化的账户字典
        saveAccount = JsonMgr.Instance.LoadData<Dictionary<string, string>>("AccountData");
        //用来测试
        saveAccount.Add("Acount123", "123456");
        //加载登陆数据
        loginData = JsonMgr.Instance.LoadData<LoginData>("LoginData");

    }
    #endregion
        
    //检测账户密码是否匹配
    public bool CheckPassword(string account, string password)
    {
        if (saveAccount.ContainsKey(account))
        {
            //存在该账户
            if (saveAccount[account] == password)
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        return false;
    }

    //注册新账号
    public void RegisterAccount(string account,string password)
    {
        if(saveAccount.ContainsKey(account))
        {
            //该账户名已经存在,唤出提示面板
            return;
        }
        saveAccount.Add(account, password);
        //更新持久化账户字典
        JsonMgr.Instance.SaveData(saveAccount, "SaveAccount");
    }
}

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class LoginPanel : BasePanel
{
    //LoginPanel相关的控件
    public TMP_InputField account_Input;    //账户输入
    public TMP_InputField password_Input;   //密码输入
    public Toggle remeberPassword_Toggle;   //记住密码选项
    public Toggle autoLogin_Toggle;         //自动登陆选项
    public Button resignAccount_Button;     //注册按钮
    public Button loginAccount_Button;      //登陆按钮

    private LoginData nowLoginData = new();
    public override void Init()
    {
        //点击注册按钮,打开注册面板
        resignAccount_Button.onClick.AddListener(() =>
        {
            Debug.Log("点击注册,打开注册面板");
        });
        //点击登陆按钮,进行登陆检测
        loginAccount_Button.onClick.AddListener(() =>
        {
            if(account_Input.text!="" &&password_Input.text!="")
            {
                //账户密码均已输入,检测是否匹配
                if(LoginManager.Instance.CheckPassword(account_Input.text, password_Input.text))
                {
                    //成功匹配,打开服务器面板,并隐藏该面板
                    Debug.Log("成功登陆,打开服务器面板");
                    UIManager.Instance.HidePanel<LoginPanel>();
                    //这里保存登陆数据
                    nowLoginData.account = account_Input.text;
                    nowLoginData.password = password_Input.text;
                    LoginManager.Instance.loginData = nowLoginData;
                    JsonMgr.Instance.SaveData(LoginManager.Instance.loginData, "LoginData");
                }
                else
                {
                    //账户或密码错误,打开提示面板
                    Debug.Log("账户或密码错误,打开提示面板");
                }
            }
            else
            {
                //账户或密码输入不全,打开提示面板
                //测试发现就算不输入也会有默认的提示,不会为null,所以不会触发
                Debug.Log("账户或密码输入不全,打开提示面板");
            }
        });

        remeberPassword_Toggle.onValueChanged.AddListener((isOn) =>
        {
            //不记住密码时,禁止自动登陆
            if (!isOn)
            {
                autoLogin_Toggle.isOn = false;
            }
            nowLoginData.isRemeberPassword = isOn;
        });
        autoLogin_Toggle.onValueChanged.AddListener((isOn) => 
        {
            //勾选自动登陆,则自动勾选记住密码
            if (isOn)
            {
                remeberPassword_Toggle.isOn = isOn;
            }
            nowLoginData.isAutoLogin = isOn;
        }); 
    }
    public override void ShowMe()
    {
        base.ShowMe();
        //得到登陆信息
        LoginData loginData = LoginManager.Instance.loginData;
        //更新UI
        if (loginData == null) 
        {
            Debug.Log("sdasd");
        }
        remeberPassword_Toggle.isOn = loginData.isRemeberPassword;
        autoLogin_Toggle.isOn = loginData.isAutoLogin;
        //是否记住密码
        if(remeberPassword_Toggle.isOn)
        {
            account_Input.text = loginData.account;
            password_Input.text = loginData.password;
        }

    }
}

效果演示:(这里可以看到可以正常触发对应的逻辑,演示只展示登陆成功)

2.4 注册面板

面板绘制:

面板设计:用来完成帐户注册,注册时需要检测该账户名是否已经注册

复制代码
using TMPro;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 注册面板脚本
/// </summary>
public class RegisterPanel : BasePanel
{
    //控件成员
    public TMP_InputField account_Input;    //账户输入
    public TMP_InputField password_Input;   //密码输入
    public Button chickConfirm_Button;  //点击确认按钮
    public Button chickCancel_Button;   //点击取消按钮
    public override void Init()
    {
        //点击确认按钮
        chickConfirm_Button.onClick.AddListener(() =>
        {
            //账号和密码全部输入
            if(account_Input.text!=""&&password_Input.text!="")
            {
                //进行账号注册
                if (LoginManager.Instance.RegisterAccount(account_Input.text, password_Input.text))
                {
                    //成功完成注册,隐藏注册面板
                    UIManager.Instance.HidePanel<RegisterPanel>();
                    UIManager.Instance.ShowPanel<LoginPanel>();
                    
                }
            }
            else
            {
                //账号或密码输入失败,打开提示面板
                Debug.Log(" 账号或密码输入失败,打开提示面板");
                //显示提示面板
                UIManager.Instance.ShowPanel<TipsPanel>();
                //获取到提示面板,更新显示
                TipsPanel tips = UIManager.Instance.canvas.GetComponentInChildren<TipsPanel>();
                tips.ChangeShowInfo("请输入完整的账号或密码");
            }
        });

        chickCancel_Button.onClick.AddListener(() => 
        {
            //点击取消按钮
            UIManager.Instance.HidePanel<RegisterPanel>();
            UIManager.Instance.ShowPanel<LoginPanel>();
        });
    }
}

这里,需要通过登陆管理器完成是否注册和注册账号的监听

复制代码
//====================【LoginManager修改RegisterAccount方法】========================
//注册新账号,并返回是否完成注册
public bool RegisterAccount(string account,string password)
{
    if(saveAccount.ContainsKey(account))
    {
        //该账户名已经存在,唤出提示面板
        Debug.Log("该账户名已经存在,唤出提示面板");
        return false;
    }
    saveAccount.Add(account, password);
    //更新持久化账户字典
    JsonMgr.Instance.SaveData(saveAccount, "SaveAccount");
    return true;
}


//====================【LoginPanel修改Init中注册按钮监听】========================
//点击注册按钮,打开注册面板
resignAccount_Button.onClick.AddListener(() =>
{
    Debug.Log("点击注册,打开注册面板");
    //隐藏登陆面板,打开注册面板
    UIManager.Instance.HidePanel<LoginPanel>();
    UIManager.Instance.ShowPanel<RegisterPanel>();
});

2.5 提示面板

面板绘制:

面板设计:用来显示提示信息,提示面板显示时不隐藏原本的面板

复制代码
using TMPro;
using UnityEngine.UI;

/// <summary>
/// 提示窗口脚本
/// </summary>
public class TipsPanel : BasePanel
{
    public Button confirmButton;    //确定按钮
    public TMP_Text tipsInfoText;   //提示信息显示
    public override void Init()
    {
        confirmButton.onClick.AddListener(() =>
        {
            //这里直接隐藏自己
            UIManager.Instance.HidePanel<TipsPanel>();
        });
    }
    public void ChangerTipsInfo(string info)
    {
        //改变显示内容
        tipsInfoText.text = info;
    }
}

提示面板出现的次数较多,涉及修改的脚本为

=========== =======【LoginManager注册功能】===========================

===========【LoginPanel初始化功能中的登陆检测】============================

【账号密码错误】

【输入信息缺失】

===================【RegisterPanel初始化检测密码输入完整】==================

复制代码
//显示提示面板
UIManager.Instance.ShowPanel<TipsPanel>();
//获取到提示面板,更新显示
TipsPanel tips = UIManager.Instance.canvas.GetComponentInChildren<TipsPanel>();
tips.ChangeShowInfo("显示内容");

截至到这,我已经理解为什么ShowPanel为什么要返回一个Panel了,这里如果是返回的,我就可以直接通过返回的Panel来直接改变显示的内容


这里我发现一个问题,就是我看到我登陆的数据成功完成了持久化,但是注册的账号没有持久化,这里可以看到持久化保存的数据,但是加载上却不可以,原因是命名出现错误

复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LoginManager
{
    #region 单例实现
    private static LoginManager instance = new();
    public static LoginManager Instance => instance;
    //存储的所有账号及对应密码
    private Dictionary<string,string> saveAccount = new();
    //存储的登陆数据
    public LoginData loginData = new();
    private LoginManager()
    {
        if (instance == null)
            instance = this;
        //加载持久化的账户字典
        saveAccount = JsonMgr.Instance.LoadData<Dictionary<string, string>>("AccountData");
        //用来测试
        //saveAccount.Add("Acount123", "123456");
        //加载登陆数据
        loginData = JsonMgr.Instance.LoadData<LoginData>("LoginData");


        //测试使用,查看存在的账号及密码
        foreach(var key in saveAccount.Keys)
        {
            Debug.Log($"账号:{key}   密码:{saveAccount[key]}");
        }


    }
    #endregion

   

    /// <summary>
    /// 检测账户密码是否匹配
    /// </summary>
    /// <param name="account">账号名</param>
    /// <param name="password">密码</param>
    public bool CheckPassword(string account, string password)
    {
        if (saveAccount.ContainsKey(account))
        {
            //存在该账户
            if (saveAccount[account] == password)
            {

                return true;
            }
            else
            {
                return false;
            }
        }
        return false;
    }

    //注册新账号,并返回是否完成注册
    public bool RegisterAccount(string account,string password)
    {
        if(saveAccount.ContainsKey(account))
        {
            //该账户名已经存在,唤出提示面板
            Debug.Log("该账户名已经存在,唤出提示面板");
            //显示提示面板
            UIManager.Instance.ShowPanel<TipsPanel>();
            //获取到提示面板,更新显示
            TipsPanel tips = UIManager.Instance.canvas.GetComponentInChildren<TipsPanel>();
            tips.ChangeShowInfo("该账号名已经存在");
            return false;
        }
        saveAccount.Add(account, password);
        //更新持久化账户字典
        JsonMgr.Instance.SaveData(saveAccount, "AccountData");
        //测试使用,查看存在的账号及密码
        foreach (var key in saveAccount.Keys)
        {
            Debug.Log($"账号:{key}   密码:{saveAccount[key]}");
        }

        return true;
    }
}

2.6 服务器面板

面板绘制

面板设计:

涉及到的控件成员包含,进入游戏按钮,点击换区按钮,退出按钮,以及显示区服信息的文本,此外需要进行Init初始化,此外还需要重写ShowMe方法,用来更新区服信息

进入游戏按钮:点击会进入游戏场景,前提是选择了区服,在没有选择时点击会打开提示面板

点击换区按钮:点击换区,会隐藏该界面,并打开换区面板

点击退出按钮:隐藏该界面,并打开登陆界面

ShowMe方法重写,更新区服信息

复制代码
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class ServerPanel : BasePanel
{
    //控件成员
    public TMP_Text nowServesInfo;  //当前服务器显示
    public Button changeServerButton;   //换服按钮
    public Button enterGameButton;      //进入游戏按钮
    public Button quitServesPanelButton;//退出服务器面板

    public override void Init()
    {
        enterGameButton.onClick.AddListener(() =>
        {
            //点击进入游戏,检测是否有选择的服务器
            if(LoginManager.Instance.loginData.selectServesID == -1)
            {
                //没有选择服务器,打开提示面板
                UIManager.Instance.ShowPanel<TipsPanel>();
                TipsPanel tips = UIManager.Instance.canvas.GetComponentInChildren<TipsPanel>();
                tips.ChangeShowInfo("请选择服务器后登陆");
            }
            else
            {
                //进入游戏场景
            }
        });
        changeServerButton.onClick.AddListener(()=>
        {
            //点击换肤按钮,打开换服面板,隐藏自己

            UIManager.Instance.HidePanel<ServerPanel>();
        });
        quitServesPanelButton.onClick.AddListener(() => 
        {
            //点击退出服务器面板,隐藏自己并打开登陆
            UIManager.Instance.HidePanel<ServerPanel>();
            UIManager.Instance.ShowPanel<LoginPanel>();
        });
    }

    public override void ShowMe()
    {
        base.ShowMe();
        //更新数据
        LoginData loginData = LoginManager.Instance.loginData;
        nowServesInfo.text = loginData.selectServesID + "区  " + loginData.selectServerName;
    }
}

2.7 换区面板

面板绘制

面板设计

第一步:动态创建分区预设体,完成根据加载到的服务器数量进行创建分区

  • 分区预设体,根据加载动态创建多个分区按钮

创建思路:根据读取到的Json数据中服务器数量划分分区,并动态创建

如图所示,动态创建了三个分区,且由于大小适配器组件,实现了分区过多时的自动扩容,前面的0-0是用来演示的0占位

这里不强制10区一个服,剩下的服务器少于10,会将剩余的服务器化为一个分区,解决方法

复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ChangeServerPanel : BasePanel
{
    List<ServerData> serverDatas = new List<ServerData>();
    public Transform leftGruop;
    public override void Init()
    {

        serverDatas = JsonMgr.Instance.LoadData<List<ServerData>>("Data/ServerInfo");
        Debug.Log("加载到的服务器共计:" + serverDatas.Count);
        CreateServerPartitions(serverDatas.Count);
    }
    public void CreateServerPartitions(int totalServersCount)
    {
        int partitionCount = totalServersCount / 10;
        if (totalServersCount%10>0)
        {
            partitionCount += 1;
        }
        Debug.Log("当前服务器数量为:"+ totalServersCount + " 共划分分区树为:" +  partitionCount);
        for (int i = 0; i < partitionCount; i++)
        {
            //创建对应个数的分区范围按钮预设体
            GameObject rangeData = Resources.Load<GameObject>("UI/Panels/ServerRangeData_Button");
            if (rangeData == null)
            {
                Debug.Log("加载分区预制体失败");
                return;
            }
            //实例化预设体
            GameObject rangeDataButton = Instantiate(rangeData);
            //在实例化的GameObject上获取脚本组件
            ServerRangeData serverRangeData = rangeDataButton.GetComponent<ServerRangeData>(); 
            serverRangeData.transform.SetParent(leftGruop, false);
            serverRangeData.startServerID = i *10 + 1;
            
            //serverRangeData.endServerID = (i+1)*10;
             serverRangeData.startServerID = i *10 + 1;
             serverRangeData.endServerID = i == partitionCount-1&&
                                                totalServersCount % 10 > 0 ?
                                                i*10 + totalServersCount % 10 : (i+1)*10;
        }
    }
}
[System.Serializable]
public class ServerData
{ 
    public int id;      //服务器编号
    public string name; //服务器名
    public int state; //服务器状态编号
    public bool isNew;  //服务器是否为新服务器

}

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class ServerRangeData : MonoBehaviour
{
    public TMP_Text serverRange;
    public Button nowServerRangeButton;
    public int startServerID;
    public int endServerID;

    private void Start()
    {
        nowServerRangeButton.onClick.AddListener(() =>
        {
            //创建服务器数据
            Debug.Log("创建服务器数据"+ startServerID + " - " + endServerID);

        });
        serverRange.text = startServerID + "-" + endServerID;
    }
}

第二步,根据点击的分区,自动更新对应数量的服务器信息

首先是服务器数据预设体,该预设体是分区选择后动态创建的基础,需要实现点击事件监听,更新自身UI,设置自身关联的服务器数据

创建思路为:当玩家点击分区按钮,根据分区信息中的范围,将指定数目的预制服务器槽激活,并设置其中的信息

复制代码
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;

public class ChangeServerPanel : BasePanel
{
    public List<ServerData> serverDatas = new List<ServerData>();
    public Transform leftGruop;
    public Transform rightGruop;
    public List<ServerInfoData> serverInfoDatas = new List<ServerInfoData>();

    public TMP_Text serverRange_Text;
    public TMP_Text serverData_Text;
    public override void Init()
    {
        //进入换服面板,显示当前的选择
        LoginData loginData = LoginManager.Instance.loginData;
        serverData_Text.text = loginData.selectServesID + "区  " + loginData.selectServerName;
        //读取服务器数据
        serverDatas = JsonMgr.Instance.LoadData<List<ServerData>>("Data/ServerInfo");
        Debug.Log("加载到的服务器共计:" + serverDatas.Count);
        //创建分区
        CreateServerPartitions(serverDatas.Count);
    }
    public void CreateServerPartitions(int totalServersCount)
    {
        int partitionCount = totalServersCount / 10;
        if (totalServersCount%10>0)
        {
            partitionCount += 1;
        }
        Debug.Log("当前服务器数量为:"+ totalServersCount + " 共划分分区树为:" +  partitionCount);
        for (int i = 0; i < partitionCount; i++)
        {
            //创建对应个数的分区范围按钮预设体
            GameObject rangeData = Resources.Load<GameObject>("UI/Panels/ServerRangeData_Button");
            if (rangeData == null)
            {
                Debug.Log("加载分区预制体失败");
                return;
            }
            //实例化预设体
            GameObject rangeDataButton = Instantiate(rangeData);
            //在实例化的GameObject上获取脚本组件
            ServerRangeData serverRangeData = rangeDataButton.GetComponent<ServerRangeData>(); 
            serverRangeData.transform.SetParent(leftGruop, false);
            serverRangeData.startServerID = i *10 + 1;
            serverRangeData.endServerID = i == partitionCount-1&&
                                               totalServersCount % 10 > 0 ?
                                               i*10 + totalServersCount % 10 : (i+1)*10;
            //为他添加设置服务器数据的方法
            serverRangeData.onRangeClicked += CreateServerDatas;
        }
    }
    
    public void CreateServerDatas(int startServerID,int endServerID)
    {
        //设置服务器数据,这里应该提前创建好10个服务器槽位
        //根据数量设置信息
        for (int i = 0; i < 10; i++)
        {
            serverInfoDatas[i].gameObject.SetActive(false);
        }
        int index =0;
        for(int i = startServerID -1;i< endServerID; i++) 
        {   
            serverInfoDatas[index].SetInfo(serverDatas[i]);
            serverInfoDatas[index].gameObject.SetActive(true);
            serverInfoDatas[index].ShowMe();
            Debug.Log("成功设置服务器ID"+ i);
            index++;
        }
        Debug.Log("设置服务器数据" + startServerID + " - " + endServerID);
        //更新UI
        ShowMe();
        serverRange_Text.text = startServerID + " - " + endServerID;
    }

    public override void ShowMe()
    {
        base.ShowMe();
        //更新UI方法
        LoginData l = LoginManager.Instance.loginData;
        serverData_Text.text = l.selectServesID + "区  " + l.selectServerName;
    }
}
[System.Serializable]
public class ServerData
{ 
    public int id;      //服务器编号
    public string name; //服务器名
    public int state; //服务器状态编号
    public bool isNew;  //服务器是否为新服务器

}

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class ServerInfoData : MonoBehaviour
{
    public TMP_Text serverInfo;
    public Button nowServerButton;
    public Image newIcon;   //新服图标
    public Image stateIcon; //服务器状态图标
    private string serverName;
    private int serverID;
    private bool isNew;
    private int stateId;

    private void Start()
    {
        nowServerButton.onClick.AddListener(() =>
        {
            //将当前服务器编号设置为
            LoginManager.Instance.loginData.selectServesID = serverID;
            LoginManager.Instance.loginData.selectServerName = serverName;
            //打开服务器面板,隐藏自身
            UIManager.Instance.HidePanel<ChangeServerPanel>();
            UIManager.Instance.ShowPanel<ServerPanel>();
        });
    }
    public void ShowMe()
    {
        serverInfo.text = serverID + "区  " + serverName;
        if (isNew)
        {
            //是新服,显示图标
            newIcon.gameObject.SetActive(true);
        }
        else
        {
            newIcon.gameObject.SetActive(false);
        }
        stateIcon.sprite = Resources.Load<Sprite>($"UI/Art/State/ui_DL_State_{stateId}");
    }
    //设置服务器数据信息
    public void SetInfo(ServerData data)
    {
        serverName = data.name;
        serverID = data.id;
        isNew = data.isNew;
        stateId = data.state;
    }
}

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class ServerRangeData : MonoBehaviour
{
    public TMP_Text serverRange;
    public Button nowServerRangeButton;
    public int startServerID;
    public int endServerID;

    // 定义委托类型
    public delegate void OnRangeClicked(int startID, int endID);
    // 委托实例
    public OnRangeClicked onRangeClicked;

    private void Start()
    {
        nowServerRangeButton.onClick.AddListener(() =>
        {
            onRangeClicked?.Invoke(startServerID,endServerID);
        });
        serverRange.text = startServerID + "-" + endServerID;
    }
}

三、分析及改善

现在,完整的走一遍账号注册、登陆、选服流程,会发现以下问题(Unity中的UI系统------UGUI关联的资料里Projects中的UGUI_Demo1为没有改善的版本,此外演示过程中没有保留过场景不销毁,且退出按钮也没,所以需要通过Win结束)

问题1,面板丢失或多余

问题1,面板隐藏与显示会冲突,可能会出现同时出现多个面板或没有界面的现象

  • 对于面板丢失:

通过测试我发现当我快速切换时很容易触发,所以我认为可能是销毁时间太快的问题,所以进行延迟1秒销毁。

但是我发现延迟销毁后触发概率更大了,而且我观察到当将要销毁的面板还存在时,就打开另一个面板就会触发

原因:这里我谈出界面,然后延迟销毁,在这期间该面板还在字典中,所以快速切换回来,会发现还存在就不会打开新的面板,而延迟时间结束后,面板销毁,所以界面中就没有面板了

解决方法:引入一个标识,用来检测当前面板是否是淡入淡出界面 或 通过协程,等上一个界面完全隐藏销毁后,再显示新界面,,这里需要将所有的显示隐藏改为ShowNewPanel统一调用

而提示面板可以同时出现,所以这里对提示面板进行以下处理

  • 对于存在多个面板

目前只知道是自动登陆的问题,目前先取消自动登陆的功能

问题2 选择服务器后,下次登陆要自动显示当前选择

这里发现是没有将服务器数据持久化的原因,又因为点击具体的服务器选择,会更新当前服务器,所以这里再此处持久化登陆信息

可是我还是发现明明可以读取到信息,但还是没有正确显示,这里我发现了一开始我们是将nowLoginData直接赋值到loginData上,会覆盖掉其中的信息

四、总结

首先,对于一个面板我们需要一个基类,来统一设计所有面板,该基类需要一个初始化面板的方法Init,以及一个显示和隐藏的方法(这两个方法可以让他们将该面板返回出来,方便直接调用其中的方法,比如提示面板就需要经常使用其中的改变显示方法)

复制代码
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Events;


/// <summary>
/// 面板积类,需要实现面板的初始化、显示隐藏、淡入淡出
/// </summary>
public abstract class BasePanel : MonoBehaviour
{
    #region 面板积类的成员变量
    private CanvasGroup canvasGroup;    //面板的画布组 => 控制面板的淡入淡出效果
    private bool isShowing; //当前是否显示
    public bool isFade; //当前是否谈出
    public float fadeSpeed = 5f;

    private UnityAction hideCallBack;   //面板成功淡出时执行的方法
    //为什么这样设计?
    //答:通过了解这样UI面板只负责实现自己的显示隐藏,淡入淡出
    //具体的创建销毁则由UI管理器负责,
    #endregion

    #region 进行面板的初始化
    //初始化面版关联的UI控件
    public abstract void Init();
    #endregion

    #region 实现面版的显示与隐藏
    public virtual void ShowMe()
    {
        //面板的显示,显示时需要动态创建对应的UI面版
        isShowing = true;
    }
    public virtual void HideMe(UnityAction callBack)
    {
        //面板的隐藏,隐藏时需要销毁对应的UI面板
        isShowing = false;
        hideCallBack = callBack;
    }
    #endregion

    protected virtual void Awake()
    {
        //获取面板上的Canvas组件,用来实现淡入淡出的效果
        canvasGroup = GetComponent<CanvasGroup>();
        if(canvasGroup == null )
            this.AddComponent<CanvasGroup>();
    }
    protected virtual void Start()
    {
        //对面板进行初始化
        Init();
    }
    protected virtual void Update()
    {
        if (isShowing&&canvasGroup.alpha<1)
        {
            //这时候要实现面板的谈入,透明度设置为0-1之间
            canvasGroup.alpha = Mathf.Clamp01( canvasGroup.alpha + Time.deltaTime * fadeSpeed); 
            isFade =true;
            if(canvasGroup.alpha>=1)
            {
                isFade =false;
            }
        }
        else if(!isShowing&&canvasGroup.alpha>0) 
        {
            isFade = true;
            //这时候要实现面板的谈出,透明度设置为0-1之间
            canvasGroup.alpha = Mathf.Clamp01(canvasGroup.alpha - Time.deltaTime * fadeSpeed);
            //这里当透明度为零时,就尝试调用
            if(canvasGroup.alpha <= 0 )
            {
                hideCallBack?.Invoke();
                isFade = false;
            }
        }
    }

}

其次,我们需要一个登陆数据管理类,该类需要包含登陆的所有信息(此外最好提供一个保存数据的方法,在每次数据改变时,进行数据持久化)

复制代码
/// <summary>
/// 登录信息,用来进行登录检测和登录面板更新的数据信息,包含当前输入的账户密码、是否记住帐户密码,是否自动登陆
/// </summary>
public class LoginData 
{
    public string account = null;  //账号
    public string password = null; //密码
    public bool isRemeberPassword = false;  //是否记住密码
    public bool isAutoLogin = false;    //是否自动登陆
    public int selectServesID = -1;     //当前选择的服务器编号
    public string selectServerName = "";
}

接下来,我们需要一个登陆数据管理器,来进行一开始数据的读取,此外也需要 检测密码正确 和注册新账号的功能

复制代码
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LoginManager
{
    #region 单例实现
    private static LoginManager instance = new();
    public static LoginManager Instance => instance;
    //存储的所有账号及对应密码
    private Dictionary<string,string> saveAccount = new();
    //存储的登陆数据
    public LoginData loginData = new();
    //游戏中所有的服务器
  

    private LoginManager()
    {
        if (instance == null)
            instance = this;
        //加载持久化的账户字典
        saveAccount = JsonMgr.Instance.LoadData<Dictionary<string, string>>("AccountData");
        //用来测试
        //saveAccount.Add("Acount123", "123456");
        //加载登陆数据
        loginData = JsonMgr.Instance.LoadData<LoginData>("LoginData");
        Debug.Log("当前读取到的服务器为" + loginData.selectServesID + "区  " + loginData.selectServerName);


        //测试使用,查看存在的账号及密码
        foreach (var key in saveAccount.Keys)
        {
            Debug.Log($"账号:{key}   密码:{saveAccount[key]}");
        }


    }
    

    #endregion

   

    /// <summary>
    /// 检测账户密码是否匹配
    /// </summary>
    /// <param name="account">账号名</param>
    /// <param name="password">密码</param>
    public bool CheckPassword(string account, string password)
    {
        if (saveAccount.ContainsKey(account))
        {
            //存在该账户
            if (saveAccount[account] == password)
            {

                return true;
            }
            else
            {
                return false;
            }
        }
        return false;
    }

    //注册新账号,并返回是否完成注册
    public bool RegisterAccount(string account,string password)
    {
        if(saveAccount.ContainsKey(account))
        {
            //该账户名已经存在,唤出提示面板
            Debug.Log("该账户名已经存在,唤出提示面板");
            //显示提示面板
            UIManager.Instance.ShowPanel<TipsPanel>();
            //获取到提示面板,更新显示
            TipsPanel tips = UIManager.Instance.canvas.GetComponentInChildren<TipsPanel>();
            tips.ChangeShowInfo("该账号名已经存在");
            return false;
        }
        saveAccount.Add(account, password);
        //更新持久化账户字典
        JsonMgr.Instance.SaveData(saveAccount, "AccountData");
        //测试使用,查看存在的账号及密码
        foreach (var key in saveAccount.Keys)
        {
            Debug.Log($"账号:{key}   密码:{saveAccount[key]}");
        }

        return true;
    }

    //存储登录数据相关
    public void SaveLoginData()
    {
        JsonMgr.Instance.SaveData(loginData, "LoginData");
    }
}

然后,就是我们 最为重要的UI面板管理器,他负责面板的显示与隐藏,该方法通过ShowNewPanel来进行统一调度

复制代码
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 该脚本为UI管理器,需要实现UI面板的显示,隐藏与获取
/// </summary>
public class UIManager :MonoBehaviour
{
    //该管理器统一管理所有UI面板
    #region 单例模式实现
    private static UIManager instance;
    public static UIManager Instance => instance;
    private void Awake()
    {
        if(instance == null)
            instance = this;
        //DontDestroyOnLoad(gameObject);
    }

    public Dictionary<string,BasePanel> nowPanels = new Dictionary<string, BasePanel>();  //该管理器现在显示的UI面板
    #endregion

    #region 面板的显示与隐藏
    //面板得Canvas父对象
    public Transform canvas;


    //显示UI面板
    public void ShowPanel<T>() where T : BasePanel
    {
        //显示面板前我们需要现创建一个UI面板
        //首先,根据泛型的具体类型,来拼接对应UI面板的路径地址
        //这里不直接得地址了,先得名字,后面检测面板是否开启时使用
        string panelName = typeof(T).Name;
        Debug.Log(panelName);
        if (nowPanels.ContainsKey(panelName))
        {
            if(nowPanels[panelName].isFade)
            {
                //这里检测是否该面板被打开,打开了就不执行后续逻辑
                return;
            }
        }
        //然后,利用该地址创建UI面板
        GameObject panelObj = Resources.Load<GameObject>("UI/Panels/" + panelName);
        if (panelObj == null)
        {
            Debug.Log("加载UI面板预制体失败");
            return;
        }
        //实例化预制体
        GameObject panelInstance = Instantiate(panelObj);
        //从实例化的GameObject上获取脚本组件
        T panel = panelInstance.GetComponent<T>();

        //问题一,这里实例化得UI面板没有成为Canvas得子对象,所以无法正常渲染
        panel.transform.SetParent(canvas, false);


        //
        if (panel == null)
        {
            Debug.Log("UI面板实列化失败");
            return;
        }
        //接下来显示该UI面板,并将其添加到显示得字典中
        panel.ShowMe();
        if (!nowPanels.ContainsKey(panelName))
        {
            nowPanels.Add(panelName, panel);
        }

    }

    IEnumerator HidePanel<T>()
    {
        //首先得到面板得名字,用来索引
        string panelName = typeof(T).Name;
        //这里发现还是用字典存储Panel面板,然后通过名字索引比较好,否则这里我需要查有没有这个面板,然后还要索引出来销毁

        if (nowPanels.ContainsKey(panelName))
        {
            //存在,则进行隐藏
            nowPanels[panelName].HideMe(() =>
            {
                //这里淡出效果结束,需要销毁该对象
                Destroy(nowPanels[panelName].gameObject);
                //并且将字典移除
                nowPanels.Remove(panelName);
            });
        }
        yield return new WaitForSeconds(1);

    }

    public void ShowNewPanel<T,K>() where T : BasePanel where K : BasePanel
    {
        StartCoroutine(ChangePanel<T,K>());
    }
    IEnumerator ChangePanel<T,K>() where T : BasePanel where K : BasePanel
    {
        //等待隐藏完全结束后,显示新面板
        yield return StartCoroutine(HidePanel<T>());
        ShowPanel<K>();
        
    }
    #endregion

    #region 面板获取
    public T GetPanel<T>() where T : BasePanel
    {
        string panelName = typeof(T).Name;
        //检测有没有该面板,有就返回
        if (nowPanels.ContainsKey(panelName))
            return nowPanels[panelName] as T;

        //没有,返回null
        return null;
    }
    #endregion

    private void Start()
    {
        ////初始时,且不存在其他面板时打开登陆面板
        if(canvas.GetComponentInChildren<BasePanel>(true) == null)
        {
            UIManager.Instance.ShowPanel<LoginPanel>();
        }
    }
}

最后,就是我们的各个面板,其中较为特殊的是提示面板,该面板显示时不会隐藏原本的面板。另外,换服面板是集成了多个分区按钮和服务器按钮

复制代码
using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class LoginPanel : BasePanel
{
    //LoginPanel相关的控件
    public TMP_InputField account_Input;    //账户输入
    public TMP_InputField password_Input;   //密码输入
    public Toggle remeberPassword_Toggle;   //记住密码选项
    public Toggle autoLogin_Toggle;         //自动登陆选项
    public Button resignAccount_Button;     //注册按钮
    public Button loginAccount_Button;      //登陆按钮

    private LoginData nowLoginData = new();
    public override void Init()
    {
        //点击注册按钮,打开注册面板
        resignAccount_Button.onClick.AddListener(() =>
        {
            Debug.Log("点击注册,打开注册面板");
            //隐藏登陆面板,打开注册面板
            UIManager.Instance.ShowNewPanel<LoginPanel,RegisterPanel>();

            //UIManager.Instance.HidePanel<LoginPanel>();
            //UIManager.Instance.ShowPanel<RegisterPanel>();
        });
        //点击登陆按钮,进行登陆检测
        loginAccount_Button.onClick.AddListener(() =>
        {
            if(account_Input.text!="" &&password_Input.text!="")
            {
                //账户密码均已输入,检测是否匹配
                if(LoginManager.Instance.CheckPassword(account_Input.text, password_Input.text))
                {
                    //成功匹配,打开服务器面板,并隐藏该面板
                    Debug.Log("成功登陆,打开服务器面板");
                    UIManager.Instance.ShowNewPanel<LoginPanel,ServerPanel>();

                    //UIManager.Instance.HidePanel<LoginPanel>();
                    //UIManager.Instance.ShowPanel<ServerPanel>();

                    //这里保存登陆数据
                    nowLoginData.account = account_Input.text;
                    nowLoginData.password = password_Input.text;
                    //这里直接等于会覆盖掉服务器数据
                    LoginManager.Instance.loginData.account = nowLoginData.account;
                    LoginManager.Instance.loginData.password = nowLoginData.password;

                }
                else
                {
                    //账户或密码错误,打开提示面板
                    //显示提示面板
                    UIManager.Instance.ShowPanel<TipsPanel>();
                    //获取到提示面板,更新显示
                    TipsPanel tips = UIManager.Instance.canvas.GetComponentInChildren<TipsPanel>();
                    tips.ChangeShowInfo("账户或密码错误");
                    Debug.Log("账户或密码错误");
                }
            }
            else
            {
                //显示提示面板
                UIManager.Instance.ShowPanel<TipsPanel>();
                //获取到提示面板,更新显示
                TipsPanel tips = UIManager.Instance.canvas.GetComponentInChildren<TipsPanel>();
                tips.ChangeShowInfo("请输入完整的账号或密码");
                Debug.Log("账户或密码输入不全,打开提示面板");
            }
        });

        remeberPassword_Toggle.onValueChanged.AddListener((isOn) =>
        {
            //不记住密码时,禁止自动登陆
            if (!isOn)
            {
                autoLogin_Toggle.isOn = false;
            }
            nowLoginData.isRemeberPassword = isOn;
        });
        autoLogin_Toggle.onValueChanged.AddListener((isOn) => 
        {
            //勾选自动登陆,则自动勾选记住密码
            if (isOn)
            {
                remeberPassword_Toggle.isOn = isOn;
            }
            nowLoginData.isAutoLogin = isOn;
        }); 
    }
    public override void ShowMe()
    {
        base.ShowMe();
        //得到登陆信息
        LoginData loginData = LoginManager.Instance.loginData;
        //更新UI
        if (loginData == null) 
        {
            Debug.Log("sdasd");
        }
        remeberPassword_Toggle.isOn = loginData.isRemeberPassword;
        autoLogin_Toggle.isOn = loginData.isAutoLogin;
        //是否记住密码
        if(remeberPassword_Toggle.isOn)
        {
            account_Input.text = loginData.account;
            password_Input.text = loginData.password;
        }
        //是否自动登陆 且记住密码
        //if(autoLogin_Toggle.isOn && remeberPassword_Toggle.isOn)
        //{
        //    UIManager.Instance.ShowNewPanel<LoginPanel, ServerPanel>();
        //    account_Input.text = loginData.account;
        //    password_Input.text = loginData.password;
        //    remeberPassword_Toggle.isOn = autoLogin_Toggle.isOn;
        //    //自动登陆这一次后,就取消自动登陆
        //    autoLogin_Toggle.isOn = false;
        //    //UIManager.Instance.HidePanel<LoginPanel>();
        //    //UIManager.Instance.ShowPanel<ServerPanel>();

        //}

    }
}

using TMPro;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 注册面板脚本
/// </summary>
public class RegisterPanel : BasePanel
{
    //控件成员
    public TMP_InputField account_Input;    //账户输入
    public TMP_InputField password_Input;   //密码输入
    public Button chickConfirm_Button;  //点击确认按钮
    public Button chickCancel_Button;   //点击取消按钮
    public override void Init()
    {
        //点击确认按钮
        chickConfirm_Button.onClick.AddListener(() =>
        {
            //账号和密码全部输入
            if(account_Input.text!=""&&password_Input.text!="")
            {
                //进行账号注册
                if (LoginManager.Instance.RegisterAccount(account_Input.text, password_Input.text))
                {
                    //成功完成注册,隐藏注册面板
                    UIManager.Instance.ShowNewPanel<RegisterPanel, LoginPanel>();

                    //UIManager.Instance.HidePanel<RegisterPanel>();
                    //UIManager.Instance.ShowPanel<LoginPanel>();
                    
                }
            }
            else
            {
                //账号或密码输入失败,打开提示面板
                Debug.Log(" 账号或密码输入失败,打开提示面板");
                //显示提示面板
                UIManager.Instance.ShowPanel<TipsPanel>();
                //获取到提示面板,更新显示
                TipsPanel tips = UIManager.Instance.canvas.GetComponentInChildren<TipsPanel>();
                tips.ChangeShowInfo("请输入完整的账号或密码");
            }
        });

        chickCancel_Button.onClick.AddListener(() => 
        {
            //点击取消按钮
            UIManager.Instance.ShowNewPanel<RegisterPanel, LoginPanel>();

            //UIManager.Instance.HidePanel<RegisterPanel>();
            //UIManager.Instance.ShowPanel<LoginPanel>();
        });
    }
}

using TMPro;
using UnityEngine.UI;

/// <summary>
/// 提示窗口脚本
/// </summary>
public class TipsPanel : BasePanel
{
    public Button confirmButton;    //确定按钮
    public TMP_Text tipsInfoText;   //提示信息显示
    public override void Init()
    {
        confirmButton.onClick.AddListener(() =>
        {
            //这里直接隐藏自己
            //UIManager.Instance.HidePanel<TipsPanel>();
            HideMe(() =>
            {
                Destroy(gameObject);
                UIManager.Instance.nowPanels.Remove("TipsPanel");
            });
        });
    }

   public void ChangeShowInfo(string info)
    {
        tipsInfoText.text = info;
    }
}

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.UI;

public class ServerPanel : BasePanel
{
    //控件成员
    public TMP_Text nowServesInfo;  //当前服务器显示
    public Button changeServerButton;   //换服按钮
    public Button enterGameButton;      //进入游戏按钮
    public Button quitServesPanelButton;//退出服务器面板

    public override void Init()
    {
        //初始化,加载上次登陆的显示信息
        ShowMe();

        enterGameButton.onClick.AddListener(() =>
        {
            //点击进入游戏,检测是否有选择的服务器
            if(LoginManager.Instance.loginData.selectServesID == -1)
            {
                //没有选择服务器,打开提示面板
                UIManager.Instance.ShowPanel<TipsPanel>();
                TipsPanel tips = UIManager.Instance.canvas.GetComponentInChildren<TipsPanel>();
                tips.ChangeShowInfo("请选择服务器后登陆");
            }
            else
            {
                //进入游戏场景
                SceneManager.LoadScene("TextScene");
            }
        });
        changeServerButton.onClick.AddListener(()=>
        {
            //点击换肤按钮,打开换服面板,隐藏自己
            UIManager.Instance.ShowNewPanel<ServerPanel,ChangeServerPanel>();
            //UIManager.Instance.HidePanel<ServerPanel>();
            //UIManager.Instance.ShowPanel<ChangeServerPanel>();
           
        });
        quitServesPanelButton.onClick.AddListener(() => 
        {
            //点击退出服务器面板,隐藏自己并打开登陆
            UIManager.Instance.ShowNewPanel<ServerPanel, LoginPanel>();
            //UIManager.Instance.HidePanel<ServerPanel>();
            //UIManager.Instance.ShowPanel<LoginPanel>();
        });
    }

    public override void ShowMe()
    {
        base.ShowMe();
        //更新数据
        LoginData loginData = LoginManager.Instance.loginData;
        Debug.Log("当前读取到的服务器为" + loginData.selectServesID + "区  " + loginData.selectServerName);
        if (loginData.selectServesID == -1)
        {
            nowServesInfo.text = "";
            return;
        }
        nowServesInfo.text = loginData.selectServesID + "区  " + loginData.selectServerName;
    }
}

换区面板包含 分区范围数据、服务器数据、分区面板三部分

复制代码
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class ServerRangeData : MonoBehaviour
{
    public TMP_Text serverRange;
    public Button nowServerRangeButton;
    public int startServerID;
    public int endServerID;

    // 定义委托类型
    public delegate void OnRangeClicked(int startID, int endID);
    // 委托实例
    public OnRangeClicked onRangeClicked;

    private void Start()
    {
        nowServerRangeButton.onClick.AddListener(() =>
        {
            onRangeClicked?.Invoke(startServerID,endServerID);
        });
        serverRange.text = startServerID + "-" + endServerID;
    }
}

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;
using UnityEngine.UI;

public class ServerInfoData : MonoBehaviour
{
    public TMP_Text serverInfo;
    public Button nowServerButton;
    public Image newIcon;   //新服图标
    public Image stateIcon; //服务器状态图标
    private string serverName;
    private int serverID;
    private bool isNew;
    private int stateId;
  
    private void Start()
    {
        nowServerButton.onClick.AddListener(() =>
        {
            //将当前服务器编号设置为
            LoginManager.Instance.loginData.selectServesID = serverID;
            LoginManager.Instance.loginData.selectServerName = serverName;
            //打开服务器面板,隐藏自身
            UIManager.Instance.ShowNewPanel<ChangeServerPanel, ServerPanel>();
            //持久化保存信息
            LoginManager.Instance.SaveLoginData();
            //UIManager.Instance.HidePanel<ChangeServerPanel>();
            //UIManager.Instance.ShowPanel<ServerPanel>();
        });
    }
    public void ShowMe()
    {
        serverInfo.text = serverID + "区  " + serverName;
        if (isNew)
        {
            //是新服,显示图标
            newIcon.gameObject.SetActive(true);
        }
        else
        {
            newIcon.gameObject.SetActive(false);
        }
        stateIcon.sprite = Resources.Load<Sprite>($"UI/Art/State/ui_DL_State_{stateId}");
    }
    //设置服务器数据信息
    public void SetInfo(ServerData data)
    {
        serverName = data.name;
        serverID = data.id;
        isNew = data.isNew;
        stateId = data.state;
    }
}

using System.Collections;
using System.Collections.Generic;
using TMPro;
using UnityEngine;

public class ChangeServerPanel : BasePanel
{
    public List<ServerData> serverDatas = new List<ServerData>();
    public Transform leftGruop;
    public Transform rightGruop;
    public List<ServerInfoData> serverInfoDatas = new List<ServerInfoData>();

    public TMP_Text serverRange_Text;
    public TMP_Text serverData_Text;
    public override void Init()
    {
        //进入换服面板,显示当前的选择
        LoginData loginData = LoginManager.Instance.loginData;
        serverData_Text.text = loginData.selectServesID + "区  " + loginData.selectServerName;
        //读取服务器数据
        serverDatas = JsonMgr.Instance.LoadData<List<ServerData>>("Data/ServerInfo");
        Debug.Log("加载到的服务器共计:" + serverDatas.Count);
        //创建分区
        CreateServerPartitions(serverDatas.Count);
    }
    public void CreateServerPartitions(int totalServersCount)
    {
        int partitionCount = totalServersCount / 10;
        if (totalServersCount%10>0)
        {
            partitionCount += 1;
        }
        Debug.Log("当前服务器数量为:"+ totalServersCount + " 共划分分区树为:" +  partitionCount);
        for (int i = 0; i < partitionCount; i++)
        {
            //创建对应个数的分区范围按钮预设体
            GameObject rangeData = Resources.Load<GameObject>("UI/Panels/ServerRangeData_Button");
            if (rangeData == null)
            {
                Debug.Log("加载分区预制体失败");
                return;
            }
            //实例化预设体
            GameObject rangeDataButton = Instantiate(rangeData);
            //在实例化的GameObject上获取脚本组件
            ServerRangeData serverRangeData = rangeDataButton.GetComponent<ServerRangeData>(); 
            serverRangeData.transform.SetParent(leftGruop, false);
            serverRangeData.startServerID = i *10 + 1;
            serverRangeData.endServerID = i == partitionCount-1&&
                                               totalServersCount % 10 > 0 ?
                                               i*10 + totalServersCount % 10 : (i+1)*10;
            //为他添加设置服务器数据的方法
            serverRangeData.onRangeClicked += CreateServerDatas;
        }
    }
    
    public void CreateServerDatas(int startServerID,int endServerID)
    {
        //设置服务器数据,这里应该提前创建好10个服务器槽位
        //根据数量设置信息
        for (int i = 0; i < 10; i++)
        {
            serverInfoDatas[i].gameObject.SetActive(false);
        }
        int index =0;
        for(int i = startServerID -1;i< endServerID; i++) 
        {   
            serverInfoDatas[index].SetInfo(serverDatas[i]);
            serverInfoDatas[index].gameObject.SetActive(true);
            serverInfoDatas[index].ShowMe();
            Debug.Log("成功设置服务器ID"+ i);
            index++;
        }
        Debug.Log("设置服务器数据" + startServerID + " - " + endServerID);
        //更新UI
        ShowMe();
        serverRange_Text.text = startServerID + " - " + endServerID;
    }

    public override void ShowMe()
    {
        base.ShowMe();
        //更新UI方法
        LoginData l = LoginManager.Instance.loginData;
        serverData_Text.text = l.selectServesID + "区  " + l.selectServerName;
    }
}
[System.Serializable]
public class ServerData
{ 
    public int id;      //服务器编号
    public string name; //服务器名
    public int state; //服务器状态编号
    public bool isNew;  //服务器是否为新服务器

}
相关推荐
郝学胜-神的一滴1 小时前
[简化版 GAMES 101] 计算机图形学 11:频域·卷积·抗锯齿
c++·unity·图形渲染·opengl·three·unreal
元气少女小圆丶18 小时前
SenseGlove Nova 2+Unity开发笔记2
笔记·unity·游戏引擎
许彰午20 小时前
状态模式实战——Row对象的状态机
java·ui·状态模式
zhbi9820 小时前
LVGL8.3标签Label高级应用
ui·lvgl
像风一样的男人@1 天前
warning: could not find UI helper ‘git-credential-manager-ui‘
git·ui
想不明白的过度思考者1 天前
Unity学习笔记——虚拟摇杆实现笔记(事件触发器的使用、UGUI 坐标转换)
笔记·学习·unity
魔士于安1 天前
unity volumefog带各种demo第一人称 wsad 穿墙控制
游戏·unity·游戏引擎·贴图·模型
ZC跨境爬虫1 天前
跟着 MDN 学CSS day_34:(CSS 布局全面解析)
前端·css·ui·html·tensorflow
ZC跨境爬虫1 天前
跟着 MDN 学CSS day_32:(Web字体深度解析与实践指南)
前端·javascript·css·ui·html