Minecraft Mod 设计模式

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
# 必填项
# mod loader的类型,对于FML,通常是javafml
modLoader="javafml"

# 必填项
# 匹配的mod loader的版本,对于FML应该写forge的版本
loaderVersion="[28,)"

# 选填项
# mod出问题后的反馈网址,可以填Github的issues区网址
issueTrackerURL="https://github.com/TheGreyGhost/MinecraftByExample/issues"

# 必填项
# mod相关信息
[[mods]]

# mod的id,需要与主类中的一致
modId="minecraftbyexample"

# 必填项
# mod的版本,可以用一些特殊变量:${global.mcVersion}, ${global.forgeVersion}, {$file.jarVersion}
# 特殊变量会自动从build.gradle中获取
version="${file.jarVersion}"

# 必填项
# mod的显示名
displayName="Minecraft By Example"

# 选填项
# 更新查询URL
updateJSONURL="http://myurl.me/"

# 选填项
# 在游戏中显示的mod主页
displayURL="https://github.com/TheGreyGhost/MinecraftByExample"

# 选填项
# 位于mod jar包根目录的文件名,在游戏中展示的logo
logoFile="thegreyghostproudlypresents.png"

# 选填项
# 致谢文字
credits="The Forge, MCP, and FML guys, for making it possible"

# 选填项
# 作者
authors="TGG and others"

# 必填项
# mod的描述文字(多行)
description='''
Minecraft By Example - a collection of simple working examples of the important concepts in Minecraft and Forge.'''

# 选填项
# 用于描述mod间的依赖关系,可以有多个
[[dependencies.examplemod]]
# 被依赖的 mod 的id(必填项)
modId="forge"

# 被依赖的mod是否必须存在(必填项)
# 如果是false,则后面的 ordering 项必须填
mandatory=true

# 被依赖mod的版本(必填项)
versionRange="[28,)"

# 是否必填与前面的 mandatory 设置有关
# 与被依赖包的关系,BEFORE/AFTER
# (说实话没太明白...)
ordering="NONE"

# 描述mod运行的位置,BOTH/CLIENT/SERVER(必填项)
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
// places in this file that you should customise:
// add your mod info to version, group, archivesBaseName
// add your mod's root source folder to minecraft.runs.client.mod, minecraft.runs.server.mod, minecraft.runs.data.mod
// add your mod's info to jar.manifest.attributes (optional)
// if you're updating the forge version:
// Easiest way is to download the new forge mdk, decompress it, then look in the build.gradle file to copy the settings for
// minecraft mappings channel, and dependencies.minecraft

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'

// 以上部分是添加forge支持必要的部分
// ============================== 分割线 ==============================
// 以下部分是可以修改的部分

apply plugin: 'eclipse'
apply plugin: 'maven-publish'

// 在这里添加mod的信息
// 版本信息
// 作者所有项目的通用顶级包名
// 归档名,项目名全小写
version = "1.15.2b"
group= "thegreyghost"// http://maven.apache.org/guides/mini/guide-naming-conventions.html
archivesBaseName = "minecraftbyexample"

// Need this here so eclipse task generates correctly.
sourceCompatibility = targetCompatibility = compileJava.sourceCompatibility = compileJava.targetCompatibility = '1.8'


