"知識的形成與權力的鞏固總是相輔相成..."
-Michel Foucault
經過了人生最長的一個假期後,大學生活終於開始了= =不過時間比高中時要充裕是肯定的,所以就抽空寫一些東西吧...
這句話是我13年9月寫的,你可以知道我的拖延症有多可怕了吧!
何為GUI?
GUI即圖形用戶界面,基本上說,你在遊戲屏幕中看到的2D圖像,比如血條、物品欄、菜單什麼的,都屬於GUI.
如何繪製GUI?
Minecraft使用OpenGL作為繪圖庫,由於其特性,所有的2D圖像在本質上其實都是投影在3D視角上的2D紋理,因此net.minecraft.client.gui包下的Gui類提供了一系列2D繪圖函數,用以輔助開發者方便地繪製2D圖形.一切和界面有關的類都直接或間接繼承自GUI類,GUI類提供的方法包括:
drawHorizontalLine(int x1, int x2, int y, int c)
繪製水平線段,極少被使用...x1和y是起點的XY坐標,(注:2D繪圖坐標系以屏幕左上角為遠點,X軸向右為正,Y軸向下為正.)x2是終點的X坐標,如果小於x1的話系統會自動糾正.c是一組8位的16進制數字,代表一個ARGB顏色.
(ARGB即Alpha(不透明度)Red(紅色)Green(綠色)Blue(藍色).用這3種顏色和1種屬性能繪製出世界上絕大多數顏色.每一種顏色/屬性對應一個2位的十六進制數字,即十進制的0~255.一組ARGB顏色的格式形如0xOOOOOOOO,如聊天框的顏色0x80000000即一個不透明度為128的純白框.)
drawVerticalLine(int x, int y1, int y2, int c)
繪製垂直線段,同上,只不過x和y1為起點的XY坐標,y2為終點的Y坐標.
drawRect(int x1, int y1, int x2, int y2, int c)
繪製一個矩形,x1和y1為起點的XY坐標,x2和y2為終點的XY坐標,兩個坐標寫反了也沒關係,系統會自動糾正,c為8位的16進制數字,代表ARGB顏色.
drawGradientRect(int x1, int y1, int x2, int y2, int c1, int c2)
同樣是繪製一個矩形,只不過顏色是漸變的!
drawCenteredString(FontRenderer,str,x,y,c)
以點(x,y)為中心繪製文本str.使用文字渲染器FontRenderer,使用RGB顏色c.(RGB顏色和ARGB相比沒有透明度信息,即你無法繪製透明文字)FontRenderer實例可以通過RenderManager.instance.getFontRenderer()來獲取.
(事實上它不是繪製在中心,我的語文水平很難表述出它的位置...所以我作了個圖,紅點是點(x,y),黑框是文本)
順便一提,drawCenteredString和下面的drawString支持Minecraft的字體樣式,字體樣式中的顏色設定會覆蓋方法中指定的默認顏色.不過有時候需要注意使用順序,比如同時使用顏色樣式(如白色§f)和字型樣式(如下劃線§n)時,你可能必須把§f放在§n前面才能同時起效.
drawString(FontRenderer,str,x,y,c)
以點(x,y)為左上角繪製文本.
drawTexturedModalRect(x,y,u,v,w,h)
截取渲染引擎中綁定的紋理,並渲染到屏幕上.這個是最常用的繪圖函數.
它的使用很特殊,你需要先向渲染引擎綁定紋理,首先你需要使用渲染引擎的bindTexture方法將一個ResourceLocation所指向的紋理綁定入渲染引擎.綁定完畢後你才可以使用drawTexturedModalRect來繪製紋理.這裡有6個參數作用的圖解.
x,y : 將要在用戶屏幕上繪製的紋理的左上角坐標.
u,v : 被綁定紋理中,需要繪製部分的左上角坐標.
w,h : 需要繪製的部分的寬和高.
繪製的流程可以這樣理解:
渲染引擎會截取被綁定的紋理,將紋理中一個以(u,v)為左上角,(u+w,v+h)為右下角的矩形區域截下來,並將其繪製到用戶屏幕上,其左上角位於點(x,y).
需要注意的是,drawTexturedModalRect只能繪製紋理大小為256x256的紋理!這是因為它內部隱含了兩個比例係數,用來將參數調整成適合OpenGL的形式,這兩個係數是在假定紋理的長寬都是256的情況下制定的,若不為256的話,則無法正確地截取紋理,具體的解決方法是使用drawModalRectWithCustomSizedTexture,或func_146110_a.
drawTexturedModelRectFromIcon(int x, int y, Icon icon, int w, int h)
另一個版本的drawTexturedModalRect,用來繪製Icon.
drawModalRectWithCustomSizedTexture(int x, int y, float u, float v, int width, int height, float textureWidth, float textureHeight)
在MC1.7中它叫func_146110_a,跟drawTexturedModalRect相比,它可以正確繪製任意尺寸的紋理.不過你需要把紋理的寬高手動代進去.
順便一提,Gui類提供的方法使用的都是屏幕坐標,即原點為窗口左上角,X軸向右為正,Y軸向下為正,這與我們大部分人的排版習慣(自上而下,從左到右)相符,目前看起來還挺好用,至少是目前...
Gui類雖然"強大",但它只是個工具包,Gui只是支畫筆,我們還需要一張畫布,在認識"畫布"前,我們先來看一下Gui類的派生類.
過去我繪製了一張Gui派生類樹狀圖,不過那已經是很早之前的版本了...(大概是MC1.2.5左右),如果你想檢索所有的Gui派生類的話,直接找到位於net.minecraft.client.gui包下的Gui類,選擇Open Type Hierarchey便能查看它的所有派生類.
圖為1.6.4的GUI派生類,在1.7.2中,多了個GuiLabel.
在MC1.6.4中,Gui類有8個直接的派生類,這8個類及其子類可分為如下三種:
- HUD
這個名詞有點扯...其實叫UI(用戶界面)也是可以的,它是指玩家在遊戲過程中看到的那些文字圖標,比如下方的物品欄和生命值飢餓度,左上角的版本信息和除錯信息(如果有的話).GuiIngame類及其子類GuiIngameForge類屬於HUD. - 窗口界面
窗口界面是指一個玩家能夠進行交互的界面,比如主菜單界面,多人模式下的服務器選擇界面,或者遊戲中玩家按下E鍵打開的物品欄窗口,打開箱子顯示的庫存窗口,窗口界面與HUD的不同之處在於窗口界面是可以打開關閉的,而HUD只要玩家在正常遊戲便會始終顯示.GuiScreen類及其派生類屬於窗口界面. - 控件
控件通常無法獨立存在,往往是依附在窗口界面中,GuiAchievement,GuiButton,GuiLabel,GuiNewChat和GuiTextField都是控件.
那麼我們依次說起這三大類.由於控件無法獨立存在,因此控件的介紹就放在另外兩類中穿插介紹.下面首先介紹的是窗口界面.
窗口界面
正如剛才所說,窗口界面就是能夠打開關閉的可交互界面,從石爐的操作窗口到遊戲中的ESC菜單,甚至主菜單和遊戲報錯的窗口都是窗口界面.由於遊戲中全部的窗口界面都是GuiScreen類及其派生類,因此只要研究明白GuiScreen類就能了解遊戲中的窗口界面了.
GuiScreen類雖然不是個抽象類,但在遊戲中直接創建並顯示一個GuiScreen類沒有什麼意義,我們主要通過創建GuiScreen的派生類來定製我們自己的窗口界面.GuiScreen類的字段和方法包括如下:
字段:
字段名 | 類型 | 訪問級 | 用途 | 備註 |
---|---|---|---|---|
itemRender | ItemRender | protected | 物品渲染器 | |
mc | Minecraft | public | 直接獲取Minecraft類的實例 | 應視為只讀變量來操作 |
width | int | public | 當前mc窗口的寬(即分辨率的x) | 應視為只讀變量來操作 |
height | int | public | 當前mc窗口的高(即分辨率的y) | 應視為只讀變量來操作 |
buttonList | ArrayList | protected | 記錄該窗口界面內現有的按鈕控件(GuiButton) | |
labelList | ArrayList | protected | 記錄該窗口界面內現有的文字標籤控件(GuiLabel) | 1.7.X下無用,因為GuiLabel在1.7.X下是未完成的... |
allowUserInput | boolean | public | 在顯示窗口時是否允許玩家等 | |
fontRendererObj | FontRenderer | protected | 字體渲染器的實例 | 應視為只讀變量來操作. btw,原來這個字段的名字叫fontRenderer,從1.7開始莫名其妙後面加了個Obj,是Forge工作組裡哪位小天才在秀智商? |
selectedButton | GuiButton | private | 指示玩家按下的按鈕(GuiButton) | private字段...不用細研究了 |
eventButton | int | private | 指示玩家按下的鼠標按鍵 | private字段...不用細研究了 |
lastMouseEvent | long | private | 上一次鼠標按鍵事件觸發時的時間 | private字段...不用細研究了. lastMouseEvent = Minecraft.getSystemTime() |
field_146298_h(1.6.4) | int | private | 跟觸摸屏相關... | private字段...不用細研究了 |
方法:
方法名 | 返回值/參數 | 訪問級 | 用途 | 重寫 (Override) |
備註 |
---|---|---|---|---|---|
構造函數 | 窗口界面不要求必須有一個非默認的構造函數 | 可 | 如果一定要用構造函數的話,只能在構造函數中初始化與繪圖及控件無關的東西(比如分析輸入來的數據,但決不能涉及到屏幕位置等換算) | ||
drawScreen | 鼠標的X坐標(int) 鼠標的Y坐標(int) delta(解釋見附錄)(float) 無返回值 |
public | 每一幀繪製窗口界面的方法,由系統調用 | 可, 建議調用基類的原方法 |
GuiScreen類的drawScreen負責完成按鈕控件(GuiButton)的繪製工作,先於調用原方法的繪製將被按鈕控件覆蓋住,反之,後於調用原方法的繪製將覆蓋在按鈕上. 對於"距離上一次渲染的時間間隔"的解釋將放在附錄A1 |
keyTyped | 輸入的字符(char) 按下的按鍵號(int) 無返回值 |
protected | 對鍵盤輸入事件的處理 | 可, 建議調用基類的原方法 |
GuiScreen類的keyTyped方法用於實現按下ESC鍵後關閉窗口. |
getClipboardString | 無參數值 返回剪切板內容(String) |
static public |
返回剪切板內的文字內容 | 否 | 如果失敗了,會返回""(非null空字符串) |
setClipboardString | 文字(String) 無返回值 |
static public |
設置剪切板的內容 | 否 | 吐槽一下,如果寫入失敗了的話,不會有任何提示或反饋,異常會被忽視. |
renderToolTip | 物品棧(ItemStack) X坐標(int) Y坐標(int) 無返回值 |
protected | 繪製工具提示 | 可 | 最終是通過調用drawHoveringText來實現 |
drawHoveringText | 字符串列表(List) X坐標(int) Y坐標(int) 字體渲染器(FontRenderer) 無返回值 |
protected | 繪製帶有懸浮框的文本. | 可 | |
func_146283_a | 字符串列表(List) X坐標(int) Y坐標(int) 無返回值 |
protected | 對drawHoveringText的簡單封裝. | 可 | |
drawCreativeTabHoveringText | 字符串(String) X坐標(int) Y坐標(int) 無返回值 |
protected | 對只有一行文字的drawHoveringText的簡單封裝. | 可 | |
mouseClicked | 鼠標的X坐標(int) 鼠標的Y坐標(int) 按下的鍵(int) 無返回值 |
protected | 響應鼠標按下事件,由HandleMouseInput調用.0是左鍵,1是右鍵. | 可, 建議調用基類的原方法 |
GuiScreen類的mouseClicked方法用於監視按鍵控件(GuiScreen)的點擊,如果不調用原方法的話就會造成按鍵點擊不能. |
mouseMovedOrUp | 鼠標的X坐標(int) 鼠標的Y坐標(int) 標示(int) 無返回值 |
protected | 響應鼠標移動和鬆開按鍵事件,標誌為-1代表為鼠標移動事件,0代表左鍵鬆開事件,1代表右鍵鬆開事件. | 可, 建議調用基類的原方法 |
GuiScreen類的mouseMovedOrUp方法用於監視按鍵控件(GuiScreen)的鬆開. |
mouseClickMove | 鼠標的X坐標(int) 鼠標的Y坐標(int) 標示(int) 無返回值 |
protected | 響應鼠標移動和鬆開按鍵事件,標誌為-1代表為鼠標移動事件,0代表左鍵鬆開事件,1代表右鍵鬆開事件. | 可 | GuiScreen類的mouseMovedOrUp方法用於監視按鍵控件(GuiScreen)的鬆開. |
actionPerformed | 按下的按鈕(GuiButton) 無返回值 |
protected | 當玩家選中一個按鈕時引發. | 可 | 只有GuiScreen類的mouseClicked被執行才有機會觸發這個方法. |
setWorldAndResolution | Minecraft實例(Minecraft) 寬(int) 高(int) 無返回值 |
public | 設置遊戲屏幕的寬和高. | 可,但不建議 | 由Minecraft來調用. |
initGui | 無參數 無返回值 |
public | 初始化窗口 | 可 | 將初始化內容寫在這裡. |
updateScreen | 無參數 無返回值 |
public | 每tick都會被系統調用一次的更新窗口界面的方法 | 可 | 將邏輯更新寫在這裡. |
handleInput | 無參數 無返回值 |
public | 輪詢所有鼠標和鍵盤操作 | 可 | 會調用handleMouseInput和handleKeyboardInput方法 |
handleMouseInput | 無參數 無返回值 |
public | 處理鼠標操作 | 可,建議調用基類的原方法 | |
handleKeyboardInput | 無參數 無返回值 |
public | 處理鍵盤操作 | 可,建議調用基類的原方法 | |
onGuiClosed | 無參數 無返回值 |
public | 當窗口界面被關閉時引發 | 可 | 值得一提的是,onGuiClosed被調用不代表這個界面就永遠不會顯示了,有可能是遊戲打開了一個它的子界面,比如添加服務器界面(GuiScreenAddServer)在被關閉時是直接返回打開它的多人遊戲界面(GuiMultiplayer)的實例,而不是創建一個新實例. |
drawDefaultBackground | V修正值(int) 無返回值 |
public | 用於繪製背景,通常在drawScreen的一開頭便調用,不在遊戲中時,它是繪製泥土背景,在遊戲中時,它是繪製半透明的黑色背景,參數通常設為0 | 可,但不建議 | V修正值是泥土背景紋理的UV中的V的修正值,似乎沒有什麼用呢... |
doesGuiPauseGame | 無參數 返回是否要求暫停(boolean) |
public | 指示在單人遊戲下打開此窗口界面時是否暫停遊戲. | 可 | |
confirmClicked | 自定義參數A(boolean) 自定義參數B(int) 無返回值 |
public | 沒有固定作用,通常是供子界面通過回調confirmClicked來將結果反饋給父界面,因此參數值的作用完全是由開發者來約定 | 可 | 比如,父界面打開了一個詢問用戶(如GuiYesNo)的子界面,子界面通過它來將用戶的選擇反饋給父界面 |
isCtrlKeyDown | 無參數值 返回結果(boolean) |
static public |
返回用戶是否按下了Ctrl鍵 | 否 | |
isShiftKeyDown | 無參數值 返回結果(boolean) |
static public |
返回用戶是否按下了Shift鍵 | 否 |
打開一個界面
打開界面通過調用Minecraft類的displayGuiScreen方法來完成.如果傳入的參數是一個GuiScreen類或其子類的實例,那麼就關閉(調用onGuiClosed)當前界面(如果有的話)然後打開(調用initGui)新界面;如果是null的話,在遊戲中則關閉一切界面,未在遊戲中則是顯示主菜單.
順便再附一張displayGuiScreen方法顯示界面的流程,這是在1.2.5時代製作的圖...不過看上去和現在的流程依然相符.
創建一個新界面
一個界面即GuiScreen類的派生類,它至少包含如下要素:
構造函數 - 用於初始化部分數據
initGui - 部署控件
drawScreen - 繪製界面,文字,紋理,別忘記調用基類.
換句話說,一個最簡單的窗口類是這樣:
public class YourGui extends GuiScreen{ private GuiScreen parentScreen; public YourGui(GuiScreen parent) { parentScreen = parent; //記下是哪個界面打開了它,以便以後返回那個界面 //在這裡初始化與界面無關的數據,或者是只需初始化一次的數據. } public void initGui() { //每當界面被打開時調用 //這裡部署控件 } public void drawScreen(int par1, int par2, float par3) { drawDefaultBackground(); //在這裡繪製文本或紋理等非控件內容,這裡繪製的東西會被控件(即按鍵)蓋住. super.drawScreen(par1,par2,par3); //在這裡繪製文本或紋理等非控件內容,這裡繪製的東西會蓋在控件(即按鍵)之上. } }
不過還有個問題是我們該如何顯示這個界面,displayGuiScreen確實可以顯示,但該在哪調用呢...過去我在測試GUI界面時喜歡隨便改一個主菜單的按鈕,但現在不能隨便改了(雖然可以用FGOW...咳咳),不過Forge倒是提供了三個很好的事件來供我們魔改任意界面:InitGuiEvent.Pre,InitGuiEvent.Post和GuiOpenEvent.
InitGuiEvent.Pre事件是在調用界面的initGui之前引發,如果它被取消了(關於事件的取消參考Extra1),則initGui根本不會被調用,這個特性允許你可以自由地修改一個界面.(但是這裡有個設計隱患,前文提到過一個界面實例可能會被反覆打開,所以這個事件傳給你的buttonList可能已經有內容了.似乎設計者希望開發者每次都手動clear一遍?)
InitGuiEvent.Post事件則是在ininGui之後引發,你可以在此進一步修改界面.
如果簡單的修改無法滿足你的需要的話,一位開源貢獻者jk-5給我們提供了一個"一勞永逸"的銀彈:GuiOpenEvent,這個事件的厲害之處在於它不但能中止一個打開界面的行為(簡單地取消掉事件即可),還能偷梁換柱,替換掉將要打開的界面.
此外,Forge還提供了ActionPerformedEvent系列事件來監聽按鈕的點擊,ActionPerformedEvent也分Pre和Post兩個實際事件,不過它們的差別不大,僅僅是一個在actionPerformed調用前,一個在調用後,這裡我們使用InitGuiEvent.Post和ActionPerformedEvent.Post來為主菜單添加一個新按鈕.不過首先,我們要先做一個代理器(Proxy),用來執行客戶端操作,老讀者應該很熟悉代理器,因為原先(MC1.6之前)需要用代理器來註冊紋理,因此教程第二章里就講了這個,但後來因為沒什麼用就移除了...這裡為新讀者簡單介紹一下,代理器是個用來區分執行客戶端與服務器操作的東西,它的原理是FML會為客戶端和服務器分別實例化兩個類,其中客戶端的類必須是服務器的類的子類,因為客戶端的操作一般是"服務器操作+客戶端Only的操作",其實"客戶端Only"一般就是Gui之類的...因此接下來我們要創建一個代理器.
首先創建一個代理器類,我這裡服務器代理器叫CommonProxy,客戶端代理器叫ClientProxy,為了圖省事直接把它弄成CommonProxy的內部靜態類.同時為它加上init方法.
public class CommonProxy { public void init() {} public static class ClientProxy extends CommonProxy { @Override public void init() { //通常這裡還會有super.init() 不過這裡我們在服務器代理器中沒有操作,因此就省去了 } } }
然後在mod主類中加上:
@SidedProxy(modId="diracon", //此處為你的modid serverSide="net.hakugyokurou.diracon.CommonProxy", clientSide="net.hakugyokurou.diracon.CommonProxy$ClientProxy") public static CommonProxy proxy;
之後別忘了在mod主類的init或preInit中加入代理器的init調用...
(上次不知為何我忘了寫上這點...於是坑了不少人...)
@EventHandler public void preInit(FMLPreInitializationEvent event) { proxy.init(); }
然後在你的客戶端代理器中加入:
//不知道有沒有人能發現這個梗,能看出這個的出處的話那一定是相當厲害了 private GuiButton btnShowNewGui = new GuiButton(223, 0, 0, "To open the door, use the code: 0012"); @SubscribeEvent public void guiScreenShow(InitGuiEvent.Post event) { if(event.gui instanceof GuiMainMenu) { GuiScreen screen = event.gui; for(Object o : event.buttonList) { GuiButton btn = (GuiButton)o; if(btn.id == 4) //退出遊戲按鈕的id btn.xPosition = (int)(screen.width * 0.75); if(btn.id == 0) //選項按鈕的id { btn.yPosition = (int)(screen.height * 0.7); btn.xPosition = (int)(screen.width * 0.75); } } btnShowNewGui.xPosition = (int)(screen.width * 0.5) - 100; btnShowNewGui.yPosition = (int)(screen.height * 0.8); btnShowNewGui.width = 200; event.buttonList.add(btnShowNewGui); } } @SubscribeEvent public void guiClickButton(ActionPerformedEvent.Post event) { if(event.button == btnShowNewGui) { Minecraft mc = Minecraft.getMinecraft(); mc.displayGuiScreen(new YourGui(mc.currentScreen)); } }
然後在你的客戶端代理器init方法中加入:
MinecraftForge.EVENT_BUS.register(this);
測試一下,我們在主菜單最底部新增了一個按鈕,同時移動了原來的"設置"和"退出"按鈕的位置.不過新菜單什麼都沒有,顯示後僅僅只有一片泥土背景(如果是遊戲中顯示則是一片透明的黑色背景).你可以通過ESC鍵來返回主菜單.
文字的繪製
我不知道有多少人會像我最開始接觸圖形編程時那樣,認為繪製圖形只需要每幀繪製變化的部分就好,事實上,早期的計算機圖形確實是這樣的...這種做法被稱為臟矩形技術,用於在機能不足的環境上進行繪製,直至今日,仍有一些圖形程序(比如一些2D的手機遊戲或html5遊戲,以及大部分客戶端程序)使用這種"局部重繪"的方式.相比之下,現代的電腦遊戲則奢侈得多,幾乎所有的遊戲都是每次繪製時清空整個圖形緩衝,然後重新繪製一遍.
文字的繪製通過在drawScreen中調用drawString和drawCenteredString來完成,這裡我們在屏幕上添加一個歡迎文本以及顯示當前鼠標坐標的文本.
在drawScreen中的super.drawScreen下面加入:
drawCenteredString(fontRendererObj, "Your §f§nPROUD§r first screen", width/2, (int)(height*0.2), 0xFFFF00); drawString(fontRendererObj, String.format("You are pointing to: (§o%d§r,§o%d§r)", par1, par2), (int)(width*0.05), (int)(height*0.9), 0xFFFFFF);
按鈕控件
按鈕控件其實剛才我們已經使用了一次,不過這一次我們在界面類中編程可以更省事一些,不用費心思去寫事件監聽.接下來我們做一個退出到上一個界面的按鈕.
按鈕類最常使用的是GuiButton類,它也是其他專用按鈕類的父類,它有兩個構造函數,6參數版本是GuiButton(int id, int x, int y, int width, int height, String text),參數含義分別是"按鈕ID","左上角X坐標","左上角Y坐標","寬","高","文字內容",4參數版本省略了寬和高,使用的默認值是200,20.
在YourGui類中加入:
private GuiButton btnClose; @Override protected void actionPerformed(GuiButton button) { if(button == btnClose) //改成button.id==0也行 mc.displayGuiScreen(parentScreen); }
在initGui方法中加入:
buttonList.add(btnClose = new GuiButton(0, (int)(width*0.75), (int)(height*0.85), 80, 20, "關閉"));
文本框控件
文本框的使用比較麻煩,它涉及到兩個問題,一個是控件焦點,如果界面中只有一個文本框控件的話還比較好辦,如果有多個的話,你得判斷當前是在哪個文本框中輸入;另一個問題是長按按鍵輸入問題,默認的界面是關閉鍵盤連續輸入的,你需要在初始化界面時打開輸入,還得在關閉界面時關閉輸入.
文本框的構造函數有5個參數,第一個是字體渲染器,一般直接使用Gui類自帶的fontRendererObj,另外四個參數是"X坐標","Y坐標","寬","高".
在YourGui類中加入:
private GuiTextField tfInput; @Override protected void keyTyped(char par1, int par2) { if(tfInput.textboxKeyTyped(par1, par2)) //向文本框傳入輸入的內容 return; super.keyTyped(par1, par2); } @Override protected void mouseClicked(int par1, int par2, int par3) { tfInput.mouseClicked(par1, par2, par3); //調用文本框的鼠標點擊檢查 super.mouseClicked(par1, par2, par3); } @Override public void onGuiClosed() { Keyboard.enableRepeatEvents(false); //關閉鍵盤連續輸入 }
在initGui方法中加入:
Keyboard.enableRepeatEvents(true); //打開鍵盤連續輸入 tfInput = new GuiTextField(fontRendererObj, (int)(width*0.5)-150, (int)(height*0.85), 300, 20); tfInput.setMaxStringLength(64); //設置最大長度,可省略 tfInput.setFocused(false); //設置是否為焦點 tfInput.setCanLoseFocus(true); //設置為可以被取消焦點
在drawScreen方法的super.drawScreen下面加入:
tfInput.drawTextBox();
文本框可通過getText來獲取內容.
紋理的繪製
之前我們提到過,我們通過Gui類的drawTexturedModalRect方法來繪製紋理,使用它繪製紋理需要先向渲染引擎中綁定紋理.
首先你要有一個紋理,先找一張256x256尺寸的圖片,然後在你的mod的素材目錄中找個地方複製過去,我習慣放在texture/gui中.這裡我選了一張名稱叫texture的png圖片.
然後需要添加紋理,在YourGui類中添加:
private ResourceLocation texture = new ResourceLocation("diracon", "textures/gui/texture.png"); //第一個參數是modid
然後在drawScreen方法的super.drawScreen上面加入:
mc.renderEngine.bindTexture(texture); //綁定紋理 drawTexturedModalRect((int)(width*0.5)-128, 0,0,0,256,256); //繪製一個256x256的紋理
如你所見...所有在super.drawScreen之前渲染的內容都會被按鈕控件遮蓋.此外你也可能注意到紋理的最下部分超出屏幕邊界了,換句話說MC在默認屏幕大小和默認GUI尺寸下,窗口高度在內部邏輯上是小於256的...大約只有239左右.
另外,drawTexturedModalRect方法有個缺陷,就是它只能"正確"繪製256x256尺寸的紋理,這是因為它的一個係數是假定紋理都是256x256尺寸的,為了繪製任意尺寸的圖片,我們可以使用func_146110_a(MC1.7),或者叫drawModalRectWithCustomSizedTexture(MC1.8)...
弄一個新的紋理,這裡我找了個尺寸為1440x900的jpg圖片.改好ResourceLocation,然後將drawTexturedModalRect替換為:
func_146110_a(0, 0, 0, 300, width, height, 1440, 900); //中間的0,300是UV偏移,根據你的紋理隨意定吧.1440,900同理.
瞬間我們的界面有了一種國產啟動器的氣勢,不過看上去圖片有些礙眼,但我們可以再渲染一個半透明的矩形.在func_146110_a下面加入:
drawRect((int)(width*0.1), (int)(height*0.1), (int)(width*0.9), (int)(height*0.7), 0x80FFFFFF);
然後再啟動看看.這次又多了一股Metro風,納德拉先生會自豪的,快拿去給巨軟安利一下,說不定MC1.9就變成Metro風格了...我先去擼一管壓壓驚
順便一提,drawModalRectWithCustomSizedTexture不能縮放繪製出的紋理,對此我們可以使用drawScaledCustomSizeModalRect,或func_152125_a,不過有個小問題是這個方法到1.7.10時才出現,如果要在1.7.2下用的話,可以自己把代碼複製到你的界面類中:
public static void drawScaledCustomSizeModalRect(int x, int y, float u, float v, int uWidth, int vHeight, int width, int height, float tileWidth, float tileHeight) { float f4 = 1.0F / tileWidth; float f5 = 1.0F / tileHeight; Tessellator tessellator = Tessellator.instance; tessellator.startDrawingQuads(); tessellator.addVertexWithUV((double)x, (double)(y + height), 0.0D, (double)(u * f4), (double)((v + (float)vHeight) * f5)); tessellator.addVertexWithUV((double)(x + width), (double)(y + height), 0.0D, (double)((u + (float)uWidth) * f4), (double)((v + (float)vHeight) * f5)); tessellator.addVertexWithUV((double)(x + width), (double)y, 0.0D, (double)((u + (float)uWidth) * f4), (double)(v * f5)); tessellator.addVertexWithUV((double)x, (double)y, 0.0D, (double)(u * f4), (double)(v * f5)); tessellator.draw(); }
將func_146110_a替換為:
drawScaledCustomSizeModalRect(0, 0, 0, 0, 1440, (int)(1440*(height/(float)width)), width, height, 1440, 900); //參數分別為x,y,u,v,u寬度,v高度(即紋理中欲繪製區域的寬高),實際寬,實際高,紋理總寬,紋理總高.
然後再看看效果,已經達到做GalGame的水平了.
在遊戲中打開界面
目前我們是在主菜單中打開界面,如果我們希望在遊戲中通過按鍵打開界面的話,可以通過監聽按鍵事件來實現.
(如果你想修改遊戲中的ESC界面的話,按剛才魔改主菜單的方式魔改GuiIngameMenu就行了)
過去FML提供了KeyBindingRegistry和KeyHandler來監聽鍵盤事件,後來從MC1.7開始,FML改為使用更輕量級的KeyInputEvent事件來監聽鍵盤操作.不過有一點需要注意,就是在MC1.7中,KeyInputEvent使用的是FML的遊戲事件總線,而不是Forge的.
在客戶端代理器的init方法中加入:
FMLCommonHandler.instance().bus().register(this);
然後在客戶端代理器中加入:
@SubscribeEvent public void keyListener(KeyInputEvent event) { if (Keyboard.getEventKey() == Keyboard.KEY_K) //獲取按下的按鍵並判斷 { Minecraft mc = Minecraft.getMinecraft(); mc.displayGuiScreen(new YourGui(mc.currentScreen)); } }
然後在測試前,先將YourGui類的drawScreen中的繪製紋理(drawScaledCustomSizeModalRect)注釋掉,不過待會它會擋住遊戲背景.
之後進入遊戲測試,在遊戲中按K鍵就能打開新界面.
GuiScreen默認在單人遊戲時會暫停遊戲,你可以在YourGui類中重寫doesGuiPauseGame來阻止暫停遊戲.
@Override public boolean doesGuiPauseGame() { return false; }
不過,使用剛才那種監聽按鍵的方式沒法讓玩家更改按鍵,因此我們可以使用一個更高大上的東西:KeyBinding.
KeyBinding即遊戲中的按鍵綁定,它可以在遊戲設置中更改,KeyBinding的構造函數有3個參數,分別是"文本Key","默認按鍵的Keycode"和"分類Key","文本Key"是這個按鍵的文字描述在語言文件中的鍵,"默認按鍵的Keycode"就是默認按鍵的鍵盤值,比如上文的Keyboard.KEY_K,"分類Key"就是按鍵分類在語言文件中的鍵.
KeyBinding在實例化後需要通過ClientRegistry.registerKeyBinding來註冊,之後通過調用實例的isPressed來獲取是否按下,監聽位置可以選擇監聽KeyInputEvent,也可以選擇監聽TickEvent,甚至是在任何地方執行都行,因為KeyBinding能累計按鍵的狀態.由於這個特性,有時我們在調用isPressed時會採用這種寫法:
while(keyBinding.isPressed()) { ... }
這樣,如果玩家在一個tick內按下多次按鍵的話,這種寫法能夠全部捕捉到.需要注意的是,isPressed的實現原理是在KeyBinding內部有一個計數器來累計按鍵的按下次數,調用isPressed其實就是判斷計數器是否大於0(如果大於0則還要遞減),如果你想規避這個特性,可以使用getIsKeyPressed.
那麼接下來我們就把原來的系統替換為使用KeyBinding來實現,在客戶端代理器中加入:
public static final KeyBinding kbShowNewGui = new KeyBinding("diracon.key.shownewgui", Keyboard.KEY_K, "diracon.keytitle");
在客戶端代理器的init中加入:
ClientRegistry.registerKeyBinding(kbShowNewGui);
然後將keyListener中的Keyboard.getEventKey() == Keyboard.KEY_K替換為:
kbShowNewGui.isPressed()
然後還有添加按鍵文本,按照基礎篇的方法添加語言文件,然後加入:
diracon.keytitle=Mod Keys diracon.key.shownewgui=Show screen
進入遊戲測試一下,你可以在按鍵設置中找到新設置的按鍵.
如果要做到像E鍵打開的物品欄界面那樣,在界面中按相同的鍵就可以關閉菜單,可以通過重寫GuiScreen類的keyTyped.
在YourGui類的keyTyped的super.keyTyped(par1, par2)的上面加入:
if(mc.theWorld != null && par2 == ClientProxy.kbShowNewGui.getKeyCode()) //mc.theWorld!=null是判斷當前是否在遊戲中 mc.displayGuiScreen(parentScreen);
然後從initGui中刪掉Keyboard.enableRepeatEvents(true),在mouseClicked中的tfInput.mouseClicked(par1, par2, par3)的下面加入:
if(tfInput.isFocused()) Keyboard.enableRepeatEvents(true); //只有當輸入框成為焦點時才打開鍵盤連續輸入,這是為了防止玩家打開菜單時按住按鍵不放,從而導致菜單打開後又立刻關閉的情況. else Keyboard.enableRepeatEvents(false);
之後再測試一下,你現在已經可以像物品欄界面那樣,按K鍵直接關閉新界面了.
在遊戲中繪製
之前我們提到Minecraft的HUD類是GuiIngame,實際上安裝了Forge的MC使用的HUD是GuiIngameForge,Forge的這層"暴力封裝"為我們修改HUD提供了很大的便利.
所有與HUD繪製相關的事件都位於RenderGameOverlayEvent類中,具體的有如下四種:
RenderGameOverlayEvent.Pre:繪製一個元素前發布,取消的話則不會繪製此元素
RenderGameOverlayEvent.Post:繪製完一個元素後發布
RenderGameOverlayEvent.Text:繼承自Pre,主要用來繪製左右兩邊的文字信息,比如Debug信息,還有Demo版信息.
RenderGameOverlayEvent.Chat:繼承自Pre,用來繪製聊天信息
HUD元素有如下幾種:
元素名 | 說明 | 備註 |
---|---|---|
ALL | 代表繪製整個HUD之前和之後 | 如果在pre中取消的話,那麼整個HUD都不會被繪製了... |
HELMET | 南瓜的頭盔效果或物品的renderHelmetOverlay | |
PORTAL | 站在下界傳送門時屏幕的紫色效果 | 不包括視角模糊的效果... |
CROSSHAIRS | 準星 | |
BOSSHEALTH | Boss的血條 | |
ARMOR | 裝甲值 | |
HEALTH | 生命值 | |
FOOD | 飽腹度 | |
AIR | 氧氣值 | |
HOTBAR | 下方的物品欄 | |
EXPERIENCE | 經驗值 | |
TEXT | 顯示在屏幕左側和右側的文本,比如Debug內容 | 沒有Pre |
HEALTHMOUNT | 坐騎的生命值 | |
JUMPBAR | 馬的跳躍蓄力 | |
CHAT | 聊天信息 | 沒有Pre |
PLAYER_LIST | Tab鍵的玩家列表 | |
DEBUG | Debug信息 |
這裡我們演示在遊戲中以文字形式顯示玩家生命值,同時加入一行新的文字,在客戶端代理器中添加:
@SubscribeEvent public void playerHealth(RenderGameOverlayEvent.Pre event) { if(event.type == ElementType.HEALTH) { event.setCanceled(true); //取消掉事件來阻止原圖標的繪製 int width = event.resolution.getScaledWidth(); int height = event.resolution.getScaledHeight(); Minecraft mc = Minecraft.getMinecraft(); String hp = String.format("Health: %d/%d", MathHelper.ceiling_float_int(mc.thePlayer.getHealth()), MathHelper.ceiling_double_int(mc.thePlayer.getEntityAttribute(SharedMonsterAttributes.maxHealth).getAttributeValue())); FontRenderer fontRenderer = mc.fontRenderer; fontRenderer.drawStringWithShadow(hp, width / 2 - 91, height - GuiIngameForge.left_height, 0xFFFFFF); //字體渲染器在渲染時會重新綁定到字型紋理上,由於一些"編程失誤",HUD在下一步繪製時不會重新綁定紋理,因此需要我們在此手動綁定. mc.renderEngine.bindTexture(Gui.icons); } } @SubscribeEvent public void playerName(RenderGameOverlayEvent.Text event) { event.left.add(0, String.format("Welcome, %s!", Minecraft.getMinecraft().thePlayer.getCommandSenderName())); }
進入遊戲觀察一下效果,遊戲的左上角會有一行歡迎文字(它還不會被Debug信息遮蓋住),原來圖標形狀的生命條被替換成了文字形式.
繪圖器的使用
之前我們一直在使用Minecraft的上層API,現在我們要使用MC中最底層的API之一,與OpenGL只有一線之隔的繪圖器(Tessellator).
想從名字上了解Tessellator的作用幾乎是不可能的,因為它的直譯細分器與圖形學上的細分技術八竿子打不着,我開始以為這是MCP中某個人隨便起的名字,後來發現Tessellator的一個異常信息中有一句"Not tesselating",可見"Tessellator"就算不是它真正的名字,也八九不離十.
它的真正作用是對OpenGL的立即模式的一個高效率封裝,立即模式是OpenGL最早的繪圖模式,通過CPU逐頂點地發送數據給顯卡(按OpenGL的專業術語是客戶端向服務器發送,不過既然我們還沒說到OpenGL,就先用通俗的畫來解釋)來繪製,在那個"巫毒和炸藥"橫飛的年代(老前輩們應該熟悉那時候獵奇的顯卡名字),通過立即模式來繪製寥寥幾百個多邊形組成的場景並不顯慢,無論是對那些剛剛被從軟件渲染中解放出來的玩家,還是對不用再自己徒手寫光柵器的程序員來說,都足夠了.而且即使再不濟,OpenGL還提供了顯示列表(Display List),它可以將一組固定的OpenGL指令和它們的參數進行化簡.
然而隨着硬件機能和業界門檻的提升,逐頂點地發送指令成為了水桶中最短的那塊板,比如這段C代碼是繪製一個平面:
glBegin(GL_QUADS); //開始 glNormal3f(-1,0,0); //繪製 glTexCoord2f(1,1);glVertex3f(1,1,0); glTexCoord2f(1,0);glVertex3f(1,0,0); glTexCoord2f(0,0);glVertex3f(0,0,0); glTexCoord2f(0,1);glVertex3f(0,1,0); glEnd(); //結束
你應該可以想象到它是有多麼累贅,對一個由有紋理有法線的三角形組成的多邊形來說,每頂點平均需要2.3條指令.顯示列表雖然可以高效繪製,但它的數據是固定的,對於一個動態的模型,比如一個正在行走的人,就無法使用顯示列表了.
於是OpenGL從1.5開始提供了頂點數組(Vertex Array,簡稱VA),它引入了頂點索引的概念,然後允許直接把頂點數據存儲在內存(不是顯存)中,當需要繪製多邊形時,只要傳入各個頂點數組的指針就能完整繪製整個多邊形,省去了函數調用的開銷,再配合顯示列表(那些指針是固定的,因此可以作為參數儲存在顯示列表中),可以實現更高效的繪製.
另外OpenGL從3.0開始,有了更魔性的繪製方式:VertexBufferObject(簡稱VBO)和VertexArrayObject,它們是直接把數據存儲在顯存中...
Tessellator就是一個通過VA來提高效率,操作方式模仿OpenGL立即模式的工具,其實早期的Tessellator還會在顯卡支持VBO的時候啟用VBO進行繪製,然而MC從1.7開始卻放棄VBO,只使用VA了,似乎Mojang認為MC的渲染效率瓶頸不在渲染方式上.
Tessellator可以直接使用它的靜態實例,也可以自己創建一個實例,正如上面立即模式下的OpenGL繪圖代碼一樣,Tessellator也分為開始,繪製,結束這三個階段.
開始階段的方法是startDrawing和startDrawingQuads. startDrawing的參數可以是:
GL_POINTS
GL_LINE_STRIP
GL_LINE_LOOP
GL_LINES
GL_TRIANGLE_STRIP
GL_TRIANGLE_FAN
GL_TRIANGLES
GL_QUAD_STRIP
GL_QUADS
GL_POLYGON
(注:它們位於GL11類中.)
關於它們的定義可以參考http://www.cnblogs.com/minggoddess/archive/2010/12/15/1907175.html和http://www.opentk.com/doc/chapter/2/opengl/geometry/primitives.其中最常用的是GL_TRIANGLES和GL_QUADS. startDrawingQuads就相當於startDrawing(GL11.QL_QUADS).
在繪製階段,有這些方法可用:
addVertex:繪製一個頂點,相當於glVertex3d. (給不了解OpenGL的人科普一下,這裡的3d不代表三維,而是參數為3個double...下面的2d同理)
addVertexWithUV:繪製一個頂點並指定它的UV,相當於glTexCoord2d+glVertex3d(但參數順序是先坐標,後UV).
setTextureUV:設置接下來繪製的頂點的UV,相當於glTexCoord2d.需要注意的是,這裡的UV的範圍是0.0~1.0,0.0相當於0(原點),1.0相當於最邊沿.
setColorOpaque_F:設置接下來繪製頂點的RGB顏色,相當於glColor3f.需要注意的是,顏色的範圍在0.0~1.0,1.0相當於255(純色)
setColorRGBA_F:設置接下來繪製頂點的RGBA顏色,相當於glColor4f.
setColorOpaque:setColorOpaque_F的整數版,範圍在0~255.
setColorRGBA:setColorRGBA_F的整數版.
setColorOpaque_I:類似setColorOpaque,但把RGB按0x00RRGGBB的格式壓縮在一個整數中作為參數.
setColorRGBA_I:類似setColorOpaque_I,但多了個Alpha.
disableColor:調用此方法後,以上的跟顏色相關的方法將不再生效.感覺是很不常用的東西...
setNormal:設置接下來繪製的頂點的法線,相當於glNormal3f.
setTranslation:設置接下來繪製的頂點的原點位置,相當於在模型視角矩陣中先glLoadIdentity再glTranslated.
addTranslation:偏移接下來繪製的頂點的原點位置,相當於在模型視角矩陣中glTranslatef.
setBrightness:設置自身光照亮度,這是個新鮮玩意,不過要留到以後再說,畢竟2D繪製幾乎用不到它,而且這裡篇幅有限嘛...它的參數的計算公式是:([環境光照強度] << 20 | [自身發光強度] << 4),光強度範圍是0~15.因此,如果參數是15728880的話, It's gonna be a sunny day at Area 51...
此外還有個setVertexState和getVertexState,用於直接設置或讀取Tessellator的狀態,正常情況下Tessellator的狀態是可以自動推導的,因此這兩個主要是用來debug的...
最後,我們在結束階段通過調用draw來結束繪製.
那麼接下來我們就用繪製器來畫一個靜態的透明2D圖片本章的結尾.還記得YourGui類中被我們注釋掉的背景圖片嗎?現在我們要重做它,讓它在遊戲中以半透明的形式渲染.
在YourGui類中的drawScreen,將那行被注釋掉的繪製紋理命令替換成:
final int textureWidth = 1440; //紋理的寬 final int textureHeight = 900; //紋理的高 float vHeight = 1440*(height/(float)width); float factorX = 1.0F / textureWidth; //將UV從整數歸一化到實數的係數. float factorY = 1.0F / textureHeight; GL11.glEnable(GL11.GL_BLEND); //打開OpenGL混合模式 Tessellator tessellator = Tessellator.instance; tessellator.startDrawingQuads(); if(mc.theWorld != null) //如果是在遊戲中,則修改顏色,增加透明效果. tessellator.setColorRGBA(255, 255, 255, 64); tessellator.addVertexWithUV(0 , height, 0, 0 , vHeight*factorY); tessellator.addVertexWithUV(width, height, 0, textureWidth*factorX, vHeight*factorY); tessellator.addVertexWithUV(width, 0 , 0, textureWidth*factorX, 0 ); tessellator.addVertexWithUV(0 , 0 , 0, 0 , 0 ); tessellator.draw(); GL11.glDisable(GL11.GL_BLEND); //用完了別忘了關掉
你會注意到這裡面亂入了個glEnable和glDisable,這兩個是用來打開和關閉OpenGL狀態的,你以前可能也聽說過OpenGL是一個狀態機,它的狀態就是通過這兩個指令來開關,OpenGL的介紹要到以後才有,但考慮到有些人可能讀完這一章就足夠了,而且有沒有下一章還是一回事...因此這裡還列出了在繪製MC2D界面時可能會用到的狀態:
GL_BLEND 混合,計算機實現透明的原理是將透明物體的顏色與背景底物按比例混合,混合有多種算法,不過最常用的是glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA)(標準的混合效果,透明物體的Alpha越大越不透明)和glBlendFunc(GL11.GL_ONE, GL11.GL_ONE)(簡單地將物體和背景的顏色相加,用來實現某些特殊效果).
GL_DEPTH_TEST 深度測試,深度測試用於判斷物體的遮掩關係,在2D繪圖中用不到深度測試,因為設計者總是希望後繪製的覆蓋原來繪製的.如果你發現你按順序繪製的東西莫名其妙地被遮住的話,嘗試關掉深度測試.
GL_ALPHA_TEST Alpha測試,Alpha測試用來篩選具有特定Alpha值的像素,通常我們也用不到...如果你發現你正確設置了混合,而繪製出的透明紋理卻不見了的話,嘗試關掉Alpha測試.
GL_CULL_FACE 背面消隱,OpenGL區分正反面,而正反面由圖元頂點的繪製順序來決定,其中背面可以被簡單理解為"不會被渲染",如果你不喜歡這個特性,就關掉它.
GL_TEXTURE_2D 使用紋理,有時候當我們想繪製一個純色時,可以用glBindTexture(GL_TEXTURE_2D, 0)來取消紋理綁定,不過一個更有效率的方式是直接關閉掉紋理效果.
於是Gui教程就此結束了,拖了一年半的坑終於填上了!當然,以後還有新的坑要填...
附錄 - 關於Delta
眾所周知,Minecraft的Tick是每秒20次,這意味着每秒只有20次邏輯更新,然後渲染卻是每秒60次或120次,怎麼才能保證運動物體能夠平滑移動?這便是Delta的存在意義,Delta表示"本次渲染距離上一個Tick相比經過了多少個Tick間隔的時間",比如在Minecraft的每秒20次Tick時間隔為50毫秒,那麼Delta為0.2時,代表本次渲染是發生在上一次Tick之後的第50*0.2=10毫秒.
有了Delta後,遊戲就可以對運動物體進行插值渲染了,比如如果在Tick1時物體在點P1以速度V進行運動,那麼毫無疑問Tick2時物體在P1+V的位置,於是當一次渲染在Tick1和Tick2之間時,物體的位置就是在P1+V*delta.
不過,Minecraft使用的差值方式卻大多採用P0+(P1-P0)*delta,因此實際上,MC渲染的始終是上一Tick的狀態...或許他們認為這樣插值移動更平滑一些.
遊戲中,插值已經在中間層進行了,因此通常來說在Renderer中不用進一步自己手動插值.
運行環境 1.7.10
博主你好 = = MinecraftForge.EVENT_BUS.register(this); 這一句一旦執行就出錯哎
我在guiScreenShow中加了輸出 還有ClientProxy 的 init 裡面加了輸出 這兩個函數都正常的走完的
但是這一句的輸出卻沒有
public void guiClickButton(ActionPerformedEvent.Post event) {
if (event.button == btnShowNewGui) {
System.out.println("新的按鈕!");
Minecraft mc = Minecraft.getMinecraft();
System.out.println("獲取屬性!");
mc.displayGuiScreen(new mygui(mc.currentScreen));
System.out.println("設置新的gui!");
}
}
}
然後就出錯崩潰了= =
sz大大你好~這裡有兩個問題想要請教一下orz
1.如果我要繪製一張圖,現在有IntBuffer的pixelBuffer(其實是從ScreenShotHelper里改過來的……)。我想要實現的效果是按下某個按鍵後將這張圖片繪製在屏幕(特定的GuiScreen)上,但使用與圖片相符的GL11.glDrawPixels參數以後得到的跟沒畫沒什麼區別……不大懂OpenGL……
2.如果我要重寫某一個特定的鍵盤事件(比如F2截屏我想把它直接替換掉)有什麼方法嗎……
沒用過glDrawPixels...我一般遇到這種情況是直接製成紋理再繪製 _(:з」∠)_ 想魔改鍵盤事件也挺麻煩,截屏事件是在Minecraft類中的dispatchKeypresses里,不用ASM的話也許只能魔改keyBindScreenshot,不過會影響到MC的鍵位綁定,如果玩家一打開設置就露餡了 233
為什麼通過熱鍵打開GUI在客戶端能夠運行,一放到服務器(我用的KCauldron-1.7.10-1614.188開服端)里怎麼按都沒反應?貌似ClientProxy里的內容就沒加載。還有,如果要通過玩家名字獲得對應的玩家實體,應該怎麼做?