本帖最后由 yuxuanchiadm 于 2013-8-4 16:58 编辑

利用Forge API开发联机MOD【基础篇】【第九章】
为你的刷怪笼添加一个可操作的GUI
作者:yuxuanchiadm

索引贴地址:http://www.mcbbs.net/thread-38211-1-1.html

请确定你已经阅读完成第八章的内容:
http://www.mcbbs.net/thread-114901-1-1.html
否则不要阅读此贴!

序:
在上一章里,我们完成了我们MOD的网络通信框架,现在,是时候阅读联机MOD篇的最后一章了:),当你阅读完这一章时,你将得到一个完整的利用ForgeAPI制作的MOD!

制作一个新的GUI:
/**知识点:什么是GUI
GUI指的是用户图形界面,在Minecraft里,所有的用户图形界面都继承自GUI类。在显示一个GUI时一般是通过调用Minecraft类里的displayGuiScreen()函数来显示一个继承自GuiScreen类的GUI,在通过调用这个函数打开GUI时会关闭其他通过这个函数打开的GUI。
**/
首先,制作一个刷怪笼设置面板GUI的背景:
在myFirstMod/sprites下新建文件:SpawnerSettingGui.png
然后乱涂成这样:)

然后,打开BlockAdvancedMobSpawner,看到当时我们留下的
  1. // 以后会在这里添加代码
复制代码
由于我们想让我们的MOD能够支持联机,所以,我们需要先向服务端发送一个数据包,然后服务端返回实体的名称。
在这里插入代码:
  1. myFristModPacket pak = new myFristModPacket();
  2. //我们要发送3个int数据,所以初始化数组大小为3
  3. pak.dataInt = new int[3];
  4. //数据包ID为0
  5. pak.packetType = 0;
  6. //方块X轴坐标
  7. pak.dataInt[0] = par2;
  8. //方块Y轴坐标
  9. pak.dataInt[1] = par3;
  10. //方块Z轴坐标
  11. pak.dataInt[2] = par4;
  12. //发送数据包
  13. PacketDispatcher.sendPacketToPlayer(pak.toPacket(), (Player)par5EntityPlayer);
复制代码
此处我们发送数据包时还发送了方块坐标,虽然方块坐标对于获取实体列表没有用,但是为了保证以后修改方块时我们能得到方块坐标,所以这里也要连同方块坐标一起发出去。

服务端发送数据包后,客户端需要接收和处理数据包,打开mod_myFirstMod类,找到handlePacketFromServer方法,写入以下内容:
  1. if(packet.packetType == 0)
  2. {
  3.     //等下会在这里添加代码
  4. }
复制代码
因为我们发送数据包时制定其ID为0,所以如果接收到的数据包ID为0则是方块右击事件发送的数据包。

其次,我们需要创建自己的一个GUI。首先新建包myFirstMod.GUI,然后再在其中新建类GuiMobSpawnerSetting。
然后,再让其继承自GuiScreen类:
  1. package myFirstMod.GUI;
  2. import net.minecraft.client.gui.GuiScreen;
  3. import cpw.mods.fml.relauncher.Side;
  4. import cpw.mods.fml.relauncher.SideOnly;

  5. @SideOnly(Side.CLIENT)
  6. public class GuiMobSpawnerSetting extends GuiScreen
  7. {
  8.    
  9. }
复制代码
我们在设置GUI的时候希望能获取到当前刷怪箱刷的怪物,和刷怪间隔,而不是从零开始,而且后面还要将刷怪箱的坐标连同所刷怪物和间隔一同发送到服务端,所以我们的构造函数需要3个参数,并将其记录到成员属性中,添加如下代码:
  1. public int SpawnerX;
  2. public int SpawnerY;
  3. public int SpawnerZ;
  4. public GuiMobSpawnerSetting(int x, int y, int z)
  5. {
  6.     SpawnerX = x;
  7.     SpawnerY = y;
  8.     SpawnerZ = z;
  9. }
复制代码
其次,我们需要将所刷怪物的ID列表复制到我们的GUI中存起来:
  1. private ArrayList<Integer> mobList = new ArrayList<Integer>();
  2. public void initGui()
  3. {
  4.     mobList.addAll(EntityList.entityEggs.keySet());
  5. }
