因为最近心血来潮在写小游戏插件,可以理解为彩虹六号:异种的MC复刻。
于是就要设计一套游戏控制流来引导玩家完成任务等操作。
然后我就写了一套挺烂的控制流(
主要部分
Game - 游戏实例
每个 Game Object 代表了一个游戏实例,下面所讲的一切都发生在 Game Object 中。
虽然一个 Spigot 实例只跑一个 Game,但是这样设计如果以后要改一个 Spigot 实例跑多个 Game 的话就不用重构插件了。
Module - 实例模块
一个 Game 中可以存在多个 Module。
每个 Module 在 Game 开始之后遵从 Minecraft 的游戏世界循环(也称 tick)而tick。并自动注册监听器。
Stage - 游戏阶段
在上面我们说了 Module 是游戏开始后就一直持续运行,并且可以存在多个同时运行的。
而 Stage 则恰恰相反,同时只有 1 个 Stage 可以执行,但是一个 Game 实例可以注册多个 Stage —— 每个 Stage 会按照注册的顺序执行。
当一个 Stage 结束后,则立刻被丢弃,不再使用。
Quest - 游戏任务
每个 Stage 中会包含多个 Quest,每个 Quest 有着自己的监听器,并且随着 Stage 的 tick 而被一同 tick。
Stage 在 tick 其包含的所有游戏任务的时候会检查他们的返回值,如果返回了一个 true,则代表该任务已结束(不管是完成还是失败,反正就是结束了)。
当所有 Quest 都返回 true 的时候,Stage 也会返回一个 true,此时 Game 的 tickStages()
方法会调用 nextStage()
方法切换到下一个 Stage 继续执行。
不过和 Stage 不一样的是,即使 Quest 返回了 true,它在 Stage 的生命周期中也会持续 tick,提供处理任务完成之后又触发了失败条件而失败的功能。
流控制
执行流
每个 Game 实例中都有个 Deque(双端队列),用以控制 Stage 的执行。
当 Game 的 start()
方法被调用时,游戏就进入 running 状态,此时 Game 实例会被注册到 Bukkit 调度器,周期化执行 onTick 方法。
同时,在 Deque 头部的 Stage 元素会被 peek 出来并跟随MC主循环开始 tick。
每个 Stage 都会先执行 startTick()
方法,然后在生命周期内持续执行自己和其包含的Quest的 tick()
方法。
当 Stage 返回 true 后,生命周期结束。使用 Deque#poll
取出这个 Stage,调用一次 endTick()
方法收尾,随后丢弃。
下一个新的 Stage 来到双端队列的头部,调用新的 Stage 的 startTick()
方法开始其生命周期,然后其余工作交给主循环。
/**
* 切换到下一个 Stage
*/
public void nextStage() {
Stage stage = stageSequences.poll();
if (stage == null)
return;
stage.endTick();
stage = stageSequences.peek();
if (stage == null)
return;
if (!stage.isPaused())
stage.startTick();
else
stage.resume();
}
/**
* Tick stages
*
* @param firstTick 是否为首次tick
* @param lastTick 是否为最后一次tick (最后一次tick可能失败,因为游戏一般退出是因为 stage 执行完毕,这种情况的lastTick由nextStage执行)
*/
private void tickStages(boolean firstTick, boolean lastTick) {
Stage stage = getTickingStage();
if (stage != null) {
if (firstTick) {
stage.startTick();
return;
}
if (lastTick) {
stage.endTick();
return;
}
if (stage.tick())
nextStage();
}
}
总结一下:
开始:Game#start() -> Stage#startTick() -> Quest#startTick()
执行:Bukkit Scheduler -> Game#tick() -> Stage#tick() -> Quest#tick()
退出: Bukkit Scheduler -> Game#tick() -> Stage#tick() -> Quest#tick() -> return true => Stage#tick() - return true => Game#tick() -> Game#nextStage() -> Deque#poll() -> Stage#endTick() -> Quest#endTick() => Stage#endTick() => nextStage -> Deque#peek()#startTick() -> NewStage#startTick() -> NewQuest#startTick() ......
流插入
还是以彩虹六号:异种举例,我们知道当干员倒地后,REACT静滞泡沫就会激活,随后你就不能动了。
游戏会立刻插入一个最高优先级的任务 “不放弃任何人”,此时原有的任务会被打断,你的队友需要先把你安排好才能继续执行任务。
所以,我也参照为 Stage 增加了几个新方法 Stage#pause
, Stage#resume
和 Stage#isPaused()
。
当我们需要打断这个任务的时候,先调用 Stage#pause
,这会注销掉此 Stage 和其下所有 Quest 的监听器。
然后我们直接将新的 Stage 在 Deque 的头部位置插入,并调用一次 Stage#startTick
,随后交给主循环开始新的 Stage 的 ticking。
不过,因为插入机制的引入,我们还需要改造一下之前提到的 Game#nextStage
方法,使其检查判断 Stage#isPaused
的结果,如果为 true,则切换到这个 Stage 的时候不执行 Stage#startTick
而是转而执行 Stage#resume
将监听器注册回来。
/**
* 插入一个 Stage 并暂停当前 Stage,先执行新的 Stage
*
* @param stage Stage
*/
public void insertSwitchStage(@NotNull Stage stage) {
if (getTickingStage() != null)
getTickingStage().pause();
stageSequences.offerFirst(stage);
stage.startTick();
}
当然,也可以通过往 Deque 追加新 Stage 延长游戏流程。
流终止
当所有 Stage 被执行完毕后,我们就可以视为 “这场游戏的所有阶段都已结束”。
此时 Deque 内为空,Game#nextStage()
会取不到任何新的 Stage,此时调用 Game#stop()
方法进行游戏结算即可
最后的效果
大概你可以这么理解:
Game:
Modules:
- 禁用友伤 # 不同的Modules,在游戏期间持续运行,可同时运行多个
- 锁定黑夜
- 队友高亮
Stage:
阶段-猎杀古菌: # 不同的阶段
- 击杀超多低语者 # 每个阶段需要完成的任务
- 击杀精英刺袭者
# 上一阶段所有任务完成 Stage 生命周期结束,转入下个阶段
阶段-追踪孵化囊:
- 定位孵化囊
# 如果队友在这里趴窝了,阶段暂停,插入新的阶段,完成后在从此处继续执行
- 安装追踪器
阶段-前往下个任务区域:
- 到达指定地点
阶段-拯救大兵Jager:
- 定位 Jager
- 和一堆古菌战斗
- 送 Jager 回家(大雾
# 所有 Stage 结束,游戏也随即结束
结尾
最近在看的东西挺多的,等这套控制流完全施工完毕后大概就去做生物和方块刷新了。
到时候会涉及处理蔓生菌蔓延啊,古菌生成等等的东西,估计还可以在水一篇文章(笑。
用 Stage 的主意来自 @hikarilan,然而经过贺兰的讲解,我除了听懂了 Stage 只能用一次就扔掉之外什么也没听懂。