创建游戏内容
恭喜你闯过了第一章————这个95%的萌新都会被拦腰斩断的地方,闯过了环境的配置,那么接下来就是真正模组的内容了。
不过,本教程还不打算让你现在就开始翱翔在Java自由的天空中,俗话说,天高任鸟摔。在能真正利用Java这股原始的力量之前,你需要先搞懂模组到底是以什么样的结构展开的,并且了解一些Mindustry固有的概念。
本节我们将提及一些模组开发中需要的 Java/Kotlin/JavaScript/Hjson 语法,及原版在语法方面的一些规定,并不是编程语言的教程或讲解,如果发现有漏洞的知识点请在他处自行学习。如果你对Java真的零基础,可以先看看下面,了解一下不需要学什么。
推荐 Java 教程:黑马程序员零基础(全看完)、韩顺平30天速成(看到547课即可)、廖雪峰官网、菜鸟教程(不推荐用这个学,适合当语法速查)
推荐 Kotlin 教程:官方文档(新语言的文档往往比较优质)、Kotlin教程视频
推荐 JavaScript 教程:FreecodeComp
不需要什么
Mindustry不需要java.[n]io / java.net / java.util以外任何Java基础库,因为安卓可能没有它们,这就包括Swing和Awt。也不需要会任何其他Java框架或中间件。不需要非JVM语言,它们不能用来写模组,包括C++和Python。不需要数据库,除非你要做一个新的服务器基础层。
Hjson语法
如果非要说的话,只有一句话:括号一定要是匹配的!!!
毕竟Mindustry原生的JSON解析过于宽松,引号和逗号笔者建议最好全都不要写,如果写了也一定要匹配,此外即使是Mjson也不支持尾随逗号。这样的话,一切语法问题就可以归结到括号不匹配上了。但如果读者使用的是MT管理器,或其他任何没有插件功能的文本编辑器,则推荐使用正规JSON语法,至少还有格式化器可用。
Java 语言基础知识清单
以下标红处为Mindustry开发与传统Java开发的差异点,我们推荐你使用Mindustry的范式。
运行机制
Java 源代码(.java)通过 javac 编译器编译成字节码文件(.class),然后在 JVM 上执行。Android 平台会将字节码进一步编译成 dex 格式由 ART 执行。
基础语法
标识符与关键字:标识符由字母、数字、_、$组成,不能以数字开头。关键字如 public、class、static 等有特殊含义。
数据类型:
- 整型:byte(1)、short(2)、int(4)、long(8)
- 浮点型:float(4)、double(8),存在精度误差
- 字符型:char(2)
- 布尔型:boolean
- 及各类型表示的最大值和最小值
//这是单行注释,在编译的时候会被编译器忽略,不会保留到最终的class字节码中(字节码会在稍后说明)
int abc; //合法,声明了一个 类型为 int (整数、定点数) 的变量
float 233; //不合法,因为是以数字开头
float _f; //合法。声明了一个 类型为 float (小数、单精度浮点数) 的变量
double DDDd; //合法。声明了一个 类型为 double (小数、双精度浮点数) 的变量
object $obj; //合法。声明了一个 类型为 object引用 的变量变量与常量:变量存储数据,常量用 final 修饰,值不可变。使用前需要初始化(Initialization)
int a = 10; //将一个 int 变量 初始化为 10
float f = 3.14f; //将一个 float 变量 初始化为 3.14
object plum = new Object( ); //将一个 object引用 变量 初始化为 new Object( )——一个对象
String str = "Hello, plum!"; //将一个 String 变量 初始化为 字符串"Hello, plum!"运算符:包括算术、关系、逻辑、位、赋值、条件运算符等,注意逻辑运算符的短路特性。
//本代码段完全符合 Java 语法,可以直接在您的IDEA里运行 (运行方式会在稍后的文章中说明)
int r = 1 + 1; // r 的值为 2,此处声明了一个变量,类型为 int (整数、定点数)
r = 1 - 1; // r 的值为 0
r = 3 * 2; // r 的值为 6
r = 36 / 3; // r 的值为 12
r = 11 / 5; // r 的值为 2,int 的除法自动舍弃余数
r = 9 % 2; // r 的值为 1,这是取余,返回 9 / 2 的余数
bool b = false; //布尔类型,只有两种值,true 和 false
b = true && false; // b 的值为 false
b = true || false; // b 的值为 true
b = ! false; // b 的值为 true
/*这是多行注释
对于布尔运算符:
&& (和)—— 左右两个操作数均为 true 时,才返回 true,否则返回 false
|| (或)——左右两个操作数至少有一个为true时,就返回 true。都没有为 true 则返回 false
! (否)——取反,使 true 变为 false,false 变为 true。
前一项值已经可以决定结果时,后一项根本就不会被计算
*/流程控制:
- 分支结构:if-else、switch-case
- 循环结构:for(含增强for)、while、do-while
- 控制关键字:break、continue、return
数组:一维和多维数组,通过索引访问,length 属性获取长度。
函数或方法
实例(Instance)的函数(Function)叫方法(Method)。
// 定义一个函数,有一个int 参数,返回值为 int 类型
int multiply2 ( int x ) { //可以看作为 f (x) = x * 2
return x * 2 ; //使用 return 关键字,返回函数的结果
}
// 函数调用
int res = multiply2 ( 5 ) ; //此时,res的值为 10
// 定义一个无返回值,无参数的函数
void bark ( ) {
System.out.println("汪汪!"); //此处调用了System类的out字段的println方法
}
// 调用无返回值的函数
bark( ) //命令行或调试窗口显示:汪汪!面向对象编程核心
三大特征:
- 封装:
- 继承:extends实现代码复用,super调用父类成员
- 多态:同一接口不同实现,包括重载
@Override(编译时)和重写(运行时)
示例代码:
// 封装示例
class Person {
public String name;
public String getName() { return name; }
}
// 继承示例
class Student extends Person {
@Override
public String getName() { return super.getName(); }
}
// 多态示例
Animal a = new Dog();
a.makeSound(); // 调用Dog的makeSound其他概念:
- 类与对象:new创建对象实例
- 构造方法:初始化对象,支持重载
- 字段:实例变量
- static:类成员,属于类而非实例
- final:常量、不可重写方法、不可继承类
- abstract:抽象类和抽象方法
- interface:定义规范,implements实现
常用基础类
Object类:所有类的超类,toString()、equals()、hashCode()等方法String类:不可变字符序列,使用字符串常量池- 原生容器类被Arc容器
arc.struct取代,原生函数式接口被Arc函数式接口arc.func取代
Seq<Tile> tiles = new Seq<>();
tiles.add(Vars.world.tile(1,1));
ObjectMap<Item, Block> map = new ObjectMap<>();
map.put(Items.copper, Blocks.copperWall);
Cons<Tile> tileConsumer = t -> {Log.info(t.toString());};
tiles.each(tileConsumer);命名规范
- 类名:大驼峰,如 BlockStateManager
- 方法名/变量名:小驼峰,如 getName
- 常量名:
- 包名:
Kotlin 语言基础知识清单
一、 基础语法:与 Java 的直观差异
- 变量声明
val: 声明只读变量(类似 Javafinal),优先使用。var: 声明可变变量。- 类型推断: 编译器可自动推断类型,类型标注可省略。kotlin
val name = "Kotlin" // String var count = 42 // Int val explicit: Double = 3.14 // 显式标注类型
- 函数定义
- 使用
fun关键字。 - 返回值写在参数列表后,用
: Type表示。 - 表达式函数体:单表达式函数可省略
{}和return。kotlin// 传统写法 fun sum(a: Int, b: Int): Int { return a + b } // 表达式函数体 fun sum(a: Int, b: Int): Int = a + b // 甚至可进一步省略返回值类型(类型推断) fun sum(a: Int, b: Int) = a + b
- 使用
空安全 (Null Safety) - 最重要的特性
- 可空类型
- 默认类型不可为
null。要允许为null,必须在类型后加?。kotlinvar neverNull: String = "Hello" // 永远不为null var canBeNull: String? = null // 可以为null
- 默认类型不可为
- 安全调用操作符 (
?.)- 如果对象不为
null,则调用方法或访问属性;否则返回null。kotlinval length: Int? = canBeNull?.length // 如果canBeNull为null,则length也是null
- 如果对象不为
- Elvis 操作符 (
?:)- 提供
null时的备用值。kotlinval length: Int = canBeNull?.length ?: 0 // 如果为null,则返回0
- 提供
- 非空断言操作符 (
!!)- 断言对象不为
null,如果为null则抛出NPE。应谨慎使用。kotlinval length: Int = canBeNull!!.length // 我保证它不是null!
- 断言对象不为
类型系统与检查
- 智能转换 (Smart Cast)
- 一旦进行了类型检查 (
is),编译器会自动转换类型,无需显式cast。kotlinfun demo(x: Any) { if (x is String) { print(x.length) // x 被自动智能转换为 String 类型 } }
- 一旦进行了类型检查 (
Any: 所有类的超类(类似 JavaObject)。Unit: 相当于 Java 的void,表示函数不返回任何有意义的值。Unit可省略。Nothing: 这个函数永不返回(例如,总是抛出异常)。
类与对象
- 主构造函数
- 简洁的声明方式,直接在类头声明。kotlin
class Person(val name: String, var age: Int) { // `val`/`var` 关键字使其同时成为类属性 // 如果没有注解或可见性修饰符,`constructor` 关键字可省略 }
- 简洁的声明方式,直接在类头声明。
init代码块- 主构造函数不能包含代码,初始化代码放在
init块中。
- 主构造函数不能包含代码,初始化代码放在
- 数据类 (
data class)- 用于只保存数据的类。编译器自动生成
equals(),hashCode(),toString(),copy()和componentN()函数。kotlindata class User(val name: String, val age: Int)
- 用于只保存数据的类。编译器自动生成
- 属性 (Property)
- Kotlin 的属性是 first-class 特性,替代了字段 + Getter/Setter 的模式。
- 完整的属性声明包括一个 Getter 和一个可选的 Setter。kotlin
class Rectangle { var width: Int = 0 var height: Int = 0 val area: Int // 只有getter的属性 get() = this.width * this.height } // 使用:直接通过 . 访问,背后是调用getter/setter val rect = Rectangle() rect.width = 10 // 实际上是调用了 setter println(rect.area) // 实际上是调用了 getter
函数式编程特性 (语言层面支持)
- Lambda 表达式
- 语法:
{参数 -> 函数体}。 - 如果 Lambda 是函数的最后一个参数,它可以移到括号外面。如果它是唯一的参数,括号可以省略。kotlin
// 假设有一个函数:fun runLater(block: () -> Unit) {} runLater({ println("Hi") }) // 传统写法 runLater() { println("Hi") } // Lambda 外置 runLater { println("Hi") } // 括号省略(最常用)
- 语法:
- 高阶函数
- 将函数用作参数或返回值的函数。kotlin
fun calculate(x: Int, y: Int, operation: (Int, Int) -> Int): Int { return operation(x, y) } val result = calculate(10, 5) { a, b -> a + b } // 传入一个Lambda作为operation参数
- 将函数用作参数或返回值的函数。
伴生对象 (Companion Object) - 替代静态成员
- Kotlin 中没有
static关键字。 - 使用伴生对象来声明类级别的变量和函数,实现类似静态成员的功能。kotlin
class MyClass { companion object { const val CONSTANT = "constant" // 编译期常量 fun create(): MyClass = MyClass() // 工厂方法 } } // 调用 val constant = MyClass.CONSTANT val instance = MyClass.create() - 使用
@JvmStatic等注解
委托
Kotlin 的委托(Delegation) 是一个非常重要且强大的特性。它提供了一种清晰、简洁的方式来实现“组合优于继承”的原则。
Kotlin 的委托主要分为两类:类委托和属性委托。
类委托 (Class Delegation)
核心思想: 将一个类的具体实现委托给另一个对象。
- Java 中的问题: 在 Java 中,你需要手动实现所有接口方法,并将调用转发给内部持有的对象,会产生大量样板代码。
- Kotlin 的解决方案: 使用
by关键字,编译器会自动为你生成所有转发方法。
语法:
interface Base {
fun print()
fun printMessage(msg: String)
}
class BaseImpl(val x: Int) : Base {
override fun print() { println("Value: $x") }
override fun printMessage(msg: String) { println("$msg: $x") }
}
// 类 `Derived` 将其接口 `Base` 的所有公有成员实现委托给 `b` 这个对象。
// 语法: `class Derived : Base by b`
class Derived(b: Base) : Base by b {
// 1. 可以重写委托的方法
override fun print() {
println("Derived's print")
// 2. 如果需要,仍然可以通过 `super.print()` 调用委托对象的实现
super.print()
}
// 3. 没有重写的方法(如 `printMessage`)会自动转发给 `b`
}
fun main() {
val baseImpl = BaseImpl(10)
val derived = Derived(baseImpl)
derived.print()
// 输出:
// Derived's print
// Value: 10
derived.printMessage("Hello")
// 自动转发给 baseImpl.printMessage("Hello")
// 输出: Hello: 10
}属性委托 (Property Delegation)
核心思想: 将一个属性(property)的 getter 和 setter 逻辑委托给另一个对象(称为委托对象)。
语法:val/var <property name>: <Type> by <delegate expression>
委托对象必须遵循属性委托约定,即提供 operator 修饰的 getValue() 和 setValue() 函数(后者用于 var 属性)。
Kotlin 标准库提供了几个极其有用的内置属性委托工厂函数(虽然你要求不包括 stdlib,但这些是理解委托不可或缺的核心例子):
lazy: 惰性初始化
- 用途: 延迟一个昂贵开销的初始化,直到第一次访问该属性。
- 要求: 必须是
val(只读),因为初始值只能在第一次访问时确定。 - 线程安全: 默认情况下
lazy()是同步的(线程安全的)。
val expensiveResource: Resource by lazy {
// 这个 lambda 只在第一次访问 expensiveResource 时执行
println("Initializing expensive resource...")
Resource() // 最后一行表达式的结果就是属性的值
}
fun main() {
println("Before access")
val resource = expensiveResource // 此时会执行 lambda 进行初始化
println("After access")
// 再次访问不会再初始化
val resource2 = expensiveResource
}
// 输出:
// Before access
// Initializing expensive resource...
// After accessb. observable 与 vetoable: 观察属性变化
observable: 在属性值被改变后触发回调。vetoable: 在属性值被改变前触发回调,允许你“否决”此次修改。
import kotlin.properties.Delegates // 这是标准库的一部分,仅作示例
var name: String by Delegates.observable("<no name>") { prop, old, new ->
println("$old -> $new") // 变化后打印
}
var positiveNumber: Int by Delegates.vetoable(0) { prop, old, new ->
// 只有当新值是正数时,修改才生效
new > 0
}
fun main() {
name = "first"
name = "second"
// 输出:
// <no name> -> first
// first -> second
positiveNumber = 1
println(positiveNumber) // 输出: 1
positiveNumber = -1 // 赋值被否决!
println(positiveNumber) // 输出: 1 (值未被改变)
}其他实用语法糖
- 字符串模板
- 在字符串中直接嵌入变量 (
$变量名) 或表达式 (${表达式})。kotlinval name = "Alice" println("Hello, $name!") // Hello, Alice! println("1 + 2 = ${1 + 2}") // 1 + 2 = 3
- 在字符串中直接嵌入变量 (
- 默认参数与命名参数
- 默认参数:函数参数可以指定默认值,避免重载。
- 命名参数:调用时使用参数名指定值,提高可读性且可任意顺序。kotlin
fun greet(name: String, msg: String = "Hi") { println("$msg $name") } greet("Bob") // 使用默认msg: Hi Bob greet(msg = "Hello", name = "Alice") // 命名参数:Hello Alice
when表达式- 强大的
switch替代品,可以匹配值、范围、类型等。 - 它可以返回值。kotlin
fun describe(obj: Any): String = when (obj) { 1 -> "One" "Hello" -> "Greeting" is Long -> "Long type" !is String -> "Not a string" else -> "Unknown" }
- 强大的
- 范围表达式 (
..和until)- 用于创建区间,常与
in操作符在循环和when中使用。kotlinfor (i in 1..5) { print(i) } // 12345 (闭区间[]) for (i in 1 until 5) { print(i) } // 1234 (半开区间[)) if (x in 1..10) { ... } // 判断x是否在1到10之间
- 用于创建区间,常与