本帖最后由 szszss 于 2014-3-24 01:02 编辑

MCPMod制作教程 Extra(1)
GUI的制作
作者:szszss

在新版MC中有些内容发生了变更,如controlList改名为buttonList,bindTexture可以直接将纹理地址作为参数等.
索引贴网址:
(没看过基础篇的...请先看教程的基础篇)

称不上序的序
蛋疼的三月就那样度过了
四月也一晃就过了
五月都过了一半了...
教程也该发了吧...
>>>>我才是正文<<<<
基础篇发布没几天后,便有人吐槽没有写Gui的制作.
不过由于3月太忙所以搁置了教程的编写.
现在有时间了,于是便编写了GUI的教程.
(结果耽误到现在还没写完-w-...)

知识点:何为GUI?
///////////////////////////////////////////////////////////////////////////////////////////////////////////
GUI即图形用户界面,游戏中你在屏幕上看到的血条,经验条,准星,物品栏,全都是GUI.
游戏中的GUI主要体现为2,UI(用户界面)和对话框(Dialog)


Minecraft拥有一套被称为"渲染器"(Render)的东西,渲染器系统负责绘制游戏内的一些事物,GUI主要由系统的渲染引擎(RenderEngine)和字体渲染(FontRenderer)来负责(其实还有个LoadingScreenRenderer用于渲染载入画面,不过这不重要)负责绘制.

渲染器的原理我们还不必了解,MC中的GUI类提供了快捷的绘制线段和纹理的方法.一切界面类都直接或间接继承自GUI,它包含了这几种方法.
drawHorizontalLine  绘制水平线段,极少被使用...
drawVerticalLine    绘制垂直线段,也很少被使用.
drawRect(x1,y1,x2,y2,c)  绘制矩形,x1y1是左上点,x2y2是右下点(如果搞反了也没关系,系统会为你修正) c是一组8位的十六进制数字,代表一个ARGB颜色.
(何为ARGB颜色?ARGBAlpha(透明度)Red(红色)Green(绿色)Blue(蓝色).用这3种颜色和1种属性几乎能会绘制出世界上任意一种颜色.每一种颜色/属性对应一个2位的十六进制数字,即十进制的0~255.一组ARGB颜色的格式形如0xOOOOOOOO,如聊天框的颜色0x80000000即一个透明度为128的纯白框.)
drawGradientRect(x1,y1,x2,y2,c1,c2) 绘制一个渐变色矩形.
drawCenteredString(FontRenderer,str,x,y,c) 以点(x,y)为中心绘制文本str.使用文字渲染器FontRenderer,使用RGB颜色c.(RGB颜色和ARGB相比没有透明度信息,即你无法绘制透明文字)
(事实上它不是绘制在中心,我的语文水平很难表述出它的位置...所以我作了个图,红点是点(x,y),黑框是文本)

drawString(FontRenderer,str,x,y,c)  以点(x,y)为左上角会置文本.
drawTexturedModalRect(x,y,u,v,w,h) 截取渲染引擎中绑定的纹理,并渲染到屏幕上.这个是最常用的方法.
它的使用很特殊,你需要向渲染引擎绑定纹理,首先你需要使用渲染引擎的getTexture方法来载入一个纹理并获得它的ID,之后你要将ID作为参数来调用渲染引擎的bindTexture方法来将它绑定上.绑定完毕后你便可以使用drawTexturedModalRect来绘制纹理.这里有6个参数作用的图解.

x,y : 将要在用户屏幕上绘制的纹理的左上角坐标.
u,v : 被绑定纹理中,需要绘制部分的左上角坐标.
w,h : 纹理的宽和高.
绘制的流程可以这样理解:
渲染引擎会截取被绑定的纹理,将纹理中一个以(u,v)为左上角,(u+w,v+h)为右下角的矩形区域截下来,并将其绘制到用户屏幕上,其左上角位于点(x,y).

GUI类仅仅提供绘图功能,你无法在屏幕上直接绘制一个GUI,你只能绘制它的派生类.下面我列出了一幅GUI类派生关系图.

