本帖最后由 berry64 于 2018-6-23 16:41 编辑


-------------------------------------------------------------------------------------------------
教程 X —— 在MC里也能吃鸡!?
第一部分:设计&加入系统
↑好尬
由于本教程是我的插件-SteadyShot的拓展开发教程,所以先厚颜无耻打一波广告:

如何吃鸡:打开物品栏拿出熟鸡肉右键 ——tada成功吃鸡
教程到此结束
本教程将依照我深(mo)思(ming)熟(qi)虑(miao)的思考顺序分步骤开发(所以会非常乱QwQ)




于是乎, 先从创建一个java工程开始这里由于我是导入的工程而并非jar包,但是效果差不多,
eclipse→右键→新建→java工程,之后右键java工程→属性→Java Build path 导入2个,分别是服务器主文件(我的是paperspigot 1.12.2) 以及SteadyShot jar (我的是工程文件)

P.S 我的拓展命名为MCBG(MineCraft BattleGrounds)

因为本拓展是已需要前置插件的插件写成的,所以理所当然的有个plugin.yml以及继承JavaPlugin的主类
主类里的onEnable我也加了一个检测前置插件的机制(虽然plugin.yml里面如果有写服务端会自动识别)以及在SteadyShot里面注册本竞技场的代码(详情请看SteadyShot发布贴)

本竞技场名为MCBG

创建新类:MCBGArena,继承net.berry64.SteadyShot.Arena.ArenaBase类 《-手动划重点
之后根据IDE提示自动添加方法,当然如果你的IDE没有提示也可以自己写


ok,编程暂时告一段落,我们来规划一下设计,这时候就要用到祖传的windows画图了
为了符合MC的方块,我们不用圆圈而用方块代替“毒”
P.S本来想加个步骤规划一下机器语言和实践方式,不过不太符合我风格所以就算了

于是乎! 我们开始写代码吧,先从玩家加入机制开始,ArenaBase已经定义好了minPlayers和maxPlayers变量,也帮我们从配置文件里面读取了,只需直接调用即可。稍微语言规划一下, 我们需要在edit里面实现设置主传送点和地图大小, 并且在玩家加入时判断玩家数量是否超过最大以及是否已经开始游戏,如果是return false 禁止加入。

