Skip to content

Latest commit

 

History

History
215 lines (151 loc) · 11.4 KB

File metadata and controls

215 lines (151 loc) · 11.4 KB

代码中的MC世界

你一定是玩过MC的. 那你一定也能想象出来代码化的MC是什么样子.

相信你有Java的开发经验的话, 一定了解JavaDoc的使用. SpigotMC官方提供了JavaDoc.

最新版本JavaDoc网址: https://hub.spigotmc.org/javadocs/spigot/index.html?overview-summary.html
旧版本JavaDoc网址(1.7.10): http://jd.bukkit.org/

国内有一群热爱开发的人做出了中文JavaDoc, 开发时可以用以参考.
最新版本中文JavaDoc网址: https://bukkit.windit.net/javadoc/
他们的GitHub地址: https://github.com/BukkitAPI-Translation-Group/Chinese_BukkitAPI

本章大致上按照JavaDoc上罗列的包的顺序来介绍BukkitAPI所提供的各个体系和系统.
本章仅仅是对各个系统做大概的描述. 后续会详细讲述各个部分的内容.

世界、方块与Material

想也不用想, MC不可能把一整个世界都存到一个文件里去, 否则这样的一个文件该有多大! 事实上, 一个完整的世界分为了很多Chunk, 也就是区块. 每个区块都是X和Z为16*16的范围. 你可以认为一个世界由许多区块组成.

在开发时, 我们把每个世界都看成一个World对象, 每个区块都看成一个Chunk对象. 如果我们想操作某个世界内的某个方块, 实际开发时我们可以直接调用World对象的方法, 而不用先寻找到Chunk再操作. 每个方块都是一个Block对象.

在BukkitAPI中引入了“材质”(Material)的概念. 比如一个石头方块, 它的材质就是STONE, 如果一个物品的材质也是STONE, 那么这个物品就是玩家手上拿着的石头方块物品了.
你可以为一个方块设置材质, 就像这样:

Block b = 方块;
b.setType(Material.STONE);

想必你也能猜到了, 怎样在一个世界里“删除掉”一个方块:

b.setType(Material.AIR); //设置成空气就好了嘛

方块与方块各不一样. 有些方块是带有特有属性和功能的. 比如告示牌上面有字. 事实上, Block类有许多子类, 每个告示牌方块无论是墙上的还是地上杵着的都是Sign对象. 比如你可以像这样修改告示牌上的字:

Block b = 你获取到的告示牌方块, 你可以用getType判断一下它的Material是不是告示牌;
Sign s = (Sign)b; //直接强制转换
s.setLine(0,"测试"); //这样就把第一行修改成了字符串“测试”

BukkitAPI的包分类是清晰的, 所有的方块子类都在org.bukkit.block包内, 你完全可以利用JavaDoc, 找到所需的子类并查看用法. Material枚举量的具体内容也能查询到. 后续将不再赘述.

生物与位置对象(Location)

生物

在MC中, 所有的生物, 例如一只羊, 乃至一个僵尸, 又或者是玩家, 都是生物, 他们都是Entity类型的对象.
这个概念还可以更加进一步的扩充, 一个被点燃的TNT, 实际上, 它也是一个实体(TNTPrimed对象).

当然, 与方块Block类类似, Entity拥有很多子类. 这其中最显眼的一个子类就是Player类了. Player类代表玩家, 每个在线的玩家都有一个Player对象. 其实也不难理解, 如果一个玩家下线了, 那么这个玩家对应的实体当然也会消失. 你也许会纳闷, 那如何去操作一个目前不在线的玩家的数据呢? 这是个比较复杂的问题, 需要分成多个情况来解决, 这里暂不赘述.

Location对象

任何一个坐标都可由一个Location代表.
常见的实体对象是Entity的子类,故都提供了getLocation方法,返回的Location对象代表着它们的坐标位置.
值得一提的是,如果应用getLocation获取实体位置,那么获取的位置是它的脚. 例如Player.getLocation()所获取的是玩家的脚的位置.
对于这些实体对象,如果想修改他们所在的坐标位置,Bukkit没有提供setLocation方法,而是提供了teleport方法. 通过teleport方法可以传送某个实体.

BlockLocation
Location中提供了getBlockLocation()getBlockX()getBlockY()getBlockZ()四个方法.
对于一个方块而言,其坐标的XYZ值均为整数,所以这些方法所获取的是此Location对应的最精确方块的坐标.
通俗的理解,可以认为获取的是将XYZ四舍五入后的坐标值.
getBlock()获取的此Location对应的最精确的方块的Block对象.

坐标运算
Location提供add(加)、subtract(减)方法.

两点间距离 Location提供distance方法,参数为另一个Location,返回值为double,代表两点间距离.
Location还提供distanceSquared方法,代表两点间的方块距离,遵循四舍五入.

物品

玩家手里拿着的东西叫物品. 物品的材质也叫Material.
特殊的是, 某些物品与其对应的方块Material不一致, 例如红石比较器.
红石比较器物品的种类是Material.REDSTONE_COMPARATOR, 而放置后的方块种类又分为Material.REDSTONE_COMPARATOR_ON(开启状态), Material.REDSTONE_COMPARATOR_OFF(关闭状态)两种, 红石比较器方块的种类不能用Material.REDSTONE_COMPARATOR来表示.

ItemStack用于反应一种描述物品堆叠的方式.
一个ItemStack的实例, 囊括了物品的种类(其对应的Material)和数量等信息.
例如, 玩家手中拿着三个苹果. 玩家手中的这三个苹果, 实质上是一个ItemStack, 它包括了这三个苹果的种类(Material.APPLE)、数量(3)与其他的一些信息.