很高能吧...这个是1.2.5版中MC所有GUI类的关系图.它准确地反映了所有类的继承关系.
GUI类 一切类的基类 不可用来直接绘制.

GUIScreen类派生自GUI,GUIScreen代表一个窗口界面,比如游戏中的主菜单或冶炼窗口.

GuiButton类派生自GUI,它是按钮控件,它与它的派生类都可以以控件的形式添加在界面类(GuiScreen的派生类),具体的使用方法我们之后再说.顺便一提,GuiSlider类是滑块控件.

GuiSlot类派生自GUI,GuiSlot代表一个槽,例如物品栏中的物品图标,多人大厅的服务器选择菜单中的项...这个东西用的并不多.

GuiAchievement类是游戏成就的图标.基本不用管它...(其实是我懒得研究它了= =真的用不上)它同样派生自GUI.

GuiTextField类是文本输入框.派生自GUI.注意它是非控件类,在界面中显示GuiTextField的方法比较特殊.

GuiPlayerInfo类代表玩家信息,它是独立的一个类,不派生自任何类.因为它其实和GUI没有任何关系,它代表一个玩家的多人联机信息(名字和延迟).它真的和GUI一点关系没有...
(作者不负责任的猜想:很可能这是MCP组在反向工程时犯下的错误,MCP组在对MC进行反向工程时需要一个个猜类的作用并为其编上名字.有些类/字段/方法的名字就猜错了...还有的是没有猜出来.很显然MCP组对GuiPlayerInfo没有太认真看.)
有谁有兴趣向MCP组反映这个错误呢? ^ ^

GuiIngame类派生自GUI,它是游戏界面,即玩家看到的血条等...
///////////////////////////////////////////////////////////////////////////////////////////////////////////

本来我还是打算保持基础教程中以范例和原理相结合的方式...但我发现这么写太费精力了...所以我决定才用详写知识点和原理的方式.

知识点:建立一个继承自GuiScreen的类
///////////////////////////////////////////////////////////////////////////////////////////////////////////
GuiScreen类是界面类或者说是菜单类,它可以是类似设置菜单的那种界面.也可以是类似物品栏的界面.

GuiScreen具有下列方法,大部分都可以被重写.

GuiScreen()
即它本身的构造函数...GuiScreen及其派生类来说,构造函数作用极其有限.你只能在构造函数中初始化与绘图及控件无关的东西(比如分析输入来的数据,但决不能涉及到屏幕位置等换算).

setWorldAndResolution(Minecraft par1Minecraft, int par2, int par3)  (不建议重写)
这个方法是用来获取游戏窗口分辨率及游戏主逻辑部分的句柄.老实说这个没什么可重写的.如果你一定要重写,务必记得要调用原方法.

initGui()
intiGui负责添加控件,例如按钮什么的.重写它不必调用原方法.

drawScreen(int par1, int par2, float par3)
这个方法会不停地被游戏主线程调用,它负责绘制界面,你需要在派生类中重写它,然后添加绘制界面的相关代码(主要是向屏幕中绘制纹理的代码),基类的drawScreen方法是用于绘制控件.你不用管基类,你只需在派生类的方法的结尾调用基类就行了.(注意,GuiTextField不属于控件,因此它必须在drawScreen方法中被手动绘制,具体的过程待会解答...)

updateScreen()
这个方法用来更新一些数据,它也是不停地被游戏主线程调用的,虽然没有经过测算,但从程序上判断它是在drawScreen之前被调用.

handleKeyboardInput()
这个方法类似于一个事件,它会在玩家按下键盘按键时引发.

handleMouseInput()
这个方法也类似于一个事件,它会在玩家触发鼠标事件时引发,鼠标事件包含按下按键和移动鼠标.

mouseClicked(int par1, int par2, int par3)
这个方法由handleMouseInput方法引发,它专门处理按下鼠标按键的情况,par3是按下的按键,知par3 = 0是按下左键.

