MinecraftMod常见问题

这篇文章收集了这四年间被问到的各种问题,因此如果你遇到问题的话,不妨现在这里找找,看看有没有已有的解决方案.
目前离完成还尚早,会不断更新...

目录:

Forge安装与开发环境配置
-ForgeGradle配置慢/配置失败
-在decompileMc或recompileMc时出错
-更换Gradle位置
-Eclipse切换工作目录后是个空的项目
-所谓的"Using incremental CMS..."
-在开发环境下无法启动服务器
-中文目录/带有空格的目录
-IDE项目缺少必备的库
-启动时出现java.lang.reflect.InvocationTargetException
砖块
-父类构造函数未定义等构造函数错误
-砖块实时更新,实时处理砖块发生的变化
-创建一种液体
-创建每面材质不同的砖块
-设定砖块在地图上的颜色
-1.8的BlockState和Property系统
实体
-实体的额外数据与持久化
世界
-世界的额外数据与持久化
-获取一条线段上最近的砖块
-生成粒子特效
-自定义粒子特效
配方
-删除已有的合成与冶炼配方
GUI
-在GUI界面中渲染玩家
事件总线
-注册Forge/FML事件总线时出错
-注册事件无效,监听方法不被调用
-总共有多少个事件总线
-为什么有两套事件总线(FML的和Guava的)
-为什么Forge和FML有独立的事件总线
-FML的事件总线能处理哪些事件
-FML的Guava事件总线能处理哪些事件
Coremod
-如何查询全混淆名和半混淆名

Forge安装与开发环境配置

ForgeGradle配置慢/配置失败
考虑使用FGOW.
不过Gradle本体下载(具体表现是第一次在cmd运行Gradle时显示形如"Downloading http://services.gradle.org/distributions/gradle-X.X-XXX.zip"的字样)的速度慢是无解的,目前国内唯一的Gradle本体的镜像只有bangbang93的BMCLAPI2提供了2.0版的镜像,启用方法是编辑forge目录下的gradle/wrapper/gradle-wrapper.properties,将其中的

distributionUrl=http\://services.gradle.org/distributions/gradle-2.0-bin.zip

改为

distributionUrl=http\://bmclapi2.bangbang93.com/gradle-2.0-bin.zip

其他版本的Gradle目前还没有镜像,也许我有时间会往FMM上传个镜像...

在decompileMc或recompileMc时出错

这个一般是由于内存不足所导致,出错信息多包含有"java.lang.OutOfMemoryError: Java heap space"之类的.解决方法是用文本编辑器打开gradlew.bat(Windows下)或gradlew(OSX或Linux),在"set DEFAULT_JVM_OPTS="的后面加上-Xmx1000m,这个是手动设置最大1000M内存,通常来说是够用了,如果还不够的话可以继续调大.

更换Gradle位置
在Win7/8/8.1中,Gradle默认是把库下载在C:\Users\[用户名]\.gradle中,如果要修改默认位置的话,在环境变量的系统变量里,添加项GRADLE_USER_HOME,内容填新的位置.
在更换目录后,你可能需要输入gradlew.bat inti来重新修复一下.

Eclipse切换工作目录后是个空的项目
大概是配置没完成吧...在setupDecompoWorkspace后别忘了再执行eclipse任务,让Gradle为项目生成内容.

所谓的"Java HotSpot(TM) 64-Bit Server VM warning: Using incremental CMS is deprecated and will likely be removed in a future release"
很多人都说在启动游戏时遇到了这个错误结果进不去游戏,尽管我从来没遇到,但我相信所有的错误肯定都不是因为它的原因...它只是因为Java8将CMS回收器设定为"不推荐使用",而Forge的默认MC启动配置是使用CMS回收器所导致.这肯定不是导致出错的原因,然而由于某些奇妙的原因,这句话往往是在控制台的最后一句话输出,因此很多从不读文字的人误将它认为是Bug原因.
如果你是使用Forge1.7.2和Java8的话,那就把Forge升级到1.7.10或1.8,或者把Java降级到Java7.

在开发环境下无法启动服务器
看看控制台信息在闪退前最后几行有没有输出"You need to agree to the EULA...",有的话打开Forge目录下的eclipse/eula.txt,将eula=false改成eula=true.

中文目录/带有空格的目录
尽量避免目录中带有中文或空格,同样的尽量避免Mod文件的文件名中包含非Ascii字符(文件内含有非Ascii字符没关系,只要文件编码正确即可).

IDE项目缺少必备的库
正常情况下ForgeGradle在配置过程中会下载或生成(比如反混淆后的Minecraft)必须的库,但在少数情况下仍会发生"Project 'Minecraft' is missing required library"这样的缺库问题,主要是这三种情况引起:

  1. 正确下载或生成了库,但在生成IDE项目时没有正确加载库:这种情况下可以执行cleanEclipse/cleanIdea清理项目文件,然后再执行eclipse/idea重新生成IDE项目.还不行的话可以尝试手动指定库的位置...
  2. 生成库失败:forgeSrc/forgeBin和start这三个库是由ForgeGradle在配置项目时生成的,这种情况下就得考虑从头开始重新配置一遍了...
  3. 下载库失败:Gradle确实有如果下载库失败时就会静默地跳过然后在生成IDE项目时给你留个缺失库的特性...然而这在ForgeGradle不太可能出现,因为ForgeGradle在生成项目时的其中一步就是重新编译MC,如果缺库的话会立刻发现的.

启动时出现java.lang.reflect.InvocationTargetException
看一下下面的Caused by是不是"java.lang.UnsatisfiedLinkError: no lwjgl in java.library.path",如果是的话则是JNI没有设定到正确的位置.
通常来讲ForgeGradle在配置时会自动设置项目引用的JNI库的位置,然而那东西你懂的...很多时候只是勉强JustWork™.可以尝试以下手动修复方式:
对于Eclipse,打开你的项目的项目属性,在Java Build Path - Libraries中,随便找一个库(比如forgeSrc),点开它前面的白箭头将它展开,双击Native Library Location,将目录设定到"C:/Users/[用户名]/.gradle/caches/minecraft/net/minecraft/minecraft_natives/[MC版本]",然后应该就可以了,如果你找不到那个目录的话,说明你配置失败了...重新配置一遍开发环境吧.
IDEA或Netbeans也同理,找各自的模块/项目配置,将一个库的JNI目录设到那个文件夹即可.

砖块

父类构造函数未定义等构造函数错误
使用/继承了错误的Block类,你在开发环境下能看到起码六种Block类,正确的Block类是net.minecraft.block包下的类.

砖块实时更新,实时处理砖块发生的变化
首先你要明白自己是在做什么,服务器的地图中你的砖块可能有少至几十块,多至几千块,如果你希望系统实时处理它们,你得知道服务器面临着多大的压力.
砖块有两种更新方式,被动更新和主动更新.
被动更新是指临近砖块发生变动时引发,比如当你调用World类的notifyBlocksOfNeighborChange方法可以让某点的上下左右前后6个方向的临近砖块进行被动更新,也可以调用notifyBlockOfNeighborChange来让某个点的砖块进行被动更新.被动更新调用的Block类方法是onNeighborBlockChange,用它你可以处理诸如红石信号变化,或者临近砖块的破坏等事件.
主动更新则是每隔一段时间固定引发一次.主动更新都是调用Block类的updateTick方法,但更新时间间隔有两种,一个是永久性随机更新,即间隔随机的一段时间时进行更新,启用随机更新的办法是在初始化砖块时调用setTickRandomly(true),随机更新适用于那些对更新频率要求不高的,比如农作物什么的;另一种更新则是一次性定时更新,通过调用World类的scheduleBlockUpdate或scheduleBlockUpdateWithPriority来让游戏在指定帧间隔后调用某点砖块的updateTick,在调用完之后,游戏就不会再调用了,除非你手动再申请一次,这种更新适合像按钮之类的会一次性定时发生变化的砖块;如果你在updateTick中申请定时更新的话,那这就变成永久性的固定间隔更新了,适用于像流体那种需要频繁更新的东西.