事件系统

事件的概念

事件是服务器里发生的事. 例如, 天气的变化, 玩家的移动. 玩家把树打掉, 又捡起了掉落地上的原木. 这些都是事件.

事件分为可控事件和不可控事件. 其最大区别在于能不能取消(也就是能不能setCancelled). 不难理解, 玩家如果退出服务器, 这不能被取消, 它是不可控事件. 玩家的移动可以被取消, 它是可控事件.

事件的作用

利用BukkitAPI, 你可以监听事件, 事件触发时执行某些代码.
例如, 你可以监听玩家登录服务器, 玩家登录服务器后你可以执行某些代码.

那么, 如果你想写登录插件, 你需要监听玩家登录服务器的事件.
玩家进入服务器以后, 记录存储起来他的用户名. 等待玩家输入指令进行登录, 登录完毕以后去掉他的用户名.
然后再监听其他的各种事件(比如监听方块破坏事件), 如果这些事件被触发, 判断是哪个玩家触发的, 看看玩家用户名有没有存储起来, 如果有, 那么他没有登录, 那就把这个事件取消掉.

通过这样的例子可以发现, 事件是一个插件最重要的组成部分!

监听器

上面我们提到可以实现事件触发时执行某些代码. 实现这个目的的方法就是写一个监听器. 监听器实质上是一个实现了Listener的类, 其中包含一些带有@EventHandler注解的方法.

一个监听器大致是这样:

public class DemoListener implements Listener { //这是你定义的监听器类, 实现了Listener接口
    @EventHandler
    public void onPlayerMove(PlayerMoveEvent e) { 
        //比如我监听了玩家移动事件, 就应该这样创建一个这样的方法
        //带有Listener注解, 参数是一个PlayerMoveEvent类型的参数, 代表你要监听的是PlayerMoveEvent事件
        //一个方法只能监听一个事件
    }
}

监听器类创建完毕后, 还需要注册它才可以真正发挥作用.

命令系统

命令

MC中的命令是一个字符串, 用来实现游戏内高级功能.

在MC客户端中, 玩家将在聊天框内输入命令. 当且仅当在“聊天”内, 命令与普通的聊天内容的区别在于其内容的第一个字符是一个斜杠/.

对命令的代码解析

例如玩家依次输入了这些命令:

/abc
/abc test 1
/def create info username

依次分析, 第一种和第二种并不是两种命令, 区分不同的命令看紧跟斜杠的词是什么, 所以第一个和第二个本质上是同一个命令.

按照一个空格一个分隔的规律, 开头的一节为命令的名称, 第二个以及第二个以后的部分都是这条命令带的参数, 也就是输入这条命令的人想要传递的数据信息部分.
参数部分可以表示成一个String数组, 以第三个命令为例, 参数部分可以表示为:

args[0]: "create"
args[1]: "info"
args[2]: "username"

假如你想实现一个命令, 玩家必须输入参数, 参数第一项是固定的几种答案, 你完全可以对args做些if判断来实现.

if(args.length==0){
    //玩家没有输入参数
} else {
    if(args[0].toLowerCase().equals("固定答案1")){ //玩家输入了 /命令名 固定答案1 格式的命令
        //你想实现的功能
    } else if(args[0].toLowerCase().equals("固定答案2")){ //玩家输入了 /命令名 固定答案2 格式的命令
        //你想实现的功能
    } else {
        //玩家没有输入固定的答案类型
    }
}

权限系统

BukkitAPI提供了一套权限系统.
利用权限系统, 你可以实现限制有某个权限的玩家能输入某个指令、做某些事情等功能, 但没有这一权限的玩家却做不了.

权限一般是指一串字符串, 一般(最好遵守这样的格式)格式是插件名.功能名.某一个项目的名称.xxx构成, 需全为英文小写. 例如testplugin.block.place.
Player类有hasPermission方法可以检查某个玩家是否有某个权限.

配置API

交流系统(Conversation API)

需要注意的是, 虽然有这样的API, 但这一API在实际开发中使用频率很低.

如果你使用过QuickShop插件, 你可能会对这一功能有印象:
玩家创建了一个箱子商店, 当另一个玩家点击箱子方块前面的告示牌时, 在聊天区域会显示出商品的详情和价格, 并提示你直接在聊天区输入一个数字代表购买物品, 发送这个数字就可以购买了.
如果你细细琢磨一下这一功能, 其实你可以把这一过程看成一种对话, 你和插件可以直接在聊天区内进行交流:

插件: 你好, 你需要多少A商品?
你: 2
插件: 好的, 购买成功!

其实这就是一个Conversation了. 你可以把这样的一个对话过程做成一个Prompt(可以把这个理解成对话的模板), 然后在需要的时候依照这个Prompt生产一个Conversation, 并给一个玩家开始对话, 过程就像这样:

Player p = 玩家;
//一些生成Conversation对象的代码之后
Conversation c = 你根据Prompt生产出的Conversation对象;
c.begin(); //开始对话

Inventory

对于玩家背包、箱子里存放的所有ItemStack对象, 我们可以认为他们都储存在了一个Inventory对象里.
也就是意味着, 一个箱子、一个玩家都对应他们专属的Inventory对象, 用来储存它们存放着的物品.

服务器底层

我们把原版服务端部分(全部都在net.minecraft.server包内)叫做NMS. 把Bukkit部分的底层实现部分(也就是CraftBukkit部分, 全部都在org.bukkit.craftbukkit包内)叫做OBC.
有时候我们需要手工发送某些数据包来达到某些目的, 这时需要对底层代码进行操作. 通常不常操作OBC.