minecraft {
// The mappings can be changed at any time, and must be in the following format.
// snapshot_YYYYMMDD Snapshot are built nightly.
// stable_# Stables are built at the discretion of the MCP team.
// Use non-default mappings at your own risk. they may not always work.
// Simply re-run your setup task after changing the mappings to update your workspace.
mappings channel: 'snapshot', version: '20200514-1.15.1'
// makeObfSourceJar = false // an Srg named sources jar is made by default. uncomment this to disable.

// accessTransformer = file('src/main/resources/META-INF/accesstransformer.cfg')

// Default run configurations.
// 可以调整的部分,对应IDE中genIdeaRuns后产生的不同运行设置
runs {
client {
workingDirectory project.file('run')

// Recommended logging data for a userdev environment
property 'forge.logging.markers', 'SCAN,REGISTRIES,REGISTRYDUMP'

// Recommended logging level for the console
property 'forge.logging.console.level', 'debug'

mods {
minecraftbyexample {
source sourceSets.main
}
}
}
clientfewerconsolemessages {
workingDirectory project.file('run')

// Recommended logging data for a userdev environment
property 'forge.logging.markers', 'SCAN,REGISTRIES,REGISTRYDUMP'

// Recommended logging level for the console
property 'forge.logging.console.level', 'warn'

mods {
minecraftbyexample {
source sourceSets.main
}
}
}
server {
workingDirectory project.file('run')

// Recommended logging data for a userdev environment
property 'forge.logging.markers', 'SCAN,REGISTRIES,REGISTRYDUMP'

// Recommended logging level for the console
property 'forge.logging.console.level', 'debug'

mods {
minecraftbyexample {
source sourceSets.main
}
}
}

data {
workingDirectory project.file('run')

// Recommended logging data for a userdev environment
property 'forge.logging.markers', 'SCAN,REGISTRIES,REGISTRYDUMP'

// Recommended logging level for the console
property 'forge.logging.console.level', 'debug'

args '--mod', 'minecraftbyexample', '--all', '--output', file('src/generated/resources/')

mods {
minecraftbyexample {
source sourceSets.main
}
}
}
}
}

dependencies {
// Specify the version of Minecraft to use, If this is any group other then 'net.minecraft' it is assumed
// that the dep is a ForgeGradle 'patcher' dependency. And its patches will be applied.
// The userdev artifact is a special name and will get all sorts of transformations applied to it.
minecraft 'net.minecraftforge:forge:1.15.2-31.2.0'

// You may put jars on which you depend on in ./libs or you may define them like so..
// compile "some.group:artifact:version:classifier"
// compile "some.group:artifact:version"

// Real examples
// compile 'com.mod-buildcraft:buildcraft:6.0.8:dev' // adds buildcraft to the dev env
// compile 'com.googlecode.efficient-java-matrix-library:ejml:0.24' // adds ejml to the dev env

// The 'provided' configuration is for optional dependencies that exist at compile-time but might not at runtime.
// provided 'com.mod-buildcraft:buildcraft:6.0.8:dev'

// These dependencies get remapped to your current MCP mappings
// deobf 'com.mod-buildcraft:buildcraft:6.0.8:dev'

// For more info...
// http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html
// http://www.gradle.org/docs/current/userguide/dependency_management.html

}

// Example for how to get properties into the manifest for reading by the runtime..
jar {
manifest {
attributes([
"Specification-Title": "minecraftbyexample",
"Specification-Vendor": "thegreyghost",
"Specification-Version": "1", // The version number of the specification, not the version of the mod
"Implementation-Title": project.name,
"Implementation-Version": "${version}", // the version number of the mod. copy the version from earlier in this file
"Implementation-Vendor" :"thegreyghost",
"Implementation-Timestamp": new Date().format("yyyy-MM-dd'T'HH:mm:ssZ")
])
}
}

