在Minecraft中,GUI是很重要的组件,工作台、熔炉、附魔都离不开它,玩家除了盖楼房和挖矿之外打交道最多的就是GUI了,但是GUI的结构很复杂,用法很多,这个教程是主要是教各位moder正确的使用GUI。
在教程开始前先确认自己是否对Java有足够的熟练度,对Forge的API是否有足够的了解,至少能创建简单的Block,Item,合成配方,如果不是很了解,在看这篇教程之前还请先补习好mod的基本内容。
GUI主要分为2种,一种是有IInventory的,一种是没有的。
这里主要介绍IInventory,IInventory本身是涵盖了部分非IInventory的内容。
常见的IInventory GUI主要是Block的GUI,代表作就是熔炉BlockFurnace,很多有代表性的GUI源码可以从熔炉去了解。Block GUI有5个组件,分别是Block、TileEntity、GUI、Container、IGuiHandler。
为了方便之后的更新,我将会直接附上工程源码而不是整个制作的流程,其中会穿插一些解释,如果没有什么太大的改动的话,有兴趣研究详细的GUI运行原理还请参考旧的教程。
先介绍一下这个示例,这个示例是一个神奇的维修台方块,放入破损的武器和燃料就可以修复那个武器了,非常bug的存在,外观是一个砖块,可以掩人耳目,不会被轻易的发现(上面都是我吹的,其实我不会写程序,我实在编不下去了)。
做那么诡异的功能还是为了能让moder们更好的了解GUI的相关功能(还有我能偷点懒,实在不想写方块外观,每次升级版本被大改的总是外观渲染类,和GUI关系不大,这一块就不写那么详细了,在1.8.9下如果没使用文章末尾的源码导致出现Block的贴图错误,请用文章末尾的源码中的resources文件覆盖掉你工程下的同名文件夹,文章末尾的源码已经配置好贴图的JSON了)。
首先是mod主文件mod_RepairTable.java
import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.client.Minecraft;
import net.minecraft.client.renderer.ItemModelMesher;
import net.minecraft.client.resources.model.ModelResourceLocation;
import net.minecraft.creativetab.CreativeTabs;
import net.minecraft.item.Item;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.fml.common.Mod;
import net.minecraftforge.fml.common.Mod.EventHandler;
import net.minecraftforge.fml.common.Mod.Instance;
import net.minecraftforge.fml.common.SidedProxy;
import net.minecraftforge.fml.common.event.FMLInitializationEvent;
import net.minecraftforge.fml.common.event.FMLPreInitializationEvent;
import net.minecraftforge.fml.common.network.NetworkRegistry;
import net.minecraftforge.fml.common.registry.GameRegistry;
import net.minecraftforge.fml.relauncher.Side;
@Mod(modid = mod_RepairTable.MODID, version = mod_RepairTable.VERSION)
public class mod_RepairTable {
public Block repairTable;
// 定义mod的ID,这个是用户给代理端识别
public static final String MODID = "newmod";
public static final String VERSION = "1.0";
// 准备mod的静态实例
@Instance(MODID)
public static mod_RepairTable instance;
// 代理配置
@SidedProxy(clientSide = "com.wind.repairtable.ClientProxy", serverSide = "com.wind.repairtable.CommonProxy")
public static CommonProxy proxy;
@EventHandler
public void preInit(FMLPreInitializationEvent e)
{
repairTable = new rtBlockRepairTable(Material.rock)
.setUnlocalizedName(MODID + ":" + "repairtable")
.setCreativeTab(CreativeTabs.tabBlock);
GameRegistry.registerBlock(repairTable, "repairtable");
GameRegistry.registerTileEntity(rtTileEntityRepairTable.class, "TileEntityRepairTable");
NetworkRegistry.INSTANCE.registerGuiHandler(this, proxy);
}
@EventHandler
public void init(FMLInitializationEvent event) {
if(event.getSide() == Side.CLIENT)
{
ItemModelMesher mesher = Minecraft.getMinecraft().getRenderItem().getItemModelMesher();
mesher.register(Item.getItemFromBlock(repairTable), 0, new ModelResourceLocation(MODID + ":" + "repairtable", "inventory"));
}
}
}
在1.8.9中(我只测试了1.8.9,之前的版本就不清楚了),与之前不同的就是需要在客户端注册方块的物品栏渲染,在init方法中先检测是否为客户端再进行注册即可。
在主文件中注册方块、注册TileEntity,注册GUI代理就不用多说了。
GUI服务端代理CommonProxy.java
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.BlockPos;
import net.minecraft.world.World;
import net.minecraftforge.fml.common.network.IGuiHandler;
public class CommonProxy implements IGuiHandler {
@Override
public Object getServerGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z)
{
switch(ID) {
case 10:
return new rtContainerRepairTable(player.inventory, (rtTileEntityRepairTable)player.worldObj.getTileEntity(new BlockPos(x, y, z)));
}
return null;
}
@Override
public Object getClientGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z)
{
return null;
}
}
和旧版本不同的是,1.8.9引入了BlockPos类用于表示Block位置,虽然个人感觉没什么卵用,估计过几个版本就又会清理掉了。
这里引入的IGuiHandler是提供给GUI接入的接口,GUI分为客户端和服务端,从接口的两个方法的名字上就可以知道了。这两个方法不同的地方在于客户端需要返回Gui实例,服务端需要返回Container实例,这两个实例有什么用后面再说。之前主文件注册的GUI代理就是需要这个类的实例。
GUI客户端代理ClientProxy.java
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.util.BlockPos;
import net.minecraft.world.World;
public class ClientProxy extends CommonProxy {
@Override
public Object getClientGuiElement(int ID, EntityPlayer player, World world, int x, int y, int z) {
switch(ID) {
case 10:
return new rtGuiRepairTable(player.inventory, (rtTileEntityRepairTable)player.worldObj.getTileEntity(new BlockPos(x, y, z)));
}
return null;
}
}
继承自服务端代理,原理和服务端一样,不同的就是返回Gui实例。
Block的后台rtTileEntityRepairTable.java
import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.init.Blocks;
import net.minecraft.init.Items;
import net.minecraft.inventory.IInventory;
import net.minecraft.item.Item;
import net.minecraft.item.ItemArmor;
import net.minecraft.item.ItemBlock;
import net.minecraft.item.ItemHoe;
import net.minecraft.item.ItemStack;
import net.minecraft.item.ItemSword;
import net.minecraft.item.ItemTool;
import net.minecraft.item.crafting.FurnaceRecipes;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.tileentity.TileEntityFurnace;
import net.minecraft.util.IChatComponent;
import net.minecraft.util.ITickable;
import net.minecraftforge.fml.common.registry.GameRegistry;
public class rtTileEntityRepairTable extends TileEntity implements ITickable,IInventory {
public int tableBurnTime = 0;
public int maxBurnTime = 0;
// 物品栈,储存放在TileEntity中的物品
private ItemStack stack[] = new ItemStack[3];
// 玩家是否可以打开,一般由container回调,用于判断玩家是否有权限打开
@Override
public boolean isUseableByPlayer(EntityPlayer var1) {
return false;
}
@Override
public boolean isItemValidForSlot(int var1, ItemStack var2) {
return false;
}
// 物品转移处理,一般照抄就行
@Override
public ItemStack decrStackSize(int par1, int par2) {
if (this.stack[par1] != null) {
ItemStack var3;
if (this.stack[par1].stackSize <= par2) {
var3 = this.stack[par1];
this.stack[par1] = null;
return var3;
} else {
var3 = this.stack[par1].splitStack(par2);
if (this.stack[par1].stackSize == 0) {
this.stack[par1] = null;
}
return var3;
}
} else {
return null;
}
}
// 从物品栈中取得对应索引的物品
@Override
public ItemStack getStackInSlot(int var1) {
return stack[var1];
}
// 取得物品栈总数
@Override
public int getSizeInventory() {
return stack.length;
}
// 取得物品栈单个最大物品堆叠数量
@Override
public int getInventoryStackLimit() {
return 64;
}
// 设置物品栈制定索引的物品
@Override
public void setInventorySlotContents(int var1, ItemStack var2) {
stack[var1] = var2;
}
// NBT标签读操作,该方法一般会在加载游戏区块的时候运行
@Override
public void readFromNBT(NBTTagCompound par1NBTTagCompound)
{
super.readFromNBT(par1NBTTagCompound);
// 取得存放物品的List标签,名称一般为“Item”,第二个参数一般为10
NBTTagList var2 = par1NBTTagCompound.getTagList("Items", 10);
// 实例化物品栈
this.stack = new ItemStack[this.getSizeInventory()];
// 遍历List,读取数据转化为物品实例
for (int var3 = 0; var3 < var2.tagCount(); ++var3)
{
NBTTagCompound var4 = (NBTTagCompound)var2.getCompoundTagAt(var3);
byte var5 = var4.getByte("Slot");
if (var5 >= 0 && var5 < this.stack.length)
{
this.stack[var5] = ItemStack.loadItemStackFromNBT(var4);
}
}
// 读取自定的运行数据,字段可以自己设计
this.tableBurnTime = par1NBTTagCompound.getShort("tableBurnTime");
this.maxBurnTime = par1NBTTagCompound.getShort("maxBurnTime");
}
// NBT标签写操作
@Override
public void writeToNBT(NBTTagCompound par1NBTTagCompound)
{
super.writeToNBT(par1NBTTagCompound);
// 写入自定的运行数据
par1NBTTagCompound.setShort("tableBurnTime", (short)this.tableBurnTime);
par1NBTTagCompound.setShort("maxBurnTime", (short)this.maxBurnTime);
// 将物品栈写入标签
NBTTagList var2 = new NBTTagList();
for (int var3 = 0; var3 < this.stack.length; ++var3)
{
if (this.stack[var3] != null)
{
NBTTagCompound var4 = new NBTTagCompound();
var4.setByte("Slot", (byte)var3);
this.stack[var3].writeToNBT(var4);
var2.appendTag(var4);
}
}
par1NBTTagCompound.setTag("Items", var2);
}
/**----------1.8.9加入或修改的方法---------**/
@Override
public String getName() {
return null;
}
@Override
public boolean hasCustomName() {
return false;
}
// 从物品栈移除制定物品的索引
@Override
public ItemStack removeStackFromSlot(int index) {
if (this.stack[index] != null)
{
ItemStack itemstack = this.stack[index];
this.stack[index] = null;
return itemstack;
}
else
{
return null;
}
}
@Override
public void openInventory(EntityPlayer player) {
}
@Override
public void closeInventory(EntityPlayer player) {
}
// 取得Field,在Container中同步服务的客户端数据用
@Override
public int getField(int id) {
switch(id) {
case 0:
return tableBurnTime;
case 1:
return maxBurnTime;
}
return 0;
}
// 设置Field,在Container中同步服务端客户端数据用
@Override
public void setField(int id, int value) {
switch(id) {
case 0:
tableBurnTime = value; break;
case 1:
maxBurnTime = value; break;
}
}
// 设置Field数量
@Override
public int getFieldCount() {
return 2;
}
@Override
public void clear() {
}
@Override
public IChatComponent getDisplayName() {
return null;
}
// 更新流程,由引入ITickable接口完成
@Override
public void update() {
if(!this.worldObj.isRemote) {
// 判断燃烧时间
if (tableBurnTime > 0) {
// 取得修复的物品
ItemStack repairItem = getStackInSlot(0);
// 取得修复好的物品
ItemStack outputItem = getStackInSlot(1);
// 确定开始修复的条件之一:修复物品槽不为空,已修复物品槽为空
if (repairItem != null && outputItem == null) {
// 判断被修复的物品是否为工具或武器
if (repairItem.getItem() == Items.iron_sword || repairItem.getItem() == Items.stone_sword ||
repairItem.getItem() == Items.golden_sword || repairItem.getItem() == Items.diamond_sword ||
repairItem.getItem() instanceof ItemArmor) {
//System.out.println("can repair");
// 判断物品是否要修理
if (repairItem.getItemDamage() > 0) {
// 修复物品
repairItem.setItemDamage(repairItem.getItemDamage() - 1);
} else {
setInventorySlotContents(1, repairItem);
setInventorySlotContents(0, null);
}
}
}
// 减少燃烧时间
tableBurnTime -= 1;
} else { // 没有燃料的情况下
// 如果有被修复的物品
if (getStackInSlot(0) != null) {
// 取得燃料槽的物品
ItemStack burnItem = getStackInSlot(2);
// 取得物品的燃烧值
int getBurnTime = TileEntityFurnace.getItemBurnTime(burnItem);
// 判断物品是否能燃烧
if (getBurnTime > 0) {
maxBurnTime = getBurnTime;
tableBurnTime = getBurnTime;
// 如果燃烧物品为岩浆桶
if (burnItem.getItem() == Items.lava_bucket) {
// 取得空桶
setInventorySlotContents(2, new ItemStack(Items.bucket,
1));
} else {
// 其他物品就减少
if (burnItem.stackSize - 1 > 0) {
burnItem.stackSize--;
setInventorySlotContents(2, burnItem);
} else {
setInventorySlotContents(2, null);
}
}
}
}
}
}
}
}
TileEntity是Block的后台,负责处理复杂的后台逻辑,简单的逻辑像沙子下落、植物生长、红石一般是由Block本身的HUD函数完成。在GUI的应用中还担负数据中心的功能,Block的相关运行数据都储存在其中,并且GUI的数据交互基本也都是有它来完成。
Block文件rtBlockRepairTable.java
import net.minecraft.block.BlockContainer;
import net.minecraft.block.material.Material;
import net.minecraft.block.state.IBlockState;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.tileentity.TileEntity;
import net.minecraft.util.BlockPos;
import net.minecraft.util.EnumFacing;
import net.minecraft.util.EnumWorldBlockLayer;
import net.minecraft.world.World;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
public class rtBlockRepairTable extends BlockContainer {
protected rtBlockRepairTable(Material p_i45386_1_) {
super(p_i45386_1_);
}
@Override
public TileEntity createNewTileEntity(World var1, int var2) {
return new rtTileEntityRepairTable();
}
@Override
public boolean onBlockActivated(World worldIn, BlockPos pos,
IBlockState state, EntityPlayer playerIn, EnumFacing side,
float hitX, float hitY, float hitZ) {
playerIn.openGui(mod_RepairTable.instance, 10, worldIn, pos.getX(), pos.getY(), pos.getZ());
return true;
}
@Override
public int getRenderType() {
return 3;
}
}
继承自BlockContainer,在createNewTileEntity返回一个TileEntity实例,onBlockActivated打开gui并返回true。
GUI客户端rtGuiRepairTable.java
import org.lwjgl.opengl.GL11;
import net.minecraft.client.gui.inventory.GuiContainer;
import net.minecraft.entity.player.InventoryPlayer;
import net.minecraft.inventory.Container;
import net.minecraft.util.ResourceLocation;
import net.minecraft.util.StatCollector;
public class rtGuiRepairTable extends GuiContainer {
private rtTileEntityRepairTable tile;
public rtGuiRepairTable(InventoryPlayer inventory, rtTileEntityRepairTable tileEntity) {
super(new rtContainerRepairTable(inventory, tileEntity));
this.tile = tileEntity;
this.doesGuiPauseGame();
}
// 描绘前景,一般用户描绘文字
@Override
protected void drawGuiContainerForegroundLayer(int par1, int par2) {
super.drawGuiContainerForegroundLayer(par1, par2);
this.fontRendererObj.drawString(StatCollector.translateToLocal("RepairTable"), 65, 6, 4210752);
this.fontRendererObj.drawString(StatCollector.translateToLocal("container.inventory"), 8, this.ySize - 96 + 2, 4210752);
}
// 描绘背景,一般用于描绘图像
@Override
protected void drawGuiContainerBackgroundLayer(float var1, int var2, int var3) {
GL11.glColor4f(1.0F, 1.0F, 1.0F, 1.0F);
this.mc.renderEngine.bindTexture(new ResourceLocation("newmod","textures/gui/RepairTable.png"));
int var5 = (this.width - this.xSize) / 2;
int var6 = (this.height - this.ySize) / 2;
this.drawTexturedModalRect(var5, var6, 0, 0, this.xSize, this.ySize);
int b = tile.tableBurnTime; // 取得Tile内的燃料燃烧时间
float maxBurnTime = tile.maxBurnTime*1.0F;// 取得最大燃料燃烧时间,用float,不用的话得不出百分比
if (b > 0 && maxBurnTime > 0) { // 确定描绘的时机,如果没有燃烧值就不进行描绘
// 描绘火焰图像
this.drawTexturedModalRect(this.guiLeft + 81, this.guiTop + 37 + (int)(14 - 14 * ((float)b / maxBurnTime)), 176, (int)(14 - 14 * ((float)b / maxBurnTime)), 14, (int)(14 * ((float)b / maxBurnTime)));
}
}
}
Gui是客户端显示GUI用的,在早期mod的客户端和服务端分离的时候服务端没有Gui,客户端没有Container,现在也一样,只不过Gui被严格放在ClientSide中执行了,之前的ClientProxy代理就已经将Gui分离到了Client上。
同步数据和处理Slot逻辑的rtContainerRepairTable.java
import java.util.Iterator;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.InventoryPlayer;
import net.minecraft.inventory.Container;
import net.minecraft.inventory.ICrafting;
import net.minecraft.inventory.Slot;
import net.minecraft.item.ItemStack;
import net.minecraft.tileentity.TileEntityFurnace;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.SideOnly;
public class rtContainerRepairTable extends Container {
private int lastTableBurnTime = 0;
private int lastMaxBurnTime = 0;
private rtTileEntityRepairTable tile;
public rtContainerRepairTable(InventoryPlayer par1InventoryPlayer, rtTileEntityRepairTable par2TileEntityRepairTable) {
tile = par2TileEntityRepairTable;
this.addSlotToContainer(new Slot(par2TileEntityRepairTable, 0, 49, 19));
this.addSlotToContainer(new Slot(par2TileEntityRepairTable, 1, 112, 19));
this.addSlotToContainer(new Slot(par2TileEntityRepairTable, 2, 80, 54));
int var3;
for (var3 = 0; var3 < 3; ++var3) {
for (int var4 = 0; var4 < 9; ++var4) {
this.addSlotToContainer(new Slot(par1InventoryPlayer, var4 + var3 * 9 + 9, 8 + var4 * 18, 84 + var3 * 18));
}
}
for (var3 = 0; var3 < 9; ++var3) {
this.addSlotToContainer(new Slot(par1InventoryPlayer, var3, 8 + var3 * 18, 142));
}
}
@Override
public boolean canInteractWith(EntityPlayer var1) {
return true;
}
public void onCraftGuiOpened(ICrafting listener)
{
super.onCraftGuiOpened(listener);
listener.sendAllWindowProperties(this, this.tile);
}
// 客户端的数据同步处理
@SideOnly(Side.CLIENT)
public void updateProgressBar(int par1, int par2) {
if (par1 == 0) {
this.tile.tableBurnTime = par2;
}
if (par1 == 1) {
this.tile.maxBurnTime = par2;
}
}
// 数据同步处理
@Override
public void detectAndSendChanges() {
super.detectAndSendChanges();
Iterator var1 = this.crafters.iterator();
while (var1.hasNext()) {
ICrafting var2 = (ICrafting) var1.next();
if (this.lastTableBurnTime != this.tile.tableBurnTime) {
var2.sendProgressBarUpdate(this, 0, this.tile.tableBurnTime);
}
if (this.lastMaxBurnTime != this.tile.maxBurnTime) {
var2.sendProgressBarUpdate(this, 1, this.tile.maxBurnTime);
}
}
this.lastTableBurnTime = this.tile.tableBurnTime;
this.lastMaxBurnTime = this.tile.maxBurnTime;
}
// Shift处理
@Override
public ItemStack transferStackInSlot(EntityPlayer par1EntityPlayer, int par2) {
ItemStack var3 = null;
Slot var4 = (Slot) this.inventorySlots.get(par2);
if (var4 != null && var4.getHasStack()) {
ItemStack var5 = var4.getStack();
var3 = var5.copy();
// 点击到Slot的ID为0-2之间的时候,,将物品送回玩家的背包中,这时相当于点击上方那3个Block的Slot,ID号在实例化的时候就已经确定了
if (par2 >= 0 && par2 <= 2) {
if (!this.mergeItemStack(var5, 3, 30, false)) {
return null;
}
var4.onSlotChange(var5, var3);
} else if (par2 >= 3 && par2 < 39) {
// 背包中点击燃料,填充燃料到燃料Slot中
if (TileEntityFurnace.isItemFuel(var5)) {
if (!this.mergeItemStack(var5, 2, 3, false)) {
return null;
}
}
// 点击到玩家的背包的时候将物品送到玩家的快捷栏中,mergeItemStack是在一个范围内寻找可以放置的位置,并将物品放置于其中
else if (par2 >= 3 && par2 < 30) {
if (!this.mergeItemStack(var5, 30, 39, false)) {
return null;
}
// 点击到玩家的快捷栏的时候将物品送到背包中
} else if (par2 >= 30 && par2 < 39) {
if (!this.mergeItemStack(var5, 3, 30, false)) {
return null;
}
}
}
if (var5.stackSize == 0) {
var4.putStack((ItemStack)null);
} else {
var4.onSlotChanged();
}
if (var5.stackSize == var5.stackSize) {
return null;
}
var4.onPickupFromSlot(par1EntityPlayer, var5);
}
return var3;
}
}
Container负责处理服务端和客户端的数据同步问题,如果不做同步处理的情况下TileEntity在打开GUI之后会出现服务端数据和客户端数据不相同的情况,会导致进度条闪动,GUI内容不符等各种问题,因为TileEntity的update是同时运行在客户端和服务端的,你可以在TileEntity中的update看到有个if(!this.worldObj.isRemote),这是判断是否为服务端(!isRemote是很老的彩蛋了,在服务端客户端合并的时候就已经这么用了),在服务端上运行的时候才会处理数据,如果不做这个判断数据会错乱得更离谱,至于为什么会出现这种状况,我暂时也没有弄清楚,只知道这么同步肯定没错。
以上就是GUI的主要源码。
本篇教程所用到的源码和资源http://pan.baidu.com/s/1kUvtFEZ
哦哦哦!1.8.9的GUI,鹅妹子吟!
被你发现了→_→
其实GUI变动不算大,连贴图路径的API都没改,依然是熟悉的各种方法改名play
不知道为什么,看完你这篇文章饿了