复制代码
我们的GUI需要几个按钮:上一个、下一个、时间+100、+10、+1、-1、-10和-100,首先创建成员属性:
  1. private GuiButton prev;
  2. private GuiButton next;
  3. private GuiButton Cut1;
  4. private GuiButton Cut10;
  5. private GuiButton Cut100;
  6. private GuiButton Add1;
  7. private GuiButton Add10;
  8. private GuiButton Add100;
复制代码
然后我们还需要2个文本框,用于显示当前间隔和当前所刷怪物,创建成员属性:
  1. private GuiTextField MobNameTextBox;
  2. private GuiTextField SpawnDelayTextBox;
复制代码
然后在initGui方法中对其进行初始化,以上一个按钮为例:
  1. prev = new GuiButton(1, (width - 175) / 2 + 20, (height - 165) / 2 + 60, 40, 20, "上一个");
  2. controlList.add(prev);
复制代码
首先new一个GuiButton对象,第一个参数表示他的ID,在点击时可以用ID来判断是哪个按钮,第二个和第三个参数是X轴和Y轴坐标,第四个和第五个参数为长和宽,最后一个是显示的字符串。第二行添加这个按钮到控件(按钮)列表。
继续初始化其他的按钮:
  1. next = new GuiButton(2, (width - 175) / 2 + 20, (height - 165) / 2 + 80, 40, 20, "下一个");
  2. controlList.add(next);
  3. Cut100 = new GuiButton(3, (width - 175) / 2 + 10, (height - 165) / 2 + 137, 20, 20, "-100");
  4. controlList.add(Cut100);
  5. Cut10 = new GuiButton(4, (width - 175) / 2 + 29, (height - 165) / 2 + 137, 20, 20, "-10");
  6. controlList.add(Cut10);
  7. Cut1 = new GuiButton(5, (width - 175) / 2 + 48, (height - 165) / 2 + 137, 20, 20, "-1");
  8. controlList.add(Cut1);
  9. Add1 = new GuiButton(6, (width - 175) / 2 + 108, (height - 165) / 2 + 137, 20, 20, "+1");
  10. controlList.add(Add1);
  11. Add10 = new GuiButton(7, (width - 175) / 2 + 127, (height - 165) / 2 + 137, 20, 20, "+10");
  12. controlList.add(Add10);
  13. Add100 = new GuiButton(8, (width - 175) / 2 + 146, (height - 165) / 2 + 137, 20, 20, "+100");
  14. controlList.add(Add100);
复制代码
接下来,我们需要获取当前刷怪笼的状态,并存储在GUI里以备用,新建成员属性:
  1. private int CurrentID;
  2. private int Delay;
复制代码
再在initGui方法中添加:
  1. TileEntity tileEntity = Minecraft.getMinecraft().theWorld.getBlockTileEntity(SpawnerX, SpawnerY, SpawnerZ);
  2. if(tileEntity instanceof TileEntityAdvancedMobSpawner)
  3. {
  4.     TileEntityAdvancedMobSpawner tileEntitySpawner = (TileEntityAdvancedMobSpawner)tileEntity;
  5.     Field stringToIDMapping = null;
  6.     Map strToIDMap = null;
  7.     try
  8.     {
  9.         stringToIDMapping = EntityList.class.getDeclaredField("stringToIDMapping");
  10.         stringToIDMapping.setAccessible(true);
  11.         strToIDMap = (Map)stringToIDMapping.get(null);
  12.     }
  13.     catch (Exception e)
  14.     {
  15.         e.printStackTrace();
  16.     }
  17.     String mobName = tileEntitySpawner.getMobID();
  18.     CurrentID = mobList.indexOf(strToIDMap.get(mobName));
  19.     Delay = tileEntitySpawner.getSpawnDelay();
  20. }
复制代码
稍微运用了下反射来获取私有字段
然后根据按钮的mobList和CurrentID的状态来调整按钮的状态:
  1. if((mobList.size() - 1) == 0)
  2. {
  3.     prev.enabled = false;
  4.     next.enabled = false;
  5. }
  6. else
  7. {
  8.     if(CurrentID > 0 && CurrentID < (mobList.size() - 1))
  9.     {
  10.         prev.enabled = true;
  11.         next.enabled = true;
  12.     }
  13.     if(CurrentID >= (mobList.size() - 1))
  14.     {
  15.         prev.enabled = true;
  16.         next.enabled = false;
  17.     }
  18.     if(CurrentID <= 0)
  19.     {
  20.         prev.enabled = false;
  21.         next.enabled = true;
  22.     }
  23. }