// Example configuration to allow publishing using the maven-publish task
// we define a custom artifact that is sourced from the reobfJar output task
// and then declare that to be published
// Note you'll need to add a repository here
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 {
// 首先是一个 MODID,唯一标识 Mod,同时一般会设置为 Mod 空间的前缀
// 这个 MODID 需要与 build.gradle file 中的匹配
// 同时需要在 resources/META-INF/mods.toml 中声明
public static final String MODID = "minecraftbyexample";


// 因为要注册很多监听器,用一个静态变量把事件总线存起来
public static IEventBus MOD_EVENT_BUS;

public MinecraftByExample() {
// 设置一些 Log 工具
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{

// 之所以选择把实例放在 StartUpCommon 而不是 StartupClientOnly
// 是因为 StartUpCommon 在服务器执行的时候绝对不能访问任何只在客户端运行的类
// 一种方块只维护一个实例对象,然后需要一个与之对应的 item 对象
public static BlockSimple blockSimple;
public static BlockItem itemBlockSimple;

@SubscribeEvent
public static void onBlocksRegistration(final RegistryEvent.Register<Block> blockRegisterEvent) {
// 实例化对象,设置方块域名(设置成主类.MODID更好)和方块本身名字
// 然后注册之
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(变体)中存放这个方块所有可能的变体的数据
"variants": {
// 如果只有一个变体,其名字用 "" 表示
"": {
// 存放模型文件的路径
"model": "minecraftbyexample:block/mbe01_block_simple_model",
// 模型在xy轴上以90度的增量旋转
"x": "90",
"y": "90",
// 是否锁定旋转,如果锁定旋转,则当方块旋转时纹理不跟着旋转
"uvlock": "true"
},
// 需要注意,如果有多个变种,则第一个变种不可以取空名字
"variants-2": [
// 一个变种可以有多个可能的模型,实例化时会从中随机选择
{
"model": "minecraftbyexample:block/mbe01_block_simple_model",
// 指定随机选择的权重(整数,默认为1)
"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
{
// 继承父类模型,子类的 elements 标签可以覆盖父类的
// 还能设置为 "builtin/generated" ,使用从指定图标创建的模型
"parent": "block/cube",
// 是否启用环境光遮蔽
"ambientocclusion": "true",
// 纹理,对应不同的面
"textures": {
// 方块对应的粒子特效
"particle": "block/simple_block",
// "all":"minecraftbyexample: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": [
{
// 起始点/终止点,数值在-16到32之间
"from": [7, 0, 7],
"to": [9, 10, 9],
// 是否渲染阴影,默认为true
"shade": false,
// 单个元素的所有面,如果没有定义该面则不渲染
// down, up, north, south, west, east
"faces": {
// uv:纹理贴图中的坐标范围,数值在0到16之间,默认为元素的位置
// texture:贴图,变量前需要加上#
// cullface:当这个方块与指定反向的方块连接时,是否需要渲染
"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
{
// 战利品类型
// minecraft:empty(不产生掉落物),minecraft:entity(实体掉落物),minecraft:block(方块 掉落物)
// 详见参考网址
"type": "minecraft:block",
// 掉落物随机池
"pools": [
{
// 从当前随机池抽取的次数
// 还可以为范围和二项分布(略)
// "rolls":{
// "min":"1",
// "max":"4"
// },
"rolls": 1,
// 当前随机池能产生的物品列表
"entries": [
{
"type": "minecraft:item",
"name": "minecraftbyexample:mbe01_block_simple_registry_name"
}
],
// 使用这个随机池的条件
"conditions": [
{
"condition": "minecraft:survives_explosion"
}
]
}
]
}

2.2.2 非完整方块

在搭建简单方块的基础上,可以搭建一些非完整的(占满 1 格空间的)方块

保持StartUpClientOnly.javaStartUpCommon.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); // use this method to add two shapes together

// 从第一个形状上剪切出形状
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 方块变体

方块变体总体有两种思路:

  1. 每个变体建一个模型,不同的变体选择不同的模型
    采用这种方法的方块有各种材料的楼梯,每个不同的反向都是采用不同模型的变体
  2. 把所有变体抽象出一个核心,然后再向不同的方向进行扩展
    采用这种方法的方块有栅栏,其核心是一根棍子,不同方向扩展的模型是横栏

第一种方法在变种较少时比较简单,但是如果变种太多时会非常繁琐(如栅栏的六个方向)

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;

// 重载 fillStateContainer 方法,并在其中把上面的变量添加进去
protected void fillStateContainer(StateContainer.Builder<Block, BlockState> builder) {
builder.add(FACING);
}

// 在构造函数中可以设置默认状态
{
BlockState defaultBlockState = this.stateContainer.getBaseState()
.with(FACING, Direction.NORTH);
this.setDefaultState(defaultBlockState);
}

// 重载 getStateForPlacement,以在方块放下的时候设置方块的状态
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;
}

//维护一个方向到模型的映射(不可修改的Map)
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);

// 最后一步,重载 getShape 方法
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
{
// 给每个方向都设置一个属性,并注册进 BlockState 中
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);
}
}

// 重载 getShape ,在不同情况下显示不同的模型
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 {
// 其中的tintIndex指的是item模型文件中纹理的层数
// "textures": {
// "layer0": "item/potion_overlay",
// "layer1": "item/potion_bottle_drinkable"
// }
// 其中layer0纹理的tintIndex即0,layer1的为1
@Override
public int getColor(ItemStack stack, int tintIndex) {
// 在获取指定层颜色的时候,从物品的NBT标签中获取数据
{
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",
// 采用的贴图为0满度贴图
"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{
// 枚举值,其中的值对应构造函数的参数:NBT_ID,名字,描述
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;
}

// 一些getter和setter就省略了
// ...

@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;
}

// 从NBT标签的数据得到对应的满度枚举对象
private static Optional<EnumBottleFullness> getFullnessFromID(byte ID) {
for (EnumBottleFullness fullness : EnumBottleFullness.values()) {
if (fullness.nbtID == ID) return Optional.of(fullness);
}
return Optional.empty();
}

// 从NBT标签构获取满度枚举
// 参数为混到一块儿的NBT标签,目标标签名
public static EnumBottleFullness fromNBT(CompoundNBT compoundNBT, String tagname){
byte fullnessID = 0;
// 试图从NBT中找到要找的数据
if (compoundNBT != null && compoundNBT.contains(tagname)) {
fullnessID = compoundNBT.getByte(tagname);
}
Optional<EnumBottleFullness> fullness = getFullnessFromID(fullnessID);
// 保护性写法,如果fullness为空则使用FULL枚举
return fullness.orElse(FULL);
}

// 把当前枚举对象写入指定NBT标签
public void putIntoNBT(CompoundNBT compoundNBT, String tagname){
compoundNBT.putByte(tagname, nbtID);
}
}

// 果汁类型枚举,与满度枚举类似,不再展开
public enum EnumBottleFlavour implements IStringSerializable{...}

// 尝龟的常数设置以及尝龟的NBT键值名
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);
// use lambda function to link the NBT fullness value to a suitable property override value
}

// 从物品中得到饮料类别信息
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();
}

// 两个尝龟setter,写入的时候直接写入物品的NBT中
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);
}

// 把当前物品派生出的所有物品都加到标签页中(2.2.7.2的方法喜加一)
@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();
}

