第三章 UI框架设定 流程逻辑
场景切换从登陆——加载——游戏界面
场景中的设置
1、给Main物体中添加SceneController脚本
其中当前场景中的EventSystem是自己手动创建用来控制UI面板的事件系统的
2、给Camera组件加入UCameraController脚本
3、设置buildingsetting来控制场景
4、Game场景中创建
代码流程
进入到登录界面的流程
1、当游戏运行时,加载Main物体中的SceneController方法
public void Awake(){ //将当前对象挂载的SceneController组件注册到全局单例GameSystemGameSystem.Instance.SceneController = this.GetComponent<SceneController>();//查找当前物体下名为Camera的子物体,获取相机上的自定义控制脚本<UCameraController>()GameSystem.Instance.CameraController = this.transform.Find("Camera").GetComponent<UCameraController>();//场景切换时不摧毁该物体GameObject.DontDestroyOnLoad(this.gameObject);SystemInit();//打开LoginViewController单例LoginViewController.Instance.Open();}private void SystemInit() {// ...其他系统初始化...ViewManager.Instance.Init(); // 关键:视图管理器初始化 }
2、ViewManager在这里对ViewManager实例进行初始化
private void SystemInit(){ ViewManager.Instance.Init(); }public class ViewManager {static ViewManager instance = new ViewManager();public static ViewManager Instance => instance;public int order;public void Init(){//注册所有界面LoginViewController.Instance.Init("UI/LoginCanvas", true, false, false);BagViewController.Instance.Init("UI/BagCanvas", true, false, true);ForgeViewController.Instance.Init("UI/ForgeCanvas", true, false, true);LoadingViewController.Instance.Init("UI/LoadingCanvas", true, false);MainViewController.Instance.Init("UI/MainCanvas", true, false);NavViewController.Instance.Init("UI/NavCanvas", true, false);PurifyViewController.Instance.Init("UI/PurifyCanvas", true, false, true);TipsViewController.Instance.Init("UI/TipsCanvas", true, false);StoreViewController.Instance.Init("UI/StoreCanvas", true, false);//神秘商店...//其他界面 也都在这里初始化 注册} }
这里调用了
LoginViewController.Instance.Init()
方法,传递了资源路径和参数
3、在ViewController类中使用Init方法进行初始化
public class ViewController<T, V> where T : new() where V : View, new() {public void Init(string resPath, bool isAddUpdateListerner, bool stopAI_OnOpen, bool open_nav) {view = new V(); // 创建LoginView实例view.Init(resPath, isAddUpdateListerner, Close, stopAI_OnOpen);// ...其他设置... }
创建了
LoginView
实例调用了
view.Init()
,传递了资源路径"UI/LoginCanvas"
注意:
在代码中创建
LoginView
实例并不会直接在 Unity 编辑器的资产面板(Project 窗口)中显示,这是因为:1. 代码创建的实例是运行时对象
当你在代码中执行
view = new V()
(对于 LoginView 是view = new LoginView()
)时:
这是在内存中创建一个 C# 对象实例
不是 在场景中创建 GameObject
4、触发LoginView打开:LoginViewController.Instance.Open()
回到Main.Awake()的最后一步:csharpLoginViewController.Instance.Open(); // 实际打开登录页面
这将执行
ViewController.Open()
方法:public void Open() {if (view.gameObject == null) // 首次打开,资源未加载{// 实例化UI预制体var go = ResourcesManager.Instance.Instantiate<GameObject>(view.resPath);// 关联视图对象view.gameObject = go;view.transform = go.transform;view.canvas = go.GetComponent<Canvas>();view.cs = go.GetComponent<CanvasScaler>();// 生命周期方法view.Awake();SetActive(true); // 关键:激活并显示UIview.Start();}else{SetActive(true); // 已加载过,直接激活} }
从登录界面到加载界面的流程
1、玩家点击登录按钮
结合EventSystem用于点击按钮触发事件public class LoginView : View {public override void Awake(){base.Awake();var new_game = transform.Find("NewGame").GetComponent<Button>();//获取NewGame预制体中的按钮组件new_game.onClick.AddListener(NewGame);//注册事件“NewGame”方法在点击该按钮的时候}private void NewGame(){//打开Loading //切换场景//更新场景的加载进度GameSystem.Instance.SceneController.Load("Game");}}
![]()
2、NewGame方法激活后,SceneController开始加载场景
需要注意的是:next参数(场景字符串)在Init方法中得到传输public void Load(string next) {// 第一步:打开加载界面LoadingViewController.Instance.Open();// 第二步:关闭登录界面LoginViewController.Instance.Close();// 第三步:开始异步加载场景StartCoroutine(LoadSceneAsync(next)); }
3、异步加载Loading场景第一阶段:资源加载(0-90%)IEnumerator LoadSceneAsync(string next) {// 开始异步加载场景var op = SceneManager.LoadSceneAsync(next);//返回异步操作状态op.allowSceneActivation = false; // 先不激活新场景// 第一阶段:加载资源(0-90%)while (op.progress < 0.9f){yield return new WaitForEndOfFrame();// 更新加载进度显示LoadingViewController.Instance.UpdateLoadProgress(op.progress);}// 允许场景激活(触发场景切换)op.allowSceneActivation = true;// 第二阶段:模拟加载完成(90%-100%)float progress = op.progress; // 初始值0.9while (progress <= 1){progress += GameTime.deltaTime; // 渐进增加LoadingViewController.Instance.UpdateLoadProgress(progress);yield return new WaitForEndOfFrame();}// 场景加载完成yield return op;// 初始化游戏对象var player = UnitManager.Instance.CreatePlayer();GameSystem.Instance.CameraController.SetTarget(player.transform);yield return new WaitForEndOfFrame();// 关闭加载界面LoadingViewController.Instance.Close();// 打开主游戏界面MainViewController.Instance.Open(); }
实际作用:加载游戏场景的资源(纹理、模型、脚本等)
不切换场景:当前仍然是登录场景,加载界面覆盖在上面
进度限制:Unity 异步加载最多显示到 0.9(90%)
第二阶段:视觉过渡(90-100%)——Loadingview类中控制加载页面进度条
非实际加载:此时真正的场景切换已在后台进行
视觉欺骗:让进度条平滑到达 100%,提升用户体验
等待时间:确保场景切换完成前保持加载界面
第三阶段:进入游戏场景页面
4、动态创建角色:
在加载页面时会调用该方法,如果玩家角色未生成,则将角色指定到"GatePoint/0"的位置
SceneController中: IEnumerator LoadSceneAsync(string next){ var player = UnitManager.Instance.CreatePlayer(); } public GameObject CreatePlayer(){if (player == null){var go = ResourcesManager.Instance.Instantiate<GameObject>("Unit/1001");var targetPoint = GameObject.Find("GatePoint/0");go.transform.position = targetPoint.transform.position;go.transform.forward = targetPoint.transform.forward;player = go.GetComponent<FSM>();}return player.gameObject;}
LoginView与LoadingView类编写作用
1、LoginView用来控制登陆面板上的操作逻辑
2、LoadingView用来控制加载面板上的加载进度控制以及场景切换控制
相机跟随
场景资源设置
代码流程
初始化阶段:
在
Main.Awake()
中初始化相机控制器,在SystemInit()
中设置相机引用:public class Main : MonoBehaviour {public void Awake(){ //从当前Main实例物体上获取子物体“Camera”,且获取组件<UCameraController> GameSystem.Instance.CameraController = this.transform.Find("Camera").GetComponent<UCameraController>();}private void SystemInit(){GameDefine._Camera = GameObject.Find("Camera").transform;}
2.目标绑定:当需要跟随角色时(如玩家生成后),调用:
csharpUCameraController.SetTarget(Transform target)
SetTarget(Transform target)会如下:
public int state;//0处于空闲的状态 1跟随状态public void SetTarget(Transform target){this.target = target;//1、绑定目标位置if (target != null){Cursor.lockState = CursorLockMode.Locked; ;//Cursor.visible = false;//2、隐藏鼠标 不可见controller = target.GetComponent<CharacterController>();hight_offset = controller.center * 1.75f;//计算角色高度偏移量(基于碰撞体中心)}state = 1;Follow(false);//立即更新相机位置而不进行插值平滑this.gameObject.SetActive(true);//控制相机//Follow()}
通过复制+粘贴可以进行整排或者整列的迁移
创建并更新敌人小兵血条UI
预制体设置
1、给敌人的层级进行如下设置:
Layer决定能不能进行受击反馈并传输数据
id=1001决定了敌人的type,并选择血条UI显隐方式
2、敌人NPC血条预制体路径
且该Canvas画布渲染方式为世界坐标,所以才能大世界中跟随玩家视角移动出现在不同敌人NPC上
代码流程
1、受击更新并创建血量UI,格挡时也会创建血量UI
//该方法处于FSM
internal void UpdateHP_OnHit(int damage){……//当前FSm持有者角色的生命百分比if (AI){//更新敌人血条 if (unitEntity.type == 3){//更新Boss的血条}else {//更新小兵的血条UpdateEnemyHUD();}}else{//更新主角的血条 }}
//该方法位于FSM中,格挡动作触发也会出现血条
internal void OnBlockSucces(FSM atk){ this.atk_target = atk;UpdateEnemyHUD();if (currentState.excel_config.on_block_succes != 0){ToNext(currentState.excel_config.on_block_succes);}}
2、根据unitEntity.type类型创建敌人血条类型,根据Resource文件夹路径创建血条预制体
注意:这里返回的值是预制体上的<EnemyHUD>组件,而不是生成的克隆预制体——这是因为:
1、我们只需要只需要操作血条逻辑,不需要知道整个游戏对象
2、返回物体进行操作可能会误改属性破坏系统,返回组件只能访问其公开方法
private void UpdateEnemyHUD(){if (AI){if (unitEntity.type == 1 || unitEntity.type == 2 || unitEntity.type == 0){if (enemyHUD == null){enemyHUD = ResourcesManager.Instance.CreateEnemyHUD();//返回预制体上的组件<EnemyHUD>赋值给enemyHUD}enemyHUD.UpdateHP(att_crn.hp / att_base.hp, this._transform, unitEntity.info);}}}//ResourceManager中血条创建
internal EnemyHUD CreateEnemyHUD(){if (hud.Count > 0){return hud.Pop();}else{var go = Instantiate<GameObject>("UI/HUD/Enemy_HUD");return go.GetComponent<EnemyHUD>();//获取生成的预制体实例上的<EnemyHUD>组件}}
可以生成如下Enemy_HUD物体,且该物体随着调用enemyHUD.UpdateHP方法每帧同步到敌人位置
3、在Enemy_HUD类中更新生命值,并且将更新过程
Enemy_HUD类https://blog.csdn.net/2303_80204192/article/details/149715559?spm=1001.2014.3001.5501#t28血条更新的过程
https://blog.csdn.net/2303_80204192/article/details/149777248?spm=1001.2014.3001.5502#t14
小结
小兵血条UI的更新相当于使用Enemy_HUD类进行全盘控制其数据和视觉UI的变化,而不是使用MVC架构进行分离数据、视觉变化和控制器的设计。
创建并更新敌人Boss血条UI
预制体设置
1、首先需要注意Layer是否设置为Enemy,这里卡了我很久才发现
2、MainCanvas中的该预制体是否存在(目前一般是处于隐藏状态)
代码流程
1、血条UI的初始化
在
MainView.Awake()
中获取Boss血条相关组件:
boss_hp_middle = GetComponent<Image>("BOSS/HP_Base/HP_Middle"); boss_hp_top = GetComponent<Image>("BOSS/HP_Base/HP_Top"); boss_root = transform.Find("BOSS"); boss_name = GetComponent<Text>("BOSS/Name/Name_Text");
2、Fsm类中计算彼此生命值
internal void UpdateHP_OnHit(int damage){//这里计算生命值this.att_crn.hp -= damage;if (this.att_crn.hp < 0){this.att_crn.hp = 0;}float v =this.att_crn.hp/this .att_base.hp;if (AI){//更新敌人血条 (type==3时为boss)if (unitEntity.type == 3){//更新Boss的血条MainViewController.Instance.UpdateBossHP(v);}else {//更新小兵的血条UpdateEnemyHUD();}}else{//更新主角的血条MainViewController.Instance.UpdatePlayerHP(v);}}
3、控制血条UI的显隐
Fsm中的OnEnable可以多次激活:
Boss 被创建时(首次激活)
Boss 被复活时(重新激活)
脚本被单独启用时(如从禁用状态启用)
//Fsm中启用FSM就调用MainViewController中的EnableBossHP private void OnEnable(){if (AI){if (unitEntity.type == 3){MainViewController.Instance.EnableBossHP(true, unitEntity.info);}}}//MainViewController中控制MainView方法 public void EnableBossHP(bool enable, string name){view.EnableBossHP(enable, name);}//MainView中的方法 public void EnableBossHP(bool enable, string name) {boss_root.gameObject.SetActive(enable); // 控制整个Boss血条容器SetText(boss_name, name); // 设置Boss名称 }
4、初始值-1代表未更新,传入新百分比值说明数据更新public float boss_hp = -1; // 初始值-1表示未更新 public void UpdateBossHP(float v) {boss_hp = v; // 存储当前血量比例(0-1) }
5、血条更新
采用三层更新架构:
`UpdateBossHP`负责接收最新的Boss血量值,而`Update`方法负责每帧根据这个值更新UI的显示。这样设计的好处是,将数据的更新和UI的渲染分离,使得逻辑更清晰。
数据层:通过
UpdateBossHP()
接收外部传输来的Boss血量数据(这里是FSM类中攻击Boss后传输的v经过MainViewController交给Mainview)csharppublic float boss_hp = -1; // 初始值-1表示未更新 public void UpdateBossHP(float v) {boss_hp = v; // 存储当前血量比例(0-1) }
逻辑层:Mainview在
Update()
中接受传入的血条相关数据触发血条更新csharppublic override void Update() {DOSetFillAmout(boss_hp, boss_hp_top, boss_hp_middle);// ...其他更新 }
表现层:MainView中通过
DOSetFillAmout()
实现双血条缓动效果csharppublic void DOSetFillAmout(float v, Image top, Image middle) {if (v == -1) return; // 跳过无效更新// 顶层血条:快速响应变化if (top.fillAmount > v) {top.SetFillAmount(v, middle_speed * 2); // 受伤时快速下降} else if (top.fillAmount < v) {top.SetFillAmount(v, middle_speed); // 恢复时慢速上升}// 中层血条:延迟缓动效果if (middle.fillAmount > v) {middle.SetFillAmount(v, middle_speed); // 受伤时慢速跟随} else if (middle.fillAmount < v) {middle.SetFillAmount(v, middle_speed * 2); // 恢复时快速填充} }
角色技能CD显示
代码逻辑
1、注册监听方法
if (AI == false)//当FSM为玩家时{foreach (var item in stateData){………………//if (item.Value.skill != null)//当配置技能表不为空{ AddListener(item.Key, StateEventType.begin, SetSkillCD);//给技能id状态注册方法}}}进入ToNext方法后调用DOstate方法才会激发SetSkillCD方法
2、根据不同技能调用不同传入不同Type
//FSM中
private void SetSkillCD(){if (currentState.id == 1009){MainViewController.Instance.SetSkillCD(0, currentState.skill.cd);}else if (currentState.id == 1010){MainViewController.Instance.SetSkillCD(1, currentState.skill.cd);}else if (currentState.id == 1011){MainViewController.Instance.SetSkillCD(2, currentState.skill.cd);}else if (currentState.id == 1012){MainViewController.Instance.SetSkillCD(3, currentState.skill.cd);}}//MainViewController中
public void SetSkillCD(int type, float cd){view.SetSkillCD(type, cd);}
3、SceneController中打开MainView
IEnumerator LoadSceneAsync(string next){……//加载场景中的一些参数设置……//玩家未出现时设定位置出现 yield return new WaitForEndOfFrame();LoadingViewController.Instance.Close();MainViewController.Instance.Open();//触发Open方法 }
public void Open(){if (view.gameObject == null){//实例化……view.canvas = go.GetComponent<Canvas>();view.cs = go.GetComponent<CanvasScaler>();view.Awake();//在这里完成view的初始化SetActive(true);view.Start();}else{//打开SetActive(true);}}
获取MainviewCanvas中的组件并赋值
float q_cd, e_cd, r_cd, t_cd;float q_cd_begin, e_cd_begin, r_cd_begin, t_cd_begin;Image mask_q, mask_e, mask_r, mask_t;Text countdown_q, countdown_e, countdown_r, countdown_t;public override void Awake(){base.Awake();…………mask_q = GetComponent<Image>("Quik/Skill_Q/Item/mask");mask_e = GetComponent<Image>("Quik/Skill_E/Item/mask");mask_r = GetComponent<Image>("Quik/Skill_R/Item/mask");mask_t = GetComponent<Image>("Quik/Skill_T/Item/mask");countdown_q = GetComponent<Text>("Quik/Skill_Q/Item/countdown");countdown_e = GetComponent<Text>("Quik/Skill_E/Item/countdown");countdown_r = GetComponent<Text>("Quik/Skill_R/Item/countdown");countdown_t = GetComponent<Text>("Quik/Skill_T/Item/countdown");skill_cd_tips = transform.Find("Skill_CD_Tips").gameObject;}
4、在MainView中对技能CD进行设置
float q_cd, e_cd, r_cd, t_cd;
float q_cd_begin, e_cd_begin, r_cd_begin, t_cd_begin;
Image mask_q, mask_e, mask_r, mask_t;
Text countdown_q, countdown_e, countdown_r, countdown_t;//MainView中对技能CD进行设置
public void SetSkillCD(int type, float cd)
{switch (type){case 0: // Q 技能q_cd_begin = GameTime.time; // 记录冷却开始时间q_cd = cd; // 设置剩余冷却时间mask_q.gameObject.SetActive(true); // 打开圆形遮罩mask_q.fillAmount = 1f; // 遮罩满圆(技能刚进入冷却)countdown_q.gameObject.SetActive(true); // 打开倒计时文本break;case 1: // E 技能e_cd_begin = GameTime.time;e_cd = cd;mask_e.gameObject.SetActive(true);mask_e.fillAmount = 1f;countdown_e.gameObject.SetActive(true);break;case 2: // R 技能r_cd_begin = GameTime.time;r_cd = cd;mask_r.gameObject.SetActive(true);mask_r.fillAmount = 1f;countdown_r.gameObject.SetActive(true);break;case 3: // T 技能t_cd_begin = GameTime.time;t_cd = cd;mask_t.gameObject.SetActive(true);mask_t.fillAmount = 1f;countdown_t.gameObject.SetActive(true);break;}
}
5、CD的更新方法
public override void Update(){……//生命值相关更新方法DOUpdatePlayerCD();//更新cdCloseCD_Tips();}public void DOUpdatePlayerCD(){UpdateSkillCD(ref q_cd, q_cd_begin, mask_q, countdown_q);UpdateSkillCD(ref e_cd, e_cd_begin, mask_e, countdown_e);UpdateSkillCD(ref r_cd, r_cd_begin, mask_r, countdown_r);UpdateSkillCD(ref t_cd, t_cd_begin, mask_t, countdown_t);}
通过 UpdateSkillCD更新数据和视觉效果
public void UpdateSkillCD(ref float cd, float begin, Image mask, Text countdown){if (cd != 0){if (mask.fillAmount != 0){var result = mask.SetFillAmount(0, 1 / cd);//这里展示Mask的冷却视觉效果if (result == true){cd = 0;mask.gameObject.SetActive(false);countdown.gameObject.SetActive(false);}}countdown.text = Math.Ceiling(cd - (GameTime.time - begin)).ToString();//让玩家看到倒计时}}
6、冷却时再次点击技能
FSM中技能冷却
var next_state = stateData[Next];if (next_state.skill != null && next_state.begin != 0 && GameTime.time - next_state.begin < next_state.skill.cd){MainViewController.Instance.OpenCD_Tips();return false;}next_state.skill != null
→ 下一个状态(next_state)有一个技能(skill)不为 null,也就是说这个状态对应一个技能。
next_state.begin != 0
→ 这个技能的开始时间 begin 不是 0,说明这个技能已经开始过。
GameTime.time - next_state.begin < next_state.skill.cd
→ 当前游戏时间 减去 技能开始时间 的差值 小于 技能的冷却时间(cd),也就是说技能还在冷却中
冷却结束算法
这里得讲讲怎么判断是否冷却结束的算法:
GameTime.time
的本质:
全局持续递增:
✅ 它是一个全局计时器(类似 Unity 的Time.time
),从游戏启动开始就持续计数,不会因为某个代码分支的执行而启动或重置。读取即当前值:
✅ 当执行到GameTime.time - next_state.begin
时,只是读取此刻的最新值,类似于查看钟表当前时间。
next_state.begin
技能开始时间:只有真正切换成功时才刷新;(在ToNext方法中调用SetBegin方法将当前全局时间记录到begin变量中)
时间关系动态演示
假设固定冷却时间
cd = 3秒
,全局时间持续递增:
事件顺序 全局时间 操作 状态开始时间 begin
时间差计算 能否切换 1. 首次切换 10.0s 设置 begin 10.0s - ✅ 2. 尝试切换 11.0s 检查:11.0 - 10.0 = 1s < 3s 保持 10.0s ❌ (冷却中) 3. 尝试切换 12.9s 检查:12.9 - 10.0 = 2.9s < 3s 保持 10.0s ❌ (冷却中) 4. 成功切换 13.0s 重置 begin 更新为 13.0s ✅ 5. 尝试切换 14.0s 检查:14.0 - 13.0 = 1s < 3s 保持 13.0s ❌ (冷却中)
在MainView中,打开技能冷却中的面板
float cd_tips_begin;internal void OpenCD_Tips(){cd_tips_begin = GameTime.time;skill_cd_tips.gameObject.SetActive(true);}public void CloseCD_Tips(){if (cd_tips_begin == 0){return;}if (GameTime.time - cd_tips_begin >= 2){cd_tips_begin = 0;skill_cd_tips.gameObject.SetActive(false);}}
OpenCD_Tips()
• 把当前时间GameTime.time
记录到cd_tips_begin
,相当于“按下秒表”。
• 立即把skill_cd_tips
(可能是弹出的文字或图标)设为可见,告诉玩家“技能正在冷却”。CloseCD_Tips()
• 如果cd_tips_begin
还是 0,说明提示根本没打开,直接返回。
• 如果距打开提示已经过去 ≥ 2 秒,就把提示关掉,并把cd_tips_begin
重置为 0,等待下一次打开
背包数据显示
父容器、子容器格子栏预制体调整
1、制作合适的预制体
BagCanvas中的Prop_0作为背包中格子父容器
这里是存放格子具体物品的子容器,其中RectTransform很重要,四角锚点分别固定在父物体的四个角 → 完全拉伸;另外其Image也是格子图片
这里还要额外修改下数量栏的预制体设置,以便数字扩展能正确的伸缩
2、给格父容器格子预制体添加Button脚本和组件
还需要给父容器勾选射线检测机制,因为BagItem中脚本中,使用拖拽功能时,eventData.pointerEnter
会优先获取鼠标悬停位置下的所有UI中视觉最近的UI(勾选Raycast Target)
显示面板预制体调整
1、设置分辨率,且Canvas Scaler属性如下:
2、改变显示面板的轴心点位置为右上角且面板及子物体都不设置射线检测目标
还需要注意的是,要是想做到如下情况:鼠标在物品格子内的任何位置,显示面板右上角始终会出现在格子中心处,就要传递位置参数eventData.pointerEnter.transform.position
因此
eventData.pointerEnter.transform.position
就是 该 UI 元素在屏幕坐标系下的世界位置(通常就是 RectTransform 的 pivot 所在的世界坐标)。在这里
eventData.pointerEnter在BagItem脚本中,该脚本又在格子物体Prop_1物体上,所以该位置就是格子中心位置
3、需要注意的是,各类显示面板需要处于Bag位置下方,否则会出现背包栏挡住显示面板的情况
4、如果词条隐藏时,需要排版位置——att3隐藏掉,att4的词条位置出现在att3的位置
给Att物体添加组件如下:
选框Canvas是Att物体,需要将轴心点放在Canvas正上方
使用面板与选中图标预制体调整
1、调整轴心点在右上角,并且勾选该面板的Raycast Target
Raycast Target 的核心机制:
UGUI中的响应鼠标点击事件,是通过勾选RaycastTarget,来接收射线。如组件Button、Image、Text、Toggle、InputField、ScrollView等
当Unity运行时,Unity会遍历所有当前已经勾选的Raycast Target的组件,找到当前点击位置的最上层的组件来作为当前点击的响应点。
射线来源:
当鼠标/触摸发生时,Unity的
EventSystem
会从当前指针位置(鼠标位置)发射一条垂直于屏幕的射线这条射线会穿过所有Canvas层级的UI元素
检测对象:
只检测勾选了
Raycast Target
的UI元素检测范围包括所有激活的(active)且未被完全遮挡的UI元素
命中优先级:
层级深度:返回的是最顶层(视觉上最靠近屏幕)的UI元素——因为最下层的子物体最后渲染,所以视觉表现上在最顶层
渲染顺序:
同Canvas内:Hierarchy中越靠下的子物体渲染在越上层
不同Canvas间:Sort Order值越大的Canvas层级越高
2、选中图标的创建和调位
因为需要选中图标和格子大小一致,所以在Prop创建子UI类型Image后在拉到Bag最后子物体,因为选中图标需要覆盖在所有背包图标上方
2、Select图标也要打开射线检测机制
因为射线优先经过选中图标,拦截了显示面板所需要的鼠标悬停导入
OnPointerEnter位置的操作,所以显示面板就被隐藏了
结果:
它拦截了所有鼠标事件(包括悬停事件)
背包物品的显示面板虽然存在,但不再接收鼠标事件
显示面板的
OnPointerExit
不会被触发,导致它保持可见但被遮挡
背包的数据控制和视图控制
特性 BagData.dct
(数据层)bag_item
(视图层)本质 游戏数据状态存储 UI视觉表现管理 数据类型 BagEntity
(业务对象)GameObject
(场景对象)持久性 永久存储 临时存在(UI打开时) 内容 物品ID、数量、属性等 3D模型、图标、文本组件等 操作方式 逻辑操作(添加/移除/查询) 渲染操作(创建/销毁/更新) 依赖关系 独立于UI系统 完全依赖UI系统 使用场景 所有背包逻辑处理 背包界面显示时
代码流程——背包数据初始化且显示背包物品
1、背包UI初始化获取(所有背包相关操作前提都是统一进行这个初始化)
//Main类中每帧检测是否按下G
void Update(){GameTime.Update();ViewManager.Instance.Update();if (Input.GetKeyDown(KeyCode.G)){BagData.Instance.Add(IntEx.Range(1001,1010),IntEx.Range(1,30),null);BagData.Instance.Add(IntEx.Range(2001, 2057), IntEx.Range(1, 10), null);BagData.Instance.Add(IntEx.Range(3001, 3013), IntEx.Range(1, 60), null);BagViewController.Instance.Open();//打开Bag面板并进行初始化BagViewController.Instance.view.Show(-1);}}
public void Open(){if (view.gameObject == null){//实例化var go = ResourcesManager.Instance.Instantiate<GameObject>(view.resPath);Debug.Log("view.resPath" + view.resPath);GameObject.DontDestroyOnLoad(go);view.gameObject = go;view.gameObject.name = view.gameObject.name.Split('(')[0];//删去Clone字符后缀view.transform = go.transform;view.canvas = go.GetComponent<Canvas>();view.cs = go.GetComponent<CanvasScaler>();view.Awake();//对生成的go实例进行数据初始化SetActive(true);view.Start();}else{//打开SetActive(true);}}
通过路径获取生成实例中子物体的组件:
public class BagView : View
{string item_path = "UI/Item/Prop_Item";string content = "Bag/Scroll View/Viewport/Content/Prop_";Transform content_parent;Transform Prop;Transform Material;Transform Equip;Text prop_name, prop_text, prop_act;Text prop_info1, prop_info2, prop_info3;public override void Awake(){base.Awake();content_parent = transform.Find("Bag/Scroll View/Viewport/Content");GetComponent<Button>("Bag/Menu/All").onClick.AddListener(ShowAll);GetComponent<Button>("Bag/Menu/Prop").onClick.AddListener(ShowProp);GetComponent<Button>("Bag/Menu/Equip").onClick.AddListener(ShowEquip);GetComponent<Button>("Bag/Menu/Material").onClick.AddListener(ShowMaterial);Prop = transform.Find("Prop");Material = transform.Find("Material");Equip = transform.Find("Equip");prop_name = GetComponent<Text>("Prop/Name");prop_text = GetComponent<Text>("Prop/Info");prop_act = GetComponent<Text>("Prop/Act");prop_info1 = GetComponent<Text>("Prop/Att/Info1");prop_info2 = GetComponent<Text>("Prop/Att/Info2");prop_info3 = GetComponent<Text>("Prop/Att/Info3");}
2、通过show方法显示UI,通过Add方法实现物品交互——对应物体的生成
1、首先需要注意Main方法中的Add方法,此方法将对应物体的数据填入到合适的格子中(背包物体数据层的放置)
BagData——Add方法https://blog.csdn.net/2303_80204192/article/details/149864883#t34
2、再用show方法逐一显示背包中的物体视图
BagView——show方法https://blog.csdn.net/2303_80204192/article/details/149864883#t54
show方法特性如下:默认状态为全部打开(type=-1)
这里我们就以全部打开为特性:根据背包数据表bag_dct,遍历生成全部物品;同时也会根据BagCanvas预制体路径找到“Content”——找到所有子物体(格子)并激活。
3、CreateItem方法生成预制体
public GameObject CreateItem(BagEntity bagEntity){var obj = ResourcesManager.Instance.CreatePropItem(bagEntity.entity.id, bagEntity.count);return obj;}
至于如何正确的生成物体:
该方法是通过路径找到预制体模板并生成
ResourceManage——CreateBag_Item方法 https://blog.csdn.net/2303_80204192/article/details/149864883#t80
根据传入的id和count——找到对应配置表数据——生成对应icon,Prop_item新名字以及物品数量
ResourceManage——CreatePropItem方法https://blog.csdn.net/2303_80204192/article/details/149864883#t79
这里额外要注意装备部位图片加载:
internal Sprite LoadIcon(PropEntity entity) {……// type == 1:装备,路径由部位决定,使用 EQ_Icon 根目录else if (entity.type == 1){return Load<Sprite>(GameDefine.GetEQ_Icon(entity.part) + entity.icon);}return null; }
加载装备图片还需要根据部位数组正确提取——“装备类型字符串”作为后缀
public static void Init(){part_dct[1] = "Helmet/";part_dct[2] = "Cloth/";part_dct[3] = "Capes/";part_dct[4] = "Pants/";part_dct[5] = "Boots/";part_dct[6] = "Weapon/";part_dct[7] = "Earrings/";part_dct[8] = "Necklaces/";part_dct[9] = "Rings/";part_dct[10] = "Belt/";}public static string GetEQ_Icon(int part){return eq_root + part_dct[part];}
代码流程——交换物品与移动物品到空闲位置
1、BagItem方法使每个格子拥有自己ID
BagItem——Awake方法https://blog.csdn.net/2303_80204192/article/details/149864883#t22
2、三种拖拽状态的方法
另外两种方法就在该链接后面:
BagItem——OnBeginDrag方法https://blog.csdn.net/2303_80204192/article/details/149864883#t23
发生交换时数据层的作用(格子之间的数据):
BagData——Modify_Grild方法https://blog.csdn.net/2303_80204192/article/details/149864883#t29
代码流程——物品分类
1、在BagData中,Awake方法初始化获取物品分类相关组件,并对对应路径上的按钮组件添加监听方法
public override void Awake(){base.Awake();content_parent = transform.Find("Bag/Scroll View/Viewport/Content");GetComponent<Button>("Bag/Menu/All").onClick.AddListener(ShowAll);GetComponent<Button>("Bag/Menu/Prop").onClick.AddListener(ShowProp);GetComponent<Button>("Bag/Menu/Equip").onClick.AddListener(ShowEquip);GetComponent<Button>("Bag/Menu/Material").onClick.AddListener(ShowMaterial);}
2、根据不同按钮,调用不同方法,进行不同类型物品展示
private void ShowMaterial(){Show(2);}private void ShowEquip(){Show(1);}private void ShowProp(){Show(0);}private void ShowAll(){Show(-1);}
3、以show(2)举例如何控制show方法显示单一类型物品
1.show(2)的前置状态
select?.gameObject.SetActive(false); // 隐藏选中状态
ClearGrild(); // 清空物品 UI 缓存
crn_type = 2; // 标记当前为材料类型
2.物品层控制(只创建Type=2的物品层)
foreach (var item in data)
{if (type == -1 || item.Value.entity.type == type) // type=2{// 只创建类型为2(材料)的物品var obj = CreateItem(item.Value);// ...挂载到对应格子...bag_item[item.Value.grild_id] = obj; // 缓存}
}
CreateItem方法会一直沿用到IconLoad方法——该方法会按照类型加载图像给物品预制体
3.格子层控制
//content_parent = transform.Find("Bag/Scroll View/Viewport/Content");//如果是单个类型,也是先遍历所有格子,获取格子中对应物体信息,类型匹配的格子激活,不匹配的隐去for (int i = 0; i < content_parent.childCount; i++) {var obj = content_parent.GetChild(i);var grildInfo = BagData.Instance.Get(i);if (grildInfo != null)//如果当前格子数据不为空{obj.gameObject.SetActive(grildInfo.entity.type == type);//type==2时的格子在此全被激活启用,就算有数据的type!=2也不会激活} else {obj.gameObject.SetActive(false);}
点击物品按键后:
4.还需要额外注意的是:隐藏中间格子栏,后续格子栏会自动补缺口,这是因为Content物体中的如下组件:
代码流程——显示面板
1、BagItem——当鼠标指针每次进入该物体的 Raycast Target 区域时调用一次该方法
public void OnPointerEnter(PointerEventData eventData){BagViewController.Instance.OnPointerEnter_Grild(grild_id, eventData);BagViewController.Instance.view.SelectObj = eventData.pointerEnter.gameObject;}
通过不同类型的物品调用不同类型面板显示:
internal void OnPointerEnter_Grild(int grild_id, PointerEventData eventData){var e = BagData.Instance.Get(grild_id);if (e != null){if (e.entity.type == 0)view.ShowPropInfo(grild_id, eventData.pointerEnter.transform.position);else if (e.entity.type == 1)view.ShowEquipInfo(grild_id, eventData.pointerEnter.transform.position);else if (e.entity.type == 2)view.ShowMaterialInfo(grild_id, eventData.pointerEnter.transform.position);} }
2、显示面板设置文字内容
该方法用于确定显示面板位置和显示面板部分内容
BagView——ShowPropInfo方法https://blog.csdn.net/2303_80204192/article/details/149864883#t36 以下方法是对词条内容赋值
SetItem(bagEntity.entity.recover1, prop_info1);
SetItem(bagEntity.entity.recover2, prop_info2);
SetItem(bagEntity.entity.recover3, prop_info3);
传递的recover词条数组如图中所示:
再用如下方法确定显示词条类型:
GameDefine——GetAttTexthttps://blog.csdn.net/2303_80204192/article/details/149864883#t49
private void SetItem(int[] config, Text text){if (config != null)//如果配置表不为空{text.gameObject.SetActive(true);//激活传输来的含有文本组件的物体SetText(text, GameDefine.GetAttText(config[0], config[1], config[2]));//对传递来的数组成员依次赋值}
根据代码得出词条文本内容如下:
3、显示其他类型面板也是同样原理
BagView——ShowEquipInfo方法https://blog.csdn.net/2303_80204192/article/details/149864883#t37
代码流程——整理背包
1、初始化整理按钮组件+注册点击事件
sort = GetComponent<Button>("Bag/Sort");
sort.onClick.AddListener(SortByType);
2、整理背包
//Bagview方法中
public void SortByType(){BagData.Instance.SortByType();//在数据层整理Show(crn_type);//在视觉层整理——默认传入type=-1}
需要注意:因为dct是静态单例容器——所以任何脚本中访问的都是一个dct,此时dct已经在SortByType重新创建了一个新的背包。
在Show方法中,先清除旧的背包布局,再传入新的背包布局显示——另外,也可以走背包中的过滤算法。(当点击“装备”按钮只显示全部装备类型时)
//Bagview方法中
public void Show(int type=-1){select.gameObject.SetActive(false);ClearGrild();crn_type = type;//根据传入type赋值到crn_typevar data = BagData.Instance.dct;foreach (var item in data){if (type == -1 || item.Value.entity.type == type){//创建这些物品 var obj = CreateItem(item.Value);var parent = content + item.Key;var p = transform.Find(parent);obj.transform.SetParent(p, false);bag_item[item.Value.grild_id] = obj;}}}
代码流程——显示物品选中
1、交由全局系统控制(EventSystem实例)
public class Main : MonoBehaviour
{public void Awake(){GameSystem.Instance.EventSystem = EventSystem.current;}
}
核心操作:将当前场景激活的
EventSystem
组件(Unity的UI事件系统)赋值给GameSystem
单例的EventSystem
属性
全局访问事件系统
将Unity场景中的EventSystem
(处理UI交互的核心组件)存储到全局单例中,使得任何代码都能通过GameSystem.Instance.EventSystem
访问它,无需反复查找。解耦与集中管理
避免在多个脚本中重复使用
EventSystem.current
(效率较低)。作为游戏系统的中央枢纽,未来可扩展其他全局组件(如音频管理、场景管理)。
public class GameSystem
{static GameSystem instance = new GameSystem(); // 静态单例实例public static GameSystem Instance => instance; // 单例访问入口……public EventSystem EventSystem; // 存储Unity事件系统的引用
}
单例模式:通过静态字段
instance
确保全局只有一个GameSystem
实例。
Instance
属性:其他类可通过GameSystem.Instance
访问单例。
EventSystem
字段:用于保存对Unity事件系统的引用
2、选中图标的使用
select_grild_id
设置一个默认值-1
,表示“当前没有选中任何格子”。
public override void Awake(){base.Awake();………………select = GetComponent<Transform>("Bag/Select");//获取选择按钮 // 给所有物品格子注册点击事件(这是创建使用/丢弃面板的注册方式)for (int i = 0; i < content_parent.childCount; i++){var btn = content_parent.GetChild(i).GetComponent<Button>();btn.onClick.AddListener(ChildOnClick);}cmd = transform.Find("CMD");GetComponent<Button>("CMD/Discard").onClick.AddListener(RemoveGrildItem);}
BagView——GrildOnClickhttps://blog.csdn.net/2303_80204192/article/details/149864883#t59
代码流程——使用面板的使用
实现功能:右键选中物体会出现选中图标与使用面板,并且点击丢弃会使物品消失
1、对使用面板的按键进行封装
public class UInput
{……internal static bool GetMouseButtonDown_1(){return Input.GetMouseButtonDown(1);}
}
2、使用面板的触发
1.每帧检测调用OnMouse方法
public override void Update(){base.Update();OnMouseButtonDown();}
2.选中SelectObj物体与鼠标悬停绑定
public void OnPointerEnter(PointerEventData eventData){BagViewController.Instance.OnPointerEnter_Grild(grild_id, eventData);//当前的鼠标悬停处的格子交由SelectObj变量存储BagViewController.Instance.view.SelectObj = eventData.pointerEnter.gameObject;}
3.右键触发逻辑:选中图标与使用面板的显隐
public void OnMouseButtonDown(){if (UInput.GetMouseButtonDown_1()){if (SelectObj != null&& SelectObj.name.StartsWith("Prop_"))//如果选中格子存在且以"Prop_"字段为首{select_grild_id = int.Parse(SelectObj.name.Split('_')[1]);//分离字段取数字部分var entity = BagData.Instance.Get(select_grild_id);//获取当前格子中的物体if (entity != null){select.gameObject.SetActive(true);//激活选中图标select.transform.position = SelectObj.transform.position;//将当前选中格子位置赋予选中图标(即将选中图标放置在当前格子位置中)cmd.gameObject.SetActive(true);//激活使用面板cmd.transform.position = SelectObj.transform.position;//使用面板放置在选中格子中}else//选中格子不存在{select.gameObject.SetActive(false);//选中图标隐藏cmd.gameObject.SetActive(false);//使用面板隐藏}}}}
4.“丢弃”按钮的使用
//Awake方法
cmd = transform.Find("CMD");
GetComponent<Button>("CMD/Discard").onClick.AddListener(RemoveGrildItem);
BagView——RemoveGrildItem方法https://blog.csdn.net/2303_80204192/article/details/149864883#t42
格子拖拽交互逻辑
因为背包栏,装备栏,快捷栏上的格子Object上都挂载着Bagitem脚本,所以其BagItem中的拖拽相关接口这三个都能调用——这三个栏的格子即可以单独实现开始拖拽到结束拖拽的所有逻辑
(之前误以为只是背包栏格子可以拖拽,其他的只是用来辅助背包栏格子的逻辑,实际上背包栏格子在此也只是拖拽逻辑中的一个分支逻辑)。
格子的初始化
public void Awake()//格子的初始化{if (this.CompareTag("Equip")){type = 1;}else if (this.CompareTag("Quick")){type = 2;}else{type = 0;}grild_id = int.Parse(transform.gameObject.name.Split("_")[1]);}
根据格子的Tag标签来确定格子类型:
根据格子名称确定当前格子的序列号grild_id
飞回原位的视觉逻辑
Transform org;
该org变量属性是场景中物体实例及其子物体数据,性质是临时存储这些数据
该变量在开始拖拽UI时生成,创建org的同时即时隐去:这意味着拖拽过程中始终有个隐藏的org实例等待处理——直到拖拽结束后对此进行处理。
public void OnBeginDrag(PointerEventData eventData){// 创建一个物品if (transform.childCount > 0){var p = transform.GetChild(0);org = p;p.gameObject.SetActive(false);Debug.Log("P.name" + p.name);int id = int.Parse(p.name);int count = int.Parse(p.transform.Find("count/key_text").GetComponent<Text>().text); …………}}
这就是飞回原位的视觉处理:
public void OnEndDrag(PointerEventData eventData){//交换位置: 有可能交换的位置 物品是空 或者不是空//Debug.LogError("结束拖拽:" + eventData.pointerEnter.gameObject.name);if (temp != null){…………if (type == 0){if (eventData.pointerEnter.gameObject.CompareTag("Equip")){…………if (data != null){if (data.entity.part == part_type){…………}else{org.gameObject.SetActive(true);//飞回原位}}}}
发现拖拽情况不匹配,在视觉上激活隐藏的org,视觉表现上就是“飞回原位”
org变量的数据逻辑
这里创建了一个Transform类型的P变量,用于存储场景里已存在的那个子物体的 Transform 组件的地址——相当于“指针”的作用。
Transform org
只是“记住”了场景里某个真实物体的位置,所有操作都在直接改那个真实物体
Transform org;public void OnBeginDrag(PointerEventData eventData){// 创建一个物品if (transform.childCount > 0) //这里transform是格子的位置{var p = transform.GetChild(0);org = p;p.gameObject.SetActive(false);Debug.Log("P.name" + p.name);int id = int.Parse(p.name);int count = int.Parse(p.transform.Find("count/key_text").GetComponent<Text>().text);……………………}}
Prop_item所生成的2027预制体,在Show方法初创调用时就出现了,详情请看背包数据显示——背包数据初始化。
对org的操作其实就是对2027物品(由拖拽点决定是哪个物体)的操作,放置为子物体也是对其2027物体本身进行操作
拖拽的视觉逻辑
这就是交由Temp来控制的变量:
GameObject temp;
开始拖拽时:
创建一个对应的id和数量的预制品物体赋值于Temp,拖拽图标时代码与流程如下
public void OnBeginDrag(PointerEventData eventData){// 创建一个物品if (transform.childCount > 0){………………temp = ResourcesManager.Instance.CreatePropItem(id, count);temp.transform.SetParent(BagViewController.Instance.view.transform, false);//放置在根目录上,拖拽时该图像UI在视觉最顶层var rt = temp.GetComponent<RectTransform>();//给拖拽时的图标设置大小rt.anchorMin = new Vector2(0.5f, 0.5f);rt.anchorMax = new Vector2(0.5f, 0.5f);rt.sizeDelta = new Vector2(120f, 120f);}}
拖拽UI的消失:结束拖拽时,无论如何,直接摧毁temp
public void OnEndDrag(PointerEventData eventData){//交换位置: 有可能交换的位置 物品是空 或者不是空//Debug.LogError("结束拖拽:" + eventData.pointerEnter.gameObject.name);if (temp != null){temp.gameObject.SetActive(false);ResourcesManager.Instance.Destroy(temp);//感觉只需要第二步就够了temp = null;}
拖拽交换位置的数据逻辑
物品数据情况如下:
public class BagEntity
{public int id;//这个物品的Idpublic int grild_id;//在任何栏中的格子IDpublic int count;//当前这个物品的数量public PropEntity entity;//这个物品的配置表相关数据(属性/可叠加性等等)
}
接下来我们要着重讲解这个grild_id:
当物品处于装备栏时:通过BagItem获取到各个栏中格子ID,通过BagData中的Add方法添加到各个格子栏中物体上。
当装备栏物体交换到背包栏时:public void AddEquipToBag(BagEntity e, int grild){//当前实体的grild_ID替换为传入的grilde.grild_id = grild;//背包中新grild用于存放该装备数据实体dct[e.grild_id] = e;}
当快捷栏物体交换到背包栏时
public void AddQuickToBag(BagEntity e, int grild){e.grild_id = grild;//传入的背包格子id赋值给该物体grilddct[e.grild_id] = e;//将该格子用于存储该物品e}
交换物体发生在两栏之间——grild_id也要跟着替换,grild_id一直是所有栏中格子找到该物体的索引号。
装备布置
装备栏预制体的设置
1、规整布置预制体容器如下,并且每个子容器的局部坐标归零,并且Item子物体全加上Tag“Equip”
2、如下设置装备栏中的Info和Item,并且Info只有取消勾选RayCast target,且Item上挂载脚本
运行时打开SystemEvent可以看见是否能正常搜寻到鼠标悬停物体(含BagItem脚本的):
也是通过此方法意识到需要取消勾选RayCast Target。
装备栏的数据控制和视图控制
equip
管“看得见的 UI 节点”,equip_dct
管“算得出的数据实体”
字典 存什么 给谁用 生命周期 主要目的 Dictionary<int, GameObject> equip
每个装备格子的 GameObject(图标、背景、特效…) UI 层(渲染、拖拽、动画) 随场景/界面打开而创建、关闭而销毁 让界面能“看见”装备 Dictionary<int, BagEntity> equip_dct
每个格子的 BagEntity(ID、属性、数量…) 逻辑层(背包系统、战斗系统、存档) 随角色数据持久化 让逻辑能“算得动”装备 正因如此,equip字典在BagView脚本中,
equip_dct字典在BagData中:各自都有自己的函数方法存储到对应脚本中
代码流程——装备的安装和卸下
1、通过Main,创建装备相关内容——Add方法添加装备数据,Show方法添加装备UI
2、在BagItem中根据拖拽相关功能
与背包中互换相同:
- 变量org的位置迁移与激活是装备安装和交换的视觉层表现(可能还要将这个物体关系放入Equip表格进行控制)
- BagData.Instance.Switch_Equip方法是数据层的修改
- 例如将装备相关属性迁移到新位置,使显示面板能正确显示
至于拖拽具体实现功能——请看格子拖拽交互逻辑相关内容
if (type == 0)
{if (eventData.pointerEnter.gameObject.CompareTag("Equip")){// 背包拖向装备栏的逻辑var n = eventData.pointerEnter.gameObject.name;if (n.StartsWith("Item_")){var part_type = int.Parse(n.Split("_")[1]);var data = BagData.Instance.Get(grild_id);if (data != null){if (data.entity.part == part_type){BagData.Instance.Switch_Equip(grild_id);if (eventData.pointerEnter.gameObject.transform.childCount == 0){// 目标格子为空,直接放入org.transform.SetParent(eventData.pointerEnter.gameObject.transform, false);org.gameObject.SetActive(true);BagViewController.Instance.RemoveBagItem(grild_id);org.transform.Find("count").gameObject.SetActive(false);}else{// 目标格子已有装备,交换org.transform.SetParent(eventData.pointerEnter.gameObject.transform, false);org.gameObject.SetActive(true);org.transform.Find("count").gameObject.SetActive(false);var c = eventData.pointerEnter.gameObject.transform.GetChild(0);c.SetParent(this.transform, false);c.transform.Find("count").gameObject.SetActive(true);BagViewController.Instance.RemoveBagItem(grild_id);BagViewController.Instance.AddEquipItem(part_type, org.gameObject);BagViewController.Instance.AddBagItem(grild_id, c.gameObject);}}else{// 部位不匹配,恢复原位org.gameObject.SetActive(true);}}}}
}
疑惑点:为什么只有交换的时候要将装备关系存储在equip表格中(AddEquipItem方法实现),而首次安装装备不需要
BagViewController.Instance.RemoveBagItem(grild_id); BagViewController.Instance.AddEquipItem(part_type, org.gameObject); BagViewController.Instance.AddBagItem(grild_id, c.gameObject);
与此同时,背包中物体也发生了变化,所以背包中的视图表bag_item表也要添加新物体(AddBagItem)实现
代码流程——装备栏上装备显示面板
1、OnEndDrag方法已经将新的装备数据和物体数据成功迁移到正确位置
2、OnPointerEnter方法使鼠标接触到合适格子时,传入对应数据(格子序号,当前鼠标悬停相关信息,格子类型)
public void OnPointerEnter(PointerEventData eventData){if (transform.childCount == 0)//格子中是否有物体{……………………}else//鼠标悬停在三种类型的格子上——0 背包上的格子 1 装备栏格子 2 快捷栏格子{if (type == 0){………………}else if (type == 1){BagViewController.Instance.OnPointerEnter_Grild(grild_id, eventData, type);BagViewController.Instance.view.SelectObj = null;}else if (type == 2){………………}}}
并且在BagViewController中判别当前物品类型,物品类型又调用不同的面板类型
格子序号,当前鼠标悬停相关信息——面板出现位置
格子类型——物品类型——面板类型
internal void OnPointerEnter_Grild(int grild_id, PointerEventData eventData, int type)
{BagEntity e = null;// 根据 type 从对应的数据源取出当前格子的物品实体if (type == 0) e = BagData.Instance.Get(grild_id); // 背包else if (type == 1) e = BagData.Instance.Get_Equip(grild_id); // 装备栏else if (type == 2) e = BagData.Instance.Get_Quick(grild_id); // 快捷栏if (e != null){// 根据物品的类型,在鼠标悬停位置弹出对应的信息面板if (e.entity.type == 0){view.ShowPropInfo(grild_id, eventData.pointerEnter.transform.position, e); // 消耗品}else if (e.entity.type == 1){view.ShowEquipInfo(grild_id, eventData.pointerEnter.transform.position, e); // 装备}else if (e.entity.type == 2){view.ShowMaterialInfo(grild_id, eventData.pointerEnter.transform.position, e); // 材料}}
}
显示面板相关流程:直接查阅——背包数据显示——面板显示
4、具体的装备面板展示:
快捷栏布置
快捷栏预制体布置
1、快捷栏预制体——格子预制体Prop都要勾选RayCast Target,挂载BagItem脚本,以及设置对应Tag
代码流程——快捷栏上放置/卸下物品(与交换)
1、通过Main,创建背包物品相关内容——Add方法添加物品数据,Show方法添加物品UI
2、背包物品向快捷栏拖拽
调用OnEndDrag
传入鼠标悬停信息——鼠标悬停处的物体(快捷栏格子)——格子名(Prop_1)——>( 1)——当前格子序列号
传入鼠标悬停信息——鼠标悬停处的物体(快捷栏格子)——位置——视觉层的父容器
public void OnEndDrag(PointerEventData eventData){//交换位置: 有可能交换的位置 物品是空 或者不是空//Debug.LogError("结束拖拽:" + eventData.pointerEnter.gameObject.name);if (temp != null){temp.gameObject.SetActive(false);ResourcesManager.Instance.Destroy(temp);temp = null;if (type == 0){…………else if (eventData.pointerEnter.gameObject.CompareTag("Quick")){//背包到快捷栏var n = eventData.pointerEnter.gameObject.name;if (n.StartsWith("Prop_")){//取得快捷栏格子序列var target_quick = int.Parse(n.Split("_")[1]);//获取背包中对应格子的物体数据var data = BagData.Instance.Get(grild_id);if (data != null){if (data.entity.type == 0)//当前物体是“物品类型”{BagData.Instance.Switch_Quick(grild_id, target_quick);//数据层在此得到改变if (eventData.pointerEnter.gameObject.transform.childCount == 0){org.transform.SetParent(eventData.pointerEnter.gameObject.transform, false);org.gameObject.SetActive(true);BagViewController.Instance.RemoveBagItem(grild_id);org.transform.Find("count").gameObject.SetActive(true);BagViewController.Instance.AddQuickItem(target_quick, org.gameObject);Debug.LogWarning("这里我额外使用AddQuickItem方法,但是老师没有,只有添加了该方法,show方法调用快捷栏才不会额外生成一个物体实例");}else//快捷栏上有物体,则发生交换{//先设置到快捷栏org.transform.SetParent(eventData.pointerEnter.gameObject.transform, false);org.gameObject.SetActive(true);org.transform.Find("count").gameObject.SetActive(true);//交换var c = eventData.pointerEnter.gameObject.transform.GetChild(0);c.SetParent(this.transform, false);c.transform.Find("count").gameObject.SetActive(true);BagViewController.Instance.RemoveBagItem(grild_id);BagViewController.Instance.AddQuickItem(target_quick, org.gameObject);BagViewController.Instance.AddBagItem(grild_id, c.gameObject);}}else{org.gameObject.SetActive(true);}}}
Switch_Quick:把背包里指定格子的物品与快捷栏指定格子互换;如果快捷栏格子空,则直接把背包物品移进去——bagEntity的迁移(数据层)
3、快捷栏向背包栏拖拽
传入鼠标悬停信息——鼠标悬停处的物体(快捷栏格子)——格子名(Prop_1)——>( 1)——当前格子序列号
传入鼠标悬停信息——鼠标悬停处的物体(快捷栏格子)——位置——视觉层的父容器
Switch_Quick方法只是数据层那些方法(AddQuickToBag)的集合使用,而且只能使用在一个场景(背包栏物品拖拽到快捷栏),但是四个场景都需要数据层的迁移。
所以这里就使用最基础的AddQuickToBag等方法反复使用完成数据层相关逻辑
else if (type == 2) // 快捷栏 → 背包
{var n = eventData.pointerEnter.gameObject.name;if (n.StartsWith("Prop_")){var quick_entity = BagData.Instance.Get_Quick(grild_id); // 被拖拽的快捷栏实体var bag_grild_id = int.Parse(n.Split("_")[1]); // 目标背包格子号var bag_entity = BagData.Instance.Get(bag_grild_id); // 目标格子里的物品if (bag_entity == null){/* 目标背包格子为空:直接把快捷栏物品移进来 */// 1. 数据层:移除快捷栏物品BagData.Instance.Remove_Quick(grild_id);BagViewController.Instance.RemoveQuickItem(grild_id);// 2. UI 层:把快捷栏 GameObject 移到背包格子org.transform.SetParent(eventData.pointerEnter.gameObject.transform, false);org.gameObject.SetActive(true);// 3. 数据层:记录物品已进入背包,也将视图交由bag_item视图表进行控制BagData.Instance.AddQuickToBag(quick_entity, bag_grild_id);BagViewController.Instance.AddBagItem(bag_grild_id, org.gameObject);}else//目标背包格子不为空{if (bag_entity.entity.type == 0){/* 目标格子也是物品:互换位置 */// 1. 数据层:移除背包物品BagData.Instance.Remove_Grild_ID(bag_grild_id, bag_entity.count, null);BagViewController.Instance.RemoveBagItem(bag_grild_id);// 2. 数据层:移除快捷栏物品BagData.Instance.Remove_Quick(grild_id);BagViewController.Instance.RemoveQuickItem(grild_id);// 3. UI 层:把快捷栏 GameObject 移到背包格子org.transform.SetParent(eventData.pointerEnter.gameObject.transform, false);org.gameObject.SetActive(true);org.transform.Find("count").gameObject.SetActive(true);// 4. 数据层:记录快捷栏物品已放入背包BagData.Instance.AddQuickToBag(quick_entity, bag_grild_id);BagViewController.Instance.AddBagItem(bag_grild_id, org.gameObject);// 5. UI 层:把背包里的物品 GameObject 移到快捷栏var c = eventData.pointerEnter.gameObject.transform.GetChild(0);c.transform.SetParent(this.transform, false);c.gameObject.SetActive(true);c.transform.Find("count").gameObject.SetActive(true);// 6. 数据层:记录背包物品已进入快捷栏BagData.Instance.Add_Quick(bag_entity.entity.part, bag_entity);BagViewController.Instance.AddQuickItem(grild_id, c.gameObject);}else{/* 目标格子里的物品无法与快捷栏物品互换:把快捷栏物品放到背包空位 */if (BagData.Instance.IsMax()){// 背包已满:飞回原位org.gameObject.SetActive(true);}else{// 1. 数据层:移除快捷栏物品BagData.Instance.Remove_Quick(grild_id);BagViewController.Instance.RemoveQuickItem(grild_id);// 2. 数据层:找下一个空背包格子var grild_next = BagData.Instance.GetGrildId_Next();BagData.Instance.AddQuickToBag(quick_entity, grild_next);// 3. UI 层:把快捷栏 GameObject 放入该空格子BagViewController.Instance.AddBagItem(grild_next, org.gameObject);var parent = BagViewController.Instance.GetGrild(grild_next);org.transform.SetParent(parent, false);org.gameObject.SetActive(true);}}}}else{// 未拖到背包格子:飞回原位org.gameObject.SetActive(true);}
}org = null;
讲orgTransform指针设为空——即取消物体地址保存关系(方便下一个物体进行拖拽)
代码流程——快捷栏上物品的叠加
1、初始化与背包物体向快捷栏拖拽请看代码流程——快捷栏上放置/卸下物品
else if (eventData.pointerEnter.gameObject.CompareTag("Quick")) {//背包到快捷栏var n = eventData.pointerEnter.gameObject.name;if (n.StartsWith("Prop_")) {//1234var target_quick = int.Parse(n.Split("_")[1]);//获取背包数据var data = BagData.Instance.Get(grild_id);if (data != null) {if (data.entity.type == 0) {BagData.Instance.Switch_Quick(grild_id, target_quick);if (eventData.pointerEnter.gameObject.transform.childCount == 0) {org.transform.SetParent(eventData.pointerEnter.gameObject.transform, false);org.gameObject.SetActive(true);BagViewController.Instance.RemoveBagItem(grild_id);org.transform.Find("count").gameObject.SetActive(true);BagViewController.Instance.AddQuickItem(target_quick, org.gameObject);Debug.LogWarning("这里我额外使用AddQuickItem方法,但是老师没有,只有添加了该方法,show方法调用快捷栏才不会额外生成一个物体实例");
AddQuickItem:主要是有这个方法将物体关系添加到quick视图表中
2、此时再次按G——同时调用Add方法和Show方法添加物品数量并展示
BagData.Instance.Add(IntEx.Range(1001, 1010), IntEx.Range(1, 30), null); BagData.Instance.Add(IntEx.Range(2001, 2057), IntEx.Range(1, 10), null);BagData.Instance.Add(IntEx.Range(3001, 3013), IntEx.Range(1, 60), null);BagViewController.Instance.Open(); BagViewController.Instance.view.Show(-1);
Main方法是先调用Add方法,在调用show方法——即先更新数据层,再更新视觉层
quick_dct数据层添加数量
public void Add(int id, int count, Action<BagEntity> callback, Action<int> error = null){………………//快捷栏是否存在相同的物品 如果存在 那直接叠加它的数量foreach (var item in quick_dct){if (item.Value.entity.id == id){item.Value.count += count;callback?.Invoke(item.Value);return;}}
quick视觉层先清除之前的旧物体,再生成新物体完成更新
疑惑点0821:虽然我不明白为什么不直接更新数量的文字
//show方法快捷栏相关 ClearGrild_Quick();//把“快捷栏”里所有格子的 GameObject 全部销毁,并清空快捷栏字典foreach (var item in BagData.Instance.quick_dct){//创建这些物品 Debug.Log("创建快捷物体");var obj = CreateItem(item.Value);var parent = quick_root + item.Key;var p = transform.Find(parent);obj.transform.SetParent(p, false);quick[item.Value.grild_id] = obj;}}
铸造功能实现
铸造页面预制体设置
1、将铸造列表的子容器,添加为预制体
2、铸造页面ForgeCanvas如下:
提示面板预制体设置
1、查看提示文本是否勾选富文本
代码流程——锻造系统显示与合成
1、通过G键给背包中生成物品,再按下J键打开铸造面板
2、铸造面板初始化,使得UI组件和子容器物体交由该脚本代码控制,并且注册按钮点击事件
public override void Awake(){base.Awake();config_content = transform.Find("Drawing/Viewport/Content");forge_info = GetComponent<Text>("Forge_Tips");target_prop = transform.Find("Prop");Mat1_Text = GetComponent<Text>("Mat1_Text");………………prop1 = transform.Find("Material/Prop_1");………………ShowConfigs();GetComponent<Button>("ForgeBtn").onClick.AddListener(ForgeOnClick);}
3、显示锻造面板
锻造面板的列表生成:
配置表的id(合成物体的id),配置表的合成材料——列表选中图标及对应合成材料选项
ForgeView——showconfigs
锻造面板的列表高亮控制及合成材料面板:
UpdateMatInfo(target_prop, p, 1, Mat4_Text, false);public void UpdateMatInfo(Transform parent, PropEntity p, int count, Text name, bool check_count = true)
target_prop——合成材料id——作为父级容器
p——该合成材料的其他数据信息如名字——用于赋值给文本显示
1——count——数量(需要的/已有的)
Mat4_Text——name——选框格子中的物品名字
check_count:可选,默认 true——是否需要自定义处理文本(如修改颜色)
ForgeView——UpdateMat列表高亮控制和材料显示控制
ForgeView——UpdateMatInfo更新材料的具体方法
代码流程——锻造合成及提示面板
1、注册点击合成按钮的事件
public override void Awake(){base.Awake();…………GetComponent<Button>("ForgeBtn").onClick.AddListener(ForgeOnClick);}
2、点击触发方法并显示提示面板
ForgeView——ForgeOnClick方法(锻造触发)
1.该方法首先查找几种合成材料是否足够,如若不够,则会记录在dct中(够的话也要检测一遍,只是dct中无值),然后弹出提示面板:TipsViewController——Show方法
2.如果材料足够,则扣除相应数量和将合成物品传入背包中,下列方法是扣除相应数量的物体:
Remove_Prop_Id方法(扣除相应数量的物品)
成功合成后也会显示提示面板
3、提示面板会出现确认,取消两个选项,都是用于结束回调方法的使用
洗练功能实现
洗练面板预制体设置
1、洗练词条预制体统一命名
2、背包格子预制体设置如下:
统一命名方式,并且添加“Button”组件,点击格子相当于点击了按钮触发事件
3、选中图标设置
同样是在Prop格子物体处新建UI——Image,使之image图像与格子框大小一致,再拖拽出来处于同级,平时默认状态下处于隐身状态
代码流程——洗练系统的运行
1、通过G键给背包中生成物品,再按下K键打开洗练面板,这里同时激活了Purify类中的OnEnable方法和Awake方法
PurifyView——Awake
public override void OnEnable(){base.OnEnable();ShowEquip();}
OnEnable方法是主要功能是调用ShowEquip()方法:
PurifyView——OnEnable方法
2、点击装备进行选取,然后再点击洗练按键
由Awake中对装备选取进行注册事件
btn.onClick.AddListener(() =>{grild_id = temp;UpdateAtt_Crn();});
点击装备触发属性文本更新和选中图标激活:PurifyView——UpdateAtt_Crn
3、点击“洗练”按钮
1.修改属性并且将导航栏上扣除对应金钱
PurifyView——PurifyOnClick
2.对应金钱扣除:
SetMenoy方法只是用来传输money的数字来作为文本显示——数值在PurifyOnClick方法改变已存在全局变量money中
public void SetMenoy(){if (IsOpen()){view.SetMenoy();}}public void SetMenoy(){SetText(money, BagData.Instance.money.ToString());}
导航面板的交互实现
导航面板预制体设置
1、为了避免UI遮挡时,按键重合,所以缩小按键范围,利用射线机制——Bag不勾选,而Tips勾选
代码流程——导航面板的运行
1、导航面板相关数据初始化
加入bool open_nav变量参数,用于控制当前类型面板是否需要打开导航面板
public class ViewController<T, V> where T : new() where V : View, new()
{………………public void Init(string resPath, bool isAddUpdateListerner = false, bool stopAI_OnOpen = false, bool open_nav = false){view = new V();view.Init(resPath, isAddUpdateListerner, Close, stopAI_OnOpen);this.open_nav_view = open_nav;GameEvent.ResetSortOrder += ResetSortOrder;}
}
初始化如下类型Canvas需要打开导航面板
public class ViewManager
{static ViewManager instance = new ViewManager();public static ViewManager Instance => instance;public int order;public void Init(){//注册所有界面LoginViewController.Instance.Init("UI/LoginCanvas", true, false, false);BagViewController.Instance.Init("UI/BagCanvas", true, false, true);//1ForgeViewController.Instance.Init("UI/ForgeCanvas", true, false, true);//2LoadingViewController.Instance.Init("UI/LoadingCanvas", true, false);MainViewController.Instance.Init("UI/MainCanvas", true, false);NavViewController.Instance.Init("UI/NavCanvas", true, false);PurifyViewController.Instance.Init("UI/PurifyCanvas", true, false, true);//3TipsViewController.Instance.Init("UI/TipsCanvas", true, false);StoreViewController.Instance.Init("UI/StoreCanvas", true, false);
2、打开面板时触发SetActive方法——激活导航栏并设为顶层
public void Open(){if (view.gameObject == null){//实例化var go = ResourcesManager.Instance.Instantiate<GameObject>(view.resPath);………………SetActive(true);view.Start();}else{//打开SetActive(true);}}
Active方法中调用OnViewChange_Begin方法
act
变量(是 "active" 的缩写)是一个布尔参数,用于表示视图应该被设置为激活还是禁用状态
控制视图的激活状态:
act = true
:表示要激活/显示视图
act = false
:表示要禁用/隐藏视图
public void SetActive(bool act){if (view._enable != act){ViewManager.Instance.OnViewChange_Begin(view.resPath, act, this.open_nav_view);view.gameObject.SetActive(act);……………………ViewManager.Instance.OnViewChange_End(view.resPath, act, this.open_nav_view);}}
在 OnViewChange_Begin
方法中:
csharp// 如果视图正在激活且需要打开导航视图,则将导航视图置顶
if (act && open_nav_view)
{NavViewController.Instance.SetTop();
}
3、在导航栏选取界面时,UI所执行逻辑
public class NavView : View
{Text money;public override void Awake(){base.Awake();money = GetComponent<Text>("Money/CoinText");//注册打开面板的按钮点击事件GetComponent<Button>("Bag").onClick.AddListener(ViewManager.Instance.OpenBagView);GetComponent<Button>("Forge").onClick.AddListener(ViewManager.Instance.OpenForgeView);GetComponent<Button>("Purify").onClick.AddListener(ViewManager.Instance.OpenPurifyView); }
选中面板UI的变化:NavView——SetButton_Select
神秘商店的功能交互
代码流程——神秘商店购买物品
1、X键打开商店面板,并且打开的同时初始化商店相关UI组件
else if (Input.GetKeyDown(KeyCode.X)){StoreViewController.Instance.OpenOrClose();}
StoreView——Awake
2、打开面板时,商店面板物品得以展示
StoreView——OnEnable :选中图标和选中序号都设置为“初始情况”,且总价设置为0,并调用show方法
StoreView——show :清理现有物品,生成随机商品并进行布局
3、点击购买
StoreView——BuyOnClick :购买验证流程,购买成功操作
字段内容的文本显示
获取正确的文本内容方法
ShowEquip方法 :这里面就是两种典型的策略:
1、根据传输的装备数据体(BagEntity)获取相关数据:
获取当前装备的相关数据内容——如2001的野兽披风的这些内容
2、根据对应数据正确获取对应文本
因为属性词条需要随机生成的功能性,所以不能直接用string固定存放到配置表中,而是需要用数字对应一个属性词条进行查找。
部位词条也是如此——但是随机性要求没那么强,主要是为了正确对应装备栏的部位
所以额外配置了部位查找表与属性查找表——根据对应内容获取对应文本字符串
正确显示文本
1、对于可以直接传输字符串内容的文本,直接将传输的字符串内容传递到对应text组件上
//该方法位于View脚本中
public void SetText(Text text, string content){text.text = content;}
因为所有Canvas视觉面板都会用到“文本显示内容”——所以将SetText方法写入View脚本
2、对于需要随机生成的词条,则是先根据配置表生成对应数字,再对应生成文本内容
SetItem(bagEntity.entity.att1, att1);
以词条1为例,传递如下数组
SetItem方法 :该方法激活含Text的物体,及调用GetAttText方法
private void SetItem(int[] config, Text text){if (config != null)//如果配置表不为空{ SetText(text, GameDefine.GetAttText(config[0], config[1], config[2]))}}
GetAttText: 根据传递数组——判断是否属性数值正负,是否为百分比
泛型加载资源和泛型加载物体(根据路径)
第一章节相关内容:
ARPG开发流程第一章——资源的加载和实例化
该方法在第三章节内容:
ResourceManager类——Load方法
ResourceManager类——Instantiate方法
根据路径创建实例
根据路径创建物体实例:
public GameObject Create_BagItem(string item_path){if (bag.Count == 0){return Instantiate<GameObject>(item_path);}……………………}
public T Instantiate<T>(string path) where T : Object{var r = Load<T>(path);if (r != null){return Object.Instantiate(r);//根据引用的资源路径复制出一份Object实例}return null;}
方法 功能 返回值 Load<T>(path)
仅加载资源 返回资源本身的引用(如预制体资源) Instantiate<T>(path)
加载并实例化 返回场景中的新实例对象
View控制StoreView(继承类View)的逻辑
public class StoreView : View
{public override void Awake(){base.Awake();Select = transform.Find("Select");}
}
Q1:这个脚本中的transform是什么?
A1:这个脚本的transform是在View脚本中
public void Open(){if (view.gameObject == null){//实例化var go = ResourcesManager.Instance.Instantiate<GameObject>(view.resPath);GameObject.DontDestroyOnLoad(go);view.gameObject = go;view.gameObject.name = view.gameObject.name.Split('(')[0];view.transform = go.transform;view.canvas = go.GetComponent<Canvas>();view.cs = go.GetComponent<CanvasScaler>();view.Awake();SetActive(true);view.Start();}else{//打开SetActive(true);}}