mouseMovedOrUp(int par1, int par2, int par3)
这个方法由handleMouseInput方法引发,它专门处理移动鼠标和松开鼠标按键的情况,已知par3 = -1是移动鼠标,par3 = 0是松开左键. par3 = 1也许是松开右键??

keyTyped(char par1, int par2)
这个方法类似于一个事件,它会在玩家按下键盘按键时引发,但如果你想让它运作,必须先在initGui方法中加入代码:
  1. Keyboard.enableRepeatEvents(true);
复制代码
来打开输入响应器,另外别忘记在onGuiClosed方法中关掉响应器.

onGuiClosed()
这个方法类似于一个事件,它在界面被关闭时引发,当你尝试打开一个新的界面时,旧的界面会自动被关闭.重写时不必调用基类.
另外,MC中的界面即使被关闭后也会保留在内存中,比如你在主菜单(GuiMainMenu)中打开多人游戏菜单(GuiMultiplayer),主菜单不会被关闭,当你返回主菜单时依然返回的是最初的主菜单.但有些情况下确实是彻底被关闭的,从面向对象编程的角度来解释,只要你有某种手段能和一个类的实例保持联系,那么那个类就是可抵达的(Reachable),JavaGC(垃圾回收器)不会销毁那个实例.倘若一个类中的所有成员(字段,方法...)永远不会被外界访问,类本身也没有被引用,那么它便成了不可抵达的(Unreachable),某些时刻JavaGC会分析内存并找出不可抵达的实例并销毁,从此这个实例便彻底消失了.

actionPerformed(GuiButton guibutton)
这个方法类似于一个事件,它会在玩家按下按钮(GuiButton与它的派生类)时引发,玩家可以通过获取按钮的id变量来确定是哪个按钮,或者是其他方法...

doesGuiPauseGame()
这个方法是根据其返回值来判断在单人游戏下打开此界面是否暂停游戏.

drawWorldBackground(int par1)
通常这个方法是在drawScreen方法的最开头调用,我强烈建议无论何时都将参数par1设为0.它负责绘制背景,在主菜单中调用,它会绘制泥土背景,在实际游戏中调用,它会绘制半透明黑色背景.

一个继承自GuiScreen的界面类应该具有如下要素:
构造函数 - 用于初始化部分数据
initGui - 部署控件
drawScreen - 绘制界面,文字,纹理,别忘记调用基类.
换句话说,以下是一个最简单的窗口类.
  1. public class YourGui extends GuiScreen{

  2.         private GuiScreen parentScreen;

  3.         public YourGui(GuiScreen parent)
  4.         {
  5.                 parentScreen = parent; //记下是哪个界面打开了它,以便以后返回那个界面
  6.                 //在这里初始化与界面无关的数据,或者是只需初始化一次的数据.
  7.         }

  8.         public void initGui()
  9.         {
  10.                 //这里部署控件
  11.         }

  12.         public void drawScreen(int par1, int par2, float par3)
  13.         {
  14.                 drawDefaultBackground();
  15.                 //在这里绘制文本或纹理等非控件内容,这里绘制的东西会被控件(即按键)盖住.
  16.                 super.drawScreen(par1,par2,par3);
  17.                 //在这里绘制文本或纹理等非控件内容,这里绘制的东西会盖在控件(即按键)之上.
  18.         }
  19. }
复制代码
它什么都没有,甚至无法退回到上一个菜单,显示后仅仅只有一片泥土背景(如果是游戏中显示则是一片透明的黑色背景).

一个界面类如果需要被显示,需要调用Minecraft类的displayGuiScreen方法.
displayGuiScreen方法的参数是一个已被实例化的界面,GuiScreen类使用名为mc的字段来储存已实例化的Minecraft,如果要在GuiScreen的派生类中显示其他界面,就调用

  1. mc.displayGuiScreen(new YourGui(this));
复制代码

如果不懂也没关系...看一看源码也能明白

最后再顺便说一下displayGuiScreen方法显示界面的流程.