复制代码
最后初始化文本框:
  1. MobNameTextBox = new GuiTextField(fontRenderer, (width - 175) / 2 + 10, (height - 165) / 2 + 30, 150, 20);
  2. MobNameTextBox.setText(StatCollector.translateToLocal("entity." + EntityList.getStringFromID(mobList.get(CurrentID)) + ".name"));
  3. SpawnDelayTextBox = new GuiTextField(fontRenderer, (width - 175) / 2 + 10, (height - 165) / 2 + 115, 150, 20);
  4. SpawnDelayTextBox.setText(Integer.toString(Delay));
复制代码
上面以刷怪名字为例,先new了个GuiTextField,然后第一个参数固定传入fontRenderer,然后是X轴和Y轴,其次是宽和高。
然后,我们需要对我们的GUI进行绘制,重写方法drawScreen:
  1. public void drawScreen(int par1, int par2, float par3)
  2. {

  3. }
复制代码
第一步先绘制默认背景:
  1. drawDefaultBackground();
复制代码
其次弄出我们最先画的材质并修正颜色后绑定:
  1. int k = mc.renderEngine.getTexture("/myFirstMod/sprites/SpawnerSettingGui.png");
  2. GL11.glColor3f(1.0F, 1.0F, 1.0F);
  3. mc.renderEngine.bindTexture(k);
复制代码
然后计算出绘制位置,并画矩形:
  1. int l = (width - 175) / 2;
  2. int i1 = (height - 165) / 2;drawTexturedModalRect(l, i1, 0, 0, 175, 165);
复制代码
然后画文本框:
  1. MobNameTextBox.drawTextBox();
  2. SpawnDelayTextBox.drawTextBox();
复制代码
然后让父类画各种按钮:
  1. super.drawScreen(par1,par2,par3);
复制代码
最后涂些字符串到GUI中:
  1. fontRenderer.drawString("刷怪笼设置面板", (width - 175) / 2 + 6, (height - 165) / 2 + 6, 0x404040);
  2. fontRenderer.drawString("生成的怪物:", (width - 175) / 2 + 10, (height - 165) / 2 + 18, 0x404040);
  3. fontRenderer.drawString("生成间隔:", (width - 175) / 2 + 10, (height - 165) / 2 + 104, 0x404040);
复制代码
然后我们需要对按钮的事件进行处理(简单明了就不说啥了):
  1. protected void actionPerformed(GuiButton par1GuiButton)
  2. {
  3.     if(par1GuiButton.id == 1)
  4.     {
  5.         CurrentID --;
  6.         if(CurrentID <= 0)
  7.         {
  8.             prev.enabled = false;
  9.         }
  10.         if(CurrentID < (mobList.size() - 1))
  11.         {
  12.             next.enabled = true;
  13.         }
  14.         MobNameTextBox.setText(StatCollector.translateToLocal("entity." + EntityList.getStringFromID(mobList.get(CurrentID)) + ".name"));
  15.     }
  16.     if(par1GuiButton.id == 2)
  17.     {
  18.         CurrentID ++;
  19.         if(CurrentID >= (mobList.size() - 1))
  20.         {
  21.             next.enabled = false;
  22.         }
  23.         if(CurrentID > 0)
  24.         {
  25.             prev.enabled = true;
  26.         }
  27.         MobNameTextBox.setText(StatCollector.translateToLocal("entity." + EntityList.getStringFromID(mobList.get(CurrentID)) + ".name"));
  28.     }
  29.     if(par1GuiButton.id == 3)
  30.     {
  31.         ChangeDelay(-100);
  32.     }
  33.     if(par1GuiButton.id == 4)
  34.     {
  35.         ChangeDelay(-10);
  36.     }
  37.     if(par1GuiButton.id == 5)
  38.     {
  39.         ChangeDelay(-1);
  40.     }
  41.     if(par1GuiButton.id == 6)
  42.     {
  43.         ChangeDelay(+1);
  44.     }
  45.     if(par1GuiButton.id == 7)
  46.     {
  47.         ChangeDelay(+10);
  48.     }
  49.     if(par1GuiButton.id == 8)
  50.     {
  51.         ChangeDelay(+100);
  52.     }
  53. }
  54. public void ChangeDelay(int i)
  55. {
  56.     Delay += i;
  57.     if(Delay > 50000)
  58.     {
  59.         Delay = 50000;
  60.     }
  61.     if(Delay < 50)
  62.     {
  63.         Delay = 50;
  64.     }
  65.     SpawnDelayTextBox.setText(Integer.toString(Delay));
  66. }
