一,TV游戏操作性
对于TV端的游戏,我们只能采取遥控器来操作,市面上不同电视的遥控器又各不相同,为了适应各种电视,我们要采取最小原则,即我们只使用遥控器的4个方向键和确定返回键来操作游戏。
二, 首先要明确的是:对于TV端的按键方向键对应键盘方向键,确定键对应enter键,返回键对应esc键
然后,tv是没有鼠标来点击的,所以我们的用enter键来选定按钮,用方向键来设定当前选定的按钮,按下enter启动该按钮,执行绑定的事件。
三,为了实现上述可操作性,我们需要将当前界面的所有按钮都注册到链表里,当按下相应按键进行查找对应方向上的按钮,我们当然还希望在不更改原来特性(例如高亮等设置)的前提下增加这些功能,所以此时要手动来调用按钮的相应事件,注意,在该模式下eventsystem接收鼠标的事件要禁掉,打包无所谓,电脑端测试会有问题。
四,由于界面上我们使用的按键很有可能在游戏中与游戏的操作逻辑冲突,所以,在游戏中打开界面,(特别是暂停界面)得让游戏暂停,如果我们在UI模块来操作游戏暂停显然不合理,我们希望的是整个TVUI模块只作为附加的,在不影响原来一切设定的基础上
五,进入正题:
经过上述需求和原理的讲解,接下来开始实现。首先,我们需要一个管理每个界面的管理器:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public delegate void OnChanged(bool active);
[Serializable ]
public class RestartEvent:UnityEvent { }
public class ChooseButton : MonoBehaviour
{
public static ChooseButton Instance;
[SerializeField]
private TVButtonCtrl cur_UI;//当前活跃的UI
[SerializeField]
private TVButtonCtrl lastClose;//上次关闭的
[SerializeField ]
private List<TVButtonCtrl > groups;//当前活跃的UI
//private int count;//退出按键调用次数
private float time;
private float space = 0.1f;
public OnChanged OnClickEsc;//点击TV遥控器退出,返回按钮事件
[SerializeField]
private TVButtonCtrl menuUI,sureToMainUI,sureToQuitUI,MainPanel;
[SerializeField ]
private EscUI callOutOnEsc = EscUI.Menu;//当前无界面时,点击退出呼出的界面
public RestartEvent OnRestart,OnBackToMain;//重玩
// Start is called before the first frame update
void Awake()
{
Instance = this;
groups = new List<TVButtonCtrl >();
//
menuUI.OnClose();
sureToQuitUI.OnClose();
sureToMainUI .OnClose ();
cur_UI = MainPanel;
}
private void OnDestroy()
{
groups.Clear();
}
public void HideUI(TVButtonCtrl ui)
{
if (lastClose == ui)
{
lastClose = null;
return;
}
//Debug.Log("hide:"+ui .gameObject .name );
lastClose = ui;//记录上次关闭的,防止多次重复关闭
ui.OnClose();
if (groups.Contains(ui))
{
groups.Remove(ui);
}
if (ui ==cur_UI)//如果关闭的是当前页面,这找到最上层的,注册按钮
{
if (groups.Count > 0)
{
cur_UI =groups [groups .Count -1];
cur_UI.OnShow();
return;
}else
{
//如果没有界面了
cur_UI = null;
}
}
}
public void ShowUI(TVButtonCtrl ui,params object []args)
{
if (groups.Contains(ui))//已经包含有
{
groups.Remove(ui);
}
groups.Add(ui);
cur_UI = ui;
ui.OnShow(args);
if (lastClose == ui)//如果再次打开该界面,则不算重复
lastClose = null;
//Debug.Log("show:"+ui.gameObject.name);
}
// Update is called once per frame
void Update()
{
if (time > 0)
{
time -= Time.deltaTime;
return;
}
if (Input.GetKeyDown(KeyCode.Escape))
{
OnEsc();
}
if (cur_UI == null) return;
if (Input.GetKeyDown(KeyCode.UpArrow))
{
time = space;
cur_UI.GoUp(-1);
}
else if (Input.GetKeyDown(KeyCode.RightArrow))
{
time = space;
cur_UI.GoLeft(1);
}
else if (Input.GetKeyDown(KeyCode.DownArrow))
{
time = space;
cur_UI.GoUp(1);
}
else if (Input.GetKeyDown(KeyCode.LeftArrow))
{
time = space;
cur_UI.GoLeft(-1);
}
else if ((Input.GetKeyDown(KeyCode.Return) || Input.GetKeyDown(KeyCode.JoystickButton0)))
{
time = space;
cur_UI.OnClick();
}
}
public void Quit()
{
Application.Quit();
}
private void OnEsc()
{
bool show = false;
if (cur_UI == menuUI ||cur_UI == sureToMainUI ||cur_UI == sureToQuitUI )//当前已经打开有相关界面,则再次点击关闭
{
HideUI(cur_UI);
}
else
{
EscUI openType = EscUI.Menu;
if (cur_UI == null)
{
openType = callOutOnEsc;
}
else
{
openType = cur_UI.callOutUIType;
}
TVButtonCtrl ui = menuUI;
string str = null;
switch (openType)
{
case EscUI.SureToQuit:
ui = sureToQuitUI;
str = "确定要退出游戏吗?";
break;
case EscUI.SureToMain:
ui = sureToMainUI;
str = "确定要返回主页吗?";
break;
}
if (string .IsNullOrEmpty(str))
{
ShowUI(ui);
}else
{
ShowUI(ui, str);
}
show = true;
}
if (OnClickEsc != null)
{
OnClickEsc.Invoke(show);
}
}
public void OpenSureToQuit()
{
ShowUI(sureToQuitUI);
}
public void OpenMainPanel()
{
for (int i = 0; i < groups.Count; i++)
{
if(groups [i]==MainPanel)
{
cur_UI = MainPanel;
MainPanel.SetDefault();
continue;
}
HideUI(groups[i]);//吧原来的都隐藏
}
if (cur_UI != MainPanel)//当前没有打开的mainpanel
ShowUI(MainPanel);
if (OnBackToMain !=null)
{
OnBackToMain.Invoke();
}
}
public void OpenSureToMain()
{
ShowUI(sureToMainUI);
}
public void Restart()
{
for (int i = 0; i < groups .Count ; i++)
{
HideUI(groups[i]);
}
cur_UI = null;
lastClose = null;
if (OnRestart != null)
{
OnRestart.Invoke();
}
}
public void OnCloseTVUI()
{
if (OnClickEsc != null)
{
OnClickEsc.Invoke(false);
}
}
}
在这里,该脚本作为全局唯一存在,为了方便使用,和减少接入代码,原本需要根据游戏状态来确定按下返回键要显示的界面,在这里我改为由每个界面指定要弹出的,并设置当前没有界面时,按下返回键弹出的界面,弹出的界面当前设置有3中:暂停页面(包括继续游戏,返回主页,重玩可根据需求自己添加),确定返回主页,确定退出游戏。并且开放了当调用返回键时可注册模块外的事件,比如暂停游戏等
接下来是每个界面自己的管理器:基类如下:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
/// <summary>
/// 点击退出按键要弹出的UI
/// </summary>
public enum EscUI
{
None,
Menu,
SureToQuit,
SureToMain,
}
[Serializable]
public class ButtonInfo
{
public Button button;
[Range(0, 10)]
public int hor_Index;
[Range(0, 10)]
public int ver_Index;
}
[Serializable]
public class TVButtonCtrl :MonoBehaviour
{
public EscUI callOutUIType= EscUI.None;//当你在此界面按下escape建希望弹出的界面
public List<ButtonInfo> buttons = new List<ButtonInfo>();
private Dictionary<int, ButtonInfo> dic_btns;
private Button cur_Select;
public int cur_Hor, cur_Ver;//当前矩阵的索引值
//[SerializeField ]
private int maxHor, maxVer;//当前最大注册索引值
private int lastvalue;
//[HideInInspector ]
public bool active;//当前界面的状态 活跃?
private EventSystem system;
[SerializeField ]
private bool DeleteOnClose;//当关闭该界面时是否删除,若false,则隐藏
void Awake()
{
active = true;
dic_btns = new Dictionary<int, ButtonInfo>();
for (int i = 0; i < buttons.Count; i++)
{
ButtonInfo info = buttons[i];
int id = info.ver_Index * 10 + info.hor_Index;
dic_btns.Add(id, info);
if (info.hor_Index > maxHor)
maxHor = info.hor_Index;
if (info.ver_Index > maxVer)
maxVer = info.ver_Index;
//Debug.Log("id:" + id + " \n name:" + info.button.name);
}
system = EventSystem.current;
}
public void OnClose()
{
gameObject.SetActive(false);
active = false;
}
//当打开界面的时候
public virtual void OnShow(params object[] args)
{
gameObject.SetActive(true);
active = true;
SetDefault();
}
//设置默认的
public void SetDefault()
{
cur_Hor = cur_Ver = 0;
for (int i = 0; i < 100; i++)
{
if (dic_btns.ContainsKey(i))
{
SetChooseBtn(dic_btns[i]);
//Debug.Log("set default button:" + cur_Select.gameObject.name);
return;
}
}
Debug.LogWarning("当前UI未注册按钮!!" + gameObject.name);
}
//向上
public void GoUp(int dir)
{
if (dic_btns.Count <= 0) return;
if ((dir ==-1&&cur_Ver <= 0)||(dir ==1&&cur_Ver >=maxVer))
{
SetChooseBtn(dic_btns[cur_Ver * 10 + cur_Hor]);
return;
}
lastvalue = cur_Ver;
ButtonInfo info;
if (dir > 0)//向下
{
while (cur_Ver <maxVer )
{
cur_Ver++;
if (dic_btns.TryGetValue(cur_Ver * 10 + cur_Hor, out info))
{
if (info.button.gameObject.activeInHierarchy)
{
SetChooseBtn(info);
return;
}
}
//整排查找
if (FindInHor() != null)
{
return;
}
}
}else
{
while (cur_Ver > 0)
{
cur_Ver--;
if (dic_btns.TryGetValue(cur_Ver * 10 + cur_Hor, out info))
{
if (info.button.gameObject.activeInHierarchy)
{
SetChooseBtn(info);
return;
}
}
//整排查找
if (FindInHor() != null)
{
return;
}
}
}
//还原
cur_Ver = lastvalue;
SetChooseBtn(dic_btns[cur_Ver * 10 + cur_Hor]);
}
public void GoLeft(int dir)
{
if (dic_btns.Count <= 0) return;
if ((cur_Hor <= 0 && dir == -1)||(cur_Hor >=maxHor &&dir ==1))
{
SetChooseBtn(dic_btns[cur_Ver * 10 + cur_Hor]);
return;
}
lastvalue = cur_Hor;
ButtonInfo info;
//单向
if (dir > 0)
{
while (cur_Hor <maxHor )
{
cur_Hor++;
if (dic_btns.TryGetValue(cur_Ver * 10 + cur_Hor, out info))
{
if (info.button.gameObject.activeInHierarchy)
{
SetChooseBtn(info);
return;
}
}
if (FindInVer ()!=null)
{
return;
}
}
}else
{
while (cur_Hor > 0)
{
cur_Hor--;
if (dic_btns.TryGetValue(cur_Ver * 10 + cur_Hor, out info))
{
if (info.button.gameObject.activeInHierarchy)
{
SetChooseBtn(info);
return;
}
}
if (FindInVer() != null)
{
return;
}
}
}
//还原
cur_Hor = lastvalue;
SetChooseBtn(dic_btns[cur_Ver * 10 + cur_Hor]);
}
//设置选择按钮
private void SetChooseBtn(ButtonInfo info)
{
cur_Select = info.button;
cur_Hor = info.hor_Index;
cur_Ver = info.ver_Index;
lastvalue = -1;
Debug.Log("current index:" + cur_Hor + ":" + cur_Ver);
//设置高亮
system.SetSelectedGameObject(cur_Select.gameObject, new BaseEventData(EventSystem.current));
}
//水平方向上寻找
private ButtonInfo FindInHor()
{
ButtonInfo info;
for (int i = cur_Ver * 10; i < maxVer * 10 +maxHor ; i++)
{
if (dic_btns.TryGetValue(i, out info))
{
//Debug.Log(i+"___"+ info.button.gameObject.activeInHierarchy);
if (info.button.gameObject.activeInHierarchy)
{
SetChooseBtn(info);
return info;
}
}
}
return null;
}
//整列查找
public ButtonInfo FindInVer()
{
ButtonInfo info;
for (int i = cur_Ver; i < maxVer; i++)
{
if (dic_btns.TryGetValue(i*10+cur_Hor, out info))
{
//Debug.Log(i+"___"+ info.button.gameObject.activeInHierarchy);
if (info.button.gameObject.activeInHierarchy)
{
SetChooseBtn(info);
return info;
}
}
}
return null;
}
public void OnClick()
{
Debug.Log("click:" + cur_Select.gameObject.name);
if (cur_Select != null)
{
cur_Select.onClick.Invoke();
}
if(gameObject .activeInHierarchy)
{
//Debug.Log("find around1"+ cur_Select.gameObject.activeInHierarchy);
//如果当前按钮点击后隐藏了,找下一个
if (!cur_Select.gameObject.activeInHierarchy)
{
// Debug.Log("find around2");
ButtonInfo info;
if (dic_btns.TryGetValue(cur_Ver * 10 + cur_Hor+1, out info))
{
SetChooseBtn(info);
return;
}
if (dic_btns.TryGetValue(cur_Ver * 10 + cur_Hor-1, out info))
{
SetChooseBtn(info);
return;
}
//左右没有相邻的。重新再整排查找
info = FindInHor();
cur_Select = info.button;
}
}
}
}
查找按钮切换等都在这里实现,这是具体的按钮操作,注册方式采用矩阵,在面板赋值并指定矩阵值
如上图可以注册为 (0,0),(0,1)(0,2)即3列,同理行为x递增
接下来是按下返回键时可弹出的几个界面:
1:菜单页
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class UIMenu : TVButtonCtrl
{
public void OnClickBackToMain()
{
ChooseButton.Instance.OpenMainPanel();
}
public void OnClickCancel()
{
ChooseButton.Instance.HideUI(this);
ChooseButton.Instance.OnCloseTVUI();
}
public void OnClickBack()
{
ChooseButton.Instance.OpenSureToMain();
}
public void OnClickQuit()
{
ChooseButton.Instance.Quit();
}
public void OnClickRestart()
{
ChooseButton.Instance.Restart();
}
}
2:确定页:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public enum BackUI
{
None,//选择None不打开任何界面
Main,//返回主页
Quit,//退出程序
}
public class UISureToChoosePanel : TVButtonCtrl
{
public Text infoTex;
public BackUI backToUI;//当点击确定按钮时,返回的界面
public override void OnShow(params object[] args)
{
if (args .Length == 1)
{
infoTex.text = (string)args[0];
}
base.OnShow();
}
//点击取消退出
public void OnClickCancle()
{
ChooseButton.Instance.HideUI(this);
}
//退出
public void OnClickSure()
{
ChooseButton.Instance.HideUI(this);
switch (backToUI)
{
case BackUI.None:
break;
case BackUI.Main:
ChooseButton.Instance.OpenMainPanel();
break;
case BackUI.Quit:
ChooseButton.Instance.Quit();
break;
}
}
}
这里将确定返回主页和退出游戏结合了,挂载不同界面选取不同的属性即可,这样提高了可扩展性
六 最重要的,使用方法:
接下来创建一个空物体,将上述3个界面作为它的子物体,挂载ChooseButton脚本,如下图
接下来将自己的每个界面添加TVButtonCtr脚本,并注册按钮:
是不是很简单,快试试吧!!