///////////////////////////////////////////////////////////////////////////////////////////////////////////

知识点:添加一个按钮控件
///////////////////////////////////////////////////////////////////////////////////////////////////////////
按钮类(GuiButton)算上其本体一共有4.GuiButton,GuiButtonLanguage,GuiSlider,GuiSmallButton.
GuiButton即最普通的按钮.
GuiButtonLanguage是主菜单左下角自带图标的那个按钮,点它会自动打开语言界面.
GuiSlider即滑动条.
GuiSmallButton...类似于选择难度的按钮,即点一下变一次文字内容,我不得不承认我自己也没搞明白它如何使用= =

添加一个按钮控件有2种办法.
第一种:
先在类中添加一个字段

  1. GuiButton button;
复制代码

然后在initGui方法中加入
  1. controlList.add(button = new GuiButton(index, x, y, w, h, str));
复制代码
index是按钮的id,一般是一个整数.id不要重复.
x,y是按钮的左上角的坐标
w,h是按钮的宽和高,另外高建议是20.
str是按钮上显示的文本

第二种:
直接在initGui方法中加入

  1. controlList.add(new GuiButton(index, x, y, w, h, str));
复制代码
两种办法的效果都是相同的,差别在于第一种你可以对按钮进行更多的操作,例如禁用,使它变灰等...
操作方法是:
button.enabled = false  使其变灰,无法点选
button.drawButton = false  隐藏这个按钮
(其实你用第二种也可以这样做,不过要麻烦很多)

之后便要添加按钮的响应事件,这需要重写GuiScreen类的actionPerformed方法,然后通过判断触发按钮的id来判断用户按下了哪个按钮.
  1. protected void actionPerformed(GuiButton par1GuiButton)
  2. {
  3.       if(par1GuiButton.id == 1) //用户按下了id为1的按钮
  4.       {
  5.       //在此添加进行的操作....
  6.       }
  7. }
复制代码
另外有一点必须要提的是,initGui方法中,一定要开始添加控件前加入一行
  1. controlList.clear();
复制代码
来清除之前的控件!
例如:

同样延续上一个范例,我们为那个什么都没有的菜单填一个"返回"按钮.
initGui方法中加入

  1. controlList.clear();
  2. controlList.add(new GuiButton(0, width / 2 - 37, height - 40, 75, 25, "返回"));
复制代码

然后在YourGui类中加入

  1. protected void actionPerformed(GuiButton par1GuiButton)
  2. {
  3.          if(par1GuiButton.id == 0)  //用户按下了id为1的按钮
  4.          {
  5.                 mc.displayGuiScreen(parentScreen);
  6.          }
  7. }
复制代码

这样你便添加了一个按钮,点击它就能返回上一个界面(主菜单)
///////////////////////////////////////////////////////////////////////////////////////////////////////////


知识点:绘制文本和纹理
///////////////////////////////////////////////////////////////////////////////////////////////////////////
之前我们提到过,我们通过Gui类的drawTexturedModalRect方法来绘制纹理,使用它绘制纹理需要先向渲染引擎中绑定纹理.
首先你需要获得欲绑定的纹理的id,MC不会一次性载入全部纹理,它只有在需要调用纹理时才会将其从硬盘中载入并分配一个id.MC的渲染引擎提供了一个getTexture(str)方法,str是目标纹理在Minecraft.jar文件中的位置.例如:

  1. int i = mc.renderEngine.getTexture("/gui/items.png");
复制代码
渲染引擎先会检查位于gui目录下的items.png有没有被载入过,如果有,则直接返回id,如没有则将其载入并分配一个id并返回.在这里我们将返回的id储存在整数变量i.

然后我们要将其绑定入渲染引擎,绑定有两种办法:
第一种:

  1. mc.renderEngine.bindTexture(i);
复制代码
这个是调用MC的渲染引擎并将idi的纹理绑定上.
第二种:

  1. GL11.glBindTexture(GL11.GL_TEXTURE_2D, i);
