2.6
This commit is contained in:
109
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/3-进阶内容/1-箱子GUI的实现.md
Normal file
109
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/3-进阶内容/1-箱子GUI的实现.md
Normal file
@@ -0,0 +1,109 @@
|
||||
---
|
||||
front:
|
||||
hard: 进阶
|
||||
time: 20分钟
|
||||
---
|
||||
|
||||
# Bukkit类与箱子GUI的实现
|
||||
|
||||
服务器里经常会利用箱子的GUI做“按钮菜单”功能. 有些服务器可能利用`ChestCommand`插件做出了各种花样的菜单.
|
||||
如何写一个插件来实现这样的箱子GUI呢?
|
||||
|
||||
# Bukkit类
|
||||
|
||||
我们早在事件监听注册监听器时就已经见过`Bukkit`类了.
|
||||
|
||||
```java
|
||||
Bukkit.getPluginManager().registerEvents(this,this);
|
||||
```
|
||||
|
||||
`Bukkit`类是服务器的单例. 我们可以通过它操作服务器.
|
||||
例如, 你可以用`Bukkit.banIP("某个IP")`来封禁某个IP号. 更多的用法可以在JavaDoc上查到.
|
||||
|
||||
你也可以利用`Server`对象操作服务器, 二者几乎没有差别(`Bukkit`类内部就是操作`Server`对象).
|
||||
插件主类提供`getServer()`方法, 返回值就是一个`Server`对象.
|
||||
|
||||
# Inventory的使用
|
||||
|
||||
箱子GUI本质是一个Inventory界面. 首先我们需要创建一个Inventory对象出来.
|
||||
但我们不必直接`new Inventory(...)`, 这是因为查阅JavaDocs可以得知,Inventory是一个接口,无法直接实例化。
|
||||
但是`Bukkit`类给我们提供了创建`Inventory`对象的方法:
|
||||
|
||||
```java
|
||||
Inventory inv = Bukkit.createInventory(player, 6*9, "URARA!");
|
||||
//第一项是主人, 在这里可以设打开界面的玩家Player对象(还记得Inventory和箱子或玩家背包等一一对应吗)
|
||||
//第二项必须是 9n (n是1≤n≤6的正整数)
|
||||
//第三项是标题
|
||||
ItemStack item_bk = new ItemStack(Material.DIAMOND);
|
||||
|
||||
//在四周设置钻石边框
|
||||
//这里用这样脑残的写法是为了告诉你一个大概的意思
|
||||
//我相信你实际写的时候不会这么简单粗暴解决问题的, 应该会用上循环解决, 对吧
|
||||
inv.setItem(0,item_bk);
|
||||
inv.setItem(1,item_bk);
|
||||
inv.setItem(2,item_bk);
|
||||
inv.setItem(3,item_bk);
|
||||
inv.setItem(4,item_bk);
|
||||
inv.setItem(5,item_bk);
|
||||
inv.setItem(6,item_bk);
|
||||
inv.setItem(7,item_bk);
|
||||
inv.setItem(8,item_bk);
|
||||
inv.setItem(9,item_bk);
|
||||
inv.setItem(17,item_bk);
|
||||
inv.setItem(18,item_bk);
|
||||
inv.setItem(26,item_bk);
|
||||
inv.setItem(27,item_bk);
|
||||
inv.setItem(35,item_bk);
|
||||
inv.setItem(36,item_bk);
|
||||
inv.setItem(44,item_bk);
|
||||
inv.setItem(45,item_bk);
|
||||
inv.setItem(46,item_bk);
|
||||
inv.setItem(47,item_bk);
|
||||
inv.setItem(48,item_bk);
|
||||
inv.setItem(49,item_bk);
|
||||
inv.setItem(50,item_bk);
|
||||
inv.setItem(51,item_bk);
|
||||
inv.setItem(52,item_bk);
|
||||
inv.setItem(53,item_bk);
|
||||
|
||||
ItemStack item_button1 = new ItemStack(Material.GOLD);
|
||||
ItemStack item_button2 = new ItemStack(Material.ANVIL);
|
||||
inv.setItem(22,item_button1);
|
||||
inv.setItem(31,item_button2);
|
||||
|
||||
//然后可以给玩家打开这个Inventory(注意, 我们还没做限制, 这个时候玩家可以自由的在这个GUI里拿东西出来)
|
||||
p.openInventory(inv);
|
||||
```
|
||||
|
||||
然后我们监听`InventoryClickEvent`实现功能和限制:
|
||||
```java
|
||||
@EventHandler
|
||||
public void onInventoryClick(InventoryClickEvent e){
|
||||
//从这里可以看出来, 标题不是随意设置的, 我们经常用标题作为区分GUI的标志
|
||||
if(e.getWhoClicked().getOpenInventory().getTitle().equals("URARA!")){
|
||||
e.setCancelled(true); //这样玩家就没办法拿出来物品了
|
||||
|
||||
//getRawSlot获得玩家点击的格子编号
|
||||
//但是玩家点击GUI之外不是格子的地方也会触发InventoryClickEvent, 需要做处理!
|
||||
if(e.getRawSlot()<0 || e.getRawSlot()>e.getInventory().getSize() || e.getInventory()==null)
|
||||
return;
|
||||
|
||||
//自从Mojang把HIM删掉以后, 能触发InventoryClickEvent的只有Player了
|
||||
//目前来说可以直接把它强转成Player
|
||||
Player p = (Player)e.getWhoClicked();
|
||||
|
||||
if(e.getRawSlot()==22){
|
||||
p.sendMessage("你点击了金锭!");
|
||||
p.closeInventory();
|
||||
} else {
|
||||
p.sendMessage("你没有点击金锭!");
|
||||
p.closeInventory();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
基于这个思路, 你可以做出一个有功能的箱子GUI了!
|
||||
|
||||
> 思考: 如果遇到了某些能够修改箱子GUI的标题的插件(比如帮助加前缀)
|
||||
> 能不能利用 InventoryHolder 来区分GUI呢?
|
||||
|
||||
145
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/3-进阶内容/2-自定义事件.md
Normal file
145
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/3-进阶内容/2-自定义事件.md
Normal file
@@ -0,0 +1,145 @@
|
||||
---
|
||||
front:
|
||||
hard: 进阶
|
||||
time: 30分钟
|
||||
---
|
||||
|
||||
# 自定义事件
|
||||
|
||||
我们现在所了解的事件都是Bukkit提供的. 例如, 玩家移动等.
|
||||
那如果我们想自己去做一个事件呢?
|
||||
|
||||
## 按需创建类
|
||||
|
||||
比如, 我想自己做出来一个`RuaEvent`, 实现在玩家聊天说`rua`的时候触发.
|
||||
很明显, Bukkit只会提供玩家发送聊天信息的事件, 肯定不会单独为了实现在玩家聊天发送`rua`的时候单独做个事件. 那应该怎么做?
|
||||
|
||||
首先想到的应该是监听玩家聊天事件, 然后判断玩家聊天发送的内容是什么, 如果是`rua`做我想做的事情. 这是常规的解决方法.
|
||||
但是如果我想做一个强化插件, 我想在玩家强化物品的时候触发一个事件给自己和其他插件, 那我应该怎么做? 不如自定义一个属于自己的事件!
|
||||
|
||||
这里我们以创建上文的`RuaEvent`事件举例, 我们的大致思路是这样的:
|
||||
1. 创建一个`RuaEvent`类.
|
||||
2. 监听玩家聊天, 判断玩家聊天内容, 如果是`rua`, 让Bukkit触发我们新建的`RuaEvent`对象.
|
||||
3. 向玩家发送消息`rua`.
|
||||
|
||||
我们就先新建一个类`RuaEvent`, 让其继承`org.bukkit.event.Event`类. 在该类中写下这些固定代码:
|
||||
```java
|
||||
public class RuaEvent extends Event{
|
||||
private static final HandlerList handlers = new HandlerList();
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return handlers;
|
||||
}
|
||||
}
|
||||
```
|
||||
HandlerList储存与监听本事件的监听器相关的对象.
|
||||
这意味着Bukkit中注册监听器的本质就是在每个对应的事件HandlerList中加入该监听器的有关对象.
|
||||
这也意味着Bukkit中事件的触发本质是遍历被触发事件的HandlerList, 调用监听器对应方法.
|
||||
|
||||
> 假如我想让服务器里的玩家触发的所有事件, 已知所有的诸如PlayerJoinEvent等玩家事件都继承了PlayerEvent, 那我可以监听PlayerEvent事件吗?
|
||||
> 答案是不可以, 因为`PlayerEvent`没有`getHandlerList()`方法, 结合上面的内容, 你应该可以意识到PlayerEvent是无法正常工作的吧.
|
||||
> 所以你只能把所有Player开头的Event监听一个遍才可以达到目的!
|
||||
|
||||
现在我们的自定义事件雏形已经完成. 你可以根据自己的需要添加相关代码!
|
||||
这里我们示例的`RuaEvent`代码最终如下:
|
||||
|
||||
```java
|
||||
public class RuaEvent extends Event {
|
||||
private static final HandlerList handlers = new HandlerList();
|
||||
private Player p;
|
||||
|
||||
public RuaEvent(Player p){
|
||||
this.p = p;
|
||||
}
|
||||
|
||||
public Player getPlayer(){
|
||||
return p;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return handlers;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 可取消事件的实现
|
||||
|
||||
等一等, 这样做出来的事件没有`setCancelled`方法和`isCancelled`方法, 这是不可取消的事件.
|
||||
如果想做成可取消事件, 需要实现`Cancellable`接口:
|
||||
```java
|
||||
public class RuaEvent extends Event implements Cancellable{
|
||||
private static final HandlerList handlers = new HandlerList();
|
||||
private Player p;
|
||||
|
||||
private boolean cancelledFlag = false;
|
||||
|
||||
public RuaEvent(Player p){
|
||||
this.p = p;
|
||||
}
|
||||
|
||||
public Player getPlayer(){
|
||||
return p;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HandlerList getHandlers() {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
public static HandlerList getHandlerList() {
|
||||
return handlers;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCancelled() {
|
||||
return cancelledFlag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCancelled(boolean cancelledFlag) {
|
||||
this.cancelledFlag = cancelledFlag;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
如果是不可取消的事件, 无需实现`Cancelled`.
|
||||
截止到现在, `RuaEvent`已经自定义成功, 现在我们只需要做第二步即可:
|
||||
|
||||
1. 如果RuaEvent是个不可取消事件
|
||||
|
||||
```java
|
||||
@EventHandler
|
||||
public void onPlayerChat_DEMO1 (PlayerChatEvent e){ //如果RuaEvent是个不可取消事件
|
||||
if(e.getMessage().equals("rua")) Bukkit.getServer().getPluginManager().callEvent(new RuaEvent(e.getPlayer())); //触发事件
|
||||
e.sendMessage("Rua!");
|
||||
}
|
||||
```
|
||||
|
||||
2. 如果RuaEvent是个可取消事件
|
||||
|
||||
```java
|
||||
@EventHandler
|
||||
public void onPlayerChat_DEMO1 (PlayerChatEvent e){ //如果RuaEvent是个可取消事件
|
||||
if(e.getMessage().equals("rua")){
|
||||
RuaEvent event = new RuaEvent(e.getPlayer());
|
||||
Bukkit.getServer().getPluginManager().callEvent(event);
|
||||
if(event.isCancelled()) {
|
||||
return; //事件被取消, 终止事件的处理
|
||||
}
|
||||
// 事件未取消对应的逻辑
|
||||
e.sendMessage("Rua!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
*在这里监听了**PlayerChatEvent**,但是此事件已被标记@Deprecated,实际的开发过程中不推荐监听此事件.*
|
||||
*实际开发中建议监听的是**AsyncPlayerChatEvent**事件. 注意这是异步监听,用法基本类同于上述事件的监听,具体请参见JavaDoc.*
|
||||
98
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/3-进阶内容/3-深入plugin配置.md
Normal file
98
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/3-进阶内容/3-深入plugin配置.md
Normal file
@@ -0,0 +1,98 @@
|
||||
---
|
||||
front:
|
||||
hard: 进阶
|
||||
time: 15分钟
|
||||
---
|
||||
|
||||
# 深入plugin.yml
|
||||
|
||||
`plugin.yml`文件是Bukkit及其衍生服务端识别插件的重要文件.
|
||||
|
||||
在服务端加载插件时, 服务端加载完毕Jar文件后做的第一件事就是读取该Jar文件的`plugin.yml`文件.
|
||||
如果把任一可正常工作的插件的Jar文件用相应的ZIP压缩软件打开, 删除`plugin.yml`文件后再启动服务端, 会抛出错误.
|
||||
```
|
||||
Could not load 'plugins\[YOUR_PLUGIN].jar' in folder 'plugins'
|
||||
org.bukkit.plugin.InvalidDescriptionException: Invalid plugin.yml
|
||||
```
|
||||
可发现, 服务端将会因为没有`plugin.yml`文件而抛出`InvalidDescriptionException`错误.
|
||||
|
||||
<br>
|
||||
|
||||
在`plugin.yml`文件中, 目前我们已知的有`name`、`version`、`main`、`author`四个项目可以设置.
|
||||
事实上, `plugin.yml`文件中还有许多可以设置的项目, 部分项目是本节的内容, 其余可以在SpigotMC的官方文档中查阅到.
|
||||
|
||||
> 目前BukkitAPI主要由SpigotMC维护, 因此大量的BukkitAPI文档都在 SpigotMC 网站上.
|
||||
> 有关plugin.yml文件的官方文档在这里:
|
||||
> https://www.spigotmc.org/wiki/plugin-yml/
|
||||
|
||||
## 必要设置项
|
||||
`plugin.yml`文件中, `name`、`main`、`version`三项必须存在.
|
||||
*这也意味着, 前面的实例中, 我们使用的`plugin.yml`文件, 删去`author`键仍可被服务端正常加载.*
|
||||
|
||||
不妨来认识一下这三个设置项.
|
||||
|
||||
### name
|
||||
顾名思义, 它定义了插件的名称.
|
||||
|
||||
对于名称, 官方WIKI中给出了严格的要求, 即只能由 **英文小写或大写字符、阿拉伯数字或下划线** 构成. 决不能出现中文字符、空格等.
|
||||
在后续生成插件配置文件夹时, 该项设置的插件名将会是插件配置文件夹的名称.
|
||||
|
||||
起名的时候应该注意, 尽可能起一个“个性”的名称, 防止与其他插件重名.
|
||||
|
||||
### version
|
||||
指插件的版本号.
|
||||
该键理论上可以在后面填写任意String内容. 但是官方WIKI要求尽可能使用X.X.X格式的版本号表示(例如: 2.3.3).
|
||||
关于版本号规则,可以参考[语义化版本](https://semver.org/lang/zh-CN/)
|
||||
|
||||
### main
|
||||
指插件的主类名.
|
||||
|
||||
在插件中, 主类有且只有一个, 且需要继承`JavaPlugin`类. 主类是插件的“入口”, 这里的`main`即意在说明主类的名称.
|
||||
这里需填写主类的全名, 也就是精确到主类所在的具体包. 说白了就是不只是需要把主类名带上, 还要把包名带上.
|
||||
|
||||
## 可选设置项
|
||||
`plugin.yml`文件只需要存在必要设置项的三个键即可.
|
||||
下面的键可选, 可有可无. 但有一些在一些特定的情况下必须要有.
|
||||
|
||||
### 依赖
|
||||
有时候你的插件可能需要调用`Vault`(用来获取玩家货币余额)或其他的插件, 即依赖其他插件.
|
||||
这时候需要在`plugin.yml`文件中进行设置告知服务端, 从而保证所依赖的插件在本插件之前被加载.
|
||||
|
||||
你可以在`plugin.yml`文件中加入`depend`键或`softdepend`键来控制依赖.
|
||||
|
||||
`depend`键或`softdepend`键接的值必须是数组. 例如这样:
|
||||
```yml
|
||||
depend: [Vault, WorldEdit]
|
||||
softdepend: [Essentials]
|
||||
```
|
||||
两个键设置的内容区别如下:
|
||||
1. depend: 插件强制要求的依赖. 如果没有这个插件, 该插件将无法正常工作, Bukkit此时会抛出相应错误.
|
||||
2. softdepend: 插件不强制要求的插件. 如果服务端内没有这个插件, 插件仍可正常工作.
|
||||
|
||||
后面设置的数组内的内容都是所依赖插件的名称, 此处名称应与所依赖的插件的`plugin.yml`文件的`name`键的值相同.
|
||||
|
||||
### loadbefore
|
||||
`depend`与`softdepend`可以实现插件在某个插件之后加载. 但也许有时你的插件可能需要实现在某个插件之前被加载.
|
||||
此时你可以使用`loadbefore`设置, 用法类似. 例如:
|
||||
```yml
|
||||
loadbefore: [Essentials, WorldEdit]
|
||||
```
|
||||
|
||||
在上面的例子中, 可保证插件在WorldEdit与Essentials插件之前被加载.
|
||||
|
||||
### commands
|
||||
如果你的插件定义了新指令, 你第一步就需要设置该项告知服务端.
|
||||
此处仅做示范:
|
||||
```yml
|
||||
commands:
|
||||
test:
|
||||
description: "Hello World!"
|
||||
```
|
||||
这可以告知服务端注册了指令`test`, 并且描述为`Hello World!`字符串, 该描述字符串将会在`/help`指令中被显示.
|
||||
|
||||
### author与authors
|
||||
此处不再赘述其作用. 如果你想表示多名作者, 你可以设置`authors`项, 值需为一个数组.
|
||||
```yml
|
||||
authors: [tdiant, Seraph_JACK]
|
||||
```
|
||||
如果同时存在`author`与`authors`, 将忽略`author`.
|
||||
212
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/3-进阶内容/4-配置API的序列化以及遍历.md
Normal file
212
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/3-进阶内容/4-配置API的序列化以及遍历.md
Normal file
@@ -0,0 +1,212 @@
|
||||
---
|
||||
front:
|
||||
hard: 进阶
|
||||
time: 30分钟
|
||||
---
|
||||
|
||||
# 配置API的序列化和遍历
|
||||
|
||||
# 序列化
|
||||
## 了解序列化
|
||||
如果我自己做了一个类型, 例如下面的`Person`类:
|
||||
|
||||
```java
|
||||
public class Person {
|
||||
public String name;
|
||||
public String introduction;
|
||||
|
||||
public Person(String name, String introduction) {
|
||||
this.name = name;
|
||||
this.introduction = introduction;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
现在我们新建一个`Person`对象:
|
||||
|
||||
```java
|
||||
Person person = new Person("tdiant", "hello!!");
|
||||
```
|
||||
|
||||
我们想把`Person`保存在配置文件里怎么办?
|
||||
很遗憾,直接`getConfig().set("demo",person);`是行不通的. 你会发现`getConfig.get("demo")`根本得不到这个对象.
|
||||
|
||||
> 哪些东西可以直接set保存呢?
|
||||
> 类似getInt, 所有拥有get方法的类型都可以直接保存. (包括`List<String>`)
|
||||
>
|
||||
> 还有一些BukkitAPI给的类型, 例如ItemStack. 但不是全部都是这样.
|
||||
> 如果你想判断一个类型是不是可以直接set, 你可以在JavaDoc中找到它, 看它是否实现了`ConfigurationSerializable`类.
|
||||
|
||||
|
||||
你可能想到了最简单粗暴的办法:
|
||||
```java
|
||||
//这样set
|
||||
getConfig().set("demo.name",test.name);
|
||||
getConfig().set("demo.introduction",test.introduction);
|
||||
//然后保存, 用的时候这样
|
||||
getConfig().getString("demo.name");
|
||||
getConfig().getString("demo.introduction");
|
||||
```
|
||||
|
||||
这的确是一种切实可行的办法. 但是这真的是太麻烦了. 有没有一种方法直接set或get这个对象的办法呢? 有! 你可以使用序列化和反序列化实现它!
|
||||
|
||||
## 让自定义类型实现序列化与反序列化
|
||||
以上文`Person`为例. 首先让他实现`ConfigurationSerializable`, 并添加`deserialize`方法. 如下:
|
||||
```java
|
||||
public class Person implements ConfigurationSerializable {
|
||||
public String name;
|
||||
public String introduction;
|
||||
|
||||
public Person(String name, String introduction) {
|
||||
this.name = name;
|
||||
this.introduction = introduction;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> serialize() {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
return map;
|
||||
}
|
||||
|
||||
public static Person deserialize(Map<String, Object> map) {
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
然后继续完善`serialize`, 实现序列化. 我们只需要把需要保存的数据写入map当中即可.
|
||||
注意, 需要保存的数据要保证可以直接set, 不能则也需要为他实现序列化与反序列化.
|
||||
```java
|
||||
@Override
|
||||
public Map<String,Object> serialize() {
|
||||
Map<String,Object> map = new HashMap<>();
|
||||
map.put("name",name);
|
||||
map.put("introduction",introduction);
|
||||
return map;
|
||||
}
|
||||
```
|
||||
|
||||
序列化后, 数据即可直接set进配置文件里. 为了实现直接get的目的, 还需要进行反序列化.
|
||||
```java
|
||||
public static Person deserialize(Map<String, Object> map) {
|
||||
return new Person(
|
||||
(map.get("name") != null ? (String) map.get("name") : ""),
|
||||
(map.get("introduction") != null ? (String) map.get("introduction") : "")
|
||||
);
|
||||
}
|
||||
```
|
||||
编写完毕后, 我们需要像注册监听器一样, 注册序列化. 在插件主类的`onEnable`中加入如下语句:
|
||||
```java
|
||||
ConfigurationSerialization.registerClass(Person.class);
|
||||
```
|
||||
为什么要这么做呢?
|
||||
可以找到 ConfigurationSerializable的JavaDocs页面
|
||||
|
||||

|
||||
|
||||
|
||||
至此, 你就可以自由地对一个自定义的对象直接地get和set了!
|
||||
下面分别演示set与get:
|
||||
```java
|
||||
// set
|
||||
Person person = new Person("tdiant", "hello!!");
|
||||
getConfig().set("demo", person);
|
||||
saveConfig();
|
||||
```
|
||||
默认配置文件(config.yml)将出现:
|
||||
```yml
|
||||
demo:
|
||||
==: myplugin.Person
|
||||
name: tdiant
|
||||
introduction: hello!!
|
||||
```
|
||||
BukkitAPI会根据`==`的值判断这段配置是由什么类序列化而来的, 进而方便其反序列化.
|
||||
不要轻易改动`==`属性的值, 否则BukkitAPI会因为找不到类或此类没有被注册(ConfigurationSerialization类的registerClass方法)而报错.
|
||||
```java
|
||||
// get
|
||||
Person person = (Person) getConfig().get("demo");
|
||||
System.out.println("name = " + person.name + " introduction = " + person.introduction);
|
||||
```
|
||||
控制台将输出:
|
||||
```
|
||||
name = tdiant introduction = hello!!
|
||||
```
|
||||
# 配置文件的遍历
|
||||
试想, 如果存在下面的配置文件:
|
||||
```yml
|
||||
demo_list:
|
||||
a: 1
|
||||
b: 233
|
||||
c: 666
|
||||
d: lalalalalal
|
||||
```
|
||||
我应该如何对`demo_list`的子键进行遍历, 得到所有子键的对应值?
|
||||
最简单错报的方式就是将`demo_list.a`键、`demo_list.b`键...依次读取. 但这是建立在你知道`demo_list`有`a`、`b`、`c`...这些子键的基础之上的.
|
||||
如果我事先不知道`demo_list`的子键都各自叫什么, 又应该如何对`demo_list`的子键进行遍历, 得到所有子键的对应值?
|
||||
|
||||
## 配置片段 ConfigurationSection
|
||||
我们可以把`demo_list`键对应的部分拆出来.
|
||||
*下文假设config对象是我们现在正在操作的FileConfiguration对象.*
|
||||
```java
|
||||
ConfigurationSection cs = config.getConfigurationSection("demo_list");
|
||||
```
|
||||
这里我们得到了`ConfigurationSection`对象, 这个对象可以当做config对象`demo_list`键部分的片段, 等效于这个yaml数据:
|
||||
```yml
|
||||
a: 1
|
||||
b: 233
|
||||
c: 666
|
||||
d: lalalalalal
|
||||
```
|
||||
对于一个`ConfigurationSection`对象, 其代表着一个完整配置数据的某个片段, 你不能直接利用诸如`saveConfig`的方式保存这个片段到另外一个yml文件里.
|
||||
|
||||
## 利用getKeys方法实现遍历
|
||||
在上面我们得到了`ConfigurationSection`对象, 这代表着config对象`demo_list`键部分的片段.
|
||||
现在问题转化成了, 如何获取到`ConfigurationSection`对象里的所有键.
|
||||
可以利用`getKeys(false)`的方式达到目的.
|
||||
|
||||
```java
|
||||
for(String key : cs.getKeys(false)) {
|
||||
System.out.println(key + " = " + cs.get(key));
|
||||
}
|
||||
```
|
||||
上面的代码将输出:
|
||||
```
|
||||
a = 1
|
||||
b = 233
|
||||
c = 666
|
||||
d = lalalalalal
|
||||
```
|
||||
这样就实现了遍历.
|
||||
|
||||
`getKeys`方法不只是`ConfigurationSection`拥有, 根据其继承关系, 我们可以推知对`FileConfiguration`类也拥有`getKeys`方法, 同理, `ConfigurationSection`类也有`getConfigurationSection`方法.
|
||||
|
||||
但是我们刚才为什么要给`getKeys`的一个`false`的参数呢? 请看下面的yaml数据:
|
||||
```yml
|
||||
test:
|
||||
a:
|
||||
b: 1
|
||||
c: 2
|
||||
d: 1
|
||||
```
|
||||
我们得到了这个配置文件的`FileConfiguration`对象`config`, 现在对其用`getKeys(false)`进行遍历, 得到所有键.
|
||||
```java
|
||||
for(String key : config.getKeys(false)) {
|
||||
System.out.println(key);
|
||||
}
|
||||
System.out.println("===================");
|
||||
for(String key : config.getKeys(true)) {
|
||||
System.out.println(key);
|
||||
}
|
||||
```
|
||||
输出结果如下:
|
||||
```
|
||||
test
|
||||
d
|
||||
===================
|
||||
test
|
||||
test.a
|
||||
test.a.b
|
||||
test.c
|
||||
d
|
||||
```
|
||||
由此可知, `getKeys(false)`只能获取“一层的键”, 不能递归获取配置文件里所有的键. 而`getKeys(true)`会递归获取配置文件里所有出现的键.
|
||||
155
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/3-进阶内容/5-多线程与多任务.md
Normal file
155
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/3-进阶内容/5-多线程与多任务.md
Normal file
@@ -0,0 +1,155 @@
|
||||
---
|
||||
front:
|
||||
hard: 困难
|
||||
time: 40分钟
|
||||
---
|
||||
|
||||
# Bukkit 的多线程多任务框架
|
||||
|
||||
# 前言
|
||||
本节前半部分内容基本是对Javadoc的复述, 以及使用它们的注意事项. 如果此前您已经使用过了此包, 或者您有良好的文档阅读及应用能力, 建议您先阅读“注意事项”和“小技巧”一栏, 这才是本节教程更重要的知识!
|
||||
|
||||
# org.bukkit.scheduler 包结构
|
||||
Bukkit 的多线程多任务框架放在了此包, 此包只含有三个接口(`BukkitSheduler`, `BukkitTask`, `BukkitWorker`)和一个抽象类(`BukkitRunnable`,实现了java.lang.Runnable). 相关实现在实现了 Bukkit API 的底层服务器代码中(比如CraftBukkit).
|
||||
他们之间的关系大致是这样的: `BukkitSheduler` 负责调度/创建任务,并管理他们(类似于线程池). `BukkitTask` 负责存储由 `BukkitSheduler` 调度的单个任务, 并提供获取它们的任务 id 以及取消它们的一系列方法. `BukkitWorker` 是处理对应异步任务的worker线程. `BukkitRunnable` 基本上是对 BukkitScheduler 的包装, 使用它比使用 BukkitScheduler 相对来说更简洁些.
|
||||
|
||||
# 访问 org.bukkit.scheduler 的两个入口
|
||||
一是使用`org.bukkit.Bukkit.getScheduler()`或`org.bukkit.Bukkit.getServer().getScheduler()`获取`BukkitScheduler`实例.
|
||||
例子:
|
||||
```java
|
||||
Bukkit.getScheduler().runTask(this, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// 逻辑代码
|
||||
}
|
||||
});
|
||||
```
|
||||
另一个是构造一个继承`org.bukkit.scheduler.BukkitRunnable`的匿名内部类, 就像这样:
|
||||
```java
|
||||
new BukkitRunnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// 您的代码逻辑
|
||||
}
|
||||
}.runxxx();
|
||||
```
|
||||
然后再调用 BukkitRunnable 里的各种方法(事实上最终它还是要访问`BukkitScheduler`, 因此两种方法是等效的). 您也可以直接在Runnable内调用BukkitRunnable的方法, 实现自我取消, 等等. 使用BukkitRunnable的优点在于它简单便捷.
|
||||
|
||||
# 如何使用
|
||||
在这里只介绍Bukkit 任务调度API的核心 ———— BukkitScheduler 的使用方法, 并且不对那些已过时的方法做解释说明(通常情况下你不应该使用它们).
|
||||
值得注意的是, Bukkit 的调度任务系统是以 Minecraft 的游戏刻为时间单位的, 其中一个游戏刻(又叫做tick, 下文都使用`tick`指代游戏刻)对应现实世界的50ms(也就是说, 理想情况下20 ticks是一秒). 但实际上受服务器性能因素的影响, 不一定每一tick都精确地经过了50ms (服务器每秒经过的ticks数可以使用命令`tps`查询). 所以在您编写Bukkit 插件时, 请把你置身于 Minecraft 的世界里:)
|
||||
如果没有特别说明, Bukkit所提供的调度任务的方法, 时间均以tick为单位. 方法全名规则是前者为方法返回值, 后者为方法名和相关参数.
|
||||
|
||||
## 调度同步任务
|
||||
### BukkitTask runTask(Plugin plugin, java.lang.Runnable task)
|
||||
这是调度**同步任务**的主要方法, 另一个方法`runTaskLater`提供了一个`delay`延迟参数, 用于指定调度任务多久后才开始执行. 不指定`delay`的情况下, delay值为1.
|
||||
### BukkitTask runTaskTimer(Plugin plugin, java.lang.Runnable task, long delay, long period)
|
||||
这是调度重复任务的方法, 所得的任务是**同步**的, `period`最低值为1,您不能将其设为比1低的值 (若设为0则等效于1, 小于0表示该任务不是重复的).
|
||||
由于是同步任务, 您在Runnable的run()方法中的代码, 是运行于服务器主线程的, 所以请仔细评估这些代码的效率, 因为这可能会影响服务器的性能(尤其是TPS指标), 从而降低服务器流畅度. 如果不与 Minecraft 有关, 请放在下面要介绍的异步任务.
|
||||
|
||||
## 调度异步任务
|
||||
### BukkitTask runTaskAsynchronously(Plugin plugin, java.lang.Runnable task)
|
||||
这是调度**异步任务**的主要方法, 另一个方法`runTaskLaterAsynchronously`提供一个`delay`延迟参数.
|
||||
### BukkitTask runTaskTimerAsynchronously(Plugin plugin, java.lang.Runnable task, long delay, long period)
|
||||
这是调度重复任务的方法, 所得的任务是**异步**的. 通常我们使用异步任务来处理非Minecraft的逻辑,比如数据库的CRUD(增删改查)操作.
|
||||
在异步任务中, 需要特别注意线程安全问题, 比如您不能随意调用 Bukkit API. 这个问题会稍后予以详细的解释说明.
|
||||
|
||||
# 注意事项
|
||||
## 线程安全
|
||||
Bukkit API文档清楚地告诉我们异步任务中不应访问某些Bukkit API, 需要着重考虑线程安全. 大多数 Bukkit API 不是线程安全的.
|
||||
什么是线程安全呢?
|
||||
> 在拥有共享数据的多条线程并行执行的程序中,线程安全的代码会通过同步机制保证各个线程都可以正常且正确的执行,不会出现数据污染等意外情况。
|
||||
> “引自百度百科”
|
||||
|
||||
大多数集合不是线程安全的, 比如经常使用的`HashMap`、`ArrayList`. 同样适用于非线程安全的对象.
|
||||
限于篇幅, 这里不作深入探讨. 想要了解更多, 请询问您的书籍与搜索引擎.
|
||||
Bukkit 中的线程安全?
|
||||
Minecraft 中几乎所有的游戏逻辑都运行于主线程中, 而插件的大多数逻辑也是运行于主线程中的, 这包括插件命令的执行、(同步)事件的处理等等.
|
||||
如果我们调度了一个异步任务, 或者处于异步事件中, 那么就不应当访问与Minecraft游戏内容有关的API(比如操作方块、加载区块、踢出玩家等). 尝试这么做极有可能得到异常, 使得插件崩溃.
|
||||
|
||||
## 如何在异步任务中调度同步任务, 以访问 Bukkit 的非线程安全的方法?
|
||||
一种就是`BukkitScheduler.runTask` (方法不带`asynchronously`字眼). 这返回的永远是同步任务, 可以大胆访问 Bukkit API, 就像这样:
|
||||
```java
|
||||
Bukkit.getScheduler().runTaskAsynchronously(this, () -> {
|
||||
// 从数据库拉取些数据
|
||||
// 执行同步任务
|
||||
Bukkit.getScheduler().runTask(ExamplePlugin.instance, () -> player.sendMessage("你好, 世界!"));
|
||||
});
|
||||
```
|
||||
另一种就是`BukkitScheduler.callSyncMethod`, 这个会在之后的小技巧一栏作介绍.
|
||||
## Bukkit API中哪些操作是非线程安全的, 哪些又是线程安全的?
|
||||
> 不完整列表. 仅供参考. 不保证线程安全的方法的行为将来会变化. 不对版本差异导致的行为不同作担保.
|
||||
|
||||
线程安全的有:
|
||||
1. scheduler包自身.
|
||||
2. Player#sendMessage()
|
||||
> 你可以发现大量插件在AsyncPlayerChatEvent事件中调用player.sendMessage(). 因此我们有理由确信这是线程安全的.
|
||||
3. PluginManager#callEvent(event)
|
||||
> 用于触发事件的方法. 在`SimplePluginManager`中, 该方法使用了synchronized关键字对其实例加锁, 因此是线程安全的. 更多细节请阅读源代码.
|
||||
4. 发包 - sendPacket
|
||||
> 为何Player#sendMessage()是线程安全的就是因为它. 我们可以深入craftbukkit乃至nms(net.minecraft.server), sendPacket不过是将数据包传入netty管道, 让netty处理. 如果某个方法仅仅执行了发包流程而没有实际从游戏里加载数据, 那么一般可视其为线程安全的. 因此利用`World#spawnParticle`发送粒子效果以及`World#playEffect`向玩家发送特效、`Player#sendTitle`向玩家发title等也是线程安全的. 我们可以把相关数学运算放到异步线程中, 算完再切换线程发粒子特效.
|
||||
|
||||
非线程安全的有:
|
||||
1. 设置/获取方块、加载/生成区块
|
||||
2. 操作实体
|
||||
3. 权限检查(是的. 某些情况下这是非线程安全的, 因为插件一同共享权限列表)
|
||||
|
||||
## 关闭插件时, 确保取消你调度的所有任务
|
||||
最简单的方法就是在插件主类的`onDisable`方法写上这一行代码:
|
||||
```java
|
||||
Bukkit.getScheduler().cancelTasks(plugin);
|
||||
```
|
||||
其中plugin是你的插件实例, 通常是`this`.
|
||||
如果不这么做,那么你的插件被关闭之后, 残存的任务(一般是重复任务)仍在运行, 任务会调用相关变量, 而你在关闭插件时如果清理了那些变量, 将会导致一些无法预料的问题.
|
||||
|
||||
# 小技巧
|
||||
## 使用 lambda 表达式替换匿名内部类
|
||||
自Java 8开始提供对 lambda 表达式的支持. 匿名内部类转 lambda 表达式可使代码看上去更加简洁漂亮. 比如
|
||||
```java
|
||||
scheduler.runTask(this, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println("这是从在任务中输出的一句话.");
|
||||
}
|
||||
});
|
||||
```
|
||||
可以替换成:
|
||||
```java
|
||||
scheduler.runTask(this, () -> System.out.println("这是从在任务中输出的一句话."));
|
||||
```
|
||||
是不是觉得匿名内部类多不优雅, 而 lambda 表达式一行就解决了所有问题? 尽早对丑陋的匿名内部类说byebye吧~
|
||||
|
||||
## 使用 BukkitScheduler 提供的`callSyncMethod`方法
|
||||
> 其实这不应出现在这里的. 不过使用这种方法有点门槛, 如果没有学过相关概念, 你可能不知道从何下手. 该方法涉及到了 Java 的 Future 和 Callable 概念. 如果不知道是什么, 可以搜索来查找资料. 相对于线程安全, Future 和 Callable 概念理解起来容易多了.
|
||||
|
||||
这也是使你的代码置于服务器主线程执行的方法之一, 通常用于需要在主线程执行操作获取数据并返回给异步线程的场景.
|
||||
下面是鄙人对这些概念的粗略理解:
|
||||
> 常规的Runnable的run方法是没有返回值的, 它是一个void方法. 这时我们需要使用`Callable`, `Callable`的call方法是有返回值的, 值类型受泛型影响. 使用Runnable还有一个缺点:我(Boss)命令手下一位职员做点任务. 命令完后(开线程, 使用Runnable), 我需要等待职员做完任务的一些反馈, 没有职员提供的数据不能继续工作. 然后在职员执行完任务之前我能干嘛? 没办法, 只能等, 无论职员会执行多久. 有没有办法, 在职员执行任务的过程中, 我还可以做点别的事情呢?
|
||||
|
||||
Java提供了Future这个模式. 于是上面的情况变成了这样:
|
||||
> 我命令手下一位职员做点任务. 命令完后(开线程, task为FutureTask), 我可以做些别的事情了, 比如与某某打情骂俏...... 之后我可以询问那位职员事情做完没有(Future#isDone()), 或者直接问他结果(Future#get()), 这个取值过程是阻塞的, 直到那位职员完成任务后才能报告结果. 如果我等不耐烦了我还可以使他停下来, 不做了(Future#cancel(boolean)). ~~甚至看不顺眼解雇他~~ 等待职员完成任务的同时, 又多了一份愉悦, 何乐而不为呢~
|
||||
|
||||
这里就不作更多介绍了. 欲了解更多内容和用法可以参考[Javadoc](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/Future.html) 以及询问搜索引擎.
|
||||
|
||||
直接上食用方法吧! 这是一个使用主线程获取当前在线玩家数量并返回的例子:
|
||||
```java
|
||||
Future<Integer> future = Bukkit.getScheduler().callSyncMethod(ExamplePlugin.instance, () -> {
|
||||
// call方法是可以抛出异常的
|
||||
// 假设这个操作有些耗时...这是对主线程的sleep(事实上这最好不要超过50ms)
|
||||
Thread.sleep(1000);
|
||||
return Bukkit.getOnlinePlayers().size();
|
||||
});
|
||||
try {
|
||||
// 比如这里是数据库操作过程, 假设连接数据库并进行操作耗时1s, 这时我们应该可以拿到在线玩家数了
|
||||
// 如果操作过程小于1s更好, 只要等上面的方法执行完即可
|
||||
// future.get()是阻塞的, 直到执行完毕
|
||||
int players = future.get();
|
||||
// 向数据库写入数据
|
||||
System.out.println("玩家数:" + players);
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
// 异常处理
|
||||
}
|
||||
```
|
||||
这段代码是在异步任务中运行的.
|
||||
|
||||
食用方法可以说是较复杂了, 如果你没有获取数据的需要, 仅仅需要在主线程内运行特定代码, 使用`BukkitScheduler#runTask()`更好. 没有必要为了 bigger 而 bigger, 唯有**simple**得人心.
|
||||
121
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/3-进阶内容/6-自定义合成表.md
Normal file
121
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/3-进阶内容/6-自定义合成表.md
Normal file
@@ -0,0 +1,121 @@
|
||||
---
|
||||
front:
|
||||
hard: 进阶
|
||||
time: 20分钟
|
||||
---
|
||||
|
||||
# 自定义合成表
|
||||
|
||||
在背包、工作台中, 玩家可以通过指定的物品摆放, 消耗所摆放的物品得到新物品, 这被称作物品的合成. 物品的摆放方式与得到的新物品即为合成表.
|
||||
|
||||
## 合成表物品摆放的文字表述法
|
||||
|
||||
如何用文字表述物品在2X2格子或3X3格子中的摆放方式?
|
||||
|
||||
首先我们来使用数学中“方程”的概念, 把金锭设成x, 把铁锭设成y. 打个比方, 我现在想实现八个金锭和一个铁锭合成一个绿宝石, 摆放方式可以这样表示:
|
||||
|
||||
```
|
||||
xxx
|
||||
xyx
|
||||
xxx
|
||||
```
|
||||
|
||||
那现在如果想表示类似“工作台”的合成方式呢? 工作台合成需要四个木板, 在背包的合成区内可以填满木板来合成, 在工作台合成区内可以有这样的摆放方式:
|
||||
|
||||
```
|
||||
设x为木板
|
||||
xx空
|
||||
xx空
|
||||
空空空
|
||||
|
||||
空xx
|
||||
空xx
|
||||
空空空
|
||||
|
||||
空空空
|
||||
xx空
|
||||
xx空
|
||||
|
||||
空空空
|
||||
空xx
|
||||
空xx
|
||||
```
|
||||
对于这样的非3X3的合成方式, 我们可以这样表示:
|
||||
|
||||
```
|
||||
xx
|
||||
xx
|
||||
```
|
||||
|
||||
这也意味着, 在用文字表述合成表时, 不一定非得是3x3的表示方式, 还可以2x2, 还可以1x1, 只要是mXn的格式即可(例如门的合成是2X3).
|
||||
|
||||
## 新建合成规则
|
||||
|
||||
以上文合成金铁锭合成绿宝石为例, 在onEnable方法的适当位置添加如下内容:
|
||||
|
||||
```java
|
||||
ShapedRecipe sr1 = new ShapedRecipe(
|
||||
new ItemStack(Material.EMERALD)) //合成出的物品(提示: 修改这个ItemStack的Amount可以控制能合成多少个目标物品)
|
||||
.shape("xxx","xyx","xxx") //这是刚才我们摆出来的文字表述
|
||||
.setIngredient('x',Material.GOLD_INGOT). //设x为金锭
|
||||
setIngredient('y',Material.IRON_INGOT); //设y为铁锭
|
||||
|
||||
getServer().addRecipe(sr1);
|
||||
```
|
||||
|
||||
在onDisable方法中添加如下内容:
|
||||
|
||||
```java
|
||||
getServer().clearRecipes();
|
||||
```
|
||||
|
||||
经验证可发现, 现在我们可以通过控制台通过在铁锭周围围一圈金锭的方式合成绿宝石了.
|
||||
|
||||
那么设x为金锭, 我想实现像熔炉那样, 八个金锭围一圈合成一个绿宝石, 不需要铁锭了. 就像这样:
|
||||
|
||||
```
|
||||
xxx
|
||||
x空x
|
||||
xxx
|
||||
```
|
||||
|
||||
中间有个位置是空的, 该怎么办? 应该设个y表示AIR吗? 不需要, 空位置可以使用空格表示. 就像下面这个例子:
|
||||
|
||||
```java
|
||||
ShapedRecipe sr2 = new ShapedRecipe(
|
||||
new ItemStack(Material.EMERALD)) //合成出的物品
|
||||
.shape("xxx","x x","xxx") //这是刚才我们摆出来的文字表述(中间的y改成了空格)
|
||||
.setIngredient('x',Material.GOLD_INGOT). //设x为金锭
|
||||
setIngredient('y',Material.IRON_INGOT); //设y为铁锭
|
||||
|
||||
getServer().addRecipe(sr2);
|
||||
```
|
||||
|
||||
shape方法的参数个数不限制, 这也意味着你可以这样表述非3X3摆放方式:
|
||||
|
||||
```java
|
||||
.shape("x") //1X1(就像按钮那样的摆放方式)
|
||||
.shape("xx","xx","xx") //2X3(就像木门那样的摆放方式)
|
||||
.shape("xx","xx") //2X2(就像合成台那样的摆放方式)
|
||||
```
|
||||
|
||||
如果你这样设置
|
||||
|
||||
```java
|
||||
.shape("xx ","xx "," ")
|
||||
```
|
||||
|
||||
那玩家在游戏中只能这样在合成台合成:
|
||||
```
|
||||
xx空
|
||||
xx空
|
||||
空空空
|
||||
```
|
||||
|
||||
而不能用其他等效的位置摆放合成, 比如这样:
|
||||
|
||||
```
|
||||
空空空
|
||||
xx空
|
||||
xx空
|
||||
```
|
||||
85
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/3-进阶内容/7-粒子效果与音效播放.md
Normal file
85
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/3-进阶内容/7-粒子效果与音效播放.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
front:
|
||||
hard: 进阶
|
||||
time: 20分钟
|
||||
---
|
||||
|
||||
# 粒子效果及音效播放
|
||||
|
||||
## 粒子效果
|
||||
|
||||
客户端正常配置时,若对草方块上使用骨粉,草方块上会长出草丛,同时还会生成绿色的颗粒动画. 这样的动画效果就是Minecraft中的粒子效果.
|
||||
|
||||
### 播放粒子效果
|
||||
如果想在某一个`Location`对象所对应的位置播放粒子效果,对于不同的Minecraft版本有不同的方案:
|
||||
|
||||
#### PlayEffect
|
||||
可以利用World类的`PlayEffect`方法:
|
||||
*对于Effect,BukkitAPI在后续的更改中,其中的枚举几乎都或多或少有些许改动。开发时应小心。*
|
||||
|
||||
```java
|
||||
Location loc = 某一Location对象;
|
||||
loc.getWorld.playEffect(loc, Effect.HAPPY_VILLAGER, 1); //播放的是绿色的闪光星星⭐效果
|
||||
```
|
||||
|
||||
`PlayEffect`方法在较早的BukkitAPI版本中即被加入. 在使用这一方法时需要与`Effect`打交道.
|
||||
`Effect`是效果枚举. 值得注意的是,这其中既包含动效(Effect.Type.VISUAL),也包含声效(Effect.Type.SOUND).
|
||||
|
||||
***作为一个老旧的API,在实际开发当中,这一方法并不常用. 其中的常见枚举(例如这里使用的HAPPY_VILLAGER)在新的API中被标记废除.***
|
||||
|
||||
#### spawnParticle
|
||||
在新版的API中加入了`spawnParticle`方法. 目前开发插件常用这一方法来播放粒子效果.
|
||||
|
||||
新版的BukkitAPI有意将`Sound`与`Visual`这两个概念分隔开,对于粒子效果,在使用`spawnParticle`方法时,取`Effect`而代之的是`Particle`枚举.
|
||||
|
||||
*spawnParticle的用法较多,在此略去大篇幅对各个方法与参数的介绍,可以查阅JavaDoc,其中有十分简单易懂的注释.*
|
||||
*BukkitAPI后续更新中,枚举或多或少都有变动,应当注意!*
|
||||
|
||||
### 播放所需的形状
|
||||
|
||||
> 开发实例: 在玩家脚底播放一圈半径为1的粒子效果
|
||||
|
||||
**分析**
|
||||
1. 几何角度考虑
|
||||
|
||||
以玩家脚底处为原点,建立平面直角坐标系. 如下图所示:
|
||||

|
||||
|
||||
*绿色部分为粒子效果*
|
||||
|
||||
由圆的定义知,所绘制的粒子为到原点的点集.
|
||||
|
||||
2. 实现
|
||||
播放想要的形状就是逐次的在所需播放的坐标处播放粒子效果.
|
||||
|
||||
*这里将不解释什么是弧度制,而是做强制要求,只要算角度都必须用这样的方法变为弧度制,有兴趣可以在网上查阅*
|
||||
|
||||
```java
|
||||
Location loc = p.getLocation().clone();
|
||||
for(int t=0;t<360;t++){ //这里的t表示旋转角,从0到360度遍历一遍就是转了一圈
|
||||
double r = Math.toRadians(t); //角度制变弧度制
|
||||
//在这里,我们使用三角函数依次计算出了对应点的坐标.
|
||||
//建议作图体会这样计算的原理.
|
||||
double x = Math.cos(r);
|
||||
double y = Math.sin(r);
|
||||
//在刚开始时,loc是坐标系原点(也就是玩家所在的位置)
|
||||
//这里我们的add将其变为了我们想要播放粒子的坐标位置
|
||||
//后面我们又subtract(减)将其又变为了坐标原点
|
||||
loc.add(x,0,y);
|
||||
loc.getWorld().spawnParticle(Particle.VILLAGER_HAPPY,loc,1,null);
|
||||
loc.subtract(x, 0, y);
|
||||
}
|
||||
```
|
||||
|
||||
这样我们就完成了这一效果.
|
||||
|
||||
依此,可以大致概括出实现粒子效果的基本步骤:
|
||||
1. 分析: 从数学角度分析, 思考怎么才能获得所需形状中所有的点;从代码角度分析,思考怎样才能依此获得这些点的坐标值
|
||||
2. 实现:利用恰当的方法播放粒子效果
|
||||
|
||||
## 音效播放
|
||||
由于`Effect`既包含动效,也包含声效,这意味着使用与上面`PlayerEffect`方法一样的方法,我们也可以播放音效.
|
||||
|
||||
在新API中提供了`playSound`方法并且加入了`Sound`枚举. 目前常用这一方法. 这一方法是World也同样是Player类的方法, 具体使用哪一方法,取决于你希望对谁播放.
|
||||
|
||||
*BukkitAPI后续更新中,枚举或多或少都有变动,应当注意!*
|
||||
126
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/3-进阶内容/8-世界生成器.md
Normal file
126
docs/mcguide/28-电脑网络游戏/课程3:编写Bukkit插件/3-进阶内容/8-世界生成器.md
Normal file
@@ -0,0 +1,126 @@
|
||||
---
|
||||
front:
|
||||
hard: 进阶
|
||||
time: 20分钟
|
||||
---
|
||||
|
||||
# 世界生成器
|
||||
>
|
||||
> *在Bukkit中, 截止到目前, BukkitAPI仍沿用旧有规则的API.*
|
||||
> *这意味着本文内容截止目前对于新版本的插件开发仍然有效.*
|
||||
|
||||
本文中使用了`Material.GRASS_BLOCK`, 这是1.13版本的新用法.
|
||||
在旧版API中, 应该使用`Material.GRASS`.
|
||||
|
||||
## 简述世界生成
|
||||
|
||||
Minecraft中, 一个世界(World)按照一定的大小被分为多个区块(Chunk).
|
||||
MC会自动地按照一定的规则卸载无人Chunk, 在需要的时候加载所需的Chunk到内存, 以此来保证一个World被加载到内存, 这样不至于整个World都需要加载到内存以备调用.
|
||||
世界的生成同样以Chunk为单位.
|
||||
|
||||
Minecraft游戏中, 世界生成分为两个阶段, 分别为 Generation 与 Population.
|
||||
|
||||
Minecraft生成一个World, 首先进入 Generation 阶段. 这一阶段主要是绘制地形等.
|
||||
1. Minecraft首先会获取该Chunk中包含的所有生物群系. 然后会根据特定的生物群系绘制基本的地形. 地形的绘制依靠了一些特殊的算法, 游戏通常会以高度63作为水平面, 通过这些特殊算法绘制基本的地形. 绘制完毕后, 整个世界只有空气、水和石头.
|
||||
2. 接着会在高度0-5范围内生成基岩, 并逐个对各个生物群系添加特有的方块. 例如, 对平原添加草方块和泥土, 对沙漠添加沙子和沙石等.
|
||||
3. 再然后会生成特殊地形. 这里的特殊地形指的是涉及到多个区块的大型地形, 例如规模很大的洞穴、村庄、矿井等.
|
||||
4. 最后会进一步处理, 做最后的准备收尾工作, 至此Generation阶段完毕.
|
||||
|
||||
Generation阶段完成, 意味着该世界的整体结构已经定型. 但是这个世界上还缺少“点缀”. 这个世界上仍然没有树、生物、沼泽上的荷叶、水边的甘蔗等. 此时进入 Population 阶段.
|
||||
1. 首先会对该世界的实体进行完善, 并生成各种各样的特殊的方块(指的是箱子等方块实体, 这些方块与其它方块相比复杂许多).
|
||||
2. 然后会生成小型地形. 比如一些地表小水坑、地表岩浆池、地下地牢等.
|
||||
3. 然后会在地下按照一定的规则生成矿物.
|
||||
4. 最后增加地面点缀, 生成水边的甘蔗、沼泽上的荷叶、地面大蘑菇和树木等物, 并增加一些生物群系特定物, 生成一些基础生物(比如牛、鸡、羊等).
|
||||
|
||||
待 Population 阶段结束后, 该Chunk的数据便会存储起来, 显示出来.
|
||||
|
||||
## 干涉Population
|
||||
|
||||
Bukkit中, 在世界初始化前会触发`WorldInitEvent`事件. 监听该事件, 我们可以对该世界生成的 Population 阶段进行干涉.
|
||||
|
||||
在下面的案例中, 我们将在Chunk的 Population 阶段, 在世界的草方块上人为的添加许多钻石块(DIAMOND_BLOCK).
|
||||
|
||||
```java
|
||||
public class WorldListener implements Listener {
|
||||
@EventHandler
|
||||
public void onWorldInit(WorldInitEvent e){
|
||||
if(e.getWorld().getName().equals("World"))
|
||||
e.getWorld().getPopulators().add(new RuaPopulator());
|
||||
}
|
||||
}
|
||||
|
||||
class RuaPopulator extends BlockPopulator {
|
||||
@Override
|
||||
public void populate(World w, Random r, Chunk c){
|
||||
final int maxn = 16; //一个区块的X或Y范围是0-16
|
||||
for(int i=0; i<12; i++){ //这里打算一个区块生成12个
|
||||
int x = r.nextInt(maxn), z = r.nextInt(maxn);
|
||||
for (int y = 125; y > 0; y--) {
|
||||
if (c.getBlock(x, y, z).getType() == Material.GRASS_BLOCK && c.getBlock(x, y + 1, z).getType() == Material.AIR) {
|
||||
c.getBlock(x, y + 1, z).setType(Material.DIAMOND_BLOCK);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
最终效果如下:
|
||||
|
||||

|
||||
|
||||
可以发现, 生成的world中, 按照我们的设定, 在地表草方块上零散的分布了钻石块.
|
||||
这说明在Bukkit中, 你可以创建一个BlockPopulator对象, 在世界初始化时添加为某一World的Populator, 依此来干涉Population阶段.
|
||||
*Bukkit中的Populator只有BlockPopulator一种.*
|
||||
*但是你可以以此类推, 通过这种方式实现在地面随机生成某种建筑等其他效果.*
|
||||
|
||||
值得注意的是, 在自定义的Populator中, populate方法的参数中有一个传入的Random对象.
|
||||
这是为了让随机数的生成符合World对应的种子. 在需要生成随机数的时候, 应尽可能使用方法参数中的Random对象.
|
||||
|
||||
## 控制Generation
|
||||
|
||||
通过控制一个世界的Generation, 我们可以控制世界的大体地形.
|
||||
下面我们将在插件加载时, 生成一个新的世界`RuaWorld`, 这个世界是一个超平坦世界, 第一第二层为基岩, 第三层为草方块.
|
||||
|
||||
```java
|
||||
public class Main extends JavaPlugin {
|
||||
public void onEnable(){
|
||||
Bukkit.getPluginManager().registerEvents(new WorldListener(),this);
|
||||
Bukkit.createWorld(new WorldCreator("RuaWorld").generator(new RuaChunkGenerator()));
|
||||
}
|
||||
|
||||
public void onDisable(){
|
||||
//
|
||||
}
|
||||
}
|
||||
|
||||
class RuaChunkGenerator extends ChunkGenerator {
|
||||
@Override
|
||||
public ChunkData generateChunkData(World w, Random r, int x, int z, BiomeGrid b) {
|
||||
ChunkData chunkData = createChunkData(w); //创建区块数据
|
||||
|
||||
//下面这行方法调用参数中, 前三个参数代表一个XYZ对, 后面又是一个XYZ对.
|
||||
//这两个XYZ对是选区的意思, 你可以结合Residence插件圈地、WorldEdit选区的思路思考.
|
||||
//提醒: 一个Chunk的X、Z取值是0-16, Y取值是0-255.
|
||||
chunkData.setRegion(0, 0, 0, 16, 2, 16, Material.BEDROCK); //填充基岩
|
||||
chunkData.setRegion(0, 2, 0, 16, 3, 16, Material.GRASS_BLOCK); //填充草方块
|
||||
|
||||
//将整个区块都设置为平原生物群系(PLAINS)
|
||||
for (int i = 0; i < 16; i++) {
|
||||
for (int j = 0; j < 16; j++) {
|
||||
b.setBiome(i, j, Biome.PLAINS);
|
||||
}
|
||||
}
|
||||
return chunkData;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
我们进入`RuaWorld`世界, 可以发现世界按照我们所需要的地形生成了.
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
front:
|
||||
hard: 进阶
|
||||
time: 3分钟
|
||||
---
|
||||
|
||||
# Title与ActionBar的显示
|
||||
>
|
||||
> *主观上,在1.12.2后我们发送ActionBar和Title.*
|
||||
> *Title只需要通过Player中的sendTitle方法.*
|
||||
> *而ActionBar发送方式则是 Player 对象封装了内部对象 spigot(),其中里面则有静态方法 sendMessage 通过传入ChatMessageType枚举参数,便可以发送ActionBar*
|
||||
> *用Spigot的sendMessage得将内容转换成TextComponent*
|
||||
|
||||
但是Bukkit就没有能够直接发送ActionBar的发送方法.
|
||||
所以我们就要通过NMS底层去自行封装一个方法.
|
||||
Reference in New Issue
Block a user