MCP&Forge的Mod教程 Extra編(4) – Gui

"知識的形成與權力的鞏固總是相輔相成..."
-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個參數作用的圖解.
E4-1
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便能查看它的所有派生類.

gui1
圖為1.6.4的GUI派生類,在1.7.2中,多了個GuiLabel.

在MC1.6.4中,Gui類有8個直接的派生類,這8個類及其子類可分為如下三種:

  1. HUD
    這個名詞有點扯...其實叫UI(用戶界面)也是可以的,它是指玩家在遊戲過程中看到的那些文字圖標,比如下方的物品欄和生命值飢餓度,左上角的版本信息和除錯信息(如果有的話).GuiIngame類及其子類GuiIngameForge類屬於HUD.
  2. 窗口界面
    窗口界面是指一個玩家能夠進行交互的界面,比如主菜單界面,多人模式下的服務器選擇界面,或者遊戲中玩家按下E鍵打開的物品欄窗口,打開箱子顯示的庫存窗口,窗口界面與HUD的不同之處在於窗口界面是可以打開關閉的,而HUD只要玩家在正常遊戲便會始終顯示.GuiScreen類及其派生類屬於窗口界面.
  3. 控件
    控件通常無法獨立存在,往往是依附在窗口界面中,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 在顯示窗口時是否允許玩家等 先別激動,這個選項沒法單獨工作(看上去是個編程失誤).表面上看當allowUserInput為真時,即使玩家開着窗口,也能繼續進行移動等鼠標鍵盤操作(1.7.2的Minecraft類第1716行),但實際上這種情況默認是不存在的...因為所有的輸入事件已經在之前(Minecraft類第1676行以及GuiScreen類的handleInput方法)被處理掉了,要想讓這個選項生效,需要重寫handleInput方法,在allowUserInput為真時跳過所有Mouse.next和Keyboard.next.或許它還有些其他用途,但我沒發覺?
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時代製作的圖...不過看上去和現在的流程依然相符.

E4-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() 不過這裡我們在服務器代理器中沒有操作,因此就省去了
		}
	}
}

E4-15

然後在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);

E4-16

測試一下,我們在主菜單最底部新增了一個按鈕,同時移動了原來的"設置"和"退出"按鈕的位置.不過新菜單什麼都沒有,顯示後僅僅只有一片泥土背景(如果是遊戲中顯示則是一片透明的黑色背景).你可以通過ESC鍵來返回主菜單.

E4-3

E4-4

文字的繪製

我不知道有多少人會像我最開始接觸圖形編程時那樣,認為繪製圖形只需要每幀繪製變化的部分就好,事實上,早期的計算機圖形確實是這樣的...這種做法被稱為臟矩形技術,用於在機能不足的環境上進行繪製,直至今日,仍有一些圖形程序(比如一些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);

E4-6

按鈕控件

按鈕控件其實剛才我們已經使用了一次,不過這一次我們在界面類中編程可以更省事一些,不用費心思去寫事件監聽.接下來我們做一個退出到上一個界面的按鈕.

按鈕類最常使用的是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, "關閉"));

E4-7

文本框控件

文本框的使用比較麻煩,它涉及到兩個問題,一個是控件焦點,如果界面中只有一個文本框控件的話還比較好辦,如果有多個的話,你得判斷當前是在哪個文本框中輸入;另一個問題是長按按鍵輸入問題,默認的界面是關閉鍵盤連續輸入的,你需要在初始化界面時打開輸入,還得在關閉界面時關閉輸入.

文本框的構造函數有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();

E4-8

文本框可通過getText來獲取內容.

紋理的繪製

之前我們提到過,我們通過Gui類的drawTexturedModalRect方法來繪製紋理,使用它繪製紋理需要先向渲染引擎中綁定紋理.

首先你要有一個紋理,先找一張256x256尺寸的圖片,然後在你的mod的素材目錄中找個地方複製過去,我習慣放在texture/gui中.這裡我選了一張名稱叫texture的png圖片.

E4-10

然後需要添加紋理,在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的紋理

E4-9

如你所見...所有在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同理.

E4-11

瞬間我們的界面有了一種國產啟動器的氣勢,不過看上去圖片有些礙眼,但我們可以再渲染一個半透明的矩形.在func_146110_a下面加入:

drawRect((int)(width*0.1), (int)(height*0.1), (int)(width*0.9), (int)(height*0.7), 0x80FFFFFF);

E4-12

然後再啟動看看.這次又多了一股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高度(即紋理中欲繪製區域的寬高),實際寬,實際高,紋理總寬,紋理總高.

E4-13

然後再看看效果,已經達到做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鍵就能打開新界面.

E4-14

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

進入遊戲測試一下,你可以在按鍵設置中找到新設置的按鍵.

E4-17

如果要做到像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()));
}

E4-18

進入遊戲觀察一下效果,遊戲的左上角會有一行歡迎文字(它還不會被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.htmlhttp://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)來取消紋理綁定,不過一個更有效率的方式是直接關閉掉紋理效果.

E4-19

於是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中不用進一步自己手動插值.