1.准备工作 导入了各种资源,创建所需文件夹,导入了json管理器,在数据持久化分类可详细了解
2.面板基类 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 using UnityEngine;using UnityEngine.Events;public abstract class BasePanel : MonoBehaviour { protected CanvasGroup canvasGroup; protected bool isShow = false ; protected float alphaSpeed = 5f ; protected UnityAction onHideComplete; protected virtual void Awake () { canvasGroup = this .GetComponent<CanvasGroup>(); if (canvasGroup == null ) canvasGroup = this .gameObject.AddComponent<CanvasGroup>(); } protected virtual void Start () { Init(); } public abstract void Init () ; public virtual void Show () { isShow = true ; canvasGroup.alpha = 0 ; } public virtual void Hide (UnityAction unityAction ) { isShow = false ; canvasGroup.alpha = 1 ; onHideComplete = unityAction; } void Update () { if (isShow&&canvasGroup.alpha<1 ) { canvasGroup.alpha += Time.deltaTime * alphaSpeed; if (canvasGroup.alpha >= 1 ) canvasGroup.alpha = 1 ; } else { if (!isShow&&canvasGroup.alpha>0 ) { canvasGroup.alpha -= Time.deltaTime * alphaSpeed; if (canvasGroup.alpha <= 0 ) { canvasGroup.alpha = 0 ; onHideComplete?.Invoke(); } } } } }
3.UI管理器 因为这是创建(实例化)函数,你手里还没对象,所以你没法传 BasePanel 的对象进去。 你用泛型 ,主要是为了让函数的“返回值”自动变成你想要的子类 ,省去了你每次拿到面板后都要写 as LoginPanel 这种恶心的强转代码。
为了每次换场景都不销毁Canvas,加上GameObject.DontDestroyOnLoad(canvasTransform.gameObject);
此外,画布离开EventSystem用不了,因此可以直接拖过来作为画布子对象 (把ui摄像机也一并拖来)
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 using System.Collections.Generic;using UnityEngine;public class UIManager { private Transform canvasTransform; private Dictionary<string , BasePanel> panelDic = new Dictionary<string , BasePanel>(); private static UIManager instance=new UIManager(); public static UIManager Instance => instance; private UIManager () { canvasTransform = GameObject.Find("Canvas" ).transform; GameObject.DontDestroyOnLoad(canvasTransform.gameObject); } public T NewPanel <T >() where T : BasePanel { string panelName = typeof (T).Name; if (panelDic.ContainsKey(panelName)) { return panelDic[panelName] as T; } GameObject panelPrefab = GameObject.Instantiate(Resources.Load<GameObject>("UI/" + panelName)); panelPrefab.transform.SetParent(canvasTransform, false ); T panel = panelPrefab.GetComponent<T>(); panelDic.Add(panelName, panel); panel.Show(); return panel; } public void DestroyPanel <T >(bool isFade=true ) where T : BasePanel { string panelName = typeof (T).Name; if (panelDic.ContainsKey(panelName)) { if (isFade==true ) { panelDic[panelName].Hide(() => { GameObject.Destroy(panelDic[panelName].gameObject); panelDic.Remove(panelName); }); } else { GameObject.Destroy(panelDic[panelName].gameObject); panelDic.Remove(panelName); }; } } public T GetPanel <T >() where T : BasePanel { string panelName = typeof (T).Name; if (panelDic.ContainsKey(panelName)) { return panelDic[panelName] as T; } return null ; } }
4.提示面板 1.拼面板 根据示意图拼凑图片、文本等内容,并在面板添加Cavans Group组件
注意事项:
2.逻辑 TipPanel:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 using UnityEngine;using UnityEngine.UI;public class TipPanel : BasePanel { [SerializeField ] private Button confirmBtn; [SerializeField ] private Text infoText; public override void Init () { confirmBtn.onClick.AddListener(() => { UIManager.Instance.DestroyPanel<TipPanel>(); }); } public void SetInfo (string info ) { infoText.text = info; } }
Main:做初步测试
1 2 3 4 5 6 7 8 9 10 using UnityEngine;public class Main : MonoBehaviour { private void Start () { UIManager.Instance.NewPanel<TipPanel>(); UIManager.Instance.GetPanel<TipPanel>().SetInfo("欢迎来到主界面" ); } }
5.登录面板 1.拼面板
2.逻辑
新建LoginData脚本来存储登录面板数据,新建LoginMgr脚本单例模式来管理LoginData的序列化与反序列化,新建LoginPanel脚本管理面板控件功能
LoginData:
1 2 3 4 5 6 7 8 9 10 using UnityEngine;public class LoginData { public string id; public string password; public bool autoLogin; public bool rememberPW; }
LoginMgr:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 using UnityEngine;public class LoginMgr { private static LoginMgr instance=new LoginMgr(); public static LoginMgr Instance=>instance; private LoginData loginData; public LoginData LoginData => loginData; private LoginMgr () { loginData = JsonMgr.Instance.LoadData<LoginData>("LoginData" ); } public void SaveData () { JsonMgr.Instance.SaveData(loginData, "LoginData" ); } }
LoginPanel:
基础逻辑是这样的 Init函数(新建面板会执行的函数)规定好各控件的功能,Show函数实现基类功能的基础上,还要加载数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 using UnityEngine;using UnityEngine.UI;public class LoginPanel : BasePanel { public Button registerBtn; public Button confirmBtn; public Toggle autoLoginToggle; public Toggle rememberPWToggle; public InputField idInput; public InputField passwordInput; public override void Init () { confirmBtn.onClick.AddListener(() => { }); registerBtn.onClick.AddListener(() => { UIManager.Instance.DestroyPanel<LoginPanel>(); }); rememberPWToggle.onValueChanged.AddListener((isOn) => { if (!isOn) { autoLoginToggle.isOn = false ; } }); autoLoginToggle.onValueChanged.AddListener((isOn) => { if (isOn) { rememberPWToggle.isOn = true ; } }); } public override void Show () { base .Show(); LoginData loginData=LoginMgr.Instance.LoginData; idInput.text = loginData.id; autoLoginToggle.isOn = loginData.autoLogin; rememberPWToggle.isOn = loginData.rememberPW; if (loginData.rememberPW) { passwordInput.text = loginData.password; } if (loginData.autoLogin) { } } }
6.注册面板 1.拼面板 部分面板有相同的部分可以直接复制粘贴,不要直接在预制体里复制,要把预制体面板拖到场景中再复制,粘贴的控件位置才是原封不动的
2.逻辑 新建注册数据脚本,存储已经注册过的账号和密码,以字典的形式存储
RegisterData:
1 2 3 4 5 6 7 using System.Collections.Generic;using UnityEngine;public class RegisterData { public Dictionary<string , string > accountDic = new Dictionary<string , string >(); }
更新LoginMgr,新增注册数据类对象,以及序列化注册数据函数,添加注册账号(用于RegisterPanel)和验证账号密码是否正确(用于LoginPanel)的函数
LoginMgr:
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 using UnityEngine;public class LoginMgr { private static LoginMgr instance=new LoginMgr(); public static LoginMgr Instance=>instance; private LoginData loginData; public LoginData LoginData => loginData; private RegisterData registerData; public RegisterData RegisterData => registerData; private LoginMgr () { loginData = JsonMgr.Instance.LoadData<LoginData>("LoginData" ); registerData = JsonMgr.Instance.LoadData<RegisterData>("RegisterData" ); } public void SaveLoginData () { JsonMgr.Instance.SaveData(loginData, "LoginData" ); } public void SaveRegisterData () { JsonMgr.Instance.SaveData(registerData, "RegisterData" ); } public bool RegisterUser (string id,string password ) { if (registerData.accountDic.ContainsKey(id)) { return false ; } registerData.accountDic.Add(id, password); SaveRegisterData(); return true ; } public bool CheckInfo (string id,string password ) { if (registerData.accountDic.ContainsKey(id)) { if (registerData.accountDic[id] == password) return true ; } return false ; } }
RegisterPanel:
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 using UnityEngine;using UnityEngine.UI;public class RegisterPanel : BasePanel { public Button cancelBtn; public Button confirmBtn; public InputField idInput; public InputField passwordInput; public override void Init () { cancelBtn.onClick.AddListener(() => { UIManager.Instance.DestroyPanel<RegisterPanel>(); UIManager.Instance.NewPanel<LoginPanel>(); }); confirmBtn.onClick.AddListener(() => { if (idInput.text.Length < 6 || passwordInput.text.Length < 6 ) { TipPanel tipPanel = UIManager.Instance.NewPanel<TipPanel>(); tipPanel.SetInfo("账号和密码必须都大于6位" ); idInput.text = "" ; passwordInput.text = "" ; return ; } if (LoginMgr.Instance.RegisterUser(idInput.text, passwordInput.text)) { LoginPanel loginPanel = UIManager.Instance.NewPanel<LoginPanel>(); loginPanel.SetAccountInfo(idInput.text, passwordInput.text); UIManager.Instance.DestroyPanel<RegisterPanel>(); } else { TipPanel tipPanel = UIManager.Instance.NewPanel<TipPanel>(); tipPanel.SetInfo("账号已存在" ); idInput.text = "" ; passwordInput.text = "" ; } }); } }
更新LoginPanel,更新点击注册按钮和确定按钮事件,更新show函数,添加设置账号密码函数,供注册成功后直接将账密迁移
LoginPanel:
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 using UnityEngine;using UnityEngine.UI;public class LoginPanel : BasePanel { public Button registerBtn; public Button confirmBtn; public Toggle autoLoginToggle; public Toggle rememberPWToggle; public InputField idInput; public InputField passwordInput; public override void Init () { confirmBtn.onClick.AddListener(() => { if (LoginMgr.Instance.CheckInfo(idInput.text, passwordInput.text)) { LoginData loginData = LoginMgr.Instance.LoginData; loginData.id = idInput.text; loginData.password = passwordInput.text; loginData.rememberPW = rememberPWToggle.isOn; loginData.autoLogin = autoLoginToggle.isOn; LoginMgr.Instance.SaveLoginData(); TipPanel tipPanel = UIManager.Instance.NewPanel<TipPanel>(); tipPanel.SetInfo("登录成功" ); UIManager.Instance.DestroyPanel<LoginPanel>(); } else { TipPanel tipPanel = UIManager.Instance.NewPanel<TipPanel>(); tipPanel.SetInfo("账号或密码错误" ); idInput.text = "" ; passwordInput.text = "" ; } }); registerBtn.onClick.AddListener(() => { UIManager.Instance.DestroyPanel<LoginPanel>(); UIManager.Instance.NewPanel<RegisterPanel>(); }); rememberPWToggle.onValueChanged.AddListener((isOn) => { if (!isOn) { autoLoginToggle.isOn = false ; } }); autoLoginToggle.onValueChanged.AddListener((isOn) => { if (isOn) { rememberPWToggle.isOn = true ; } }); } public override void Show () { base .Show(); LoginData loginData=LoginMgr.Instance.LoginData; idInput.text = loginData.id; autoLoginToggle.isOn = loginData.autoLogin; rememberPWToggle.isOn = loginData.rememberPW; if (loginData.rememberPW) { passwordInput.text = loginData.password; } if (loginData.autoLogin) { TipPanel tipPanel = UIManager.Instance.NewPanel<TipPanel>(); tipPanel.SetInfo("自动登录成功" ); } } public void SetAccountInfo (string id, string password ) { idInput.text = id; passwordInput.text = password; } }
7.服务器面板 1.拼面板 别忘了每个面板都要加canvas group组件
2.逻辑 ServerPanel:
每次打开这个面板,都要展示上次选过的区服,所以要重载show
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 using UnityEngine;using UnityEngine.UI;using UnityEngine.SceneManagement;public class ServerPanel : BasePanel { public Button backBtn; public Button enterBtn; public Button changeBtn; public Text serverText; public override void Init () { backBtn.onClick.AddListener(() => { UIManager.Instance.DestroyPanel<ServerPanel>(); UIManager.Instance.NewPanel<LoginPanel>(); }); changeBtn.onClick.AddListener(() => { }); enterBtn.onClick.AddListener(() => { UIManager.Instance.DestroyPanel<ServerPanel>(); SceneManager.LoadScene("GameScene" ); }); } public override void Show () { base .Show(); LoginData loginData = LoginMgr.Instance.LoginData; } }
在LoginData里新增服务器区服id
LoginData:
1 2 3 4 5 6 7 8 9 10 11 using UnityEngine;public class LoginData { public string id; public string password; public bool autoLogin; public bool rememberPW; public int serverID=-1 ; }
LoginPanel在成功登录后要判断是否直接进入服务器面板,还是第一次选服进入选服面板
LoginPanel(节选):
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 confirmBtn.onClick.AddListener(() => { if (LoginMgr.Instance.CheckInfo(idInput.text, passwordInput.text)) { LoginData loginData = LoginMgr.Instance.LoginData; loginData.id = idInput.text; loginData.password = passwordInput.text; loginData.rememberPW = rememberPWToggle.isOn; loginData.autoLogin = autoLoginToggle.isOn; LoginMgr.Instance.SaveLoginData(); if (loginData.serverID == -1 ) { UIManager.Instance.DestroyPanel<LoginPanel>(); } else { UIManager.Instance.DestroyPanel<LoginPanel>(); UIManager.Instance.NewPanel<ServerPanel>(); } } else { TipPanel tipPanel = UIManager.Instance.NewPanel<TipPanel>(); tipPanel.SetInfo("账号或密码错误" ); idInput.text = "" ; passwordInput.text = "" ; } });
8.选服面板 1.拼面板
2.数据准备 ServerInfo.Json:
使用excel表编写,通过网页转换成Json文件,并放入StreamingAssets文件夹
在Data文件夹下新建选服信息脚本
ServerInfo:
1 2 3 4 5 6 7 8 9 using UnityEngine;public class ServerInfo { public int id; public string name; public int status; public bool isNew; }
在LoginMgr中新建List表读取Json
LoginMgr(节选):
1 2 3 4 5 6 7 8 private List<ServerInfo> serverData;public List<ServerInfo> ServerData => serverData;private LoginMgr (){ loginData = JsonMgr.Instance.LoadData<LoginData>("LoginData" ); registerData = JsonMgr.Instance.LoadData<RegisterData>("RegisterData" ); serverData = JsonMgr.Instance.LoadData<List<ServerInfo>>("ServerInfo" ); }
3.左侧按钮逻辑 ServerLeftBtn:
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 using UnityEngine;using UnityEngine.UI;public class ServerLeftBtn : MonoBehaviour { public Button leftBtn; public Text serverRangeText; public int beginIndex; public int endIndex; private void Start () { leftBtn.onClick.AddListener(()=> { }); } private void InitInfo (int begin,int end ) { beginIndex = begin; endIndex = end; serverRangeText.text = beginIndex + " - " + endIndex + "区" ; } }
4.右侧按钮逻辑 试着打个图集,将四种服务器状态图放一起(放的是sprite图,因为image源图像是sprite格式的,并且代码里加载图别忘了stateImg.sprite)
ServerRightBtn:
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 UnityEngine.U2D;using UnityEngine.UI;public class NewMonoBehaviourScript : MonoBehaviour { public Button rightBtn; public Text name; public Image stateImg; public Image newImg; private ServerInfo serverInfo; private void Start () { rightBtn.onClick.AddListener(() => { UIManager.Instance.NewPanel<ServerPanel>(); }); } public void InitInfo (ServerInfo info ) { serverInfo = info; name.text = info.id + "区 " + info.name; newImg.gameObject.SetActive(serverInfo.isNew); SpriteAtlas atlas = Resources.Load<SpriteAtlas>("Login" ); switch (serverInfo.state) { case 0 : stateImg.sprite = atlas.GetSprite("ui_DL_liuchang_01" ); break ; case 1 : stateImg.gameObject.SetActive(false ); break ; case 2 : stateImg.sprite = atlas.GetSprite("ui_DL_huobao_01" ); break ; case 3 : stateImg.sprite = atlas.GetSprite("ui_DL_fanhua_01" ); break ; case 4 : stateImg.sprite = atlas.GetSprite("ui_DL_weihu_01" ); break ; } } }
Resources 是一个被施了魔法的“特殊文件夹”
在 Unity 中,叫 Resources 的文件夹有着至高无上的特权。
代码层面上 :Resources 其实是 Unity 引擎自带的一个 C# 类(UnityEngine.Resources),Load 是这个类里面的一个静态方法。它并不是指代路径的字符串。
打包层面上(最致命的区别) :当你把你辛辛苦苦做的游戏打包成 .exe 或 .apk 时,Unity 会进行极其严格的“瘦身运动”。普通文件夹(比如 UI_Images、MyPrefabs) :如果里面的图片、预制体没有被场景里的物体挂载(没有被引用),Unity 会认为:“哦,这些东西游戏里用不到,全部当垃圾扔掉!不打包进去! ”Resources 文件夹 :Unity 对它绝对敬畏。只要是放在 Resources 文件夹里的东西,不管你有没有在场景里用到它,Unity 都会把它们强制压缩打包进游戏的底层核心文件里!
总结一下用到的文件夹:
要动态读游戏资产(预制体、图片图集、音频) ➡️ 用 Resources 文件夹。
要动态读纯文本数据(JSON 配置表、外部 MP4 视频) ➡️ 用 StreamingAssets 文件夹,不能改变其名称,Application.streamingAssetsDataPath是封装好的静态属性。
要存玩家的打怪存档 ➡️ 用 Application.persistentDataPath 路径。
5.动态创建按钮 创建选服面板,主要逻辑是在创建时生成左侧按钮,随后更新右侧相关对象
ChooseServerPanel:
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 using System.Collections.Generic;using UnityEngine;using UnityEngine.UI;using UnityEngine.U2D;public class ChooseServerPanel : BasePanel { public Button lastServerBtn; public Text lastServerText; public Image lastServerStateImg; public Text serverRangeText; public ScrollRect leftScrollRect; public ScrollRect rightScrollRect; public override void Init () { List<ServerInfo> serverData = LoginMgr.Instance.ServerData; int count=serverData.Count/5 +(serverData.Count%5 ==0 ?0 :1 ); for (int i = 0 ; i < count; i++) { GameObject leftBtnPrefab = Instantiate(Resources.Load<GameObject>("UI/ServerLeftBtn" )); leftBtnPrefab.transform.SetParent(leftScrollRect.content, false ); ServerLeftBtn serverLeftBtn = leftBtnPrefab.GetComponent<ServerLeftBtn>(); serverLeftBtn.InitInfo(i * 5 + 1 , serverData.Count < (i + 1 ) * 5 ? serverData.Count : (i + 1 ) * 5 ); } } public override void Show () { base .Show(); LoginData loginData = LoginMgr.Instance.LoginData; if (loginData.serverID != -1 ) { ServerInfo lastServerInfo = LoginMgr.Instance.ServerData[loginData.serverID - 1 ]; lastServerText.text = lastServerInfo.id + "区 " + lastServerInfo.name; SpriteAtlas atlas = Resources.Load<SpriteAtlas>("Login" ); switch (lastServerInfo.state) { case 0 : lastServerStateImg.sprite = atlas.GetSprite("ui_DL_liuchang_01" ); break ; case 1 : lastServerStateImg.gameObject.SetActive(false ); break ; case 2 : lastServerStateImg.sprite = atlas.GetSprite("ui_DL_huobao_01" ); break ; case 3 : lastServerStateImg .sprite = atlas.GetSprite("ui_DL_fanhua_01" ); break ; case 4 : lastServerStateImg.sprite = atlas.GetSprite("ui_DL_weihu_01" ); break ; } } else { lastServerText.text = "暂无服务器记录" ; lastServerStateImg.gameObject.SetActive(false ); } UpdateList(1 , 5 >LoginMgr.Instance.ServerData.Count ? LoginMgr.Instance.ServerData.Count : 5 ); } public void UpdateList (int begin,int end ) { serverRangeText.text = "服务器 " + begin + "-" + end; foreach (Transform child in rightScrollRect.content){ Destroy(child.gameObject); } for (int i = begin-1 ; i < end; i++) { List<ServerInfo> serverData = LoginMgr.Instance.ServerData; GameObject rightBtnPrefab = Instantiate(Resources.Load<GameObject>("UI/ServerRightBtn" )); rightBtnPrefab.transform.SetParent(rightScrollRect.content, false ); ServerRightBtn serverRightBtn = rightBtnPrefab.GetComponent<ServerRightBtn>(); serverRightBtn.InitInfo(serverData[i]); } } }
同时完善左侧按钮逻辑与右侧按钮逻辑
当点击左侧按钮时,右侧会更新按钮列表
ServerLeftBtn(节选):
1 2 3 4 5 6 7 8 private void Start (){ leftBtn.onClick.AddListener(()=> { UIManager.Instance.GetPanel<ChooseServerPanel>().UpdateList(beginIndex,endIndex); }); }
当点击右侧按钮时,会记录当前服务器id同步至loginData,并返回服务器面板,展示所选服务器名称
ServerRightBtn(节选):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private void Start (){ rightBtn.onClick.AddListener(() => { LoginData loginData = LoginMgr.Instance.LoginData; loginData.serverID = serverInfo.id; UIManager.Instance.DestroyPanel<ChooseServerPanel>(); UIManager.Instance.NewPanel<ServerPanel>(); }); }
ServerPanel(节选):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public override void Show (){ base .Show(); LoginData loginData = LoginMgr.Instance.LoginData; if (loginData.serverID != -1 ) { ServerInfo serverInfo = LoginMgr.Instance.ServerData[loginData.serverID - 1 ]; serverText.text = serverInfo.id + "区 " + serverInfo.name; } else { serverText.text = "无服务器记录" ; } }
6.逻辑串联 一下是一些细节层面的逻辑串联
注册账号成功后,我们需要清除logindata中服务器号以及是否记住密码和自动登录的信息,因此在LoginMgr加入清除登录数据函数,并在RegisterPanel注册成功后调用
LoginMgr(节选):
1 2 3 4 5 6 7 public void ClearLoginData (){ loginData.rememberPW = false ; loginData.autoLogin = false ; loginData.serverID = -1 ; }
在选完服务器后,logindata会记下服务器id,但当玩家点击进入游戏后,才会调用savelogindata来真正保存服务器id
ServerPanel(节选):
1 2 3 4 5 6 7 8 9 10 11 enterBtn.onClick.AddListener(() => { LoginMgr.Instance.SaveLoginData(); UIManager.Instance.DestroyPanel<ServerPanel>(); UIManager.Instance.DestroyPanel<BKPanel>(); SceneManager.LoadScene("GameScene" ); });
哦对了,为了登录结束后让背景图一并消失,可以创建新面板BKPanel来只放一个image背景图,其脚本也只是继承BasePanel什么也不实现
为这张背景图添加组件Aspect Ratio Fitter(宽高比适配器)
对比维度
Canvas Scaler (画布缩放)
Aspect Ratio Fitter (宽高比适配)
作用目标
整个屏幕/整个UI系统
单个指定的 UI 元素 (比如一张图)
解决的问题
适配不同型号手机的屏幕分辨率
保证单张图片/容器的长宽比例不失调
触发时机
当游戏窗口大小/手机屏幕分辨率改变时
当它自己的宽度或高度发生改变时
生活中的比喻
放映机 :调整整个画面的投影大小
相框 :保证里面的照片不会被强行拉成畸形
场景还原:做一张全屏的加载背景图(Loading 背景)。
大环境交给大元帅 :Canvas Scaler 设为 1920x1080,Match 设为 0.5(宽高中立)。这保证了所有的 UI 基础缩放是对的。
但遇到了问题 :全面屏手机特别长,1920x1080 的背景图铺上去,左右两边会露出黑边。如果强行把图拉长,图里的人物就变胖了(变形了)。
单兵作战派上用场 :选中这张背景 Image,给它挂上 Aspect Ratio Fitter,模式选择 Envelope Parent(包裹父节点) ,比例设为原始图片的比例(比如 1.77)。
完美结果 :这张背景图会按等比例完美放大,直到填满整个全面屏的左右黑边!虽然图的上下部位会被屏幕裁掉一点点,但画面绝对没有变形拉伸 ,视觉体验极佳!
自动登录功能也在loginPanel中实现
LoginPanel(节选):
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 public override void Show (){ base .Show(); LoginData loginData=LoginMgr.Instance.LoginData; idInput.text = loginData.id; autoLoginToggle.isOn = loginData.autoLogin; rememberPWToggle.isOn = loginData.rememberPW; if (loginData.rememberPW) { passwordInput.text = loginData.password; } if (loginData.autoLogin) { UIManager.Instance.DestroyPanel<LoginPanel>(false ); if (loginData.serverID == -1 ) { UIManager.Instance.NewPanel<ChooseServerPanel>(); TipPanel tipPanel = UIManager.Instance.NewPanel<TipPanel>(); tipPanel.SetInfo("自动登录成功" ); } else { UIManager.Instance.NewPanel<ServerPanel>(); TipPanel tipPanel = UIManager.Instance.NewPanel<TipPanel>(); tipPanel.SetInfo("自动登录成功" ); } } }
但点击服务器面板的左上返回按钮,会因为自动登录勾选上始终显示不出来登录面板,因此点击返回按钮要取消自动登录的勾选
ServerPanel(节选):
1 2 3 4 5 6 7 8 9 10 backBtn.onClick.AddListener(() => { if (LoginMgr.Instance.LoginData.autoLogin) { LoginMgr.Instance.LoginData.autoLogin = false ; } UIManager.Instance.DestroyPanel<ServerPanel>(); UIManager.Instance.NewPanel<LoginPanel>(); });
9.图集整理
可以将整个文件夹放入图集,一般工作时设计人员会按照文件夹分类好
建议工作流:
建一个 CommonAtlas(通用图集) :把各个面板都会用到的按钮底板、弹窗背景、关闭X图标塞进去。
按“大系统”建立业务图集 :不要细到“每个小面板”,而是按大功能 分。比如:MainUI_Atlas:主界面上那十几个常驻图标和玩家头像框。Login_Atlas:只在登录、注册页面用的图。BattleUI_Atlas:战斗内用的血条、技能摇杆、伤害数字背景(战斗对性能要求高,最好单独打图集)。
UI 预制体的制作规范 :在做一个新面板时,它的图片要么来自 CommonAtlas,要么来自它专属的业务图集,绝对不要去跨界借用其他业务图集的图片! (比如商城面板千万不要去用背包图集里的某个特定发光框,如果要用,就把那个发光框移到 CommonAtlas 里去)。
大的背景图不要放入图集,大图极其浪费图集空间(很容易导致图集出现大量无法利用的缝隙),而且图集变大后会导致极高的内存峰值。大图应该直接设为单张 Sprite 动态加载。
🛠️ 终极除虫武器:Frame Debugger(帧调试器)
Unity 官方提供的终极性能透视镜
操作步骤:
在顶部菜单栏点击 Window -> Analysis -> Frame Debugger 。
运行游戏,点击面板左上角的 Enable(启用) 。
此时游戏画面会冻结。
奇迹出现了 :这里列出了你这三四十个 Batch 到底是怎么画出来的!你可以按键盘的上下键,一步一步看 Unity 是先画了哪个图,再画了哪个字。
最神的地方 :当你选中一个 Batch 时,面板右侧会有一行字,极其明确地告诉你为什么它没有和上一个合并! (比如提示:Different Material 材质不同、Intersecting with A different Texture 发生了重叠穿插…)
这个DrawTransparentObjects才是ui的batches,其余的是URP管线带的特效和摄影机渲染等等带来的batches