0. 概述 源代码:MinecraftByExample 机制分析:MinecraftModding
本文主要是把各种特定的目标的特定模式(套路)总结下来(针对 1.15.x,1.16.x 应该也 ok,但是更早的版本就不可以了)
1. 项目模式 1.1 mods.toml 首先 toml 是一种文档格式:toml 文档格式 然后 mods.toml 位于resources/META-INF/mods.toml
中,用于描述 mod 的一些配置 forge 的 mods.toml 格式:mods.toml 格式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 modLoader ="javafml" loaderVersion ="[28,)" issueTrackerURL ="https://github.com/TheGreyGhost/MinecraftByExample/issues" [[mods]] modId ="minecraftbyexample" version ="${file.jarVersion}" displayName ="Minecraft By Example" updateJSONURL ="http://myurl.me/" displayURL ="https://github.com/TheGreyGhost/MinecraftByExample" logoFile ="thegreyghostproudlypresents.png" credits ="The Forge, MCP, and FML guys, for making it possible" authors ="TGG and others" description =''' Minecraft By Example - a collection of simple working examples of the important concepts in Minecraft and Forge.''' [[dependencies.examplemod]] modId ="forge" mandatory =true versionRange ="[28,)" ordering ="NONE" side ="BOTH" [[dependencies.examplemod]] modId ="minecraft" mandatory =true versionRange ="[1.14.4]" ordering ="NONE" side ="BOTH"
1.2 build.gradle 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 buildscript { repositories { maven { url = 'https://files.minecraftforge.net/maven' } jcenter() mavenCentral() } dependencies { classpath group : 'net.minecraftforge.gradle' , name: 'ForgeGradle' , version: '3.+' , changing: true } } apply plugin: 'net.minecraftforge.gradle' apply plugin: 'eclipse' apply plugin: 'maven-publish' version = "1.15.2b" group = "thegreyghost" archivesBaseName = "minecraftbyexample" sourceCompatibility = targetCompatibility = compileJava.sourceCompatibility = compileJava.targetCompatibility = '1.8' minecraft { mappings channel: 'snapshot' , version: '20200514-1.15.1' runs { client { workingDirectory project .file ('run' ) property 'forge.logging.markers' , 'SCAN,REGISTRIES,REGISTRYDUMP' property 'forge.logging.console.level' , 'debug' mods { minecraftbyexample { source sourceSets .main } } } clientfewerconsolemessages { workingDirectory project .file ('run' ) property 'forge.logging.markers' , 'SCAN,REGISTRIES,REGISTRYDUMP' property 'forge.logging.console.level' , 'warn' mods { minecraftbyexample { source sourceSets .main } } } server { workingDirectory project .file ('run' ) property 'forge.logging.markers' , 'SCAN,REGISTRIES,REGISTRYDUMP' property 'forge.logging.console.level' , 'debug' mods { minecraftbyexample { source sourceSets .main } } } data { workingDirectory project .file ('run' ) property 'forge.logging.markers' , 'SCAN,REGISTRIES,REGISTRYDUMP' property 'forge.logging.console.level' , 'debug' args '--mod' , 'minecraftbyexample' , '--all' , '--output' , file ('src/generated/resources/' ) mods { minecraftbyexample { source sourceSets .main } } } } } dependencies { minecraft 'net.minecraftforge:forge:1.15.2-31.2.0' } jar { manifest { attributes([ "Specification-Title" : "minecraftbyexample" , "Specification-Vendor" : "thegreyghost" , "Specification-Version" : "1" , "Implementation-Title" : project .name, "Implementation-Version" : "${version}" , "Implementation-Vendor" :"thegreyghost" , "Implementation-Timestamp" : new Date().format("yyyy-MM-dd'T'HH:mm:ssZ" ) ]) } } def reobfFile = file ("$buildDir/reobfJar/output.jar" )def reobfArtifact = artifacts .add('default' , reobfFile) { type 'jar' builtBy 'reobfJar' } publishing { publications { mavenJava(MavenPublication) { artifact reobfArtifact } } repositories { maven { url "file:///${project.projectDir}/mcmodsrepo" } } }
2. 代码模式 2.1 Mod 主类 注册时总线触发事件的顺序:
Registry Events
onBlocksRegistration
onItemsRegistration
Data Generation
Common Setup
Sided Setup
FMLClientSetupEvent
FMLDedicatedServerSetupEvent
InterModComms
InterModEnqueueEvent
InterModProcessEvent
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 @Mod(MinecraftByExample.MODID) public class MinecraftByExample { public static final String MODID = "minecraftbyexample" ; public static IEventBus MOD_EVENT_BUS; public MinecraftByExample () { final boolean HIDE_CONSOLE_NOISE = false ; if (HIDE_CONSOLE_NOISE) { ForgeLoggerTweaker.setMinimumLevel(Level.WARN); ForgeLoggerTweaker.applyLoggerFilter(); } MOD_EVENT_BUS = FMLJavaModLoadingContext.get().getModEventBus(); registerCommonEvents(); DistExecutor.runWhenOn(Dist.CLIENT, () -> MinecraftByExample::registerClientOnlyEvents); } public static void registerCommonEvents () { MOD_EVENT_BUS.register(minecraftbyexample.mbe01_block_simple.StartupCommon.class); ... } public static void registerClientOnlyEvents () { MOD_EVENT_BUS.register(minecraftbyexample.mbe01_block_simple.StartupClientOnly.class); ... }
2.2 方块 2.2.1 简单方块 2.2.1.1 SimpleBlock.java 定义一个类,继承 Block 类,重载 getRenderType 函数,返回一个渲染方法 不同渲染方法介绍:渲染方法
2.2.1.2 StartUpClientOnly.java 提供仅在客户端完成的初始化(同时需要在主类中添加这个类的监听)
1 2 3 4 5 6 7 8 public class StartupClientOnly { @SubscribeEvent public static void onClientSetupEvent (FMLClientSetupEvent event) { RenderTypeLookup.setRenderLayer(StartupCommon.blockSimple, RenderType.getSolid()); } }
2.2.1.3 StartUpCommon.java 提供客户端和服务器都需要完成的设置(同时需要在主类中添加这个类的监听)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class StartupCommon { public static BlockSimple blockSimple; public static BlockItem itemBlockSimple; @SubscribeEvent public static void onBlocksRegistration (final RegistryEvent.Register<Block> blockRegisterEvent) { blockSimple = (BlockSimple)(new BlockSimple().setRegistryName("minecraftbyexample" , "mbe01_block_simple_registry_name" )); blockRegisterEvent.getRegistry().register(blockSimple); } @SubscribeEvent public static void onItemsRegistration (final RegistryEvent.Register<Item> itemRegisterEvent) { final int MAXIMUM_STACK_SIZE = 20 ; Item.Properties itemSimpleProperties = new Item.Properties() .maxStackSize(MAXIMUM_STACK_SIZE) .group(ItemGroup.BUILDING_BLOCKS); itemBlockSimple = new BlockItem(blockSimple, itemSimpleProperties); itemBlockSimple.setRegistryName(blockSimple.getRegistryName()); itemRegisterEvent.getRegistry().register(itemBlockSimple); } @SubscribeEvent public static void onCommonSetupEvent (FMLCommonSetupEvent event) { } }
2.2.1.4 国际化与区域化(人话:设置翻译文件) 在assets.examplemod.lang
目录下添加语言文件:en_us.json
,zh_cn.json
等 更早的文件格式是.lang
格式(参考早期的 forge 文档:1.12.x_forge 文档 )
其中添加材料在代码中的名命与对应的翻译
1 2 3 4 { "block.examplemod.simpleblock: "简单方块", "...":"..." }
2.2.1.5 模型相关的文件结构 在定义完方块的类之后,是没有给方块指定模型和纹理的 Forge 通过额外的配置文件来定义这些内容(而不是用 Java 代码进行注册) 下面的三个文件具体结构可以参考:模型相关文件结构
simple_block_registry.json 在assets.examplemod.blockstates
文件夹中定义特定的文件,来把定义的 Block 类和指定的模型,纹理连接起来
还有更复杂的条件语句,详细可以参考模型相关文件结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 { "variants" : { "" : { "model" : "minecraftbyexample:block/mbe01_block_simple_model" , "x" : "90" , "y" : "90" , "uvlock" : "true" }, "variants-2" : [ { "model" : "minecraftbyexample:block/mbe01_block_simple_model" , "weight" : "2" }, { "model" : "minecraftbyexample:block/mbe01_block_simple_model" , "weight" : "2" }, { "model" : "minecraftbyexample:block/mbe01_block_simple_model" , "weight" : "7" } ] } }
simple_block_model.json 在assets.examplemod.models.block
中定义模型文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 { "parent" : "block/cube" , "ambientocclusion" : "true" , "textures" : { "particle" : "block/simple_block" , "down" : "minecraftbyexample:block/simple_block" , "up" : "minecraftbyexample:block/simple_block" , "north" : "minecraftbyexample:block/simple_block" , "east" : "minecraftbyexample:block/simple_block" , "south" : "minecraftbyexample:block/simple_block" , "west" : "minecraftbyexample:block/simple_block" }, "elements" : [ { "from" : [7 , 0 , 7 ], "to" : [9 , 10 , 9 ], "shade" : false , "faces" : { "down" : { "uv" : [7 , 13 , 9 , 15 ], "texture" : "#torch" , "cullface" : "south" }, "up" : { "uv" : [7 , 6 , 9 , 8 ], "texture" : "#torch" } } }, { "from" : [7 , 0 , 0 ], "to" : [9 , 16 , 16 ], "shade" : false , "faces" : { "west" : { "uv" : [0 , 0 , 16 , 16 ], "texture" : "#torch" }, "east" : { "uv" : [0 , 0 , 16 , 16 ], "texture" : "#torch" } } } ] }
simple_item_model.json 在assets.examplemod.models.item
中定义物品模型文件 其文件结构与方块模型文件结构基本一致,详细可以参考模型相关文件结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "parent" : "minecraftbyexample:block/mbe02_block_partial_model" , "display" : { "thirdperson_righthand" : { "rotation" : [ 66 , 2 , 0 ], "translation" : [ 0.00 , 5.50 , 3.25 ], "scale" : [ 0.60 , 0.60 , 0.60 ] }, "thirdperson_lefthand" : { "rotation" : [ 66 , 2 , 0 ], "translation" : [ 0.00 , 5.50 , 3.25 ], "scale" : [ 0.60 , 0.60 , 0.60 ] } }
simple_block_loot_table.json 在data.examplemod.loot_tables.blocks
中定义方块被破坏后的loot_table
(战利品表)文件 详细可以参考:战利品表
简单方块的示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 { "type" : "minecraft:block" , "pools" : [ { "rolls" : 1 , "entries" : [ { "type" : "minecraft:item" , "name" : "minecraftbyexample:mbe01_block_simple_registry_name" } ], "conditions" : [ { "condition" : "minecraft:survives_explosion" } ] } ] }
2.2.2 非完整方块 在搭建简单方块的基础上,可以搭建一些非完整的(占满 1 格空间的)方块
保持StartUpClientOnly.java
和StartUpCommon.java
基本一致 主要改动在模型文件和方块类本身
2.2.2.1 BlockPartial.java 与普通方块最大的不同在于两点:设置可以通过和修改渲染模型
可以通过重载不同的方法,来修改不同的模型,不同的模型作用及制作方法介绍:模型作用及制作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class BlockPartial extends Block { private static final Vec3d BASE_MIN_CORNER = new Vec3d(2.0 , 0.0 , 0.0 ); private static final Vec3d BASE_MAX_CORNER = new Vec3d(14.0 , 1.0 , 16.0 ); private static final Vec3d PILLAR_MIN_CORNER = new Vec3d(7.0 , 1.0 , 6.0 ); private static final Vec3d PILLAR_MAX_CORNER = new Vec3d(9.0 , 8.0 , 10.0 ); private static final VoxelShape BASE = Block.makeCuboidShape( BASE_MIN_CORNER.getX(), BASE_MIN_CORNER.getY(), BASE_MIN_CORNER.getZ(), BASE_MAX_CORNER.getX(), BASE_MAX_CORNER.getY(), BASE_MAX_CORNER.getZ()); private static final VoxelShape PILLAR = Block.makeCuboidShape( PILLAR_MIN_CORNER.getX(), PILLAR_MIN_CORNER.getY(), PILLAR_MIN_CORNER.getZ(), PILLAR_MAX_CORNER.getX(), PILLAR_MAX_CORNER.getY(), PILLAR_MAX_CORNER.getZ()); private static VoxelShape COMBINED_SHAPE = VoxelShapes.or(BASE, PILLAR); private static VoxelShape EMPTY_SPACE = VoxelShapes.combineAndSimplify(VoxelShapes.fullCube(), COMBINED_SHAPE, IBooleanFunction.ONLY_FIRST); public BlockPartial () { super (Block.Properties.create(Material.WOOD).doesNotBlockMovement()); } @Override public VoxelShape getShape (BlockState state, IBlockReader worldIn, BlockPos pos, ISelectionContext context) { return COMBINED_SHAPE; } }
2.2.2.2 partial_block_model.json 然后是需要模型文件与主类文件中声明的模型对应起来(也可以不对应起来,就像告示牌一样)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 { "ambientocclusion" : false , "textures" : { "particle" : "block/lapis_block" , "base" : "block/lapis_block" , "pole" : "block/stone" }, "elements" : [ { "from" : [2 , 0 , 0 ], "to" : [14 , 1 , 16 ], "shade" : false , "faces" : { "down" : { "uv" : [2 , 0 , 14 , 16 ], "texture" : "#base" }, "up" : { "uv" : [2 , 0 , 14 , 16 ], "texture" : "#base" }, "east" : { "uv" : [16 , 0 , 0 , 1 ], "texture" : "#base" }, "west" : { "uv" : [0 , 0 , 16 , 1 ], "texture" : "#base" }, "north" : { "uv" : [14 , 0 , 2 , 1 ], "texture" : "#base" }, "south" : { "uv" : [2 , 0 , 14 , 1 ], "texture" : "#base" } } }, { "from" : [7 , 1 , 6 ], "to" : [9 , 8 , 10 ], "shade" : false , "faces" : { "up" : { "uv" : [7 , 6 , 9 , 10 ], "texture" : "#pole" }, "east" : { "uv" : [6 , 1 , 10 , 8 ], "texture" : "#pole" }, "west" : { "uv" : [6 , 1 , 10 , 8 ], "texture" : "#pole" }, "north" : { "uv" : [7 , 1 , 9 , 8 ], "texture" : "#pole" }, "south" : { "uv" : [7 , 1 , 9 , 8 ], "texture" : "#pole" } } } ] }
2.2.3 方块变体 方块变体总体有两种思路:
每个变体建一个模型,不同的变体选择不同的模型 采用这种方法的方块有各种材料的楼梯,每个不同的反向都是采用不同模型的变体
把所有变体抽象出一个核心,然后再向不同的方向进行扩展 采用这种方法的方块有栅栏,其核心是一根棍子,不同方向扩展的模型是横栏
第一种方法在变种较少时比较简单,但是如果变种太多时会非常繁琐(如栅栏的六个方向)
2.2.3.1 多模型方法 首先一点,想要使用不同的模型只需要在调用getShape
方法时返回不同的模型即可 其他的一些扩展(如颜色)可以直接通过实例化多个对象,然后赋值不同的颜色即可
2.2.3.1.1 不同的颜色 在内部维护一个颜色变量(EnumColour)用来表示不同的方块颜色 然后在StartUpCommon.java
中实例化多个不同颜色的方块并实例化即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 { public enum EnumColour implements IStringSerializable { BLUE("blue" ), RED("red" ), GREEN("green" ), YELLOW("yellow" ); @Override public String toString () { return this .name; } public String getName () { return this .name; } private final String name; private EnumColour (String i_name) { this .name = i_name; } } }
2.2.3.1.2 不同的模型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 { private static final DirectionProperty FACING = HorizontalBlock.HORIZONTAL_FACING; protected void fillStateContainer (StateContainer.Builder<Block, BlockState> builder) { builder.add(FACING); } { BlockState defaultBlockState = this .stateContainer.getBaseState() .with(FACING, Direction.NORTH); this .setDefaultState(defaultBlockState); } public BlockState getStateForPlacement (BlockItemUseContext blockItemUseContext) { World world = blockItemUseContext.getWorld(); BlockPos blockPos = blockItemUseContext.getPos(); Direction direction = blockItemUseContext.getPlacementHorizontalFacing(); float playerFacingDirectionAngle = blockItemUseContext.getPlacementYaw(); BlockState blockState = getDefaultState().with(FACING, direction); return blockState; } private static final Map<Direction, VoxelShape> POST_SHAPES = ImmutableMap.of(Direction.NORTH,POST_SHAPE_N, Direction.EAST,POST_SHAPE_E, Direction.SOUTH,POST_SHAPE_S, Direction.WEST,POST_SHAPE_W); public VoxelShape getShape (BlockState state, IBlockReader worldIn, BlockPos pos, ISelectionContext context) { Direction direction = state.get(FACING); VoxelShape voxelShape = POST_SHAPES.get(direction); return voxelShape != null ? voxelShape : VoxelShapes.fullCube(); } }
2.2.3.2 核心-扩展方法 四个方向,如果用第一种方法,一共需要 16 种模型,可以使用核心-扩展的方法简化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 { public static final BooleanProperty UP = BlockStateProperties.UP; public static final BooleanProperty DOWN = BlockStateProperties.DOWN; public static final BooleanProperty WEST = BlockStateProperties.WEST; public static final BooleanProperty EAST = BlockStateProperties.EAST; public static final BooleanProperty NORTH = BlockStateProperties.NORTH; public static final BooleanProperty SOUTH = BlockStateProperties.SOUTH; protected void fillStateContainer (StateContainer.Builder<Block, BlockState> builder) { builder.add(UP, DOWN, WEST, EAST, NORTH, SOUTH, WATERLOGGED); } public BlockState getStateForPlacement (BlockItemUseContext blockItemUseContext) { World world = blockItemUseContext.getWorld(); BlockPos blockPos = blockItemUseContext.getPos(); BlockState blockState = getDefaultState(); blockState = setConnections(world, blockPos, blockState); return blockState; } private BlockState setConnections (IBlockReader iBlockReader, BlockPos blockPos, BlockState blockState) { return blockState .with(UP, canWebAttachToNeighbourInThisDirection(iBlockReader, blockPos, Direction.UP)) .with(DOWN, canWebAttachToNeighbourInThisDirection(iBlockReader, blockPos, Direction.DOWN)) .with(WEST, canWebAttachToNeighbourInThisDirection(iBlockReader, blockPos, Direction.WEST)) .with(EAST, canWebAttachToNeighbourInThisDirection(iBlockReader, blockPos, Direction.EAST)) .with(NORTH, canWebAttachToNeighbourInThisDirection(iBlockReader, blockPos, Direction.NORTH)) .with(SOUTH, canWebAttachToNeighbourInThisDirection(iBlockReader, blockPos, Direction.SOUTH)); } public BlockState updatePostPlacement (BlockState thisBlockState, Direction directionFromThisToNeighbor, BlockState neighborState,IWorld world, BlockPos thisBlockPos, BlockPos neighborBlockPos) { switch (directionFromThisToNeighbor) { case UP: thisBlockState = thisBlockState.with(UP, canWebAttachToNeighbourInThisDirection(world, thisBlockPos, directionFromThisToNeighbor)); break ; case DOWN: thisBlockState = thisBlockState.with(DOWN, canWebAttachToNeighbourInThisDirection(world, thisBlockPos, directionFromThisToNeighbor)); break ; default : LOGGER.error("Unexpected facing:" + directionFromThisToNeighbor); } return thisBlockState; } private static HashMap<BlockState, VoxelShape> voxelShapeCache = new HashMap<>(); private void initialiseShapeCache () { for (BlockState blockState : stateContainer.getValidStates()) { VoxelShape combinedShape = CORE_SHAPE; if (blockState.get(UP).booleanValue()) { combinedShape = VoxelShapes.or(combinedShape, LINK_UP_SHAPE); } if (blockState.get(DOWN).booleanValue()) { combinedShape = VoxelShapes.or(combinedShape, LINK_DOWN_SHAPE); } if (blockState.get(WEST).booleanValue()) { combinedShape = VoxelShapes.or(combinedShape, LINK_WEST_SHAPE); } if (blockState.get(EAST).booleanValue()) { combinedShape = VoxelShapes.or(combinedShape, LINK_EAST_SHAPE); } if (blockState.get(NORTH).booleanValue()) { combinedShape = VoxelShapes.or(combinedShape, LINK_NORTH_SHAPE); } if (blockState.get(SOUTH).booleanValue()) { combinedShape = VoxelShapes.or(combinedShape, LINK_SOUTH_SHAPE); } voxelShapeCache.put(blockState, combinedShape); } } public VoxelShape getShape (BlockState state, IBlockReader worldIn, BlockPos pos, ISelectionContext context) { VoxelShape voxelShape = voxelShapeCache.get(state); return voxelShape != null ? voxelShape : VoxelShapes.fullCube(); } }
2.2.4 方块动态模型(不常用) 需要渲染动态模型的例子:一个总是模仿周围方块的变色龙方块,指南针方块
首先,Forge 在初始化的时候,会读取模型文件,并构造一个IBakedModel
对象来代表方块的模型 构造的对象会连同ModelResourceLocation
对象一起存到一个哈希表中
我们要做的就是自己实现一个IBakedModel
子类,构造一个对象,并替换掉 Forge 自己哈希表中ModelResourceLocation
对象对应的IBakedModel
对象
这个方面比较繁琐,同时用的比较少,以后用到了再看 具体细节可以参照 MBE 项目的第 4 个项目
2.2.5 多层模型(不常用) 比如一个灯,外面时透明材质,里面是动画效果,则不能用一个单一的模型/材质渲染出来,可以考虑用多层模型来做 用的比较少,用到了再看,对应 MBE 中的第五个项目
2.2.6 红石相关(非常不常用) 主要用于处理红石相关的特性,对应 MBE 中的第六个项目 先留个坑,以后用到了再填
2.2.7 创造模式标签页 我们想要把我们的方块放到创造模式下的标签页中,同时能用搜索功能搜索到
2.2.7.1 构造标签页 物品标签页通过实现一个抽象类的具体方法实现(createIcon,用于创建标签栏的图标) 可以直接通过匿名类实现,也可以写一个这个类的子类来实现 这个步骤在StartUpCommon.java
中实现
1 2 3 4 5 6 7 8 9 10 { { customItemGroup = new ItemGroup("mbe08_item_group" ) { @Override public ItemStack createIcon () { return new ItemStack(Items.GOLD_NUGGET); } }; } }
2.2.7.2 把物品添加到标签页 一共两种方法(两种方法选一即可):
在注册物品的时候,直接设置物品所在的标签页
1 2 3 4 5 6 7 8 9 10 11 12 { { Item.Properties properties = new Item.Properties() .maxStackSize(MAXIMUM_STACK_SIZE_TESTBLOCK) .group(customItemGroup); testItemBlock = new BlockItem(testBlock, properties); testItemBlock.setRegistryName(testBlock.getRegistryName()); itemRegisterEvent.getRegistry().register(testItemBlock); } }
还有一种是,通过覆盖标签页抽象类的一个方法,一次性从所有物品中筛选出目标物品
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { @Override public void fill (NonNullList<ItemStack> itemsToShowOnTab) { for (Item item : ForgeRegistries.ITEMS) { if (item != null ) { if (item.getRegistryName().getNamespace().equals("minecraftbyexample" )) { item.fillItemGroup(ItemGroup.SEARCH, itemsToShowOnTab); } } } } }
2.3 物品 2.3.1 简单物品 首先创建一个物品类,继承自Item
类
1 2 3 4 5 6 7 8 9 10 11 public class ItemSimple extends Item { static private final int MAXIMUM_NUMBER = 6 ; public ItemSimple () { super (new Item.Properties() .maxStackSize(MAXIMUM_NUMBER) .group(ItemGroup.MISC) ); } }
然后再StartUpCommon.java
中注册物品(物品不需要渲染,故StartUpClient.java
中什么都不用写)
1 2 3 4 5 6 7 { { itemSimple = new ItemSimple(); itemSimple.setRegistryName("mbe10_item_simple_registry_name" ); itemRegisterEvent.getRegistry().register(itemSimple); } }
2.3.2 物品变体 物品变体例子:一个瓶子,其中可以装各种果汁,不同果汁颜色和功能不同,同时瓶子中的液体有多个容量状态
2.3.2.1 颜色类 我们自己的颜色类需要实现IItemColor
接口,其中只有一个需要实现的函数:getColor
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public class LiquidColour implements IItemColor { @Override public int getColor (ItemStack stack, int tintIndex) { { switch (tintIndex) { case 0 : return Color.WHITE.getRGB(); case 1 : { ItemVariants.EnumBottleFlavour enumBottleFlavour = ItemVariants.getFlavour(stack); return enumBottleFlavour.getRenderColour().getRGB(); } default : { return Color.BLACK.getRGB(); } } } } }
2.3.2.2 物品模型 对于果汁瓶子来说,每个满度都需要一个模型(其中仅仅是瓶中液体的贴图不同) 然后再用一个总的模型,通过不同条件选择不同满度的模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 { "parent" : "builtin/generated" , "textures" : { "layer0" : "minecraftbyexample:item/mbe11_item_variants_layer0" , "layer1" : "minecraftbyexample:item/mbe11_item_variants_layer1_0pc" }, "display" : { "thirdperson_righthand" : { "rotation" : [0 , 0 , 0 ], "translation" : [0.0 , 2.5 , 1.0 ], "scale" : [0.45 , 0.45 , 0.45 ] } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 { "parent" : "builtin/generated" , "textures" : { "layer0" : "minecraftbyexample:item/mbe11_item_variants_layer0" , "layer1" : "minecraftbyexample:item/mbe11_item_variants_layer1_0pc" }, "overrides" : [ { "predicate" : { "fullness" : 0 }, "model" : "minecraftbyexample:item/mbe11_item_variants_0pc" }, { "predicate" : { "fullness" : 1 }, "model" : "minecraftbyexample:item/mbe11_item_variants_25pc" }, { "predicate" : { "fullness" : 2 }, "model" : "minecraftbyexample:item/mbe11_item_variants_50pc" }, { "predicate" : { "fullness" : 3 }, "model" : "minecraftbyexample:item/mbe11_item_variants_75pc" }, { "predicate" : { "fullness" : 4 }, "model" : "minecraftbyexample:item/mbe11_item_variants_100pc" } ] }
2.3.2.3 物品类 对于果汁瓶子,其功能和渲染效果受两个影响:果汁品种和满度 果汁品种主要影响颜色(如果也影响其他功能的话,照着满度再搞一遍就行了),我们用2.3.2.1
中的颜色类实现 然后是瓶子的满度,我们为物品类添加一个特性(property)
为了维护颜色和满度两个特性,物品类内部维护两个内部枚举作为工具
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 public class ItemVariants extends Item { public enum EnumBottleFullness implements IStringSerializable { EMPTY(0 , "0pc" , "empty" ), ONE_QUARTER(1 , "25pc" , "nearly empty" ), ONE_HALF(2 , "50pc" , "half full" ), THREE_QUARTERS(3 , "75pc" , "mostly full" ), FULL(4 , "100pc" , "full" ); private final byte nbtID; private final String name; private final String description; EnumBottleFullness(int i_NBT_ID, String i_name, String i_description){ this .nbtID = (byte )i_NBT_ID; this .name = i_name; this .description = i_description; } @Override public String toString () { return this .description; } public float getPropertyOverrideValue () {return nbtID;} public EnumBottleFullness decreaseFullnessByOneStep () { if (nbtID ==0 ) return this ; for (EnumBottleFullness fullness : EnumBottleFullness.values()) { if (fullness.nbtID == nbtID - 1 ) return fullness; } return this ; } private static Optional<EnumBottleFullness> getFullnessFromID (byte ID) { for (EnumBottleFullness fullness : EnumBottleFullness.values()) { if (fullness.nbtID == ID) return Optional.of(fullness); } return Optional.empty(); } public static EnumBottleFullness fromNBT (CompoundNBT compoundNBT, String tagname) { byte fullnessID = 0 ; if (compoundNBT != null && compoundNBT.contains(tagname)) { fullnessID = compoundNBT.getByte(tagname); } Optional<EnumBottleFullness> fullness = getFullnessFromID(fullnessID); return fullness.orElse(FULL); } public void putIntoNBT (CompoundNBT compoundNBT, String tagname) { compoundNBT.putByte(tagname, nbtID); } } public enum EnumBottleFlavour implements IStringSerializable {...} static private final int MAXIMUM_NUMBER_OF_BOTTLES = 1 ; public static final String NBT_TAG_NAME_FLAVOUR = "colour" ; public static final String NBT_TAG_NAME_FULLNESS = "fullness" ; public ItemVariants () { super (new Item.Properties().maxStackSize(MAXIMUM_NUMBER_OF_BOTTLES).group(ItemGroup.BREWING)); this .addPropertyOverride(new ResourceLocation("fullness" ), ItemVariants::getFullnessPropertyOverride); } public static EnumBottleFlavour getFlavour (ItemStack stack) { CompoundNBT compoundNBT = stack.getOrCreateTag(); return EnumBottleFlavour.fromNBT(compoundNBT, NBT_TAG_NAME_FLAVOUR); } public static EnumBottleFullness getFullness (ItemStack stack) { CompoundNBT compoundNBT = stack.getOrCreateTag(); return EnumBottleFullness.fromNBT(compoundNBT, NBT_TAG_NAME_FULLNESS); } private static float getFullnessPropertyOverride ( ItemStack itemStack, @Nullable World world, @Nullable LivingEntity livingEntity) { EnumBottleFullness enumBottleFullness = getFullness(itemStack); return enumBottleFullness.getPropertyOverrideValue(); } public static void setFlavour (ItemStack stack, EnumBottleFlavour enumBottleFlavour) { CompoundNBT compoundNBT = stack.getOrCreateTag(); enumBottleFlavour.putIntoNBT(compoundNBT, NBT_TAG_NAME_FLAVOUR); } public static void setFullness (ItemStack stack, EnumBottleFullness enumBottleFullness) { CompoundNBT compoundNBT = stack.getOrCreateTag(); enumBottleFullness.putIntoNBT(compoundNBT, NBT_TAG_NAME_FULLNESS); } @Override public void fillItemGroup (ItemGroup tab, NonNullList<ItemStack> subItems) { if (this .isInGroup(tab)) { for (EnumBottleFlavour flavour : EnumBottleFlavour.values()) { ItemStack subItemStack = new ItemStack(this , 1 ); setFlavour(subItemStack, flavour); setFullness(subItemStack, EnumBottleFullness.FULL); subItems.add(subItemStack); } } } @Override public UseAction getUseAction (ItemStack stack) { return UseAction.DRINK; } @Override public int getUseDuration (ItemStack stack) { final int TICKS_PER_SECOND = 20 ; final int DRINK_DURATION_SECONDS = 2 ; return DRINK_DURATION_SECONDS * TICKS_PER_SECOND; } @Override public ActionResult<ItemStack> onItemRightClick (World worldIn, PlayerEntity playerIn, Hand hand) { ItemStack itemStackHeld = playerIn.getHeldItem(hand); EnumBottleFullness fullness = getFullness(itemStackHeld); if (fullness == EnumBottleFullness.EMPTY) return new ActionResult(ActionResultType.FAIL, itemStackHeld); playerIn.setActiveHand(hand); return new ActionResult(ActionResultType.PASS, itemStackHeld); } @Override public ItemStack onItemUseFinish (ItemStack stack, World worldIn, LivingEntity entityLiving) { EnumBottleFullness fullness = getFullness(stack); fullness = fullness.decreaseFullnessByOneStep(); fullness.putIntoNBT(stack.getTag(), NBT_TAG_NAME_FULLNESS); return stack; } @Override public String getTranslationKey (ItemStack stack) { EnumBottleFlavour flavour = getFlavour(stack); return super .getTranslationKey(stack) + "." + flavour.getName(); } @Override public ITextComponent getDisplayName (ItemStack stack) { String fullnessText= getFullness(stack).getDescription(); return new TranslationTextComponent(this .getTranslationKey(stack), fullnessText); } }
2.3.2.4 注册 物品的注册还是在StartUpCommon.java
中进行,只需要注册物品即可,没有对应的方块 但是由于颜色是自定义的颜色类,需要对其进行注册
1 2 3 4 5 6 7 public class StartupClientOnly { @SubscribeEvent public static void onColorHandlerEvent (ColorHandlerEvent.Item event) { event.getItemColors().register(new LiquidColour(), StartupCommon.itemVariants); } }
2.3.3 NBT 动画 具体效果就是使用一个物品时利用 NBT 触发一些特效
2.3.3.1 模型文件 首先是和 2.3.2 章类似的模型文件结构,根据一些值设定不同的物品模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 { "parent" : "builtin/generated" , "textures" : { "layer0" : "minecraftbyexample:item/mbe12_item_nbt_animate_0" }, "overrides" : [ { "predicate" : { "chargefraction" : 0.0 }, "model" : "minecraftbyexample:item/mbe12_item_nbt_animate_0" }, { "predicate" : { "chargefraction" : 0.001 }, "model" : "minecraftbyexample:item/mbe12_item_nbt_animate_1" }, { "predicate" : { "chargefraction" : 0.2 }, "model" : "minecraftbyexample:item/mbe12_item_nbt_animate_2" }, { "predicate" : { "chargefraction" : 0.4 }, "model" : "minecraftbyexample:item/mbe12_item_nbt_animate_3" }, { "predicate" : { "chargefraction" : 0.6 }, "model" : "minecraftbyexample:item/mbe12_item_nbt_animate_4" }, { "predicate" : { "chargefraction" : 0.8 }, "model" : "minecraftbyexample:item/mbe12_item_nbt_animate_5" } ] }
2.3.3.2 ItemNBTanimationTimer.java 需要一个类来控制触发效果的控制,首先要了解一下 MC 渲染物品时的行为
MC 要对物品进行渲染,然后发现模型中需要根据一些变量采用不同的模型 MC 找到与物品这个属性对应的IItemPropertyGetter
对象(需要实现一个 call 函数) 调用IItemPropertyGetter
对象的 call 函数,得到变量的值 找到对应的模型并渲染
我们要做的就是实现自定义的IItemPropertyGetter
类,并注册给物品
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public class ItemNBTanimationTimer implements IItemPropertyGetter { private long startingTick = -1 ; private boolean animationHasStarted = false ; @Override public float call (ItemStack stack, @Nullable World worldIn, @Nullable LivingEntity entityIn) { final float IDLE_FRAME_INDEX = 0.0F ; final float FULLY_CHARGED_INDEX = 1.0F ; if (worldIn == null && entityIn != null ) { worldIn = entityIn.world; } if (entityIn == null || worldIn == null ) return IDLE_FRAME_INDEX; if (!entityIn.isHandActive()) { animationHasStarted = false ; return IDLE_FRAME_INDEX; } long worldTicks = worldIn.getGameTime(); if (!animationHasStarted) { startingTick = worldTicks; animationHasStarted = true ; } final long ticksInUse = worldTicks - startingTick; if (ticksInUse <= ItemNBTAnimate.CHARGE_UP_INITIAL_PAUSE_TICKS) { return IDLE_FRAME_INDEX; } final long chargeTicksSoFar = ticksInUse - ItemNBTAnimate.CHARGE_UP_INITIAL_PAUSE_TICKS; final double fractionCharged = chargeTicksSoFar / (double ) ItemNBTAnimate.CHARGE_UP_DURATION_TICKS; if (fractionCharged < 0.0 ) return IDLE_FRAME_INDEX; if (fractionCharged > FULLY_CHARGED_INDEX) return FULLY_CHARGED_INDEX; return (float ) fractionCharged * FULLY_CHARGED_INDEX; } }
2.3.4 物品视角设置(不常用) 主要是在item_model.json
中设置display
节点 MBE 作者推荐的两个工具:软件工具 | BlockBench Mod 工具 | ItemTransformHelper
2.3.5 动态物品模型(不常用) 对应 MBE 项目中的 MBE15 部分,以后用到了再看
2.4 TileEntity TileEntity 如其名,瓦片实体,不单独存在,需要与其他实体联合使用(二者坐标完全相同) 普通实体本身负责渲染,提供方块属性等 与普通方块连接的 TileEntity 负责进行各种功能,GUI 显示,存储数据等(如箱子,熔炉等)
TileEntity 和 NBT 结合紧密,MBE 作者推荐了一个查看 NBT 结构的工具(TileEntityData.java 的注释中):NBTexplorer
2.4.1 存储数据与触发动作 2.4.1.1 方块主类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class BlockTileEntityData extends Block { public BlockTileEntityData () { super (Block.Properties.create(Material.ROCK)); } private final int TIMER_COUNTDOWN_TICKS = 20 * 10 ; @Override public boolean hasTileEntity (BlockState state) { return true ; } @Override public TileEntity createTileEntity (BlockState state, IBlockReader world) {return new TileEntityData();} @Override public void onBlockPlacedBy (World worldIn, BlockPos pos, BlockState state, LivingEntity placer, ItemStack stack) { super .onBlockPlacedBy(worldIn, pos, state, placer, stack); TileEntity tileentity = worldIn.getTileEntity(pos); if (tileentity instanceof TileEntityData) { TileEntityData tileEntityData = (TileEntityData)tileentity; tileEntityData.setTicksLeftTillDisappear(TIMER_COUNTDOWN_TICKS); } } }
2.4.1.2 TileEntity 类 实现方块的特殊功能,在这个例子里,方块在放下 10 秒后会随机变成其他东西
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 public class TileEntityData extends TileEntity implements ITickableTileEntity { private final int [] testIntArray = {5 , 4 , 3 , 2 , 1 }; private final double [] testDoubleArray = {1 , 2 , 3 , 4 , 5 , 6 }; private final Double [] testDoubleArrayWithNulls = {61.1 , 62.2 , null , 64.4 , 65.5 }; private final ItemStack testItemStack = new ItemStack(Items.COOKED_CHICKEN, 23 ); private final String testString = "supermouse" ; private final BlockPos testBlockPos = new BlockPos(10 , 11 , 12 ); public TileEntityData () { super (StartupCommon.tileEntityDataTypeMBE20); } private final int INVALID_VALUE = -1 ; private int ticksLeftTillDisappear = INVALID_VALUE; public void setTicksLeftTillDisappear (int ticks) { ticksLeftTillDisappear = ticks; } @Override public CompoundNBT write (CompoundNBT parentNBTTagCompound) { super .write(parentNBTTagCompound); parentNBTTagCompound.putInt("ticksLeft" , ticksLeftTillDisappear); parentNBTTagCompound.putString("testString" , testString); CompoundNBT blockPosNBT = new CompoundNBT(); blockPosNBT.putInt("x" , testBlockPos.getX()); blockPosNBT.putInt("y" , testBlockPos.getY()); blockPosNBT.putInt("z" , testBlockPos.getZ()); parentNBTTagCompound.put("testBlockPos" , blockPosNBT); CompoundNBT itemStackNBT = new CompoundNBT(); testItemStack.write(itemStackNBT); parentNBTTagCompound.put("testItemStack" , itemStackNBT); parentNBTTagCompound.putIntArray("testIntArray" , testIntArray); ListNBT doubleArrayNBT = new ListNBT(); for (double value : testDoubleArray) { doubleArrayNBT.add(DoubleNBT.valueOf(value)); } parentNBTTagCompound.put("testDoubleArray" , doubleArrayNBT); ListNBT doubleArrayWithNullsNBT = new ListNBT(); for (int i = 0 ; i < testDoubleArrayWithNulls.length; ++i) { Double value = testDoubleArrayWithNulls[i]; if (value != null ) { CompoundNBT dataForThisSlot = new CompoundNBT(); dataForThisSlot.putInt("i" , i + 1 ); dataForThisSlot.putDouble("v" , value); doubleArrayWithNullsNBT.add(dataForThisSlot); } } parentNBTTagCompound.put("testDoubleArrayWithNulls" , doubleArrayWithNullsNBT); return parentNBTTagCompound; } @Override public void read (CompoundNBT parentNBTTagCompound) { super .read(parentNBTTagCompound); final int NBT_INT_ID = NBTtypesMBE.INT_NBT_ID; int readTicks = INVALID_VALUE; if (parentNBTTagCompound.contains("ticksLeft" , NBT_INT_ID)) { readTicks = parentNBTTagCompound.getInt("ticksLeft" ); if (readTicks < 0 ) readTicks = INVALID_VALUE; } ticksLeftTillDisappear = readTicks; String readTestString = null ; final int NBT_STRING_ID = NBTtypesMBE.STRING_NBT_ID; if (parentNBTTagCompound.contains("testString" , NBT_STRING_ID)) { readTestString = parentNBTTagCompound.getString("testString" ); } if (!testString.equals(readTestString)) { System.err.println("testString mismatch:" + readTestString); } CompoundNBT blockPosNBT = parentNBTTagCompound.getCompound("testBlockPos" ); BlockPos readBlockPos = null ; if (blockPosNBT.contains("x" , NBT_INT_ID) && blockPosNBT.contains("y" , NBT_INT_ID) && blockPosNBT.contains("z" , NBT_INT_ID) ) { readBlockPos = new BlockPos(blockPosNBT.getInt("x" ), blockPosNBT.getInt("y" ), blockPosNBT.getInt("z" )); } if (readBlockPos == null || !testBlockPos.equals(readBlockPos)) { System.err.println("testBlockPos mismatch:" + readBlockPos); } CompoundNBT itemStackNBT = parentNBTTagCompound.getCompound("testItemStack" ); ItemStack readItemStack = ItemStack.read(itemStackNBT); if (!ItemStack.areItemStacksEqual(testItemStack, readItemStack)) { System.err.println("testItemStack mismatch:" + readItemStack); } int [] readIntArray = parentNBTTagCompound.getIntArray("testIntArray" ); if (!Arrays.equals(testIntArray, readIntArray)) { System.err.println("testIntArray mismatch:" + readIntArray); } final int NBT_DOUBLE_ID = NBTtypesMBE.DOUBLE_NBT_ID; ListNBT doubleArrayNBT = parentNBTTagCompound.getList("testDoubleArray" , NBT_DOUBLE_ID); int numberOfEntries = Math.min(doubleArrayNBT.size(), testDoubleArray.length); double [] readDoubleArray = new double [numberOfEntries]; for (int i = 0 ; i < numberOfEntries; ++i) { readDoubleArray[i] = doubleArrayNBT.getDouble(i); } if (doubleArrayNBT.size() != numberOfEntries || !Arrays.equals(readDoubleArray, testDoubleArray)) { System.err.println("testDoubleArray mismatch:" + readDoubleArray); } final int NBT_COMPOUND_ID = NBTtypesMBE.COMPOUND_NBT_ID; ListNBT doubleNullArrayNBT = parentNBTTagCompound.getList("testDoubleArrayWithNulls" , NBT_COMPOUND_ID); numberOfEntries = Math.min(doubleArrayNBT.size(), testDoubleArrayWithNulls.length); Double [] readDoubleNullArray = new Double[numberOfEntries]; for (int i = 0 ; i < doubleNullArrayNBT.size(); ++i) { CompoundNBT nbtEntry = doubleNullArrayNBT.getCompound(i); int idx = nbtEntry.getInt("i" ) - 1 ; if (nbtEntry.contains("v" , NBT_DOUBLE_ID) && idx >= 0 && idx < numberOfEntries) { readDoubleNullArray[idx] = nbtEntry.getDouble("v" ); } } if (!Arrays.equals(testDoubleArrayWithNulls, readDoubleNullArray)) { System.err.println("testDoubleArrayWithNulls mismatch:" + readDoubleNullArray); } } @Override @Nullable public SUpdateTileEntityPacket getUpdatePacket () { CompoundNBT nbtTagCompound = new CompoundNBT(); write(nbtTagCompound); int tileEntityType = 42 ; return new SUpdateTileEntityPacket(this .pos, tileEntityType, nbtTagCompound); } @Override public void onDataPacket (NetworkManager net, SUpdateTileEntityPacket pkt) { read(pkt.getNbtCompound()); } @Override public CompoundNBT getUpdateTag () { CompoundNBT nbtTagCompound = new CompoundNBT(); write(nbtTagCompound); return nbtTagCompound; } @Override public void handleUpdateTag (CompoundNBT tag) { this .read(tag); } @Override public void tick () { if (!this .hasWorld()) return ; World world = this .getWorld(); if (world.isRemote) return ; ServerWorld serverWorld = (ServerWorld)world; if (ticksLeftTillDisappear == INVALID_VALUE) return ; --ticksLeftTillDisappear; if (ticksLeftTillDisappear > 0 ) return ; Block [] blockChoices = {Blocks.DIAMOND_BLOCK, Blocks.OBSIDIAN, Blocks.AIR, Blocks.TNT, Blocks.CORNFLOWER, Blocks.OAK_SAPLING, Blocks.WATER}; Random random = new Random(); Block chosenBlock = blockChoices[random.nextInt(blockChoices.length)]; world.setBlockState(this .pos, chosenBlock.getDefaultState()); if (chosenBlock == Blocks.TNT) { Blocks.TNT.catchFire(Blocks.TNT.getDefaultState().with(TNTBlock.UNSTABLE, true ), world, pos, null , null ); world.removeBlock(pos, false ); } else if (chosenBlock == Blocks.OAK_SAPLING) { SaplingBlock blockSapling = (SaplingBlock)Blocks.OAK_SAPLING; } } }
2.4.1.3 注册 注册带有 TileEntity 的方块时,需要额外加上一个对 TileEntity 的注册
1 2 3 4 5 6 7 8 9 10 11 12 13 14 { public static BlockTileEntityData blockTileEntityData; public static TileEntityType<TileEntityData> tileEntityDataTypeMBE20; @SubscribeEvent public static void onTileEntityTypeRegistration (final RegistryEvent.Register<TileEntityType<?>> event) { tileEntityDataTypeMBE20 = TileEntityType.Builder.create(TileEntityData::new , blockTileEntityData).build(null ); tileEntityDataTypeMBE20.setRegistryName("minecraftbyexample:mbe20_tile_entity_type_registry_name" ); event.getRegistry().register(tileEntityDataTypeMBE20); } }
2.4.2 渲染(TODO) 啊…太多内容了… 以后用到了再看 对应于 MBE 项目的 mbe_21
2.5 存储方块(inventory)(TODO) 2.5.4 合成表 合成表配方比较简单,只需要在data.examplemod.recipes.block_name.json
添加 json 格式的配方文件即可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 { "type" : "minecraft:crafting_shaped" , "pattern" : ["xxa" , "x x" , "xxx" ], "key" : { "x" : { "tag" : "forge:gems/diamond" }, "a" : { "item" : "mymod:myfirstitem" } }, "result" : { "item" : "mymod:myitem" , "count" : 9 } }
2.6 命令(TODO) 2.6.1 注册命令 首先在StartupCommon.java
中对ServerLifecycleEvents
进行注册
1 2 3 4 5 6 { @SubscribeEvent public static void onCommonSetupEvent (FMLCommonSetupEvent event) { MinecraftForge.EVENT_BUS.register(ServerLifecycleEvents.class); } }
然后在 ServerLifecycleEvents 中对命令进行注册
1 2 3 4 5 6 7 8 public class ServerLifecycleEvents { @SubscribeEvent public static void onServerStartingEvent (FMLServerStartingEvent event) { CommandDispatcher<CommandSource> commandDispatcher = event.getCommandDispatcher(); MBEsayCommand.register(commandDispatcher); MBEquoteCommand.register(commandDispatcher); } }
2.6.2 构造命令 然后是在在 ServerLifecycleEvents 中注册的命令类中构造命令 其中操作的基本结构基本一样
1 2 3 4 5 6 7 8 9 10 11 12 public class MBEsayCommand { public static void register (CommandDispatcher<CommandSource> dispatcher) { LiteralArgumentBuilder<CommandSource> mbesayCommand = Commands.literal("mbesay" ) .requires ((commandSource) -> commandSource.hasPermissionLevel(2 )) .then(Commands.argument("message" , MessageArgument.message()) .executes(MBEsayCommand::sendPigLatinMessage) ); dispatcher.register(mbesayCommand); } }
其中的基规则;
函数
作用
literal
匹配一个命令 token
argument
匹配一个参数 token
then
匹配下一个 token
requires
匹配一些权限要求
executes
执行一个命令
redirect
重定向命令
suggests
提供自动补全
fork
执行其他命令
2.7 粒子效果(TODO) 2.8 网络通信 这一部分会构造一个法杖物品,使用后引发一场空袭(随机召唤一些实体)
客户端使用法杖,构造一个包含位置和召唤物种类的信息,发送给服务器
服务器处理信息,发送一个包含位置的特效信息给所有同一维度的玩家
服务器在指定位置生成召唤物(自动同步给玩家)
服务器在指定位置生成雷声(自动同步给玩家)
2.8.1 MessageHandlerOnClient.java 用于客户端处理接收到的数据 需要注意的是,这个类是运行在一个单独的线程中,故不能随意调用 Forge 和 MC 的对象 在需要调用一些其他函数的时候,通过在任务列表中添加任务代替直接调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public class MessageHandlerOnClient { public static void onMessageReceived (final TargetEffectMessageToClient message, Supplier<NetworkEvent.Context> ctxSupplier) { NetworkEvent.Context ctx = ctxSupplier.get(); LogicalSide sideReceived = ctx.getDirection().getReceptionSide(); ctx.setPacketHandled(true ); if (sideReceived != LogicalSide.CLIENT) { LOGGER.warn("TargetEffectMessageToClient received on wrong side:" + ctx.getDirection().getReceptionSide()); return ; } if (!message.isMessageValid()) { LOGGER.warn("TargetEffectMessageToClient was invalid" + message.toString()); return ; } Optional<ClientWorld> clientWorld = LogicalSidedProvider.CLIENTWORLD.get(sideReceived); if (!clientWorld.isPresent()) { LOGGER.warn("TargetEffectMessageToClient context could not provide a ClientWorld." ); return ; } ctx.enqueueWork(() -> processMessage(clientWorld.get(), message)); } private static void processMessage (ClientWorld worldClient, TargetEffectMessageToClient message) { Random random = new Random(); final int NUMBER_OF_PARTICLES = 100 ; final double HORIZONTAL_SPREAD = 1.5 ; for (int i = 0 ; i < NUMBER_OF_PARTICLES; ++i) { Vec3d targetCoordinates = message.getTargetCoordinates(); double spawnXpos = targetCoordinates.x + (2 *random.nextDouble() - 1 ) * HORIZONTAL_SPREAD; double spawnYpos = targetCoordinates.y; double spawnZpos = targetCoordinates.z + (2 *random.nextDouble() - 1 ) * HORIZONTAL_SPREAD; worldClient.addParticle(ParticleTypes.INSTANT_EFFECT, spawnXpos, spawnYpos, spawnZpos, 0 , 0 , 0 ); } return ; } public static boolean isThisProtocolAcceptedByClient (String protocolVersion) { return StartupCommon.MESSAGE_PROTOCOL_VERSION.equals(protocolVersion); } private static final Logger LOGGER = LogManager.getLogger(); }
2.8.2 MessageHandlerOnServer.java 用于服务器处理接收到的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 public class MessageHandlerOnServer { public static void onMessageReceived (final AirstrikeMessageToServer message, Supplier<NetworkEvent.Context> ctxSupplier) { NetworkEvent.Context ctx = ctxSupplier.get(); LogicalSide sideReceived = ctx.getDirection().getReceptionSide(); ctx.setPacketHandled(true ); if (sideReceived != LogicalSide.SERVER) { LOGGER.warn("AirstrikeMessageToServer received on wrong side:" + ctx.getDirection().getReceptionSide()); return ; } if (!message.isMessageValid()) { LOGGER.warn("AirstrikeMessageToServer was invalid" + message.toString()); return ; } final ServerPlayerEntity sendingPlayer = ctx.getSender(); if (sendingPlayer == null ) { LOGGER.warn("EntityPlayerMP was null when AirstrikeMessageToServer was received" ); } ctx.enqueueWork(() -> processMessage(message, sendingPlayer)); } static void processMessage (AirstrikeMessageToServer message, ServerPlayerEntity sendingPlayer) { TargetEffectMessageToClient msg = new TargetEffectMessageToClient(message.getTargetCoordinates()); DimensionType playerDimension = sendingPlayer.dimension; StartupCommon.simpleChannel.send(PacketDistributor.DIMENSION.with(() -> playerDimension), msg); Random random = new Random(); final int MAX_NUMBER_OF_PROJECTILES = 20 ; final int MIN_NUMBER_OF_PROJECTILES = 2 ; int numberOfProjectiles = MIN_NUMBER_OF_PROJECTILES + random.nextInt(MAX_NUMBER_OF_PROJECTILES - MIN_NUMBER_OF_PROJECTILES + 1 ); for (int i = 0 ; i < numberOfProjectiles; ++i) { World world = sendingPlayer.world; final double MAX_HORIZONTAL_SPREAD = 4.0 ; final double MAX_VERTICAL_SPREAD = 20.0 ; final double RELEASE_HEIGHT_ABOVE_TARGET = 40 ; double xOffset = (random.nextDouble() * 2 - 1 ) * MAX_HORIZONTAL_SPREAD; double zOffset = (random.nextDouble() * 2 - 1 ) * MAX_HORIZONTAL_SPREAD; double yOffset = RELEASE_HEIGHT_ABOVE_TARGET + (random.nextDouble() * 2 - 1 ) * MAX_VERTICAL_SPREAD; Vec3d releasePoint = message.getTargetCoordinates().add(xOffset, yOffset, zOffset); EntityType entityType = message.getProjectile().getEntityType(); CompoundNBT spawnNBT = null ; ITextComponent customName = null ; PlayerEntity spawningPlayer = null ; BlockPos spawnLocation = new BlockPos(releasePoint); boolean SPAWN_ON_TOP_OF_GIVEN_BLOCK_LOCATION = false ; boolean SEARCH_DOWN_WHEN_PLACED_ON_TOP_OF_GIVEN_BLOCK_LOCATION = false ; Entity spawnedEntity = entityType.spawn(world, spawnNBT, customName, spawningPlayer, spawnLocation, SpawnReason.SPAWN_EGG, SPAWN_ON_TOP_OF_GIVEN_BLOCK_LOCATION, SEARCH_DOWN_WHEN_PLACED_ON_TOP_OF_GIVEN_BLOCK_LOCATION); switch (message.getProjectile()) { case FIREBALL: { FireballEntity fireballEntity = (FireballEntity)spawnedEntity; final double Y_ACCELERATION = -0.5 ; fireballEntity.accelerationX = 0.0 ; fireballEntity.accelerationY = Y_ACCELERATION; fireballEntity.accelerationZ = 0.0 ; break ; } default : { break ; } } final float VOLUME = 10000.0F ; final float PITCH = 0.8F + random.nextFloat() * 0.2F ; PlayerEntity playerCausingSound = null ; world.playSound(playerCausingSound, releasePoint.x, releasePoint.y, releasePoint.z, SoundEvents.ENTITY_LIGHTNING_BOLT_THUNDER, SoundCategory.WEATHER, VOLUME, PITCH); } return ; } public static boolean isThisProtocolAcceptedByServer (String protocolVersion) { return StartupCommon.MESSAGE_PROTOCOL_VERSION.equals(protocolVersion); } private static final Logger LOGGER = LogManager.getLogger(); }
2.8.3 TargetEffectMessageToClient.java 服务器发送给客户端的消息类型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 public class TargetEffectMessageToClient { private Vec3d targetCoordinates; private boolean messageIsValid; private static final Logger LOGGER = LogManager.getLogger(); public TargetEffectMessageToClient (Vec3d i_targetCoordinates) { targetCoordinates = i_targetCoordinates; messageIsValid = true ; } public Vec3d getTargetCoordinates () { return targetCoordinates; } public boolean isMessageValid () { return messageIsValid; } public TargetEffectMessageToClient () { messageIsValid = false ; } public static TargetEffectMessageToClient decode (PacketBuffer buf) { TargetEffectMessageToClient retval = new TargetEffectMessageToClient(); try { double x = buf.readDouble(); double y = buf.readDouble(); double z = buf.readDouble(); retval.targetCoordinates = new Vec3d(x, y, z); } catch (IllegalArgumentException | IndexOutOfBoundsException e) { LOGGER.warn("Exception while reading TargetEffectMessageToClient: " + e); return retval; } retval.messageIsValid = true ; return retval; } public void encode (PacketBuffer buf) { if (!messageIsValid) return ; buf.writeDouble(targetCoordinates.x); buf.writeDouble(targetCoordinates.y); buf.writeDouble(targetCoordinates.z); } @Override public String toString () { return "TargetEffectMessageToClient[targetCoordinates=" + String.valueOf(targetCoordinates) + "]" ; } }
2.8.4 AirstrikeMessageToServer.java 客户端发送给服务器的消息类型 与TargetEffectMessageToClient.java
基本一致
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 public class AirstrikeMessageToServer { private Vec3d targetCoordinates; private Projectile projectile; private boolean messageIsValid; private static final Logger LOGGER = LogManager.getLogger(); public AirstrikeMessageToServer (Projectile i_projectile, Vec3d i_targetCoordinates) { projectile = i_projectile; targetCoordinates = i_targetCoordinates; messageIsValid = true ; } public Vec3d getTargetCoordinates () { return targetCoordinates; } public Projectile getProjectile () { return projectile; } public boolean isMessageValid () { return messageIsValid; } private AirstrikeMessageToServer () { messageIsValid = false ; } public static AirstrikeMessageToServer decode (PacketBuffer buf) { AirstrikeMessageToServer retval = new AirstrikeMessageToServer(); try { retval.projectile = Projectile.fromPacketBuffer(buf); double x = buf.readDouble(); double y = buf.readDouble(); double z = buf.readDouble(); retval.targetCoordinates = new Vec3d(x, y, z); } catch (IllegalArgumentException | IndexOutOfBoundsException e) { LOGGER.warn("Exception while reading AirStrikeMessageToServer: " + e); return retval; } retval.messageIsValid = true ; return retval; } public void encode (PacketBuffer buf) { if (!messageIsValid) return ; projectile.toPacketBuffer(buf); buf.writeDouble(targetCoordinates.x); buf.writeDouble(targetCoordinates.y); buf.writeDouble(targetCoordinates.z); } public enum Projectile { PIG(1 , "PIG" , EntityType.PIG), SNOWBALL(2 , "SNOWBALL" , EntityType.SNOWBALL), TNT(3 , "TNT" , EntityType.TNT), SNOWMAN(4 , "SNOWMAN" , EntityType.SNOW_GOLEM), EGG(5 , "EGG" , EntityType.EGG), FIREBALL(6 , "FIREBALL" , EntityType.FIREBALL); private final byte projectileID; private final String name; private final EntityType entityType; private Projectile (int i_projectileID, String i_name, EntityType i_entityType) { projectileID = (byte )i_projectileID; name = i_name; entityType = i_entityType; } public void toPacketBuffer (PacketBuffer buffer) { buffer.writeByte(projectileID); } public EntityType getEntityType () {return entityType;} public static Projectile fromPacketBuffer (PacketBuffer buffer) throws IllegalArgumentException { byte ID = buffer.readByte(); for (Projectile projectile : Projectile.values()) { if (ID == projectile.projectileID) return projectile; } throw new IllegalArgumentException("Unrecognised Projectile ID:" + ID); } public static Projectile getRandom () { Random random = new Random(); AirstrikeMessageToServer.Projectile [] choices = AirstrikeMessageToServer.Projectile.values(); return choices[random.nextInt(choices.length)]; } @Override public String toString () {return name;} } @Override public String toString () { return "AirstrikeMessageToServer[projectile=" + String.valueOf(projectile) + ", targetCoordinates=" + String.valueOf(targetCoordinates) + "]" ; } }
2.8.5 ItemAirStrike.java 一个法杖,使用后引发”空袭”,随机召唤一些东西
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 public class ItemAirStrike extends Item { static private final int MAXIMUM_NUMBER_OF_ITEMS = 1 ; public ItemAirStrike () { super (new Item.Properties().maxStackSize(MAXIMUM_NUMBER_OF_ITEMS).group(ItemGroup.MISC)); } @Override public ActionResultType onItemUse (ItemUseContext context) { if (!context.getWorld().isRemote) { return ActionResultType.PASS; } Vec3d targetLocation = context.getHitVec(); callAirstrikeOnTarget(targetLocation); return ActionResultType.SUCCESS; } @Override public ActionResult<ItemStack> onItemRightClick (World worldIn, PlayerEntity playerIn, Hand hand) { ItemStack itemStackIn = playerIn.getHeldItem(hand); if (!worldIn.isRemote) { return new ActionResult(ActionResultType.PASS, itemStackIn); } final float PARTIAL_TICKS = 1.0F ; Vec3d playerLook = playerIn.getLookVec(); Vec3d playerFeetPosition = playerIn.getEyePosition(PARTIAL_TICKS).subtract(0 , playerIn.getEyeHeight(), 0 ); final double TARGET_DISTANCE = 6.0 ; final double HEIGHT_ABOVE_FEET = 0.1 ; Vec3d targetPosition = playerFeetPosition.add(playerLook.x * TARGET_DISTANCE, HEIGHT_ABOVE_FEET, playerLook.z * TARGET_DISTANCE); callAirstrikeOnTarget(targetPosition); return new ActionResult(ActionResultType.SUCCESS, itemStackIn); } public void callAirstrikeOnTarget (Vec3d targetPosition) { AirstrikeMessageToServer.Projectile projectile = AirstrikeMessageToServer.Projectile.getRandom(); AirstrikeMessageToServer airstrikeMessageToServer = new AirstrikeMessageToServer(projectile, targetPosition); StartupCommon.simpleChannel.sendToServer(airstrikeMessageToServer); return ; } @Override public UseAction getUseAction (ItemStack stack) { return UseAction.BLOCK; } @Override public void addInformation (ItemStack stack, @Nullable World worldIn, List<ITextComponent> tooltip, ITooltipFlag flagIn) { tooltip.add(new StringTextComponent("Right click on target to call an air strike" )); } }
2.8.6 StartUpCommon.java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class StartupCommon { public static ItemAirStrike itemAirStrike; public static SimpleChannel simpleChannel; public static final byte AIRSTRIKE_MESSAGE_ID = 35 ; public static final byte TARGET_EFFECT_MESSAGE_ID = 63 ; public static final String MESSAGE_PROTOCOL_VERSION = "1.0" ; public static final ResourceLocation simpleChannelRL = new ResourceLocation("minecraftbyexample" , "mbechannel" ); @SubscribeEvent public static void onItemsRegistration (final RegistryEvent.Register<Item> itemRegisterEvent) { itemAirStrike = new ItemAirStrike(); itemAirStrike.setRegistryName("mbe60_item_airstrike_registry_name" ); itemRegisterEvent.getRegistry().register(itemAirStrike); } @SubscribeEvent public static void onCommonSetupEvent (FMLCommonSetupEvent event) { simpleChannel = NetworkRegistry.newSimpleChannel(simpleChannelRL, () -> MESSAGE_PROTOCOL_VERSION, MessageHandlerOnClient::isThisProtocolAcceptedByClient, MessageHandlerOnServer::isThisProtocolAcceptedByServer); simpleChannel.registerMessage(AIRSTRIKE_MESSAGE_ID, AirstrikeMessageToServer.class, AirstrikeMessageToServer::encode, AirstrikeMessageToServer::decode, MessageHandlerOnServer::onMessageReceived, Optional.of(PLAY_TO_SERVER)); simpleChannel.registerMessage(TARGET_EFFECT_MESSAGE_ID, TargetEffectMessageToClient.class, TargetEffectMessageToClient::encode, TargetEffectMessageToClient::decode, MessageHandlerOnClient::onMessageReceived, Optional.of(PLAY_TO_CLIENT)); } }
2.9 Capability 机制(TODO) 2.10 测试 MBE 中演示的游戏内测试框架一个两种:
使用一个测试物品触发测试函数
使用命令触发测试函数
其中还可以使用命令设置一些参数,使得使用命令触发测试的时候可以做到交互式测试
2.10.1 使用测试物品测试 使用物品测试就简单了
首先做一个测试类,其中留有一些测试函数供其他类调用
然后注册一个测试物品,右键使用这个物品的时候调用测试函数,同时根据一些条件设置不同的参数
2.10.1.1 测试类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 public class TestRunner { public boolean runServerSideTest (World worldIn, PlayerEntity playerIn, int testNumber) { boolean success = false ; switch (testNumber) { case 1 : { success = test1(worldIn, playerIn); break ; } default : { LOGGER.error("Test Number " + testNumber + " does not exist on server side." ); return false ; } } LOGGER.error("Test Number " + testNumber + " called on server side:" + (success ? "success" : "failure" )); return success; } public boolean runClientSideTest (World worldIn, PlayerEntity playerIn, int testNumber) { boolean success = false ; switch (testNumber) { case -1 : { break ; } default : { LOGGER.error("Test Number " + testNumber + " does not exist on client side." ); return false ; } } LOGGER.error("Test Number " + testNumber + " called on client side:" + (success ? "success" : "failure" )); return success; } private boolean test1 (World worldIn, PlayerEntity playerIn) { return true ; } private boolean teleportPlayerToTestRegion (PlayerEntity player, BlockPos location) { if (!(player instanceof ServerPlayerEntity)) { throw new UnsupportedOperationException("teleport not supported on client side; server side only" ); } CommandSource commandSource = player.getCommandSource(); String tpCommand = "/tp " + location.getX() + " " + location.getY() + " " + location.getZ(); int success = 0 ; try { success = player.getServer().getCommandManager().handleCommand(commandSource, tpCommand); } catch (Exception e) { return false ; } return (success != 0 ); } private static final Logger LOGGER = LogManager.getLogger(); }
2.10.1.2 测试物品 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public class ItemTestRunner extends Item { static final int MAX_TEST_NUMBER = 64 ; public ItemTestRunner () { super (new Item.Properties().maxStackSize(MAX_TEST_NUMBER).group(ItemGroup.MISC)); } @Override public void addInformation (ItemStack stack, @Nullable World worldIn, List<ITextComponent> tooltip, ITooltipFlag flagIn) { tooltip.add(new StringTextComponent("Right click: conduct test" )); tooltip.add(new StringTextComponent("Stacksize: change test #" )); tooltip.add(new StringTextComponent(" (64 = test all)" )); } @Override public UseAction getUseAction (ItemStack stack) { return UseAction.BLOCK; } @Override public int getUseDuration (ItemStack stack) { return 20 ; } @Override public ActionResult<ItemStack> onItemRightClick (World worldIn, PlayerEntity playerIn, Hand hand) { ItemStack itemStackIn = playerIn.getHeldItem(hand); if (itemStackIn.isEmpty()) { return new ActionResult<ItemStack>(ActionResultType.FAIL, itemStackIn); } int testNumber = itemStackIn.getCount(); TestRunner testRunner = new TestRunner(); if (worldIn.isRemote) { testRunner.runClientSideTest(worldIn, playerIn, testNumber); } else { testRunner.runServerSideTest(worldIn, playerIn, testNumber); } return new ActionResult<ItemStack>(ActionResultType.PASS, itemStackIn); } }
2.10.2 使用命令测试 使用命令一共分为四个部分:
构建一个存储交互式设置的参数的类:DebugSetting
构建一个存有测试函数的类:TestRunner(和使用测试物品测试一样)
构建一个在命令设置完参数后触发测试函数的监听器:DebugTestWatcher
构建一个命令系统,用于设置参数和触发测试:MBEdebugCommand
完成必要的注册
2.10.2.1 交互式存储参数的类 简而言之,这个类提供了一系列的 setter 和 getter
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 public class DebugSettings { public static final int NO_TEST_TRIGGERED = -1 ; private static int debugTest = NO_TEST_TRIGGERED; private static HashMap<String, Optional<Double>> debugParameters = new HashMap<>(); public static synchronized void setDebugParameter (String parameterName, double value) { debugParameters.put(parameterName, Optional.of(value)); } public static synchronized void clearDebugParameter (String parameterName) { debugParameters.put(parameterName, Optional.empty()); } public static synchronized Optional<Double> getDebugParameter (String parameterName) { Optional<Double> value = debugParameters.get(parameterName); if (value == null ) { debugParameters.put(parameterName, Optional.empty()); return Optional.empty(); } return value; } public static synchronized Set<String> listAllDebugParameters () { return debugParameters.keySet(); } public static synchronized void setDebugParameterVec3d (String parameterName, Vec3d value) { debugParameterVec3ds.put(parameterName, Optional.of(value)); } public static synchronized void clearDebugParameterVec3d (String parameterName) { debugParameterVec3ds.put(parameterName, Optional.empty()); } public static synchronized Optional<Vec3d> getDebugParameterVec3d (String parameterName) { Optional<Vec3d> value = debugParameterVec3ds.get(parameterName); if (value == null ) { debugParameterVec3ds.put(parameterName, Optional.empty()); return Optional.empty(); } return value; } public static synchronized Set<String> listAllDebugParameterVec3ds () { return debugParameterVec3ds.keySet(); } private static HashMap<String, Optional<Vec3d>> debugParameterVec3ds = new HashMap<>(); public static synchronized void setDebugTrigger (String parameterName) { debugTriggers.put(parameterName, true ); } public static synchronized boolean getDebugTrigger (String parameterName) { Boolean value = debugTriggers.getOrDefault(parameterName, false ); debugTriggers.put(parameterName, false ); return value; } public static synchronized Set<String> listAllDebugTriggers () { return debugTriggers.keySet(); } private static HashMap<String, Boolean> debugTriggers = new HashMap<>(); public static synchronized void setDebugTest (int testnumber) { debugTest = testnumber; } public static synchronized int getDebugTest (int testNumberMin, int testNumberMaxPlusOne) { if (debugTest < testNumberMin || debugTest >= testNumberMaxPlusOne) return NO_TEST_TRIGGERED; int value = debugTest; debugTest = NO_TEST_TRIGGERED; return value; } }
2.10.2.2 触发测试的监听器 监听器每个 tick 进行一次检查,在服务器上且玩家通过命令触发测试的时候调用测试函数
1 2 3 4 5 6 7 8 9 10 11 12 13 public class DebugTestWatcher { @SubscribeEvent public static void onServerTick (TickEvent.PlayerTickEvent event) { if (event.side != LogicalSide.SERVER) return ; int testNumber = DebugSettings.getDebugTest(0 , 5 ); if (testNumber == DebugSettings.NO_TEST_TRIGGERED) return ; testRunner.runServerSideTest(event.player.world, event.player, testNumber); } public static TestRunner testRunner = new TestRunner(); }
2.10.2.3 命令系统 略,见2.6
章
2.10.2.4 注册 首先在StartUpCommon.java
中进行注册 ServerLifecycleEvents 类
1 2 3 4 5 6 { @SubscribeEvent public static void onCommonSetupEvent (FMLCommonSetupEvent event) { MinecraftForge.EVENT_BUS.register(ServerLifecycleEvents.class); } }
然后在 ServerLifecycleEvents 类中注册触发测试的监听器 同时把测试相关的命令给注册了
1 2 3 4 5 6 7 8 9 public class ServerLifecycleEvents { @SubscribeEvent public static void onServerStartingEvent (FMLServerStartingEvent event) { CommandDispatcher<CommandSource> commandDispatcher = event.getCommandDispatcher(); MBEdebugCommand.register(commandDispatcher); MinecraftForge.EVENT_BUS.register(DebugTestWatcher.class); } }
2.11 模型渲染(TODO) 2.12 投掷物(TODO) 3. 实用工具(TODO) 3.1 实用函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public class UsefulFunctions { public static double interpolate_with_clipping (double x, double x1, double x2, double y1, double y2) { if (x1 > x2) { double temp = x1; x1 = x2; x2 = temp; temp = y1; y1 = y2; y2 = temp; } if (x <= x1) return y1; if (x >= x2) return y2; double xFraction = (x - x1) / (x2 - x1); return y1 + xFraction * (y2 - y1); } public static Vec3d scalarMultiply (Vec3d source, double multiplier) { return new Vec3d(source.x * multiplier, source.y * multiplier, source.z * multiplier); } public static ListNBT serializeVec3d (Vec3d vec3d) { ListNBT listnbt = new ListNBT(); listnbt.add(DoubleNBT.valueOf(vec3d.x)); listnbt.add(DoubleNBT.valueOf(vec3d.y)); listnbt.add(DoubleNBT.valueOf(vec3d.z)); return listnbt; } public static Vec3d deserializeVec3d (CompoundNBT nbt, String tagname) { ListNBT listnbt = nbt.getList(tagname, NBTtypesMBE.DOUBLE_NBT_ID); Vec3d retval = new Vec3d(listnbt.getDouble(0 ), listnbt.getDouble(1 ), listnbt.getDouble(2 )); return retval; } }
3.2 BlockStateFlag 工具 在使用SetBlockState
函数设置方块状态的时候,不同的数字意义不易读 可以使用这个枚举快速设置,多个状态可以用或连接
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public enum SetBlockStateFlag { BLOCK_UPDATE(1 ), SEND_TO_CLIENTS(2 ), DO_NOT_RENDER(4 ), RUN_RENDER_ON_MAIN_THREAD(8 ), PREVENT_NEIGHBOUR_REACTIONS(16 ), NEIGHBOUR_REACTIONS_DONT_SPAWN_DROPS(32 ), BLOCK_IS_BEING_MOVED(64 ); public static int get (SetBlockStateFlag... flags) { int result = 0 ; for (SetBlockStateFlag flag : flags) { result |= flag.flagValue; } return result; } SetBlockStateFlag(int flagValue) {this .flagValue = flagValue;} private int flagValue; }
3.3 阻止生物生成工具 如下监听器监听生物生成事件,同时根据设置的参数决定是否生成 与/kill @e[type=!Player]
命令一同使用,可以在调试时阻止生物的干扰
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class DebugSpawnInhibitor { @SubscribeEvent public static void checkForSpawnDenial (LivingSpawnEvent.CheckSpawn event) { if (DebugSettings.getDebugParameter("preventspawning" ).isPresent()) { ResourceLocation entityname = ForgeRegistries.ENTITIES.getKey(event.getEntity().getType()); if (entityname.getNamespace().equals("minecraft" )) { event.setResult(Event.Result.DENY); } else { event.setResult(Event.Result.DEFAULT); } } else { event.setResult(Event.Result.DEFAULT); } } }