复制代码
然后在关闭GUI时回发数据包:
  1. public void onGuiClosed()
  2. {
  3.     TileEntity tileEntity = Minecraft.getMinecraft().theWorld.getBlockTileEntity(SpawnerX, SpawnerY, SpawnerZ);
  4.     if(tileEntity instanceof TileEntityAdvancedMobSpawner)
  5.     {
  6.         TileEntityAdvancedMobSpawner tileEntitySpawner = (TileEntityAdvancedMobSpawner)tileEntity;
  7.         tileEntitySpawner.setMobID(EntityList.getStringFromID(mobList.get(CurrentID)));
  8.         tileEntitySpawner.setSpawnDelay(Delay);
  9.     }
  10.     myFristModPacket pak = new myFristModPacket();
  11.     //我们要发送4个int数据,所以初始化数组大小为4
  12.     pak.dataInt = new int[4];
  13.     //我们要发送1个String数据,所以初始化数组大小为1
  14.     pak.dataString = new String[1];
  15.     //数据包ID为1
  16.     pak.packetType = 1;
  17.     //方块X轴坐标
  18.     pak.dataInt[0] = SpawnerX;
  19.     //方块Y轴坐标
  20.     pak.dataInt[1] = SpawnerY;
  21.     //方块Z轴坐标
  22.     pak.dataInt[2] = SpawnerZ;
  23.     //刷怪间隔
  24.     pak.dataInt[3] = Delay;
  25.     //怪物名称
  26.     pak.dataString[0] = EntityList.getStringFromID(mobList.get(CurrentID));
  27.     //发送数据包
  28.     PacketDispatcher.sendPacketToServer(pak.toPacket());
  29. }
复制代码
最后,我不希望打开GUI时暂停游戏:
  1. public boolean doesGuiPauseGame()
  2. {
  3.     return false;
  4. }
复制代码
现在,我们再转到mod_myFirstMod,离成功还有一步之遥了,看到:
  1. if(packet.packetType == 0)
  2. {
  3.     //等下会在这里添加代码
  4. }
复制代码
添加代码:
  1. Minecraft.getMinecraft().displayGuiScreen(new GuiMobSpawnerSetting(packet.dataInt[0], packet.dataInt[1], packet.dataInt[2]));
复制代码
最后,看到handlePacketFromClient方法,在这个方法中对客户端GUI发回的消息进行处理:
  1. if(packet.packetType == 1)
  2. {
  3.     TileEntity tileEntity = player.worldObj.getBlockTileEntity(packet.dataInt[0], packet.dataInt[1], packet.dataInt[2]);
  4.     if(tileEntity instanceof TileEntityAdvancedMobSpawner)
  5.     {        TileEntityAdvancedMobSpawner tileEntitySpawner = (TileEntityAdvancedMobSpawner)tileEntity;
  6.         if(tileEntitySpawner != null)
  7.         {
  8.             tileEntitySpawner.setSpawnDelay(packet.dataInt[3]);
  9.             tileEntitySpawner.setMobID(packet.dataString[0]);
  10.         }
  11.     }
  12. }
复制代码
现在,你的刷怪笼已经可以通过蹲下右键打开GUI,并设置其属性了!
总结:
首先祝贺你已经完成了大部分联机MOD的制作学习,并且做出了一个属于你自己动手创作的MOD:),你已经可以毫不犹豫的说:“我是一个Forge MOD的制作者!”。但是,此MOD仍然没有完成,只能在MCP下运行,在玩家手里就运行不能了。原因就是我们使用了反射,所以产生了反混淆问题,下一章我们将详细讨论如何解决这个问题。