// 根据不同的满度修改显示名字(物品类名还是不变的)
// 需要翻译文件进行配合(派生物名:派生物显示名):
// {
// // %s 的位置会被这个函数影响
// "item.modname.item_variants_registry_name.orange": "MBE11 Item Variants orange (%s)"
// }
@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 {

// 特效开始的tick数量,是否开始特效
private long startingTick = -1;
private boolean animationHasStarted = false;

// worldIn: 当被一个GUI渲染的时候为null(如物品栏,快捷栏,展示框)
// entityIn: 当直接在世界中渲染的时候为null(如扔到地面上)
@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;

// 指明方块有一个TileEntity
@Override
public boolean hasTileEntity(BlockState state){
return true;
}

// 方块被放置,或者客户端加载了这个新的方块
// 返回一个与这个方块配合的TileEntity对象实例
@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 tileentity = worldIn.getTileEntity(pos);
// 保护性写法,防止为null或者TileEntity类型不匹配
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;
}

// 把要保存的数据写入NBT中
@Override
public CompoundNBT write(CompoundNBT parentNBTTagCompound){
// The super call is required to save the tile's location
// 父类调用会写入这个TileEntity的位置信息,所以必须写上
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();
// 读取的默认值时是0,所以写入的时候尽量避开0
dataForThisSlot.putInt("i", i + 1);
dataForThisSlot.putDouble("v", value);
doubleArrayWithNullsNBT.add(dataForThisSlot);
}
}
parentNBTTagCompound.put("testDoubleArrayWithNulls", doubleArrayWithNullsNBT);
return parentNBTTagCompound;
}