这里我希望定义一个全局位置(大厅传送点)以及一个全局timer, 因为本游戏只会拥有一个使用中的计时器。
之后,我需要在addPlayer方法里写入这些东西:
- 确认玩家数量是否超额&确认游戏不在进行中
- 如果玩家是第一个,把竞技场状态从空设置到等待中
- 向本部以及全局玩家列表里添加玩家
- 开启倒计时
于是乎,代码这样:
  1. package net.berry64.SteadyShotAddons.MCBG;

  2. import java.io.File;

  3. import org.bukkit.Location;
  4. import org.bukkit.configuration.file.YamlConfiguration;
  5. import org.bukkit.entity.Player;
  6. import org.bukkit.event.Listener;
  7. import org.bukkit.scheduler.BukkitRunnable;
  8. import org.bukkit.scheduler.BukkitTask;

  9. import net.berry64.SteadyShot.Arenas.ArenaBase;
  10. import net.berry64.SteadyShot.Arenas.ArenaState;

  11. public class MCBGArena extends ArenaBase implements Listener{
  12.         Location lobbyLocation = null;
  13.         BukkitTask timer = null;
  14.         
  15.         @Override
  16.         public boolean startUp() {
  17.                 // TODO Auto-generated method stub
  18.                 return false;
  19.         }

  20.         @Override
  21.         public void shutDown() {
  22.                 // TODO Auto-generated method stub
  23.                
  24.         }

  25.         @Override
  26.         public boolean edit(Player editor, String[] args) {
  27.                 // TODO Auto-generated method stub
  28.                 return false;
  29.         }

  30.         @Override
  31.         public boolean addPlayer(Player p) {
  32.                 if(players.size() >= maxPlayers || state == ArenaState.IN_GAME)  //检测玩家数量&竞技场状态
  33.                         return false;
  34.                
  35.                 if(players.size() <= 0)  //更改状态
  36.                         state = ArenaState.WAITING;
  37.                
  38.                 listAdd(p);
  39.                 this.sendMessage("§6"+p.getName()+" §a加入了游戏,当前人数 §e"+players.size()+"§a/§6 "+maxPlayers);
  40.                 if(timer == null && players.size() >= minPlayers) {
  41.                         this.sendMessage("§a游戏将会在 §660 §a秒后开始");
  42.                         timer = new BukkitRunnable() {
  43.                                 int secsleft = 60; //一分钟
  44.                                 
  45.                                 @Override
  46.                                 public void run() {
  47.                                         for(Player p : players) {
  48.                                                 p.setLevel(secsleft);   //玩家经验条等级设置为倒计时器
  49.                                         }
  50.                                         if(secsleft <= 0) {   //到时间
  51.                                                 beginGame();   //private方法
  52.                                                 timer = null;    // 方便判断
  53.                                                 this.cancel();     //取消计时器
  54.                                         }
  55.                                 }
  56.                                 
  57.                         }.runTaskTimer(MCBGMain.instance, 0L, 20L);  //每20ticks(约1秒)计时一次
  58.                 }
  59.                 p.teleport(lobbyLocation); //传送至大厅
  60.                 p.getInventory().clear();
  61.                 return true;
  62.         }
  63.         //其他一些系统默认没有更改的方法就没有写上了
复制代码

到此,已经实现了入场传送&计时器

回看设计图,现在需要的功能就是设置地图四角,随机生成天空平台,以及给予玩家翅膀

先从设置开始,找到继承的edit, 用处理类似指令的方法处理,这里我会使用我自己的风格,不好看/不实用请谅解

地图四角不需要储存/读取y轴,只需X,Z即可,而且只需要2个点即可构成一个方块平面,配置文件大概这样吧:

  1. lobby:
  2. #bukkit自己直接储存location,将会包含X,Y,Z,世界和角度,这里是大厅传送点

  3. map:
  4.     world:  世界名
  5.     1:
  6.       X:
  7.       Z:
  8.     2:
  9.       X:
  10.       Z:
复制代码



这里为了方便操作,我们创建两个class第一个是CornerArea, 也就是通过两个角落生成的长方形面积
第二个是CenterArea,而这个就是以中心点和两个半径生成的长方形面积直接上代码:

  1. class CornerArea {
  2.         int ax,az,bx,bz;
  3.         
  4.         public CornerArea(int x1,int z1,int x2,int z2) {
  5.                 ax = x1;
  6.                 az = z1;
  7.                 bx = x2;
  8.                 bz = z2;
  9.         }
  10.         public void setCorner(Location loc, int point) {
  11.                 if(point == 1) {
  12.                         ax = loc.getBlockX();
  13.                         az = loc.getBlockZ();
  14.                 } else {
  15.                         bx = loc.getBlockX();
  16.                         bz = loc.getBlockZ();
  17.                 }
  18.         }
  19.         public void setCorners(int x1, int z1, int x2, int z2) {
  20.                 ax = x1;
  21.                 az = z1;
  22.                 bx = x2;
  23.                 bz = z2;
  24.         }
  25.         public int getBaseX() {
  26.                 return Math.min(ax, bx);
  27.         }
  28.         public int getBaseZ() {
  29.                 return Math.min(az, bz);
  30.         }
  31.         public int getSizeX() {
  32.                 return Math.abs(ax-bx);
  33.         }
  34.         public int getSizeZ() {
  35.                 return Math.abs(az-bz);
  36.         }
  37.         public int getRandomCenterX(int margins) {
  38.                 return getBaseX()+margins+(int)(Math.random()*(getSizeX()-(2*margins)));
  39.         }
  40.         public int getRandomCenterZ(int margins) {
  41.                 return getBaseZ()+margins+(int)(Math.random()*(getSizeZ()-(2*margins)));
  42.         }
  43.         public CenterArea getCenterArea() {
  44.                 return new CenterArea(getBaseX()+(getSizeX()/2), getBaseZ()+(getSizeZ()/2), getSizeX()/2, getSizeZ()/2);
  45.         }
  46.         
  47.         public void paintEdges(paintType t) {
  48.                 if(t == paintType.RED) {
  49.                         
  50.                 }
  51.                 else if(t==paintType.WHITE) {
  52.                         
  53.                 }
  54.                 else if(t == paintType.REMOVE) {
  55.                         
  56.                 }
  57.         }
  58.         enum paintType{
  59.                 RED,
  60.                 WHITE,
  61.                 REMOVE;
  62.         }
  63. }
复制代码
到这里应该很简单理解,就是以角落的长方形和附属的一些简易算法,之后是CenterArea

  1. class CenterArea {
  2.         int cx,cz,xr,zr;
  3.         
  4.         public CenterArea(int centerx,int centerz, int rangex, int rangez) {
  5.                 cx = centerx;
  6.                 cz = centerz;
  7.                 xr = rangex;
  8.                 zr = rangez;
  9.         }
  10.         public void setCenter(int x, int z) {
  11.                 cx = x;
  12.                 cz = z;
  13.         }
  14.         public void setRange(int rangex,int rangez) {
  15.                 xr = rangex;
  16.                 zr = rangez;
  17.         }
  18.         public CornerArea getCornerArea() {
  19.                 return new CornerArea(cx-xr,cz-zr,cx+xr,cz+zr);
  20.         }
  21. }
复制代码
这里应该更简单,因为CenterArea的目的只是为了方便计算,游戏的大部分操作都是在CornerArea里面进行的,所以这个class只要有能转换成CornerArea的方法应该就可以了
然后编辑地图部分:

  1. @Override
  2. public boolean edit(Player editor, String[] args) {
  3.                 if(args.length <= 0)
  4.                         editor.sendMessage("§c错误:请输入可以识别的指令");   //排除arrayindexoutofbounds
  5.                
  6.                 else if(args[0].equalsIgnoreCase("setlobby")) {
  7.                         lobbyLocation = editor.getLocation();       //免去设置后需要重读文件
  8.                         yml.set("lobby", editor.getLocation());   //bukkit可以直接储存location信息
  9.                         save();     //刚刚写的储存文件的方法,可以直接用yml.save(f)代替
  10.                         editor.sendMessage("§a成功设置 §6大厅");
  11.                 }
  12.                
  13.                 else if(args[0].equalsIgnoreCase("setWorld")) {
  14.                         //设置世界
  15.                         if(args.length > 1) {
  16.                                 yml.set("map.world", args[1]);
  17.                                 world = args[1];
  18.                                 editor.sendMessage("§a成功设置,如果输入的世界不存在将会报错");
  19.                         } else {
  20.                                 yml.set("map.world", editor.getWorld().getName());
  21.                                 world = editor.getWorld().getName();
  22.                                 editor.sendMessage("§a成功设置为当前世界");
  23.                         }
  24.                         save();
  25.                 }
  26.                
  27.                 else if(args[0].equalsIgnoreCase("setmap")) {
  28.                         if(args.length > 1) {
  29.                                 int loc = 1;
  30.                                 
  31.                                 try { loc = Integer.valueOf(args[1]); } catch (NumberFormatException e) {editor.sendMessage("§c你必须输入一个数字!"); return true;}
  32.                                 if(loc <= 0 || loc >= 3) {editor.sendMessage("§c数字必须为1或2"); return true;}
  33.                                 
  34.                                 gameboard.setCorner(editor.getLocation(), loc);
  35.                                 yml.set("map."+loc+".x", editor.getLocation().getBlockX());
  36.                                 yml.set("map."+loc+".z", editor.getLocation().getBlockZ());
  37.                                 save();
  38.                                 editor.sendMessage("§a设置成功");
  39.                         } else {
  40.                                 editor.sendMessage("§c使用方法: §esetmap [1/2]");
  41.                         }
  42.                 }
  43.                
  44.                 else {
  45.                         editor.sendMessage("§c错误: 未识别的命令");
  46.                 }
  47.                 return true;  //信仰:永不return false
  48. }
复制代码
于是乎,又完成了一个功能,下一个,在loadArena里面写上读取世界,大厅,以及几个点的位置(启动时加载)
,直接代码:


  1. @Override
  2. public boolean loadArena() {
  3.                 lobbyLocation = (Location) yml.get("lobby");    //大厅
  4.                
  5.                 world = yml.getString("map.world");      //变量world赋值(刚刚定义的一个string)
  6.                 int x1 = yml.getInt("map.1.x"); int z1 = yml.getInt("map.1.z");
  7.                 int x2 = yml.getInt("map.2.x"); int z2 = yml.getInt("map.2.z");
  8.                 gameboard = new CornerArea(x1,z1,x2,z2);
  9.                 return true;
  10.         }
复制代码

如果不理解请返回目录去看之前教程

本部分最后,在刚刚定义的beginGame方法内写上随机生成天空平台&给予所有玩家翅膀
  1. private void beginGame() {
  2.         state = ArenaState.IN_GAME;       //设定状态为游戏中(至此游戏已经开始)
  3.         int x = x1+(x2-x1)/5+(int)(Math.random()*(x2-x1-(x2-x1)/5));      //x2-x1获取竞技场宽度,随机生成但不太靠边缘,距离边缘1/5距离
  4.         int z = z1+(z2-z1)/5+(int)(Math.random()*(z2-z1-(z2-z1)/5));      //同上,这部分可能有点复杂,仔细想想就好了
  5.         
  6.         //在y=175生成一个5*5的玻璃平台
  7.         World w = MCBGMain.instance.getServer().getWorld(world);
  8.         for(int i = x-2; i <= x+2; i++) {
  9.                 for(int k = z-2; k <= z+2; k++) {
  10.                         w.getBlockAt(i,175,k).setType(Material.GLASS);
  11.                 }
  12.         }
  13.         
  14.         for(Player p:players) {
  15.                 p.teleport(new Location(w,x,176,z));  //传送所有玩家
  16.                 p.getInventory().setChestplate(new ItemStack(Material.ELYTRA, 1));  //给予玩家翅膀并装备到胸甲栏
  17.         }
  18. }
复制代码


至此本部分已经完成,下一部分: 游戏机制的设计&实践, 敬请期待教程10.2
如果有其他建议/意见 请在本帖回复

P.S. 人气不要钱,给了又不会少块肉