创建一种液体
Forge提供了一套流体系统,具体见http://www.minecraftforge.net/wiki/Create_a_Fluid,Wiki上难得一篇写的好的文章...我不觉得我能写的比它更清楚.

创建每面材质不同的砖块(即多材质砖块)
1.7:最简单的是像书架那样与方向无关的多面砖块,只要重写砖块类的getIcon,根据不同面(第一个参数)来返回不同材质,然后再重写registerBlockIcons来加载多个材质就行.比如:

public class BlockSkybox extends Block {

	private IIcon[] icons;
	
	public BlockSkybox() {
		super(Material.rock);
		setBlockName("SkyboxBlock");
		setCreativeTab(CreativeTabs.tabBlock);
		//因为下面我们是手动加载材质,所以就不需要setBlockTextureName了
	}

	@Override
	public IIcon getIcon(int face, int meta) {
		return icons[face];
	}

	@Override
	public void registerBlockIcons(IIconRegister iIconRegister) {
		icons = new IIcon[6];
		icons[0] = iIconRegister.registerIcon("examplemod:csskybox_down");
		icons[1] = iIconRegister.registerIcon("examplemod:csskybox_up");
		icons[2] = iIconRegister.registerIcon("examplemod:csskybox_north");
		icons[3] = iIconRegister.registerIcon("examplemod:csskybox_south");
		icons[4] = iIconRegister.registerIcon("examplemod:csskybox_west");
		icons[5] = iIconRegister.registerIcon("examplemod:csskybox_east");
	}
}

2015-11-03_21.52.55
而如果要实现像熔炉或活塞那样有方向的多材质方块就有点复杂,首先是要判断砖块该朝向哪个方向,BlockPistonBase类的determineOrientation方法可以用来判断合适的朝向,返回的数值即砖块正向所在的面;然后是数值的存储和读取,这个可以用metadata来完成.下面给出一个不完善的例子,不完善的地方在稍后解释:

public class BlockKokoro extends BlockDirectional {

	private IIcon forward, left, right, top, other;
	
	public BlockKokoro() {
		super(Material.iron);
		setBlockName("KokoroBlock");
		setCreativeTab(CreativeTabs.tabBlock);
	}

	@Override
	public void onBlockAdded(World world, int x, int y, int z) { //通过setBlock放置的砖块
		world.setBlockMetadataWithNotify(x, y, z, 2, 1 | 2); //一个更复杂的机制是判断附近哪个方向没有其他砖块,然后朝向那里.这里我们简单地让砖块朝北.
	}

	@Override
	public void onBlockPlacedBy(World world, int x, int y, int z, EntityLivingBase entity, ItemStack itemStack) {
		int meta = BlockPistonBase.determineOrientation(world, x, y, z, entity);
		world.setBlockMetadataWithNotify(x, y, z, meta, 1 | 2); //1|2的来历见World类的setBlock
	}
	
	@Override
	public IIcon getIcon(int face, int meta) {
		switch (meta) {
		case 0: //朝下
			face = Facing.oppositeSide[face];
		case 1: //朝上
			switch (face)
			{
			case 0: return other;
			case 1: return forward;
			case 2: return top;
			case 3: return other;
			case 4: return right;
			case 5: return left;
			}
			break;
		case 2: //朝北
			face = Facing.oppositeSide[face];
		case 3: //朝南
			switch (face)
			{
			case 0: return other;
			case 1: return top;
			case 2: return other;
			case 3: return forward;
			case 4: return right;
			case 5: return left;
			}
			break;
		case 4: //朝西
			face = Facing.oppositeSide[face];
		case 5: //朝东
			switch (face)
			{
			case 0: return other;
			case 1: return top;
			case 2: return left;
			case 3: return right;
			case 4: return other;
			case 5: return forward;
			}
			break;
		}
		return other;
	}

	@Override
	public void registerBlockIcons(IIconRegister iIconRegister) {
		forward = iIconRegister.registerIcon("examplemod:kokoro_f");
		left = iIconRegister.registerIcon("examplemod:kokoro_l");
		right = iIconRegister.registerIcon("examplemod:kokoro_r");
		top = iIconRegister.registerIcon("examplemod:kokoro_t");
		other = iIconRegister.registerIcon("examplemod:kokoro_o");
	}
}

这个跟上一个相比主要是多了个onBlockPlacedBy和onBlockAdded,用来判断放置砖块时的朝向,然后在getIcon里根据渲染面和砖块朝向的关系来决定材质.这个方案的不完善之处在于材质不会旋转,当正面朝上或朝下时问题尤为明显,在1.7下解决方案恐怕只有将侧面材质设计为与方向无关的对称图形,或者为侧面材质额外制作其他的版本,比如左旋和右旋版本.
2015-11-03_22.50.22
2015-11-03_22.50.42

1.8:
待补充,如果我过了很久都忘了写的话尽管spam我!

设定砖块在地图上的颜色
砖块在地图上的颜色由它的材质决定,材质的构造函数中的参数就是地图颜色,遗憾的是游戏似乎不支持自定义颜色...

1.8的BlockState和Property
这部分其实应该放在砖块章节,然而由于教程目前尚未全面更新到1.8,因此现在这里写一下.
在1.8之前,我们如果想在砖块中存储额外信息的话只能使用4位的Metadata.而从1.8开始,Minecraft提供了BlockState系统.
BlockState系统的总体思想是提供一种更好地表达一个砖块的状态的方案,这个方案既要能直白地表达要存储的状态;又要足够的小,不至于让内存或硬盘爆掉;同时也要足够的快.最终产物就是BlockState.
原版BlockState系统(Forge对它做了扩展,这个以后有时间再写)主要由这三部分组成:
BlockState类:描述一种砖块(即一个Block类的实例)所有可能的状态.
IBlockState接口:代表一种状态.
IProperty接口:代表状态中的一种属性.
我们以1.8的石头砖块的BlockStone类为例来解释各部分的作用和使用方法.
首先它先声明了一种属性:

public static final PropertyEnum VARIANT = PropertyEnum.create("variant", BlockStone.EnumType.class);

从代码中可以很直观地看到VARIANT是一种枚举类型的属性,PropertyEnum继承自PropertyHelper,而PropertyHelper则实现了IProperty.Minecraft默认提供了这三种属性类型的实现:连续整数(PropertyInteger),枚举(PropertyEnum)和布尔值(PropertyBool),其中枚举还有一个专门用于描述方向的封装PropertyDirection.
然后我们要声明BlockState,这部分的代码在稍微靠下的地方:

protected BlockState createBlockState()
{
    return new BlockState(this, new IProperty[] {VARIANT});
}

它重写了基类的createBlockState,声明了一个拥有VARIANT属性的BlockState,它的构造函数会自动计算所有属性能组成的全部状态,用数学的说法是求它们的笛卡尔积,比如如果一个BlockState有一个取值范围在0~2的连续整数属性和一个布尔值属性,那么它就有6种状态,分别是{0 False, 0 True, 1 False, 1 True, 2 False, 2 True}.
我们之前提到IBlockState接口的实例代表一种状态,首先我们先说明如何修改状态,修改状态是通过IBlockState的withProperty方法,这个方法的参数是给定一个待更新属性和它的待更新值,返回值是更新后的状态,还是以刚才的为例,假如那个整数属性叫INT,那么要将一个值为{0 False}的状态改成{2 False}的话,代码是:

IBlockState state = oldState.withProperty(INT, Integer.valueOf(2)); //oldState为旧状态,state为修改值后的新状态

需要注意的是,withProperty不会对旧状态产生任何影响,上述代码在运行完后oldState不会有任何改变,因此你永远无需担心调用withProperty会破坏旧状态,你可能会认为Minecraft默认的IBlockState实现采用的是不变量(Immutable)设计,即每次修改值时始终返回一个新实例而不会改变原实例,这并不是完全正确的,实际上是BlockState已经预先生成好了所有可能的状态,每次调用withProperty时只是根据一个状态转移表查找到应转移到的状态,然后返回那个状态的实例.
通过withProperty,我们可以将任何一个已有状态修改成我们想要的状态,但是我们该如何获取"第一个"状态,也就是如何凭空获取一个IBlockState实例? BlockState提供了getBaseState方法可以获取一个空的状态,然而这个空状态的值是不确定的,每次手工赋值必然很麻烦,因此Block类允许配置一个默认状态,设置默认状态的办法是调用Block类的setDefaultState,以BlockStone为例:

this.setDefaultState(this.blockState.getBaseState().withProperty(VARIANT, BlockStone.EnumType.STONE));

这个代码是设置该砖块的默认状态为{STONE},以后如果要获取默认状态,直接调用Block类的getDefaultState方法即可.
最后我们要考虑的是如何保存状态,现在我们又要和老朋友Metadata打招呼了,MC1.8仍然使用Metadata系统,并且也依然受最多16种取值的限制,所以说你的BlockState虽然最多可能有超过16种状态,但在保存时必须只保留最关键的属性,将它简化到16种以下,比如说如果你设计了一种管道砖块,它有2种属性,一个有16种取值的用于标识管线类型的枚举属性,和一个范围在0~63用于标识管道与周围连接情况的整数属性(如果你好奇0~63是怎么来的话...使用6个二进制位来标识这个砖块与周围6个方向的连通情况),显然它们的笛卡尔积有多达1024种情况,然而我们可以只保存标识管线类型的那个属性,标识连接情况的属性可以在运行时通过判断周围砖块的类型来推算出来.
保存、读取和计(bu)算(wan)非关键属性的方法以及它们的使用范例分别是:

public int getMetaFromState(IBlockState state)
{
    //返回值为该状态的Metadata的值,不需要的话就返回0或者干脆不重写这个方法
    return ((EnumPipeType)state.getValue(pipeType)).getMetadata(); //模仿BlockStone的写法
}
public IBlockState getStateFromMeta(int meta)
{
    //返回值为该Metadata对应的状态,不需要的话就返回getDefaultState或者干脆不重写这个方法
    return getDefaultState().withProperty(pipeType, EnumPipeType.byMetadata(meta)); //模仿BlockStone的写法
}
public IBlockState getActualState(IBlockState state, IBlockAccess worldIn, BlockPos pos)
{
    //返回值为补全了非关键属性的状态,如果不需要补全数据的话,直接"return state;"即可;或者干脆不重写这个方法.
    int i = 0;
    i |= (worldIn.getBlockState(pos.up()   ).getBlock() == this ? 1 : 0) << EnumFacing.UP   .getIndex();
    i |= (worldIn.getBlockState(pos.down() ).getBlock() == this ? 1 : 0) << EnumFacing.DOWN .getIndex();
    i |= (worldIn.getBlockState(pos.east() ).getBlock() == this ? 1 : 0) << EnumFacing.EAST .getIndex();
    i |= (worldIn.getBlockState(pos.west() ).getBlock() == this ? 1 : 0) << EnumFacing.WEST .getIndex();
    i |= (worldIn.getBlockState(pos.north()).getBlock() == this ? 1 : 0) << EnumFacing.NORTH.getIndex();
    i |= (worldIn.getBlockState(pos.south()).getBlock() == this ? 1 : 0) << EnumFacing.SOUTH.getIndex();
    return state.withProperty(linkingState, Integer.valueOf(i));
}

原版BlockState系统的内容便就此结束了,如果你问BlockState系统的作用的话,那就是它创造了数据驱动渲染的可能,数据驱动就是指通过外部数据来决定程序运作流程,而不是通过硬编码的代码,比如BlockModelShapes类中的这行代码:

this.registerBlockWithStateMapper(Blocks.stone, (new StateMap.Builder()).setProperty(BlockStone.VARIANT).build());

这样便建立了砖块状态与渲染的映射关系,游戏会根据VARIANT属性的值从assets/minecraft/bloackstates中选择合适的数据文件用于渲染.

实体

实体的额外数据与持久化
调用实体的getEntityData方法可以获取一个Forge提供的专门用于存储自定义信息的NBT组件,服务器会自动完成它的保存和读取.然而在多人游戏中这些数据不会被服务器同步给客户端,因此你可能需要借助DataWatcher或自定义封包等方式来实现客户端到服务器的数据同步,这个问题将在网络章节中讨论.
此外,对于玩家实体来说还有一种方式来实现持久化,就是监听PlayerEvent.SaveToFile事件和PlayerEvent.LoadFromFile事件,这两个是在服务器保存和读取一个玩家的信息时发生,这两个事件均提供了名叫getPlayerFile的方法,通过这个方法你可以让Forge为你推荐一个指定后缀名的存储文件位置(并不会立刻创建文件),举例:

    @SubscribeEvent
    public void onPlayerLoad(PlayerEvent.LoadFromFile event) {
    	File file = event.getPlayerFile("nvm");
    	Date registerTime;
    	if(!file.exists())
    	{
    		ObjectOutputStream stream = null;
    		registerTime = new Date();
    		try {
				stream = new ObjectOutputStream(new FileOutputStream(file));
				stream.writeObject(registerTime);
			} catch (IOException e) {
				e.printStackTrace();
			} finally {
				if(stream != null)
					try { stream.close(); } catch (Exception e2) {}
			}
    	}
    	else
    	{
    		ObjectInputStream stream = null;
    		try {
    			stream = new ObjectInputStream(new FileInputStream(file));
    			registerTime = (Date)stream.readObject();
    		} catch (Exception e) {
    			e.printStackTrace();
    			registerTime = new Date();
    		} finally {
				if(stream != null)
					try { stream.close(); } catch (Exception e2) {}
			}
    	}
    	//把registerTime存在某处,比如一个map中...
    }
//saves\New World\playerdata\a498be5d-0142-3d23-a17c-8823a1cd27b0.nvm

这段代码用来记录玩家首次登陆服务器的"注册时间"(虽然这用NBT能更好地完成).游戏会在玩家进入游戏时检查存档中的playerdata目录下有没有名为"[玩家的uuid].nvm"的文件,如果没有的话,则会创建一个并写入玩家的登陆时间;有的话则会读取.
这段代码没有考虑文件写入的原子性,如果文件较大,或者纯粹是运气不好的话,可能在你向磁盘中写入文件时恰好赶上服务器崩溃、电源掉电、机柜被维修工当成空调拉走等各种让程序中断的情况,如果此时正巧你在向硬盘中写入数据的话,文件就损坏了,可以想象当重新上线的玩家看到他的数据一夜回到解放前时会有多么愤怒,因此如果你要保存的文件较大的话,应当考虑先输出到临时文件当中,在输出完毕后删掉旧文件并重命名临时文件.
如果你是个超负责任的人的话,你会发现这里还有个小问题,就是删掉旧文件和重命名临时文件这两个操作也不是原子性的...可能旧文件删掉后临时文件尚未改名系统便崩溃了,对于这种情况有3种办法:
1.自验证完整性:在保存的文件中加入能够验证完整性的内容,比如在结尾加入个特殊字符什么的,如果在载入文件时发现有临时文件存在的话,就验证临时文件是否完整,完整的话就使用临时文件的内容,另外别忘了顺便把之前未完成的工作完成了.
2.旧文件备份:这个的思路是在完成临时文件的写入后,不立刻删掉旧文件,而是将旧文件改名为一个备份文件,然后将临时文件改名为正常文件.当读取文件时,如果正常文件不存在的话,就去读备份文件.
3.原子性替换:大部分符合POSIX标准的操作系统都支持原子性文件替换,Windows号称部分符合POSIX标准,遗憾的是这不是其中之一.不过Java7提供了跨平台的原子性文件替换方法,比如:

Files.move(tempFile.toPath(), oldFile.toPath(), StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE);

然而这个是Java7的功能,MC要求的最低版本是Java6,虽然我不觉得现在还有土鳖在使用Java6,但谁知道会不会真有人在用呢...而且这个方法有潜在的失败可能,文档中明确提到如果原子操作不可行(比如在Windows中进行跨盘复制时)的话会立刻抛出异常.
如果不考虑方法3的话,方法1和方法2的区别在于1是在发生问题时尽量使用新文件,2是在发生问题时尽量使用旧文件;1需要特别设计的文件格式,而2则不需要.

世界

世界的额外数据与持久化
WorldInfo类的setAdditionalProperties和getAdditionalProperty方法可以分配或获取一个NBT节点,这些数据会随着该WorldInfo所属的World被一并保存或读取.显然在多人游戏中这些数据不会被服务器自动同步给客户端,如果你需要的话,可能得通过自定义封包来手动同步数据.
获取WorldInfo的办法是调用World类的getWorldInfo.
此外,GameRuler类也可以用来保存数据,然而它只能存储字符串.

获取一条线段上最近的砖块
World类提供了rayTraceBlocks方法用于查找一条有向线段中第一个接触线段的砖块,它有如下3种重载(其中一个在1.7.10中叫func_147447_a):
rayTraceBlocks(Vec3 start, Vec3 end, boolean includeLiquidAndFire, boolean ignoreBlockWithoutBoundingBox, boolean returnLastUncollidableBlock)
start和end为线段的起点和终点;includeLiquidAndFire为是否包括火焰和液体;ignoreBlockWithoutBoundingBox为是否忽略无碰撞(也就是没有碰撞箱)的砖块;returnLastUncollidableBlock为是否一定要返回一个非null结果,如果此参数为true的话,即使未找到结果也会返回最后一个无碰撞砖块的位置,同时返回结果(一个MovingObjectPosition实例)中的typeOfHit字段会被标记为MISS,如果为false,或者连无碰撞砖块都找不到的话,则会返回null.
rayTraceBlocks(Vec3 start, Vec3 end, boolean includeLiquidAndFire)
重载上一个,ignoreBlockWithoutBoundingBox和returnLastUncollidableBlock都为false.
rayTraceBlocks(Vec3 start, Vec3 end)
重载上一个,includeLiquidAndFire为false.

生成粒子特效
生成粒子特效可以用World类的spawnParticle,它的7个参数分别是"粒子类型"、粒子的初始位置和初始速度矢量,可用的粒子类型详见http://minecraft.gamepedia.com/Particles中的Particle name一项.
此外,只有在客户端调用spawnParticle才会有效果,在服务器端调用会被静默地忽视掉.