// 恢复数据会自动调用
@Override
public void read(CompoundNBT parentNBTTagCompound){
// The super call is required to load the tiles location
// 父类调用会读取TileEntity的位置信息
super.read(parentNBTTagCompound);

// important rule: never trust the data you read from NBT, make sure it can't cause a crash
// 永远不要轻信从NBT中读取到的信息,要验证一下!!!

// int类型
final int NBT_INT_ID = NBTtypesMBE.INT_NBT_ID;
int readTicks = INVALID_VALUE;
// 如果没有这个标签,则会读取到默认值0
if (parentNBTTagCompound.contains("ticksLeft", NBT_INT_ID)) {
readTicks = parentNBTTagCompound.getInt("ticksLeft");
if (readTicks < 0) readTicks = INVALID_VALUE;
}
ticksLeftTillDisappear = readTicks;

// string类型
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);
}
}

// 被加载后,服务器需要把TileEntity的数据发送给客户端
// 方法一共有两对:
// 更新单个TileEntity: getUpdatePacket()--onDataPacket()
// 更新一整个chunk中的所有tTileEntity:getUpdateTag()--handleUpdateTag()
@Override
@Nullable
public SUpdateTileEntityPacket getUpdatePacket(){
CompoundNBT nbtTagCompound = new CompoundNBT();
write(nbtTagCompound);
// arbitrary number; only used for vanilla TileEntities. You can use it, or not, as you want.
// 任意数字,仅用于原生TileEntity,可以用也可以不用,随你开心
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);
}

// 每个tick被调用一次
@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;

// 在服务端的一些更改需要重新发送给客户端
// 但是在这个例子中不用,因为这个计时本身没有任何作用,只需要把最后方块的结果发送过去即可
// this.markDirty();

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;
// blockSapling.generateTree(world, this.pos, blockSapling.getDefaultState(),random);
}
}

}
2.4.1.3 注册

注册带有 TileEntity 的方块时,需要额外加上一个对 TileEntity 的注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
// 被注册的方块实例
public static BlockTileEntityData blockTileEntityData;
// 被注册的TileEntity的实例
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
{
// 配方类型:有序合成,无需合成,熔炉配方,高炉配方,营火配方,锻造台配方...
// 详细可以参照:https://minecraft-zh.gamepedia.com/配方
// 当然这里所有的节点都可以在上面找到
"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 网络通信

这一部分会构造一个法杖物品,使用后引发一场空袭(随机召唤一些实体)

  1. 客户端使用法杖,构造一个包含位置和召唤物种类的信息,发送给服务器
  2. 服务器处理信息,发送一个包含位置的特效信息给所有同一维度的玩家
  3. 服务器在指定位置生成召唤物(自动同步给玩家)
  4. 服务器在指定位置生成雷声(自动同步给玩家)

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;
}

// 创建一个任务,这个任务将在下一个tick执行
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");
}

// 创造一个任务,下一个tick执行任务
ctx.enqueueWork(() -> processMessage(message, sendingPlayer));
}

// 在目标位置附近生成一些投掷物
static void processMessage(AirstrikeMessageToServer message, ServerPlayerEntity sendingPlayer){
// 1. 给每个玩家都发送一个信息,然后生成一个任务,在下一个tick执行
// 这一步只是为了让玩家生成一些特效而已,后面的实体生成和雷声都会自动传给玩家
// 一些相似的任务:
// 给一个玩家发送信息:
// simpleChannel.send(PacketDistributor.PLAYER.with(playerMP), new MyMessage());
// 给一个区域的玩家发送信息:
// simpleChannel.send(PacketDistributor.TRACKING_CHUNK.with(chunk), new MyMessage());
// 给所有玩家发送信息:
// simpleChannel.send(PacketDistributor.ALL.noArg(), new MyMessage());
// 给一个维度中的所有玩家发送信息:
// simpleChannel.send(PacketDistributor.DIMENSION.with(() -> playerDimension), msg);
TargetEffectMessageToClient msg = new TargetEffectMessageToClient(message.getTargetCoordinates());
DimensionType playerDimension = sendingPlayer.dimension;
StartupCommon.simpleChannel.send(PacketDistributor.DIMENSION.with(() -> playerDimension), msg);

// 2. 在服务器上生成这些投掷物
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; // not 100% sure of what this does...(奇奇怪怪...)
boolean SEARCH_DOWN_WHEN_PLACED_ON_TOP_OF_GIVEN_BLOCK_LOCATION = false; // not 100% sure of what this does...

// 根据以上属性生成实例对象
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);

