前言
Untiy中設計UI不會使用GUI,因為它必須要運行代碼才可以看出UI的布局結果,而且GUI不支持屏幕分辨率自適應,所以一般會使用UGUI等設計,但是為了搞清楚高級UI的原理,通過GUI去設計一個類似于UGUI的工具是個很好的學習方法。所以本文屬于是脫褲子放屁,但是只有脫下褲子把屁放出來才能研究屁的物質組成。
九宮格布局概念
來看這樣一張圖
把屏幕分成九個部分,將每個部分看作是一個單獨的坐標系,然后每個部分的原點如圖中的紅點所示
然后再將每個控件也分成九宮格,如下圖所示
我們可以選擇不同的部分作為按鈕的“中心點”
再有就是偏移位置,這是我們自行設置的。舉個例子,看下圖
????????????????????????????????????????????????????????????????????????圖1?
在這幅圖中,選取了屏幕的左上部分作為按鈕確認位置的原點,按鈕的中心點設置為了按鈕的左上角,x和y分別是按鈕距離原點的偏移位置(人為設置),最終根據一個公式得出按鈕的左上角的坐標為A點的坐標加上中心點偏移位置再加上人為設置的偏移位置就可以得出按鈕的左上角的坐標,然后繪制出整個按鈕。
所以這個位置的公式
控件坐標位置 = 相對屏幕位置 + 中心點偏移位置 + 偏移位置
不太好理解,但是保留這個疑問,先來寫代碼,一邊寫一邊體會。
基類
位置類
首先要去計算每個空間在屏幕上的位置,具體的原理是根據上面的公式。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
//這個枚舉用來表示對齊方式 一共九種
public enum E_Aligment_Type
{
Up,
Down,
Left,
Right,
Center,
Left_Up,
Left_Down,
Right_Up,
Right_Down,
}
[System.Serializable]
public class CustomGUIPOS
{
//這個變量會返回給外部 用來繪制一個控件
private Rect rPos = new Rect(0, 0, 100, 100);
//這個是屏幕對齊方式
public E_Aligment_Type screen_Alignment_Type = E_Aligment_Type.Center;
//這個是控件的對齊方式
public E_Aligment_Type controller_Alignment_Type = E_Aligment_Type.Center;
//這個是手動在外部設置的偏移位置
public Vector2 pos;
//這個表示控件的寬和高
public float width = 100f;
public float height = 50f;
//這個是控件用來計算位置的中心位置
private Vector2 centerPos;
//此函數用來計算控件中心點的偏移,可以參考之前的圖
private void CalcCenterPos()
{
switch (controller_Alignment_Type)
{
case E_Aligment_Type.Up:
centerPos.x = -width / 2;
centerPos.y = 0;
break;
case E_Aligment_Type.Down:
centerPos.x = -width / 2;
centerPos.y = -height;
break;
case E_Aligment_Type.Left:
centerPos.x = 0;
centerPos.y = -height / 2;
break;
case E_Aligment_Type.Right:
centerPos.x = -width;
centerPos.y = -height / 2;
break;
case E_Aligment_Type.Center:
centerPos.x = -width / 2;
centerPos.y = -height / 2;
break;
case E_Aligment_Type.Left_Up:
centerPos.x = 0;
centerPos.y = 0;
break;
case E_Aligment_Type.Left_Down:
centerPos.x = 0;
centerPos.y = -height;
break;
case E_Aligment_Type.Right_Up:
centerPos.x = -width;
centerPos.y = 0;
break;
case E_Aligment_Type.Right_Down:
centerPos.x = -width;
centerPos.y = -height;
break;
}
}
//這個函數用來計算空間中心點的坐標
private void CalcPos()
{
switch (screen_Alignment_Type)
{
case E_Aligment_Type.Up:
rPos.x = Screen.width / 2 + centerPos.x + pos.x;
rPos.y = centerPos.y + pos.y;
break;
case E_Aligment_Type.Down:
rPos.x = Screen.width / 2 + centerPos.x + pos.x;
//為什么減去pos.y 是因為要方便輸入
rPos.y = Screen.height + centerPos.y - pos.y;
break;
case E_Aligment_Type.Left:
rPos.x = centerPos.x + pos.x;
rPos.y = Screen.height / 2 + centerPos.y + pos.y;
break;
case E_Aligment_Type.Right:
rPos.x = Screen.width + centerPos.x - pos.x;
rPos.y = Screen.height / 2 + centerPos.y + pos.y;
break;
case E_Aligment_Type.Center:
rPos.x = Screen.width / 2 + centerPos.x + pos.x;
rPos.y = Screen.height / 2 + centerPos.y + pos.y;
break;
case E_Aligment_Type.Left_Up:
rPos.x = centerPos.x + pos.x;
rPos.y = centerPos.y + pos.y;
break;
case E_Aligment_Type.Left_Down:
rPos.x = centerPos.x + pos.x;
rPos.y = Screen.height + centerPos.y - pos.y;
break;
case E_Aligment_Type.Right_Up:
rPos.x = Screen.width + centerPos.x - pos.x;
rPos.y = centerPos.y + pos.y;
break;
case E_Aligment_Type.Right_Down:
rPos.x = Screen.width + centerPos.x - pos.x;
rPos.y = Screen.height + centerPos.y - pos.y;
break;
}
}
//這個屬性用來得到控件最終的位置以及寬和高
public Rect Pos
{
get
{
CalcCenterPos();
CalcPos();
rPos.width = width;
rPos.height = height;
return rPos;
}
}
}
這就是整個計算控件位置寬高的類。
注意為什么有的地方是減去pos.x,這是為了方便輸入,其實加上也是可以的,只是到時候在編輯界面輸入可能會有點別扭,這么做是為了保持坐標軸和屏幕坐標軸保持一致。
以圖1為例,此時屏幕九宮格和控件中心的對齊方式都為Left_Up,偏移量人為設置成(10,10),根據兩個函數的計算,rPos最終等于(10,10,100,50),然后在GUI函數中去繪制這個Button控件。
此外,還要加上特性[System.Serializable]才能在編輯界面改變
控件基類
所有的控件都有很多共同特征,所以完全可以寫一個抽象類來抽象出它們的共同特征,并讓特定的控件繼承此抽象類。代碼如下
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public enum E_Style_OnOff
{
On,
Off,
}
public abstract class CustomController : MonoBehaviour
{
//提取控件的共同信息
//位置信息
public CustomGUIPos guiPos;
//顯示內容信息
public GUIContent content;
//自定義樣式
public GUIStyle style;
//自定義樣式是否啟用的開關 默認關閉
public E_Style_OnOff styleOnOrOff = E_Style_OnOff.Off;
public void DrawGUI()
{
switch (styleOnOrOff)
{
case E_Style_OnOff.On:
StyleOnDraw();
break;
case E_Style_OnOff.Off:
StyleOffDraw();
break;
}
}
/// <summary>
/// 自定義樣式開始時的繪制方法
/// </summary>
protected abstract void StyleOnDraw();
/// <summary>
/// 自定義樣式關閉時的繪制方法
/// </summary>
protected abstract void StyleOffDraw();
}
這個類的主要作用就是提取控件的共同特征,此外代碼中還寫了兩個抽象函數,因為使用GUI繪制控件可以分成兩類,比如GUI.Button(guiPos.Pos, content)和GUI.Button(guiPos.Pos, content,style),繪制這兩個按鈕時一個使用了自定義樣式,一個不支持,所以我們可以抽象出兩個函數,并且利用枚舉E_Style_OnOff來讓用戶在自定義界面選擇是否自定義控件的樣式,使用該枚舉定義出一個編輯界面可以更改的字段styleOnOrOff?,當選擇On時,表示的就是自定義樣式,這樣我們只用在控件子類中重寫這兩個抽象函數即可。
畫布類(根類)
使用UGUI時,系統(tǒng)會自動創(chuàng)建一張畫布出來,所有的控件都會繪制在這張畫布上,所以需要有一個畫布類。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[ExecuteAlways]
public class CustomGUIRoot : MonoBehaviour
{
private CustomController[] customControllers;
private void Start()
{
customControllers = this.GetComponentsInChildren<CustomController>();
}
//在這統(tǒng)一繪制子對象控件 這相當于一張畫布
private void OnGUI()
{
//每一次繪制之前 得到所有子對象空間的父類腳本
//這一句代碼會浪費性能 因為OnGUI函數總是不停的執(zhí)行 這里不停的去獲取有點浪費性能
customControllers = this.GetComponentsInChildren<CustomController>();
//遍歷每一個控件 讓其執(zhí)行繪制
for (int i = 0; i < customControllers.Length; i++)
{
customControllers[i].DrawGUI();
}
}
}
因為所有的控件都繪制在畫布上,這一段代碼主要利用GetComponentsInChildren函數去獲取畫布的子物體上的CustomController腳本,然后調用里面的DrawGUI函數。
其中還有一個浪費性能的問題,這里暫時不解決。
此外,為了讓我們不在游戲運行狀態(tài)下就可以看到布局情況,這里使用了一個特性[ExecuteAlways],這個特性不應該被亂用。
最后,我們需要在start函數中獲取一次用來初始化,否則報空引用的錯誤,這里是我剛做的時候犯得錯誤,留意一下。
控件子類
下面就可以來寫控件子類了,只需要繼承CustomController類即可。
標簽Label
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CustomGUILabel : CustomController
{
protected override void StyleOffDraw()
{
GUI.Label(guiPos.Pos, content);
}
protected override void StyleOnDraw()
{
GUI.Label(guiPos.Pos, content,style);
}
}
標簽是最簡單的,只需要繪制出來就好,這里使用的都是父類中的信息。
這個時候在Unity編輯界面就制作一個空物體,命名為Root,掛載上腳本CustomGUIRoot,為其創(chuàng)建一個子物體Label,為Label掛在上腳本CustomGUILabel,這個時候就可以看到如下的界面
這樣即使在不運行的情況下,我們創(chuàng)建除了一個Label,并且可以改變它的位置和對齊方式,這是在GUIPos選項卡中,改變width和height的值就可以改變這個Label的位置。
圖片Texture
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CustomGUITexture : CustomController
{
//縮放模式
public ScaleMode scaleMode = ScaleMode.StretchToFill;
protected override void StyleOffDraw()
{
GUI.DrawTexture(guiPos.Pos, content.image, scaleMode);
}
protected override void StyleOnDraw()
{
GUI.DrawTexture(guiPos.Pos, content.image, scaleMode);
}
}
圖片沒啥好說的,就多加了一個縮放模式,這里的模式有三種,每種模式有什么不同可以參考Unity官方文檔。?
按鈕Button
按鈕有所不同,先上代碼
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public class CustomGUIButton : CustomController
{
//提供給外部點擊的事件
//只要給外部給予了響應函數就會執(zhí)行
public event UnityAction clickevent;
protected override void StyleOffDraw()
{
if(GUI.Button(guiPos.Pos, content))
{
clickevent?.Invoke();
}
}
protected override void StyleOnDraw()
{
if(GUI.Button(guiPos.Pos, content,style))
{
clickevent?.Invoke();
}
}
}
因為按鈕和標簽不一樣,在點擊按鈕時需要響應,因此定義一個事件,在我們點擊此按鈕時,會響應這個事件。這樣在外部為事件添加函數即可。這個在測試部分具體闡述。
輸入框Input
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public class CustomGUIInput : CustomController
{
public event UnityAction<string> textChange;
private string oldStr = "";
protected override void StyleOffDraw()
{
content.text = GUI.TextField(guiPos.Pos, content.text);
if(oldStr != content.text)
{
textChange?.Invoke(oldStr);
oldStr = content.text;
}
}
protected override void StyleOnDraw()
{
content.text = GUI.TextField(guiPos.Pos, content.text,style);
if (oldStr != content.text)
{
textChange?.Invoke(oldStr);
oldStr = content.text;
}
}
}
輸入框和按鈕又有所不同,在一般情況下使用GUI中的TextField時,一般是這樣使用的
string inputStr = "";
inputStr = GUI.TextField(new Rect(0, 0, 100, 30), inputStr);
必須要聲明一個字符串來接收輸入框的變化情況。
因此在CustomGUIInput中也要這么寫,但是又聲明了一個舊的oldStr,原因是為了只有在輸入內容發(fā)生變化時才會調用事件響應函數,如果不這么做,那樣的話在CustomGUIRoot類中的OnGUI中會一直調用事件,這樣浪費了很多性能,使用一個oldStr來記錄變化情況,這就避免了性能的浪費。
多選框Toggle
多選框和上面的iuput一樣的道理,這里不多贅述
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public class CustomGUIToggle : CustomController
{
public bool isSel;
public event UnityAction<bool> changeValue;
private bool isOldSel;
protected override void StyleOffDraw()
{
isSel = GUI.Toggle(guiPos.Pos, isSel, content);
//只有變化時 才告訴外部 執(zhí)行函數 否則沒有必要一致傳參數
if(isOldSel != isSel)
{
changeValue?.Invoke(isSel);
isOldSel = isSel;
}
}
protected override void StyleOnDraw()
{
isSel = GUI.Toggle(guiPos.Pos, isSel, content,style);
if (isOldSel != isSel)
{
changeValue?.Invoke(isSel);
isOldSel = isSel;
}
}
}
滑動條Slider
滑動條只是多了幾個參數,此外還要分為水平和豎直的滑動條,因此可以定義一個枚舉,在不同的情況下繪制不同的滑動條就可以了。還有一點就是,在選擇自定義style后,滑動條有兩個皮膚,一個是背景,一個是滑塊,因此這里需要多定義一個GUIStyle。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
public enum E_Slider_Type
{
Horizontal,
Vertical,
}
public class CustomGUISlider : CustomController
{
public float minValue = 0;
public float maxValue = 1;
//當前值
public float nowValue = 0;
public E_Slider_Type type = E_Slider_Type.Horizontal;
public GUIStyle sliderStyle;
public event UnityAction<float> changeValue;
private float oldValue = 0f;
protected override void StyleOffDraw()
{
switch (type)
{
case E_Slider_Type.Horizontal:
nowValue = GUI.HorizontalSlider(guiPos.Pos, nowValue, minValue, maxValue);
break;
case E_Slider_Type.Vertical:
nowValue = GUI.VerticalSlider(guiPos.Pos, nowValue, minValue, maxValue);
break;
default:
break;
}
if(oldValue != nowValue)
{
changeValue?.Invoke(nowValue);
oldValue = nowValue;
}
}
protected override void StyleOnDraw()
{
switch (type)
{
case E_Slider_Type.Horizontal:
nowValue = GUI.HorizontalSlider(guiPos.Pos, nowValue, minValue, maxValue,style,sliderStyle);
break;
case E_Slider_Type.Vertical:
nowValue = GUI.VerticalSlider(guiPos.Pos, nowValue, minValue, maxValue, style, sliderStyle);
break;
default:
break;
}
if (oldValue != nowValue)
{
changeValue?.Invoke(nowValue);
oldValue = nowValue;
}
}
}
單選框
最麻煩的是單選框,單選框的要求是幾個選項中必須選一個,不能空選,比如男女選項,必須選一個,不能多選,也不能不選。
使用多選框來完成。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CustomGUIToggleGroup : MonoBehaviour
{
public CustomGUIToggle[] toggles;
//記錄上一次為true的tiggle
private CustomGUIToggle frontTrueTog;
void Start()
{
if (toggles.Length == 0) return;
//通過遍歷 來為多個 多選框添加監(jiān)聽事件函數
//在函數中做處理
//當一個為true時 另外兩個變?yōu)閒alse
for (int i = 0; i < toggles.Length; i++)
{
CustomGUIToggle toggle = toggles[i];
toggle.changeValue += (value) =>
{
//當傳入的value為true時 需要把另外兩個變?yōu)閒alse
if (value)
{
for (int j = 0; j < toggles.Length; j++)
{
//這里有閉包 toggle就是上一個函數中聲明的變量
//改變了它的生命周期
if (toggles[j] != toggle)
{
toggles[j].isSel = false;
}
}
//記錄上一次為true的toggle
frontTrueTog = toggle;
}
//來判斷 當前變成false的這個toggle是不是上一次為true
//如果是 就不應該讓它變成false
else if (toggle == frontTrueTog)
{
//強制轉換為true
toggle.isSel = true;
}
};
}
}
}
在使用該單選框時,需要首先創(chuàng)建幾個多選款,然后在編輯界面將他們設置為CustomGUIToggle的元素。然后必須要運行才可以顯示出效果,也就是只能在多個選項中選一個。
這里代碼的邏輯就是記錄某一個多選框中的值的變化情況,有一個變化為true,那么其他的就要強制變?yōu)閒alse,但是為了做到不空選,所以還要在變化是同一個toggle時,強制將其變化為true。
測試
請注意:首先在Root沒有子對象的情況下將其設置為預制體,其他的也是這樣,創(chuàng)建對應名稱的子物體,然后掛載上對應的腳本,在設置為預制體,這樣就可以起到和UGUI類似的作用了,以下是一個示例
如圖,創(chuàng)建一個空物體,掛載上測試腳本,然后添加子物體Root,Root中再從預制體中拖入相應的控件,最終效果如下:
簡單的創(chuàng)建了一個面板,點擊面板中的按鈕,效果如Console框中的打印結果。注意:需要先運行游戲再點擊按鈕才有效果
這是測試腳本
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TestScript : MonoBehaviour
{
public CustomGUIButton startButton;
public CustomGUIButton exitButton;
void Start()
{
startButton.clickevent += () =>
{
Debug.Log("進入游戲");
};
exitButton.clickevent += () =>
{
gameObject.SetActive(false);
};
}
// Update is called once per frame
void Update()
{
}
}
就和之前說的那樣,只需要為按鈕的事件添加函數即可響應按鈕點擊。
這個UI界面是會根據屏幕分辨率自適應的,這里做到的是位置自適應,大小還沒做。如下圖
文章來源:http://www.zghlxwxcb.cn/news/detail-756063.html
可以看到,即使我改變Game的大小,整個面板還是保持在屏幕中央。文章來源地址http://www.zghlxwxcb.cn/news/detail-756063.html
到了這里,關于Unity使用GUI封裝一個UI系統(tǒng)的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!