[1.8.9]当风过时的GUI教程

By | 2016年2月16日

在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

本文链接地址:https://www.windworkshop.cn/?p=889 »文章允许转载 ,转载请注明出处,谢谢。

3 thoughts on “[1.8.9]当风过时的GUI教程

    1. 当风过时 Post author

      被你发现了→_→
      其实GUI变动不算大,连贴图路径的API都没改,依然是熟悉的各种方法改名play

      Reply

发表评论

邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据