自定义粒子特效
如果现有的粒子特效不够用的话,可以自行创建一个EntityFX的子类,然后在客户端的代理器(Proxy)通过Minecraft.getMinecraft().effectRenderer.addEffect来添加粒子特效.
需要注意的有两个问题,一个是添加自定义粒子特效必须在客户端进行,因为服务器就压根没EffectRenderer和EntityFX这两个东西...第二个是在添加时必须根据待添加的世界的isRemote方法判断是否为客户端世界,即使你的代码已经是在客户端代理器中也必须进行这个判断,并且只在客户端世界中添加特效,如果不这样做的话,你会有一定几率在单人游戏时遇到一个奇特的竞态条件,具体表现形式为在创建特效时游戏会有一定几率弹出,错误原因是随机的一个实体的moveEntity方法在服务器线程中出现了一个NullPointerException,出现位置是在一个"for (int i = 0; i < list.size(); ++i)"的循环体中,其中list.size()为0...看起来像是服务器线程和客户端线程在同时操作一个列表时发生了冲突. 这里给出一个简单的示例 - 砍怪时飙血. 首先是在客户端代理器中监听实体遭受攻击事件,然后创建相应的粒子特效: [code lang="java"] @SubscribeEvent public void onEntityAttacked(LivingAttackEvent event) { Entity attacking = event.source.getSourceOfDamage(); //attacking为攻击者 if(attacking == null || !attacking.worldObj.isRemote) //对于攻击者不是实体,或者该世界不为客户端世界的情况,直接返回 return; EntityLivingBase attacked = event.entityLiving; //attacked为受害者 for(int i = 0; i < 10; i++) { Minecraft.getMinecraft().effectRenderer.addEffect( //添加一个待会制作的EntityBlood实体,它的参数是游戏世界,3个double表示的粒子位置,以及1个Vec3表示的方向矢量 new EntityBlood(attacked.worldObj, attacked.posX, attacked.posY + attacked.getEyeHeight(), attacked.posZ, Vec3.createVectorHelper(attacking.posX - attacked.posX, //根据攻击者和被攻击者的位置计算出血喷出的方向 attacking.posY - attacked.posY - attacked.getEyeHeight(), attacking.posZ - attacked.posZ).normalize())); //方向矢量最好是单位长度,因此做一次规格化 } } [/code] 然后要制作EntityBlood类,它的代码并不复杂. [code lang="java"] @SideOnly(Side.CLIENT) public class EntityBlood extends EntityFX { private final float PI3 = (float)(Math.PI / 3.0); private final float PI6 = (float)(Math.PI / 6.0); public EntityBlood(World world, double posX, double posY, double posZ, Vec3 direction) { super(world, posX, posY, posZ); double speed = rand.nextDouble() * 0.3d; //设定速度为0.0~0.3范围内的随机数 direction.rotateAroundY((rand.nextFloat() - 0.5f) * PI3); //让喷血方向带有一定的随机性 direction.rotateAroundX((rand.nextFloat() - 0.5f) * PI6); direction.rotateAroundZ((rand.nextFloat() - 0.5f) * PI6); setVelocity(direction.xCoord * speed, direction.yCoord * speed, direction.zCoord * speed); //根据方向矢量设定速度矢量 setSize(0.1f, 0.1f); //设定粒子的碰撞尺寸,如果不想要碰撞的话不妨试试noClip = true ? this.particleMaxAge = (int)(10f / (this.rand.nextFloat() * 0.75f + 0.25f)); //设定粒子的寿命,单位为tick this.particleGravity = 0.3f; //受重力加速度影响的系数,1.0为每tick Y方向速度减-0.04 (不考虑空气阻力的话) this.particleGreen = 0.0f; //RGB三颜色中的G系数,默认全为1.0,即白色/显示纹理正常颜色 this.particleBlue = 0.0f; //将G和B全改为0的话,那就是红色/纹理"偏"红 this.particleAlpha = 1.0f; //不透明度,默认就是1.0... this.particleScale = 3.0f; //尺寸系数,默认是1.0~2.0之间的随机数 } } [/code] 2015-12-30_00.33.03

关于粒子纹理图可以见http://minecraft.gamepedia.com/File:Particles.png,默认的粒子纹理是左上角的那个小方点.想修改的话可以用setParticleTextureIndex来设定所使用的部分,计算方法是把图片分成16x16共256份,然后按照从左往右,从上往下的顺序数,第一行第一列(也就是默认部分)为0,第一行第二列为1,第二行第一列为16.愤怒的村民脸为83.
想让血液纹理为默认的8种爆炸烟雾之一的话,只要在构造函数中添加:

setParticleTextureIndex(rand.nextInt(8));

2015-12-30_00.37.42

如果想要自定义纹理的话倒也不是很麻烦,但效率很低,由于默认的EffectRenderer的限制,要在每次渲染时重启Tessellator,然后重新绑定纹理,完成渲染后还要再次重启Tessellator并绑定回原纹理.这里给出一个效率不是很高的实现:

@SideOnly(Side.CLIENT)
public class EntityBlood extends EntityFX {
	private static TextureManager textureManager = Minecraft.getMinecraft().renderEngine;
	//值得注意的是,这里ResourceLocation必须写完整地址,不能像Block或Item中那样简写为"examplemod:splatt"了...
	private static ResourceLocation texture = new ResourceLocation("examplemod:textures/particles/splatt.png");
	private final float PI3 = (float)(Math.PI / 3.0);
	private final float PI6 = (float)(Math.PI / 6.0);

	public EntityBlood(World world, double posX, double posY, double posZ, Vec3 direction) {
		super(world, posX, posY, posZ);
		double speed = rand.nextDouble() * 0.3d; //设定速度为0.0~0.3范围内的随机数
		direction.rotateAroundY((rand.nextFloat() - 0.5f) * PI3); //让喷血方向带有一定的随机性
		direction.rotateAroundX((rand.nextFloat() - 0.5f) * PI6);
		direction.rotateAroundZ((rand.nextFloat() - 0.5f) * PI6);
		setVelocity(direction.xCoord * speed, direction.yCoord * speed, direction.zCoord * speed); //根据方向矢量设定速度矢量
		setSize(0.1f, 0.1f); //设定粒子的碰撞尺寸,如果不想要碰撞的话不妨试试noClip = true ?
		this.particleMaxAge = (int)(10f / (this.rand.nextFloat() * 0.75f + 0.25f)); //设定粒子的寿命,单位为tick
		this.particleGravity = 0.3f; //受重力加速度影响的系数,1.0为每tick Y方向速度减-0.04 (不考虑空气阻力的话)
		this.particleGreen = 0.0f; //RGB三颜色中的G系数,默认全为1.0,即白色/显示纹理正常颜色
		this.particleBlue = 0.0f; //将G和B全改为0的话,那就是红色/纹理"偏"红
		this.particleAlpha = 1.0f; //不透明度,默认就是1.0...
		this.particleScale = 3.0f; //尺寸系数,默认是1.0~2.0之间的随机数
	}
	
	@Override
	public int getFXLayer() {
		//EffectRenderer根据使用的纹理图不同将粒子分为了3类(层),0类使用粒子纹理图,1类使用砖块纹理图,2类使用物品纹理图.
		//这里我们跟2类共用一层倒也没什么玄学,纯粹是因为物品纹理图(TextureMap.locationItemsTexture)是public,而粒子纹理图是private...
		return 2;
	}

	@Override
	public void renderParticle(Tessellator tessellator, float delta, float rotationX, float rotationXZ,
			float rotationZ, float rotationYZ, float rotationXY) {
		tessellator.draw(); //Tessellator一次渲染只能使用一种纹理,因此要重启并重新绑定
		tessellator.startDrawingQuads();
		textureManager.bindTexture(texture);
		float f6 = 0.0f; //这4个参数是用来控制UV的,如果你是一个粒子对应一整个纹理的话,那就保留这个设定就行了
        float f7 = 1.0f;
        float f8 = 0.0f;
        float f9 = 1.0f;
        float scale = 0.1F * this.particleScale;

        float f11 = (float)(this.prevPosX + (this.posX - this.prevPosX) * (double)delta - interpPosX);
        float f12 = (float)(this.prevPosY + (this.posY - this.prevPosY) * (double)delta - interpPosY);
        float f13 = (float)(this.prevPosZ + (this.posZ - this.prevPosZ) * (double)delta - interpPosZ);
        tessellator.setColorRGBA_F(this.particleRed, this.particleGreen, this.particleBlue, this.particleAlpha);
        tessellator.addVertexWithUV((double)(f11 - rotationX * scale - rotationYZ * scale), (double)(f12 - rotationXZ * scale), (double)(f13 - rotationZ * scale - rotationXY * scale), (double)f7, (double)f9);
        tessellator.addVertexWithUV((double)(f11 - rotationX * scale + rotationYZ * scale), (double)(f12 + rotationXZ * scale), (double)(f13 - rotationZ * scale + rotationXY * scale), (double)f7, (double)f8);
        tessellator.addVertexWithUV((double)(f11 + rotationX * scale + rotationYZ * scale), (double)(f12 + rotationXZ * scale), (double)(f13 + rotationZ * scale + rotationXY * scale), (double)f6, (double)f8);
        tessellator.addVertexWithUV((double)(f11 + rotationX * scale - rotationYZ * scale), (double)(f12 - rotationXZ * scale), (double)(f13 + rotationZ * scale - rotationXY * scale), (double)f6, (double)f9);
        tessellator.draw();
		tessellator.startDrawingQuads();
        textureManager.bindTexture(TextureMap.locationItemsTexture); //绑定回物品纹理图
	}
}

splatt

这里的纹理我放在了"assets/examplemod/textures/particles"中,纹理我使用的是一张HL1的喷漆图,希望G胖不会来爆我菊花吧 233333

Gabe:(此处应用古朗特的语气)Too late! YOU ARE DEAD!

BoDTO5WIQAAGy1b

2015-12-30_00.31.55

不管怎么说,效果还是很好的,虽然说是效率有问题,但对于几十甚至几百个粒子来说,还是足以力保60fps大关的,如果你真的追求高性能自定义粒子渲染的话,可以不妨自己写一个粒子渲染器,EffectRenderer的代码并不复杂,RenderWorldLastEvent也可以作为自定义渲染器启动渲染的触发事件.

配方

删除已有的合成与冶炼配方
所有的合成配方都储存在CraftingManager的recipes字段中,你可以通过调用CraftingManager.getInstance().getRecipeList()来获取它,它储存有所有已注册的合成配方,你可以遍历这个列表然后找出指定的合成并删掉它,这里有一个现成的已封装好的代码,以及使用范例:

    /**
     * 移除一个合成配方,此版本仅对比合成的输出物,而且输出物的类型,数量和itemDamage(如果配方指定了itemDamage的话)必须全部匹配才会执行删除操作.
     * @param output 输出物的物品栈
     * @return 是否找到并移除了一个配方
     */
    public boolean removeRecipe(ItemStack output) {
    	for(Iterator<Object> iterator = CraftingManager.getInstance().getRecipeList().iterator(); iterator.hasNext();)
    	{
    		IRecipe recipe = (IRecipe)iterator.next();
    		ItemStack recipeOutput = recipe.getRecipeOutput();
    		if(OreDictionary.itemMatches(recipeOutput, output, false) && recipeOutput.stackSize == output.stackSize)
    		{
    			iterator.remove();
    			return true;
    		}
    	}
    	return false;
    }
    
    /**
     * 移除一个合成配方,此版本会详细对比合成的输入物,并且考虑输入物的类型和itemDamage(如果配方指定了itemDamage的话).
     * @param input 见removeRecipe(ItemStack[] input, ItemStack output)
     * @return 是否找到并移除了一个配方
     */
    public boolean removeRecipe(ItemStack[] input) {
    	return removeRecipe(input, null);
    }
    
    /**
     * 移除一个合成配方,此版本会详细对比合成的输入物,并且考虑输入物的类型和itemDamage(如果配方指定了itemDamage的话).
     * 此外,还可以指定一个输出物用于筛选配方提高效率,输出物只考虑类型,不考虑数量和itemDamage.
     * @param input 输入物组成的合成公式,需要严格按照添加合成配方时的样式进行,比如火炬是长度为2的数组,里面是1个煤和1个木棍;
     * 				工具台是长度为4的数组,里面是4个木头;告示牌是长度为9的数组,里面是6个木头,1个null,1个木棍,然后再跟1个null.
     * @param output 输出物,用于预剔除来提高效率,可选参数,可以是null - 不进行预剔除.
     * @return 是否找到并移除了一个配方
     */
    public boolean removeRecipe(ItemStack[] input, ItemStack output) {
    	dirtygoto: for(Iterator<Object> iterator = CraftingManager.getInstance().getRecipeList().iterator(); iterator.hasNext();)
    	{
    		IRecipe recipe = (IRecipe)iterator.next();
    		ItemStack recipeOutput = recipe.getRecipeOutput();
    		if(recipeOutput == null || (output != null && recipeOutput.getItem() != output.getItem()))
    			continue;
    		Object[] targetStacks;
    		if(recipe instanceof ShapedRecipes)
    			targetStacks = ((ShapedRecipes)recipe).recipeItems;
    		else if(recipe instanceof ShapedOreRecipe)
    			targetStacks = ((ShapedOreRecipe)recipe).getInput();
    		else if(recipe instanceof ShapelessRecipes)
    			targetStacks = ((ShapelessRecipes)recipe).recipeItems.toArray();
    		else if(recipe instanceof ShapelessOreRecipe)
    			targetStacks = ((ShapelessOreRecipe)recipe).getInput().toArray();
    		else
    			continue;
			if(targetStacks.length == input.length)
			{
				for(int i = 0; i < targetStacks.length; i++)
				{
					if(targetStacks[i] instanceof ItemStack)
					{
						if(!OreDictionary.itemMatches((ItemStack)targetStacks[i], input[i], false))
							continue dirtygoto;
					}
					else if(targetStacks[i] instanceof ArrayList)
					{
						boolean match = false;
						ArrayList list = (ArrayList)targetStacks[i];
						for(Object object : list)
						{
							if(OreDictionary.itemMatches((ItemStack)object, input[i], false))
							{
								match = true;
								break;
							}
						}
						if(!match)
							continue dirtygoto;
					}
				}
				iterator.remove();
    			return true;
			}
    	}
    	return false;
    }
    @EventHandler
    public void postInit(FMLPostInitializationEvent event) {
    	//删除工具台
    	ItemStack itemStack = new ItemStack(Blocks.crafting_table);
    	removeRecipe(itemStack);
    	
    	//删除告示牌
    	ItemStack plank = new ItemStack(Blocks.planks);
    	ItemStack stick = new ItemStack(Items.stick);
    	ItemStack sign = new ItemStack(Items.sign);
    	ItemStack[] itemStacks = new ItemStack[] {
    			plank, plank, plank,
    			plank, plank, plank,
    			null,  stick, null
    			};
    	removeRecipe(itemStacks, sign);
    	
    	//删除末地眼
    	ItemStack enderPearl = new ItemStack(Items.ender_pearl);
    	ItemStack powerDaze = new ItemStack(Items.blaze_powder);
    	ItemStack enderEye = new ItemStack(Items.ender_eye);
    	itemStacks = new ItemStack[] {enderPearl, powerDaze};
    	removeRecipe(itemStacks, enderEye);
    	
    	//删除火炬 - 火炬有2种合成,因此要删两遍
    	ItemStack torch = new ItemStack(Blocks.torch);
    	itemStacks = new ItemStack[] {new ItemStack(Items.coal, 1, 0), stick}; //使用煤炭的合成
    	removeRecipe(itemStacks, torch);
    	itemStacks = new ItemStack[] {new ItemStack(Items.coal, 1, 1), stick}; //使用焦煤的合成
    	removeRecipe(itemStacks, torch);
    }

GUI

在GUI界面中渲染玩家
如果是在游戏中的话这倒不是难事,GuiInventory类的drawEntityOnScreen就可以绘制一个生物实体到屏幕上.但如果不在游戏中,比如在主菜单时,想绘制一个玩家的话就不容易了,因为这时没有游戏世界,也没有玩家尸体.但也并非不可实现.这里给出一个参考示例,一个开箱即用的玩家渲染器.

16-1-25更新:修复了导致游戏内玩家模型渲染错误的bug...两个版本的变更代码均为static块内的内容.

1.7.10版

/**
 * 在屏幕中以正交投影渲染一个玩家,建议在DrawScreenEvent.Post事件或界面类的drawScreen中进行.
 */
@SideOnly(Side.CLIENT)
public class ScreenPlayerRenderer {
	//玩家模型
	private static final ModelBiped steveModel;
	public static final ResourceLocation defaultSteveTexture = new ResourceLocation("textures/entity/steve.png");
	private static final Map<String, ResourceLocation> skinCache = new ConcurrentHashMap<String, ResourceLocation>();
	
	private ModelBiped model = steveModel;
	private boolean isAlex = false;
	private float headPitch = 0f, headYaw = 0f;
	private float frame = 0f, walk = 0f;;
	
	static {
		steveModel = new ModelBiped();
	}
	
	/**
	 * 设置皮肤贴图为默认贴图
	 */
	public void setSkin() {
		Minecraft.getMinecraft().renderEngine.bindTexture(defaultSteveTexture);
	}
	
	/**
	 * 根据玩家名称自动从网上获取贴图,在贴图读取完毕前,玩家贴图会显示为默认贴图. 
	 */
	public void setSkin(String name) {
		ResourceLocation resourceLocation = skinCache.get(name);
		if(resourceLocation == null)
		{   //此处为net.minecraft.util下的StringUtils
			resourceLocation = new ResourceLocation("skins/" + StringUtils.stripControlCodes(name));
			AbstractClientPlayer.getDownloadImageSkin(resourceLocation, name);
			skinCache.put(name, resourceLocation);
		}
		Minecraft.getMinecraft().renderEngine.bindTexture(resourceLocation);
	}
	
	/**
	 * 设置皮肤贴图为一个已有的贴图
	 */
	public void setSkin(ResourceLocation resourceLocation) {
		Minecraft.getMinecraft().renderEngine.bindTexture(resourceLocation);
	}
	
	/**
	 * 设置头部的朝向,默认是平视正前方
	 * @param pitch 负数朝上看,正数朝下看,单位为角度制
	 * @param yaw 负数朝左看,正数朝右看,单位为角度制
	 */
	public void setHeadLook(float pitch, float yaw) {
		headPitch = pitch;
		headYaw = yaw;
	}
	
	/**
	 * 手动设定当前帧数,通常来说是不需要的
	 */
	public void setFrame(float frameTime) {
		frame = frameTime;
	}
	
	/**
	 * 设定人物行走动作,0.0为原地站立不动,1.0为正常行走
	 * @param speed
	 */
	public void setWalk(float speed) {
		walk = speed;
	}
	
	public void render(int posX, int posY, float pitch, float yaw, float roll, float size) {
		render(posX, posY, pitch, yaw, roll, size, 0f);
	}
	
	/**
	 * 在屏幕上渲染玩家.
	 * @param posX 屏幕坐标上的原点位置的X坐标
	 * @param posY 屏幕坐标上的原点位置的Y坐标
	 * @param pitch 绕X轴旋转角度,单位为角度制.负数上仰,正数下俯.注意坐标原点是在玩家的脚下而不是正中心.
	 * @param yaw 绕Y轴旋转的角度,单位为角度制.负数向左,正数向右.
	 * @param roll 绕Z轴旋转的角度,单位为角度制
	 * @param size 缩放倍数,0.0的话就看不见了...建议在2.0~4.0之间.
	 * @param delta 递增帧数的参数,用于维持播放玩家的行走动作和手臂自然摆动等,通过控制数值大小可以控制动作速度.
	 */
	public void render(int posX, int posY, float pitch, float yaw, float roll, float size, float delta) {
		frame += delta;
		model.setRotationAngles(frame * walk, walk, frame, headYaw, headPitch, 0f, null);
		
		GL11.glEnable(GL11.GL_COLOR_MATERIAL);
		GL11.glPushMatrix();
		//这里是个很玄学的地方,需要把深度测试的方式颠倒一下才能正常渲染,我承认我也不知道这为什么...但它JustWork™!
		//不这样做的话,渲染出来的效果会跟没开深度测试似的
		GL11.glEnable(GL11.GL_DEPTH_TEST);
		GL11.glDepthFunc(GL11.GL_GREATER);
		GL11.glTranslatef(posX, posY, -500.0f);
		GL11.glScalef(size, size, size);		
		GL11.glRotatef(roll, 0.0f, 0.0f, 1.0f);
		GL11.glRotatef(yaw, 0.0f, 1.0f, 0.0f);
		GL11.glRotatef(pitch, 1.0f, 0.0f, 0.0f);      
        GL11.glColor3f(1.0f, 1.0f, 1.0f);
		model.bipedHead.render(1.0f);
		model.bipedBody.render(1.0f);
		model.bipedRightArm.render(1.0f);
		model.bipedLeftArm.render(1.0f);
		model.bipedRightLeg.render(1.0f);
        model.bipedLeftLeg.render(1.0f);
        model.bipedHeadwear.render(1.0f);
		
		GL11.glDepthFunc(GL11.GL_LEQUAL);;
		GL11.glDisable(GL11.GL_DEPTH_TEST);
		GL11.glPopMatrix();
        GL11.glDisable(GL11.GL_COLOR_MATERIAL);
	}
}

1.8版

1.8版有个小小的缺陷,就是无法自动判断玩家使用的皮肤贴图类型是Steve还是Alex.

/**
 * 在屏幕中以正交投影渲染一个玩家,建议在DrawScreenEvent.Post事件或界面类的drawScreen中进行.
 */
@SideOnly(Side.CLIENT)
public class ScreenPlayerRenderer {
	//Steve和Alex的模型
	private static final ModelBiped steveModel, alexModel;
	public static final ResourceLocation defaultSteveTexture = new ResourceLocation("textures/entity/steve.png"),
								   			defaultAlexTexture = new ResourceLocation("textures/entity/alex.png");
	private static final Map<String, ResourceLocation> skinCache = new ConcurrentHashMap<String, ResourceLocation>();
	
	private ModelBiped model = steveModel;
	private boolean isAlex = false;
	private float headPitch = 0f, headYaw = 0f;
	private float frame = 0f, walk = 0f;;
	
	static {
		steveModel = new ModelBiped();
		alexModel = new ModelBiped();
		copyModel(new ModelPlayer(0f, false), steveModel);
		copyModel(new ModelPlayer(0f, true), alexModel);
	}
	
	/**
	 * 1.8中玩家模型是ModelPlayer类,它有个缺点是setRotationAngles的实体参数必须为非null 
	 * (ModelBiped类的setRotationAngles的实体参数就可以是null) 故此这里创建一个替代品并把必要的参数复制过去
	 */
	private static void copyModel(ModelPlayer src, ModelBiped dest) {
		dest.bipedBody = src.bipedBody;
		dest.bipedHead = src.bipedHead;
		dest.bipedHeadwear = src.bipedHeadwear;
		dest.bipedLeftArm = src.bipedLeftArm;
		dest.bipedRightArm = src.bipedRightArm;
		dest.bipedLeftLeg = src.bipedLeftLeg;
		dest.bipedRightLeg = src.bipedRightLeg;
	}
	
	/**
	 * 设置皮肤类型是否为Alex
	 */
	public void setAlex(boolean isAlex) {
		this.isAlex = isAlex;
		model = isAlex ? alexModel : steveModel;
	}

	/**
	 * 设置皮肤贴图为默认贴图
	 */
	public void setSkin() {
		Minecraft.getMinecraft().renderEngine.bindTexture(isAlex ? defaultAlexTexture : defaultSteveTexture);
	}
	
	/**
	 * 根据玩家名称自动从网上获取贴图,在贴图读取完毕前,玩家贴图会显示为默认贴图. 
	 */
	public void setSkin(String name) {
		ResourceLocation resourceLocation = skinCache.get(name);
		if(resourceLocation == null)
		{   //此处为net.minecraft.util下的StringUtils
			resourceLocation = new ResourceLocation("skins/" + StringUtils.stripControlCodes(name));
			AbstractClientPlayer.getDownloadImageSkin(resourceLocation, name);
			skinCache.put(name, resourceLocation);
		}
		Minecraft.getMinecraft().renderEngine.bindTexture(resourceLocation);
	}
	
	/**
	 * 设置皮肤贴图为一个已有的贴图
	 */
	public void setSkin(ResourceLocation resourceLocation) {
		Minecraft.getMinecraft().renderEngine.bindTexture(resourceLocation);
	}
	
	/**
	 * 设置头部的朝向,默认是平视正前方
	 * @param pitch 负数朝上看,正数朝下看,单位为角度制
	 * @param yaw 负数朝左看,正数朝右看,单位为角度制
	 */
	public void setHeadLook(float pitch, float yaw) {
		headPitch = pitch;
		headYaw = yaw;
	}
	
	/**
	 * 手动设定当前帧数,通常来说是不需要的
	 */
	public void setFrame(float frameTime) {
		frame = frameTime;
	}
	
	/**
	 * 设定人物行走动作,0.0为原地站立不动,1.0为正常行走
	 * @param speed
	 */
	public void setWalk(float speed) {
		walk = speed;
	}
	
	public void render(int posX, int posY, float pitch, float yaw, float roll, float size) {
		render(posX, posY, pitch, yaw, roll, size, 0f);
	}
	
	/**
	 * 在屏幕上渲染玩家.
	 * @param posX 屏幕坐标上的原点位置的X坐标
	 * @param posY 屏幕坐标上的原点位置的Y坐标
	 * @param pitch 绕X轴旋转角度,单位为角度制.负数上仰,正数下俯.注意坐标原点是在玩家的脚下而不是正中心.
	 * @param yaw 绕Y轴旋转的角度,单位为角度制.负数向左,正数向右.
	 * @param roll 绕Z轴旋转的角度,单位为角度制
	 * @param size 缩放倍数,0.0的话就看不见了...建议在2.0~4.0之间.
	 * @param delta 递增帧数的参数,用于维持播放玩家的行走动作和手臂自然摆动等,通过控制数值大小可以控制动作速度.
	 */
	public void render(int posX, int posY, float pitch, float yaw, float roll, float size, float delta) {
		frame += delta;
		model.setRotationAngles(frame * walk, walk, frame, headYaw, headPitch, 0f, null);
		
		GlStateManager.enableColorMaterial();
		GlStateManager.pushMatrix();
		//这里是个很玄学的地方,需要把深度测试的方式颠倒一下才能正常渲染,我承认我也不知道这为什么...但它JustWork™!
		//不这样做的话,渲染出来的效果会跟没开深度测试似的
		GlStateManager.depthFunc(GL11.GL_GREATER);
		GlStateManager.translate(posX, posY, -500.0f); //正常情况下深度应该是500,但由于这里的深度测试被颠倒了,因此要取-500才会渲染出来
		GlStateManager.scale(size, size, size);
		GlStateManager.rotate(roll, 0.0f, 0.0f, 1.0f);
		GlStateManager.rotate(yaw, 0.0f, 1.0f, 0.0f);
		GlStateManager.rotate(pitch, 1.0f, 0.0f, 0.0f);
        GlStateManager.color(1.0f, 1.0f, 1.0f);
		model.bipedHead.render(1.0f);
		model.bipedBody.render(1.0f);
		model.bipedRightArm.render(1.0f);
		model.bipedLeftArm.render(1.0f);
		model.bipedRightLeg.render(1.0f);
        model.bipedLeftLeg.render(1.0f);
        model.bipedHeadwear.render(1.0f);
		
		GlStateManager.depthFunc(GL11.GL_LEQUAL);
		GlStateManager.popMatrix();
        GlStateManager.disableColorMaterial();
	}
}

使用方法,比如在主菜单渲染玩家:

    private ScreenPlayerRenderer spr;
    @EventHandler
    public void init(FMLPostInitializationEvent event)
    {
    	spr = new ScreenPlayerRenderer();
    }
    
    //别忘了在MinecraftForge.EVENT_BUS中注册事件句柄
    @SubscribeEvent
    public void onGuiRender(DrawScreenEvent.Post event) {
    	if(event.gui instanceof GuiMainMenu)
    	{
	    	spr.setSkin("szszss");
	    	spr.setWalk(1.0f);
	    	spr.setHeadLook(-10f, 30f);
	    	spr.render(55, 55, 10f, -30f, 0.0f, 4.0f, event.renderPartialTicks);
    	}
    }

2016-01-17_19.33.09

事件总线

注册Forge/FML事件总线时出错
在Forge事件总线(EVENT_BUS、TERRAIN_GEN_BUS和ORE_GEN_BUS)和FML事件总线中注册事件总线时如果遇到了IllegalArgumentException异常的话,很可能是类中包含了带有@SubscribeEvent注解,但却有非法参数的方法.事件监听方法要求必须有并且只能有一个类型为Event或其派生类的参数.

注册事件无效,监听方法不被调用
注意监听方法的访问级,必须是非静态的公共方法,否则会被Forge默默地跳过...
此外还有其他因素,如果你的Mod在与其他Mod协同工作的话,事件是否被更高优先级的监听器拦截并取消?声明@SubscribeEvent注解时将receiveCanceled改为true可以接受被取消的事件;是否注册了恰当的事件总线?

总共有多少个事件总线
FML/Forge中总共有5条事件总线.Forge有3个通过FML的EventBus实现的,分别是处理所有的实体事件和绝大部分游戏事件的EVENT_BUS,处理地形生成的TERRAIN_GEN_BUS和处理矿物生成的ORE_GEN_BUS. FML有2个,分别是用自己的EventBus实现的(通过FMLCommonHandler.INSTANCE.bus()来获取)和用Guava的EventBus实现的(Coremod所使用的ModContainer的registerBus方法接受的那个参数即为该总线).

为什么有两套事件总线(FML的和Guava的)
大概是因为Guava事件总线处理的事件都是游戏初始化时的事件,此时FML尚未初始化完毕,总线系统尚不可用,因此就暂用Guava的.
Guava和FML事件总线的最大区别在于Guava事件总线是用反射来调用监听方法,FML事件总线是通过实时字节码生成来即时生成一个调用器,以直接调用的方式来调用监听方法,效率比反射高到不知道哪去了(顺便悄悄安利一下,我做的AsmEventBus也是通过字节码生成的高效事件总线,当时以为是epic work,后来发现FML早在我之前就实现了...fucking life).
此外,FML的事件总线使用@SubscribeEvent来标记订阅者,普通Mod的主类用@EventHandler来标记初始化事件订阅者,而Coremod的事件处理类用@Subscribe来处理初始化事件订阅者.

为什么Forge和FML有独立的事件总线
客观原因:FML是一个取代ModLoader,负责实现Mod的加载和管理,以及必要的接口的工具,而Forge从最初到现在一直只是个功能库,虽然Forge出现早于FML,但在层次上现在的Forge是基于FML的,这注定它无法处理一些底层事物,比如玩家的输入以及服务器状态改变等.
主观原因:Forge和FML是两个联姻的不同项目,别忘了直到1.8的时候cpw宣布"Fuck, I quit"之前,FML一直是在一个独立的包下.
真要严格区分两者几乎是不可能的,如果只是粗略总结的话,Forge的事件总线用来处理顶层逻辑(比如实体之类的)以及GUI.FML的事件总线用来处理底层操作,比如玩家的鼠标键盘输入,服务器中玩家的登入登出.但由于各种历史原因,两者之间互有交集,比如Forge也会处理鼠标事件,而FML会处理冶炼和合成事件.

FML的事件总线能处理哪些事件
见cpw.mods.fml.common.gameevent和cpw.mods.fml.client.event包下的类.
好吧我知道如果我不写出来的话有些人是永远不会去看的...这里大概写一下能处理什么功能.

  • 玩家的鼠标与键盘输入操作.(FML将它归位了Common,不过这难道不应该是ClientOnly吗?算了不管它了...)
  • 玩家的捡起物品,合成物品,取出冶炼物品,登录服务器,登出服务器,复活,(字面含义上)前往另一个世界
  • 每帧(Tick)触发的事件,可以是ServerTickEvent(服务器每帧开始和结束时触发),ClientTickEvent(客户端每帧开始和结束时触发),WorldTickEvent(WorldServer的tick开始与结束时触发),PlayerTickEvent(EntityPlayer的onUpdate开始与结束触发)和RenderTickEvent(EntityRenderer的updateCameraAndRender开始与结束时触发)
  • 客户端独有,ConfigChangedEvent改变Mod配置时触发

FML的Guava事件总线能处理哪些事件
见cpw.mods.fml.common.event包下的类.基本上所有在Mod主类中能处理的那些事件都是由Guava事件总线来实现的.

Coremod

如何查询全混淆名和半混淆名
问这个问题的人按说应该读过Coremod教程,那他是怎么才会没看到教程中的AsmShooterMappingData呢...不管怎么说,这里再贴一遍.
ASMShooter全称Aya Syameimaru's Miniskirt Shooter,简称ASMShooter,是本文作者开发的一个黑项目,现已由于过于黑,而被SCP基金会勒令停止开发并无限期冻结.说白了,这项目至今没出成果,而且已经停止开发了......
不过别急着关窗口,它的一个子项目已经开发完成了,我们可以利用它的产品:ASMShooterMappingData,进行查表定位.
ASMShooterMappingData是一个xml文件,它构建了一个完整的未混淆-半混淆-全混淆名的映射关系表,并且还详细记述了每一个方法的参数和返回值,非常便于查询.
更赞的是,在v2版协议中,它还增加了方法的描述符(由于FML内置的反混淆器存在,方法描述符在开发环境和游戏环境下都是通用的)和类的全名,更方便开发者快速查询信息.
E3-14 E3-15
ASMShooterMappingData在文件头的MinecraftVersion标识了它对应的Minecraft版本,ProtocolVersion标识了格式版本.当前的格式(Protocol 3)为:
XML头结点
--包结点(以Package命名 属性Name为包名)
--|--类节点(以Class命名 属性Unobscured为未混淆名,属性Obscured为混淆名,属性FullName为完整的未混淆类名)
--|--|--字段节点(以Field命名 属性Unobscured为未混淆名,属性Searge为半混淆名,属性Obscured为混淆名)
--|--|--方法节点(以Method命名 属性Unobscured为未混淆名,属性Searge为半混淆名,属性Obscured为混淆名)
--|--|--|--参数结点(以Param命名 内容文字为参数类型的未混淆名)
--|--|--|--返回值结点(以Return命名 内容文字为返回值类型的未混淆名)
--|--|--|--描述符结点(以Desc命名 内容文字为方法的未混淆描述符)
下载地址:
1.6.2,1.6.4,1.7.2,1.7.10:http://1drv.ms/1DDv2j2