你一定是玩过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所提供的各个体系和系统.
本章仅仅是对各个系统做大概的描述. 后续会详细讲述各个部分的内容.
想也不用想, 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枚举量的具体内容也能查询到. 后续将不再赘述.
在MC中, 所有的生物, 例如一只羊, 乃至一个僵尸, 又或者是玩家, 都是生物, 他们都是Entity类型的对象.
这个概念还可以更加进一步的扩充, 一个被点燃的TNT, 实际上, 它也是一个实体(TNTPrimed对象).
当然, 与方块Block类类似, Entity拥有很多子类. 这其中最显眼的一个子类就是Player类了.
Player类代表玩家, 每个在线的玩家都有一个Player对象. 其实也不难理解, 如果一个玩家下线了, 那么这个玩家对应的实体当然也会消失. 你也许会纳闷, 那如何去操作一个目前不在线的玩家的数据呢? 这是个比较复杂的问题, 需要分成多个情况来解决, 这里暂不赘述.
任何一个坐标都可由一个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, 但这一API在实际开发中使用频率很低.
如果你使用过QuickShop插件, 你可能会对这一功能有印象:
玩家创建了一个箱子商店, 当另一个玩家点击箱子方块前面的告示牌时, 在聊天区域会显示出商品的详情和价格, 并提示你直接在聊天区输入一个数字代表购买物品, 发送这个数字就可以购买了.
如果你细细琢磨一下这一功能, 其实你可以把这一过程看成一种对话, 你和插件可以直接在聊天区内进行交流:
插件: 你好, 你需要多少A商品?
你: 2
插件: 好的, 购买成功!
其实这就是一个Conversation了. 你可以把这样的一个对话过程做成一个Prompt(可以把这个理解成对话的模板), 然后在需要的时候依照这个Prompt生产一个Conversation, 并给一个玩家开始对话, 过程就像这样:
Player p = 玩家;
//一些生成Conversation对象的代码之后
Conversation c = 你根据Prompt生产出的Conversation对象;
c.begin(); //开始对话对于玩家背包、箱子里存放的所有ItemStack对象, 我们可以认为他们都储存在了一个Inventory对象里.
也就是意味着, 一个箱子、一个玩家都对应他们专属的Inventory对象, 用来储存它们存放着的物品.
我们把原版服务端部分(全部都在net.minecraft.server包内)叫做NMS. 把Bukkit部分的底层实现部分(也就是CraftBukkit部分, 全部都在org.bukkit.craftbukkit包内)叫做OBC.
有时候我们需要手工发送某些数据包来达到某些目的, 这时需要对底层代码进行操作. 通常不常操作OBC.