一,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腳本,並註冊按鈕:
是不是很簡單,快試試吧!!