// special cases handled by switch() - clumsy method for purposes of simplicity only...
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;
}
}

// 3. 搞个雷声
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;
}

// for use by the message handler only.
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);

// 这些注释是MBE项目中源码的注释,但是据考证,1.12之后的版本都没有这些工具了...
// https://forge.yue.moe/javadoc/1.12.2/
// https://forge.yue.moe/javadoc/1.16.4/
// 一些读取其他对象的方法:
// Itemstacks: ByteBufUtils.readItemStack()
// NBT: ByteBufUtils.readTag()
// String: ByteBufUtils.readUTF8String()
// 其中PacketBuffer是ByteBuf的子类

} 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);

// 这些注释是MBE项目中源码的注释,但是据考证,1.12之后的版本都没有这些工具了...
// https://forge.yue.moe/javadoc/1.12.2/
// https://forge.yue.moe/javadoc/1.16.4/
// 一些读取其他对象的方法:
// Itemstacks: ByteBufUtils.writeItemStack()
// NBT: ByteBufUtils.writeTag();
// String: ByteBufUtils.writeUTF8String();
}

@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;
}

// BLOCK is a useful 'do nothing' animation for this item
// 啥也不做哈哈哈哈
@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;

// 用来排错用的信息ID,不要用0
public static final byte AIRSTRIKE_MESSAGE_ID = 35;
public static final byte TARGET_EFFECT_MESSAGE_ID = 63;

// a version number for the protocol you're using. Can be used to maintain backward
// compatibility. But to be honest you'll probably never need it for anything useful...
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);
}

// 注册一个通信通道,可以在一个通道中发送多种信息,通常一个MOD只用一个通道就够了
@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: { // dummy (do nothing) - can never be called, just to prevent unreachable code compiler error
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)"));
}

// what animation to use when the player holds the "use" button
@Override
public UseAction getUseAction(ItemStack stack) {
return UseAction.BLOCK;
}

// how long the player needs to hold down the right button before the test runs again
@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); // just in case.
}
int testNumber = itemStackIn.getCount(); // getStackSize()
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 {

// 记录是否触发测试,以及测试的编号(供DebugTestWatcher使用)
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());
}

/**
* Gets the value of the given debug parameter; or empty if not previously set.
* @param parameterName
* @return
*/
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());
}

/**
* Gets the value of the given debug parameter; or 0 if not previously set
* @param parameterName
* @return
*/
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);
}

/**
* Returns true if the trigger is set. Resets to false.
* @param parameterName
* @return
*/
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;
}

/**
* Checks the specified range of test numbers and returns the currently triggered test number if it lies within the checked range.
* If the currently triggered test lies within the checked range, it is reset
* Beware - if you have multiple callers looking for debug tests in the same range using getDebugTest, only the first caller will ever find one.
*
* eg:
* 1) caller calls setDebugTest(65)
* 2a) getDebugTest(0, 10) returns NO_TEST_TRIGGERED
* 2b) getDebugTest(60, 65) returns NO_TEST_TRIGGERED
* 2c) getDebugTest(60, 66) returns 65
* 2d) calling getDebugTest(60, 66) for a second time now returns NO_TEST_TRIGGERED
*
* @param testNumberMin lowest test number to check
* @param testNumberMaxPlusOne highest test number to check plus one
* @return the test number to execute, or NO_TEST_TRIGGERED if none triggered
*/
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);
}


// 向量转NBT
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;
}

// NBT转向量
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 {
/**
* Sets a block state into this world.Flags are as follows:
* 1 will cause a block update.
* 2 will send the change to clients.
* 4 will prevent the block from being re-rendered.
* 8 will force any re-renders to run on the main thread instead
* 16 will prevent neighbor reactions (e.g. fences connecting, observers pulsing).
* 32 will prevent neighbor reactions from spawning drops.
* 64 will signify the block is being moved.
* Flags can be OR-ed
*/

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);
}
}
}