复制代码
这个是调用最底层的OpenGL渲染器并将纹理绑定上.同时还特别注明是渲染2D纹理.

在界面绘制纹理上,这两种办法没有什么太大的差别.有一点需要注意的是,在绑定完毕后(或者前),需要重设底层渲染器的染色参数.代码是:

  1. GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
复制代码
:

之后便可以绘制纹理了,你需要测定出你所要绘制的部分在纹理贴图中的坐标(在这里用uv表示)和长宽.比如在items.png,弓箭的uv坐标就是(80,16),长宽是16x16.如果你要以屏幕的(24,24)为左上角来绘制一个弓箭图标,那么在你绑定上items.png,使用
  1. drawTexturedModalRect(24,24,80,16,16,16);
复制代码
来绘制纹理.

至于绘制文字...我已经在Gui的介绍中说过了哦,唯一需要注意的是文字和纹理的渲染顺序,后渲染的会覆盖在先渲染的之上(如果他们的位置有重叠的话)另外不要忘记drawScreen的基类方法负责渲染按钮(Button) 所以如果你希望纹理覆盖在按钮之上,就一定要把纹理渲染写在调用基类的代码之后.:

(第一行:super.drawScreen(par1,par2,par3)是调用原drawScreen方法,或者说是基类的drawScreen方法)
///////////////////////////////////////////////////////////////////////////////////////////////////////////


知识点:绘制文本框
///////////////////////////////////////////////////////////////////////////////////////////////////////////
文本框可供玩家输入文字,它的绘制比较麻烦...
首先,你要在类中添加一个文本框(GuiTextField)的字段

  1. GuiTextField textBox;
复制代码

如:
然后在initGui方法中加入:

  1. Keyboard.enableRepeatEvents(true);//开启键盘输入功能.
  2. textBox = new GuiTextField(fontRenderer, width / 2 - 100, 76, 200, 20);
  3. //fontRenderer是文本渲染器,后面的4个参数分别对应X,Y,宽,高(高建议为20)
  4. textBox.setFocused(true);  //设置是否已被设为焦点
  5. //设为焦点,相当于玩家左键一个文本框,即准备往里输入文字
  6. //如果有多个文本框,不要同时都设为焦点
  7. textBox.setText(""); //设置文本框默认文本
  8. textBox.setMaxStringLength(128);  //此为设置最大长度,可以省略
复制代码
来初始化文本框.
(注:从MCP6.5起,原func_50033_b方法已被更正为setFocused)

然后在onGuiClosed方法(如果没有就自己添加一个)中加入

  1. Keyboard.enableRepeatEvents(false);
复制代码
以在退出界面时关闭键盘输入.

然后在类中加入:

  1. protected void keyTyped(char par1, int par2)
  2.     {
  3.         //textBox是你之前添加的文本框,有多个文本框
  4.         //下面这个代码是向文本框追加文本,如果你有多个文本框,就为每一个文本框都调用一次
  5.         //别担心,方法会自动判断此文本框是否被设为焦点的
  6.         textBox.textboxKeyTyped(par1, par2);
  7.         if (par1 == '\r')
  8.         {
  9.          //在此添加玩家按下回车键后的操作
  10.         }
  11. }
复制代码
(注:从MCP6.5起,原func_50037_a方法已被更正为textboxKeyTyped)

在类中加入:

  1. protected void mouseClicked(int par1, int par2, int par3)
  2.     {
  3.         super.mouseClicked(par1, par2, par3);
  4.         textBox.mouseClicked(par1, par2, par3);
  5.         //同样,如果你有多个文本框,则每个都要来一遍
  6. }
复制代码
最后,drawScreen方法中加入:

  1. textBox.drawTextBox(); //绘制文本框,同样每个都要来一遍...
复制代码

至此,你便完成了文本框的添加,至少你已经了解单文本框的使用了,如果对多文本框还有不懂,可以参考GuiScreenAddServer.
///////////////////////////////////////////////////////////////////////////////////////////////////////////


持续更新中...