本帖最后由 pca006132 于 2017-9-29 18:14 编辑

-------命令进阶-------
第六章 命令实战

本文首发于命令进阶教程全集
使用CC BY-SA-NC 4.0协议,转载请注明本页面链接


本章节将会介绍一些实际系统里常用的做法、想法,讲解构造系统时的思路以及需要考虑的东西。

并且我们会**一大堆例子。如果读者是一个命令的新手的话,可以先看看例子 6.3.1 - 6.3.5,然后先跳过后方的例子,之后再看也不迟。(因为后面的例子难度高了不少)
作为移植页面,格式可能会有偏差,请到教程全集中获得完整体验。
本页面可能随着版本推进而过期,内容以全集为准。




游戏内物件
我们需要明白游戏内物品,如实体(entity)、方块(block)、物品(Item)以及显示的用处及大致特点,才能写出优秀的命令系统。它们就是我们的主要操作目标。
我们不需要严格定义它们,只需要根据它们对我们的用处来分辨它们就好了。


实体
能被目标选择器(target selector)中的 @e 选择到的,我们就会当作是实体处理了。
一般来说实体能够和其他物件互动、有坐标,并且需要储存大量资讯。比如玩家、生物、矿车等等都是一个实体。
而大部分方块也不是实体,因为它们储存的数据有限。但部分储存大量数据的方块,如箱子,则属于方块和实体的范畴,我们称之为方块实体,那些方块将会在下一部分详细讨论。


实体数据
一般来说实体都有三种数据:ID、UUID以及其他数据。

ID:实体都有它们的种类,如 playerarea_effect_cloud 等。(从1.11之前的版本过来的朋友请注意,1.11把实体名称修改为以下划线( _ )分开单词、全部小写。然而也有部分实体名称出现严重更改,如 FallingSand 被改为 falling_block ,请自行查阅wiki)

注意,有两个特例。
闪电有实体ID,但它不是实体。
鱼钩没有实体ID,但它是实体。(我们一般从名字检测,名字和语言有关,英文时为unknown,中文时为未知)




UUID: 就是一个实体的标识(Minecraft除了实体之外也有部分地方使用UUID),在游戏内应该是独一无二的。UUID是游戏追踪实体的主要方法,因为实体名称可能会重复。
UUID其实就是一个128bit,32个16进制的数字。
假设我们有一个UUID为 50CB2286-33E6-90F6-598C-510538629432
其UUIDMost就是 50CB2286-33E6-90F6 ,转为十进制就是 5821784903098208502
其UUIDLeast就是 598C-510538629432 ,转为十进制就是 6452621448977749042

注意:不是直接转,我们得考虑补码的概念(甚至需要补零)。因为十进制的时候容许负数,十六进制的时候不容许负数。
如果不是特别需要的话其实可以使用生成器...这些东西就让编程的去头疼吧√

如果UUID发生碰撞(出现的几率微乎其微,一般为人手造成的),会导致很严重并且难以靠游戏自行修复的Bug,如无法找到重复UUID的实体等,需要玩家自行使用外部软件修改游戏存档才能解决。




实体的其他资料都储存在其NBT内(其实全部数据都储存在NBT内,就是这些比较特殊特别提醒。)


实体特点
  • 优点
    • 能轻易被选择、根据特定条件过滤(这是最重要的,能使用选择器)
    • 能储存大量信息(NBT、记分板变量、Tag、队伍)
    • 坐标较精确(小数,即使该实体本来是自动调整到方块位置也可以通过骑乘的方式绕过该特性)
    • 能在同一点放置大量实体
    • 坐标限制较少(可以在Y为负数或大于256的地方生成实体,不过还是比较少在Y为负数的地方生成,因为可能会掉入虚空死掉)
  • 缺点
    • 一般实体太多会导致严重卡顿(特别是该实体能被看见时)
    • 没法修改模型


分类
实体下还有很多不同分类的,那些分类大多都有特别的数据或表现

  • 实体
    • 生物(Mob)
      • 可繁殖
      • 可驯化
      • 怪物
        • 不死类怪物
        • Boss(或许同时是不死类怪物)

    • 移动中方块/可移动方块(平滑移动,如掉落方块及TNT)
    • 投射物
    • 其他...

对于生物,它们能够有状态效果(Status effect)及属性(Attribute)。

不死类怪物、Boss这些一般指影响实体的行为,而不影响其数据。然而行为对实体来说也是很重要的,比如不死类怪物的治疗、伤害效果是完全相反的。


状态效果
状态效果能给予一些一只生物不同效果。

每个效果由三部分组成:

  • 效果ID,决定效果的作用。
  • 效果倍率(=等级-1),决定效果的强度。然而如果是128-255,则会当作等级 -127 - 0。故此128开始的效果可能会出现反效果(但也不一定),需要多加小心。而250开始的效果可能是非常弱的某些效果,也可以多加测试。
  • 时间,以秒为单位。注意如果时间很长的话会显示为 **:** ,然而这并不代表无限长时间,时间还是一样只是显示不了而已。
一只生物能同时拥有多个效果,即使那些效果是完全相反的。
然而,它不可能同时拥有多个相同效果(即使是不同等级),如果新的效果倍率大于等于当前效果倍率,将会把当前效果覆盖(比如新的效果是255倍率,时间为0,则会消除当前的那个效果)。

注意,虽然实体有 ActiveEffects 的NBT,然而我们并不建议向那个NBT进行赋值,因为部分效果是和属性相关的,修改 ActiveEffects 对那些效果并无任何作用。



属性
属性定义了生物的不同属性,如攻击强度、检测敌人范围、血量等。
属性由两部分组成,包括属性基础(base)及修改器(modifier)。

属性基础就是生物基础的属性,直接写一个数字(浮点数)决定。
至于属性修改器则可以修改属性(增减属性数值)而不需要修改基础属性,并且能够通过物品为生物加上。修改器包括以下几个数据:

  • 名称。基本上是没用的
  • AttributeName。修改啥属性
  • Amount。修改的数值。
  • Operation。如何修改。
    • 0为直接加上指定数值,最先计算所有Operation为0的modifier
    • 1为乘法,把属性数值(经过Operation为0的modifier的修改后的数值)* (1+n),而n为所有对此属性的Operation为1的modifier的数值的总数(类似计算单利息)。这在计算完所有Operation为0的modifier之后计算。
    • 2为乘法,每个modifier都会把属性 * (1 + k),k为当前modifier的数值(类似计算复息)。这在计算完所有Operation为1的modifier之后计算。
  • UUIDLeast及UUIDMost。识别每个modifier的UUID。参见上方UUID部分。冲突了也会出问题。
  • Slot。准确来说这不算是modifier一部分,这是物品里面的AttributeModifiers里会决定的。指定后,只有当物品被放进某个格子才会触发modifier。
参考:https://minecraft.gamepedia.com/Attribute
注意,和血量有关的一般都比较多bug。


方块

资料
ID,指定方块的种类




方块状态(block state),储存方块的一些额外资料,如活塞的朝向 facing=top 、活塞是否已经伸出 extended=false 等。一个方块可以同时有几个方块状态(这也是很常见的情况),之间以 , 间隔,如 facing=top,extended=false
检测的时候可以以 * 代表不检测方块状态。

我们不建议使用方块元数据(Metadata)




NBT。部分方块,如箱子等容器,需要储存额外的资料(因为方块状态不足以储存大量资料)。它们就是方块实体(block-entity),会使用NBT来储存那额外的资料。


特点
  • 不能与其他方块处于同一个坐标(两块台阶会变为一块双台阶,故不算两个方块占据同一个坐标,其他特殊方块亦如是)
  • 不能横跨多个坐标(活塞推出后,推出的部分为活塞头,分别为两个方块,只是破坏一个会导致另一个同时被破坏)
  • 从y=0到y=255之间(并且在世界边界,即worldborder,之内)都是方块("没有方块"其实就是空气方块)
  • 能够通过资源包修改方块模型(Block model), 来修改方块的样子(实体无法做到)。相应的,模型越复杂就需要越多时间渲染该方块,因此过多复杂模型的方块会导致卡顿。
  • 方块也会对方块更新作出反应,如检查自己能不能在这环境存在、维持原貌等(如沙子在下方没有支撑方块时会掉下)。这方面相信红石玩家会有更多研究,请查阅红石教程,本文就不多讲了。
  • 优缺点基本上就是实体的相反...

方块更新
因为方块的某些性质会受到相邻方块的影响,于是方块被替换时(方块ID或方块状态的变化,NBT变化不包括在内),会引发相邻6个方块的更新,如果某个方块在更新中产生了变化,会把更新传递下去。

这特性在系统里有时候也十分重要,比如我们不能以 fill 命令填充一大片地狱传送门,因为fill命令是逐个方块进行替换,替换多个连续方块时会产生方块更新,而地狱传送门不能独立存在,故此会马上消失。但是我们能够通过 setblock 命令放置一个独立的地狱传送门,因为我们只放置一个方块是只会更新旁边的方块,而不会更新自身的(如果旁边的方块没有因此而发生状态改变)。

相应的,有时候我们希望手动触发更新,这情况下我们会使用范围 clone 命令,使用 force 模式,强制替换到原地,触发方块更新的同时亦不更改任何一个方块。

如果希望知道更多有关方块更新的知识,请自行寻找红石方面的教程或者是相关玩家询问,他们对此比较多研究。


物品
物品只会存在于实体/方块NBT(也就是存在于背包/储存格内),或者是以掉落物形式存在(这是一种实体)。


资料
ID,即物品种类,如 minecraft:stoneminecraft:stone_sword 。部分方块的物品状态ID与方块ID相同,然而也有部分是不同的,因此得小心分辨。也有部分方块是没有物品的,请小心。




数量(Count),因为相同物品能够堆叠,因此数量是十分重要的。不同物品最大堆叠数也不同,有些是1个,有些是16,有些是64,不能一概而论。不过通过修改NBT可以一次过弄超过最大堆叠数量的物品。




数据值(Damage Value/Data Value),范围为-32,768至32,767,整数。最初用于记录工具的磨损程度(0为全新,数据值+1为耐久度-1),但已经被用于储存别的数值(一般是物品种类),如羊毛颜色、铁砧耐久等。
然而需要注意的是,即使该物品是方块的物品状态,这数据值的目的也是储存方块的不同状态,数据值也未必与方块元数据一样,如铁砧便是一个好例子。故此,必须区分数据值及方块元数据两个概念。

简单点理解就是,数据值和方块元数据不同。




格子编号(Slot ID)。这个数值用作储存物品所在的格子编号,掉落物状态时物品不存在此数值。


至于小箱子则从左上开始为0,右一格为1,如此类推。
大箱子则为两个小箱子的组合,一个负责上半部分物品,一个负责下半部分物品。和小箱子的表现类似,但是得分别设置两个方块才可以修改上半及下半。




其余数据。其实物品所有数据都会放在NBT里( tag 里,详细可以回去看看NBT实战部分),其余数据亦如是,可以储存物品的其他资料,如显示的名称、介绍、附魔等。


用途
  • 常见游戏内物品的功能,如药水、装备等等
  • 为玩家添加buff (基于附魔或Attribute Modifier)
  • 容器内GUI (界面),因为命令能检测物品所在的格子。
    此外,亦能通过修改物品模型(Item model)的显示(display)中的 gui 选项,让物品看起来更像一个控件(control)。
  • 自定义显示。
    物品模型能分别对不同数据值的工具设置模型,故此能自定义大量模型。
    通过如刷怪笼等方法甚至更能做到大量的自定义方块材质。
注意: 不是只有物品才能让生物手持的。如末影人手持的实际上是方块。


显示
本文说的显示,就是一些只有玩家才能看到、听到,并且无法与游戏内其他东西进行互动的物件(GUI的话倒是能和玩家互动的,不过那也算作显示了)。


类型
  • 聊天栏信息(不算clickEvent的话)
  • Title信息(在屏幕中央的或快捷栏上方)
  • 声音
  • 粒子(particle)
  • 模型
  • 用户界面等等...

用途
这些显示元素主要在地图及原版模组内会大量使用,以提高用户体验。由于用处十分广泛,可以参考后方资源包部分,以及询问有地图制作经验的玩家。


命令系统测试及调试
命令系统测试(Testing)及调试(Debugging)是一个非常复杂的话题...

为了让一个命令系统不会出错(Bug),或者把错误减少到一个可接受的程度,我们需要进行测试或者检查。检查出问题了我们就需要调试以解决问题(虽然我认为重写是比较方便快捷的方法,如果那bug不是我造成的话)。

其实我也没太多可以讲的,毕竟这很需要天分(bug体质和特别聪明的脑袋)以及经验。只能讲一些比较常见的思路及错误,以及一些能够尽量避免发生那些错误的办法。


测试
地图的测试就没啥好讲的,把地图拿去玩几回看看能不能玩出bug呗。

上面其实准确来说也是没错的,然而我们可以更仔细的检查。


时序问题
我们的地图大多依赖于高频来不断执行一些操作,这代表了我们的命令之间是有时序分别的,这可能带来时序方面的问题。

模块(一组用作某个功能的命令)有两种,一种是独立模块,一种是有依赖的模块。
前者是一个模块做完整个完整的功能,故此一般对时序要求不大。
后者为依赖别的模块做完某个功能,故此必须仔细检查时序。

比如之后的熔炉过热系统就是一个独立模块的例子。里面所有功能都写在一个很简单的模块里,不依赖于别的模块。这种模块假如是一些针对玩家行为的功能,比如说是  当玩家吃了块饼干,就怎样怎样 ,这种情况可以尝试把模块扔给进度进行调用。

而之后的技能系统就是一个有依赖的模块的例子。里面分为几个模块,比如说技能事件依赖于道具触发模块,冷却补给依赖于道具触发模块。这种情况就需要小心检查其依赖性,避免发生一些时序问题。


多目标问题
比如有一个命令系统是:

  1. # test:item

  2. # 测试石头是否存在
  3. testfor @e[type=item] {Item:{id:"minecraft:stone",Damage:0s}}
  4. # 假设我们的执行者的stats变量分数=SuccessCount,成功时就干掉这物品
  5. execute @s[score_stats_min=1] ~ ~ ~ kill @e[type=item]
复制代码
这命令没问题么?
其实有很大的问题,这问题就是,当场地上有多个物品,并且不是全部都是石头的时候。

我们来模拟一次,当场地上有一个石头物品的时候,那 testfor 命令就会成功。 stats 分数就会为1。
然后我们就会杀掉所有物品。
那么,如果还有其他物品咋办?结果就是和石头陪葬。




这例子展示了一个常见的问题:多目标的问题。我们只是检查某个特地的东西是否存在,然后却把所有同类的东西全部进行某个操作,当还有其他东西同时存在时就可能导致错误操作。简单来说就是,我们限制好我们的目标。

我们处理可能出现多个目标(包括玩家)的情况时,检查条件是否成功应该改为添加标签/更改分数等。
比如上方的命令可以改为

  1. scoreboard players tag @e[type=item] add stone {Item:{id:"minecraft:stone",Damage:0s}}
  2. kill @e[tag=stone]
复制代码

很编程的测试
地图的系统一般都会分作几部分(什么?你说你没分?那么不是你的地图太小就是你的做法不行了),我们一般会逐个功能部分进行测试(这个我们叫作单元测试,unit testing)。

单元测试让我们把大型的系统分拆作小部分测试,更容易渗透进不同部分进行测试,发现隐藏bug。然而,即使我们把所有系统都进行单元测试了,这代表我们的系统没有bug么?

当然不是了,我们整合不同系统的时候也可能会出现问题(比如顺序问题),这时候我们就需要集成测试(Integration testing)了。

简单来说就是...整个系统来测试。

这些我也不太懂,你们上网自己找找吧


归类、边缘及错误情况
会编程的基本上能跳过这部分了
别看,真的别看,我不想被你们揍

我们如果要测试一个命令系统是没问题的,我们需要准备测试数据。
然而数据范围那么大,我们难道是随便扔一堆数据进去?还是全部都测试了?

当然不可能了,我们自然是不能全部都测试了。然而选择数据的时候也不是随便选择的,我们需要把情况归类。以某些条件为分界线,分出不同的类别,还有那些条件本身作为边缘情况。

那么错误的情况呢?不需要理会么?当然不是。我们做系统的需要假设一件事(而且经常是事实):

玩家/用户都是白痴,而且他们不会看 玩家须知

所以我们还得弄进一些错误情况来测试系统能不能正常处理。




以一个输入考试分数的系统为例子,0-59分就 say 你这学渣哈哈哈 ,60-79分就 say 哎哟你竟然合格了 ,80-89分就 say 这次还不错 ,90-100分就 say 大神受我一拜

正常情况有啥呢?

  • 1-58分随便拿一个
  • 61-78分随便拿一个
  • 81-88分随便拿一个
  • 91-99分随便拿一个
然后边缘情况

  • 0
  • 59
  • 60
  • 79
  • 80
  • 89
  • 90
  • 100
还有错误情况

  • -1
  • 101
  • bla bla bla
  • (啥也没有)
对于这些我们一般还是直接脑补就算了,没啥必要开游戏。


游戏Bug
游戏的特性真的让人欲罢不能,即使你写得很正常,有时候事情还是会出问题。

一些比如是 进度(Advancement)、判据(Criteria)、命令统计(Stats)、NBT(特别是Attribute相关),经常出现特性。使用的时候记得先进行独立测试

一句总结:你们要学会原谅Mojang。


命令输出
有时候错误还是能从输出中略窥一二的,所以测试小模块时不必关掉命令反馈。

如果发现出错但无法定位问题时也可以使用命令输出寻找,或使用以下技巧:

挂着记分板的显示位置显示分数(当然就是看看那些分数有没有问题啊)。
甚至会使用 saytellraw 命令显示出某些执行期间的数据,让我们看看有没有错。(如果没记错这好像叫echo checking?)


模拟多人
一个人的时候怎么测试多人系统?

如果那多人系统不涉及玩家专有的功能,而是一些逻辑相关的东西,则可以使用普通实体充当玩家。

如果真的是多人系统,那你可以开个盗版服务器,然后开几个客户端进入测试。这也是没办法之中的办法了。


找其他人
其他人没有制作者的偏见,是比较容易发现一些隐藏的问题的。
至于一些常见问题,或许找着个大佬一眼就能看出来了。

所以不耻下问还是很重要的。


找UP主
不知道为什么,我们发现很多UP主都是Bug体质。

找UP主玩或许能够发现更多错误,因为他们不会有地图制作者的偏见或者预设,能够更深入测试。(不看简介直接开始者更佳)


能够接受的错误
部分功能是很难用命令系统完美实现的,总是有一些精准度问题的。
比如说向着玩家朝向发射东西,锁定哪根箭是玩家发出的等等。虽然可以通过一些方法(前者则穷举大量角度,后者则加上大量限制),然而理论上还是有精准度问题,还是有可能出现错误的。
针对这些情况呢,我们会选择接受,选择原谅。因为我们真的没有更好的方法了。

不过,在实现那些功能前你们需要问一下自己两个问题

  • 这精准度问题真的很影响用户体验么?
    如果没啥大问题的话就不管好了。
  • 这精准度问题能通过穷举/加入其它限制解决么?如果是穷举的话,穷举量在1000以内么?
    太多或者没法搞的话我们的建议就是放弃这个功能。因为命令真的有其限制,不是没有限制的。

常见错误
这里是一些常见的错误...

解决错误的最好方法其实是一开始就不犯这个错误。

  • 写错字,如 redTeam 写成 radTeamredTaem 。或者是把 I (大写的 i )认成 l (小写的 L )。
    解决方法:使用自动补全或者是命令检查系统。这绝对不是广告
  • 命令参数次序错误。比如是 effect 命令里的倍率和秒数错误。这就真的没法方便的找到错误了...一开始就应该看着wiki写。
  • 格式错误,如NBT转义错误,JSON忘记了 "" ,括号不平衡等等。建议就是一开始写好一点。
    括号方面建议先输入一套括号然后再输入内容。
  • 选择器错误,如多个相同的参数,或把 score_xxx= 误解成检查分数等于,其实真正的意思是分数小于等于。
  • 忘记初始化,如忘记添加记分板变量,或者 stats 命令忘了初始化分数,把没有分数当0分等。
  • 记分板分数 operation 命令错误。如把“把每个实体的a分数赋值到其b分数”写成 scoreboard players operation @e b = @e a 。其实应该得使用 execute 命令逐个实体执行赋值。
  • 错误预测,如Attribute实际执行和预测不一样。
  • 命令顺序错误。(常见于模块整合时,如先reset了分数再使用分数的数值,当然出问题了。)
  • 游戏特性。这真的没办法。或许你可以选择多看看 Bug列表https://bugs.mojang.com/projects/MC/issues/
常见错误就只能列到这里了,有啥常见的记住跟我们讲,我们之后加上。


命令实战
本章节将会介绍一些实际系统里常用的做法、想法,讲解构造系统时的思路以及需要考虑的东西。

并且我们会**一大堆例子。如果读者是一个命令的新手的话,可以先看看例子 6.3.1 - 6.3.5,然后先跳过后方的例子,之后再看也不迟。(因为后面的例子难度高了不少)


熔炉过热
为什么熔炉能够无限制燃烧?为什么熔炉不会过热(Overheat)?
好吧,我们想个办法让这玩意更真实一点  。

// 明明是更扯淡一点!


目的

设定目的
一个系统必定有他存在的目的,而
以下是我当初在The FORTUNE这个坑里面写的关于熔炉的过热的设定,仅用作参考:

  • 燃烧50秒后开始冒黑烟
  • 燃烧60秒后开始冒更多黑烟+迸出火星
  • 燃烧70秒后直接爆炸
  • 熔炉停止燃烧的时候渐渐冷却

实际目的
上面列举的都是很简单的判定。我们再把这些目的细分,那么实际上需要实现的就是:

  • 检测熔炉开始燃烧
  • 检测熔炉停止燃烧
  • 让每一个熔炉都在自己燃烧的时间达到一定值的时候执行操作,即支持多个熔炉同时运行
在设定的目的基础上明确了实际的目的之后,就开始着手编写核心部分了,至于特效(类似颗粒效果和提示等)可以在后期美化时再加上,测试时你甚至可以用一条 /say 阶段1 之类的简易输出来检测。

以下高频执行的内容保存在furnace:loop下,创建记分板等只需初始执行一次的内容保存在 furnace:_init 下。

另外,为了减少与主题不相关的命令数量,本文内的熔炉默认都处于一个特定的盔甲架(作为Marker)下,该盔甲架的名字为furnaceMk。因此下文在检测熔炉位置的时候都会直接通过execute选择此盔甲架来检测。


检测熔炉开始燃烧
熔炉在燃烧的时候,其方块ID会发生变化,即从 "minecraft:furnace" 变为 "minecraft:lit_furnace" ,因此我们可以通过这个来检测熔炉是否开始燃烧。

首先我们需要创建一个记分板变量来记录熔炉燃烧的时间:

  1. scoreboard objectives add BurningTime dummy
复制代码
只要熔炉开始燃烧,即Marker处的方块变成lit_furnace,则开始加分:

  1. execute @e[name=furnaceMk] ~ ~ ~ detect ~ ~ ~ lit_furnace -1 scoreboard players add @s BurningTime 1
复制代码

熔炉停止燃烧时的处理
当熔炉停止燃烧,即重新变回furnace的时候,就会停止加分;如果还需要做到冷却效果,可以像这样处理:

在给燃烧着的熔炉加分之前,先高频给Marker减分,当然如果分数已经达到0的情况下是不会再减分:

  1. scoreboard players remove @e[name=furnaceMk,score_BurningTime_min=0] BurningTime 1
复制代码
然后我们把上面的那条命令稍加修改,以一个更高的分数来增加:

  1. execute @e[name=furnaceMk] ~ ~ ~ detect ~ ~ ~ lit_furnace -1 scoreboard players add @s BurningTime 2
复制代码
这样一来,当熔炉处于燃烧状态时分数因为相抵消便会同样以20分/秒的速度增加,反之则以20分/秒的速度减少。





熔炉在燃烧到一定时间后的处理
这部分其实非常简单,对记分板稍有些了解的玩家都能完成这部分的命令。实质非常简单,就是判定分数+execute Marker来执行。

颗粒效果建议根据实际情况调整。


阶段1:燃烧50秒后冒黑烟
当Marker的分数处于1000-1199时,执行 /particle 命令:

  1. execute @e[name=furnaceMk,score_BurningTime_min=1000,score_BurningTime=1199] ~ ~ ~ particle smoke ~ ~ ~ 0.5 0.5 0.5 0.1 10
复制代码

阶段2:燃烧60秒后冒更多黑烟+迸出火花
当分数处于1200-1399时,执行 /particle 命令:

  1. execute @e[name=furnaceMk,score_BurningTime_min=1200,score_BurningTime=1399] ~ ~ ~ particle smoke ~ ~ ~ 0.6 0.6 0.6 0.1 2
  2. execute @e[name=furnaceMk,score_BurningTime_min=1200,score_BurningTime=1399] ~ ~ ~ particle lava ~ ~ ~ 0.5 0.7 0.5 0.1 3
复制代码

阶段3:燃烧70秒后直接爆炸
当分数达到1400时,直接在Marker处生成一个即时引爆的TNT:

  1. execute @e[name=furnaceMk,score_BurningTime_min=1400,score_BurningTime=1400] ~ ~ ~ summon PrimedTnt ~ ~ ~ {CustomName:"Furnace",Fuse:0s}
复制代码
// 至于这个TNT的名字我特意写成了Furnace,就是为了当玩家被炸死的时候看上去像是被熔炉炸死的233

最后是对Marker的重置处理:

  1. kill @e[name=furnaceMk,score_BurningTime_min=1400,score_BurningTime=1400]
复制代码

总结
最后我们把命令全部放在一起看一下:

  1. # furnace:_init

  2. #创建记分板变量
  3. scoreboard objectives add BurningTime dummy
复制代码



  1. # furnace:loop

  2. #给燃烧着的熔炉处的Marker加分
  3. scoreboard players remove @e[name=furnaceMk,score_BurningTime_min=0] BurningTime 1

  4. execute @e[name=furnaceMk] ~ ~ ~ detect ~ ~ ~ lit_furnace -1 scoreboard players add @s BurningTime 2

  5. #对燃烧到一定时间的熔炉处理
  6. execute @e[name=furnaceMk,score_BurningTime_min=1000,score_BurningTime=1199] ~ ~ ~ particle smoke ~ ~ ~ 0.5 0.5 0.5 0.1 20

  7. execute @e[name=furnaceMk,score_BurningTime_min=1200,score_BurningTime=1399] ~ ~ ~ particle smoke ~ ~ ~ 0.6 0.6 0.6 0.1 2

  8. execute @e[name=furnaceMk,score_BurningTime_min=1200,score_BurningTime=1399] ~ ~ ~ particle lava ~ ~ ~ 0.5 0.7 0.5 0.1 3

  9. execute @e[name=furnaceMk,score_BurningTime_min=1400,score_BurningTime=1400] ~ ~ ~ summon PrimedTnt ~ ~ ~ {CustomName:"Furnace",Fuse:0s}

  10. kill @e[name=furnaceMk,score_BurningTime_min=1400,score_BurningTime=1400]
复制代码

其他可添加内容
上面所述的只是这个熔炉的核心内容,当然你也可以在自己的基础上设计一些其他的附加内容,例如:

  • 添加可拆卸功能,当Marker处为空气的时候删除Marker
  • 把特定的熔炉转换为普遍的情况,即玩家放置的每一个熔炉都会被认为可以进行过热处理
  • 通过检测熔炉内燃烧的燃料种类来判定BurningTime分数增加的速度,例如烧岩浆比烧木棍过热得要快



毒苹果
这个例子只是为了展示进度(Advancement)的其中一个用处:事件检测。

阅读此例子前需要先学习进度

假设我们要弄一个特殊的毒苹果,玩家吃了就会死,我们要怎样弄呢?

其中一个思路就是,检测玩家手上的物品,然后当玩家吃掉苹果之前1gt还是拿着那毒苹果的话,就给予中毒效果。这的确可行,然而这太麻烦了,而且得依赖高频。我们可以通过进度更方便的做到。


进度文件
假设那毒苹果的显示名称为 毒苹果 ,我们要检测玩家吃掉这个苹果,我们可以使用进度中的 minecraft:consume_item 判据。

由于我们不希望显示这个进度,所以我们不需要设定任何和显示、父进度有关的属性。(其实就是从例子里复制的)
获得了这个进度后我们就需要执行命令函数,因此我们需要 rewards

进度 example:ate_apple

  1. {
  2.     "criteria": {
  3.         "ate_poison_apple": {
  4.             "trigger": "minecraft:consume_item",
  5.             "conditions": {
  6.                 "item": "minecraft:apple",
  7.                 "nbt": "{display:{Name:"毒苹果"}}"
  8.             }
  9.         }
  10.     },
  11.     "rewards": {
  12.         "function": "example:poison_apple"
  13.     }
  14. }
复制代码

命令函数文件
由于进度只能获得一次,因此获得进度之后我们需要为玩家移除那进度,让他们能够再次获得(检测)那进度。
然后我们就需要给予玩家那个效果,我们会使用 effect 命令。

  1. # example:poison_apple
  2. advancement revoke @s only example:ate_apple
  3. effect @s minecraft:poison 10 0
复制代码



通过进度进行事件检测,可以减少高频需要的检测数量,减少需要的记分板数量,减少那些复杂的逻辑。十分方便。

不过需要注意,由于MC-118841的缘故,如果判据和玩家背包的物品改变有关(无论是 inventory_changed 还是 consume_item 等等),就不能马上对其进行有关物品操作,比如 give 或者 clear 物品都会导致所谓的幽灵物品(Ghost item)出现。请使用一些方法延迟1gt,比如高频等。


下雨检测
我们要怎么检测下雨呢?
我们可以使用 实体着火时淋雨会被扑灭 的特性。(这说明了玩命令也得会其他游戏机制。)

我们会使用盔甲架( armor_stand )为检测用的实体。

[原作者为 hqaazz]

着火
实体里有一个这样的标签:


这个 Fire 标签就是该实体是否在着火的状态,不是的话数值就是-20,否则就是一个正数。

我们可以高频,首先检查是否着火,然后让它继续(或重新)着火。
当下雨的时候,火焰就会被扑灭, Fire 就会等于-20,那么我们就得重新点火继续监测。
没有下雨的时候, Fire 就会慢慢减少,所以我们得继续让实体持续下去。

为了避免这盔甲架被烧坏,我们需要设置 Invulnerable:1b ,代表这个玩意不会死亡。

  1. # rain:_init

  2. summon armor_stand ~ ~ ~ {Invisible:1b,Invulnerable:1b,CustomName:"FireDetector"}
复制代码
  1. # rain:main
  2. # 移除之前可能加上的tag
  3. scoreboard players tag @e[name=FireDetector] remove rain
  4. scoreboard players tag @e[name=FireDetector] add rain {Fire:-20s}
  5. entitydata @e[name=FireDetector] {Fire:15s}
复制代码

Debug
然后我就跑去游戏跑了一下,发现这玩意是不成功的。
为啥呢?我检查了一下NBT,发现原来扑灭了以后的 Fire 数值是-1,或许wiki说的是之前没着火的情况吧。(尽信书不如无书啊,wiki也会有错的)
所以我们需要改改

  1. # rain:main
  2. # 移除之前可能加上的tag
  3. scoreboard players tag @e[name=FireDetector] remove rain
  4. scoreboard players tag @e[name=FireDetector] add rain {Fire:**}
  5. entitydata @e[name=FireDetector] {Fire:15s}
复制代码
这就很棒棒了。


放置位置
要淋雨嘛,位置自然得露天了,这一点我们可以通过 spreadplayers 命令来做到。

  1. # rain:new_place

  2. spreadplayers ~ ~ 0 10 false @e[name=FireDetector]
复制代码
然而什么情况下才需要重新找位置呢?就是当实体的位置上方有方块,也就是不露天了。
这个我们可以通过 stats  +  clone 来做到

  1. scoreboard players set @e[name=FireDetector] stats 0
  2. stats entity @e[name=FireDetector] set AffectedBlocks @s stats
  3. execute @e[name=FireDetector] ~ ~ ~ clone ~ ~ ~ ~ 255 ~ ~ ~ ~ masked force

  4. function rain:new_place if @e[name=FireDetector,score_stats_min=1]
复制代码
那条 clone 的作用就是,把那FireDetector头顶到方块最高点(Y=255)的非空气方块强制复制到原位。其实对方块是没有任何影响的,然而当头顶有非空气方块的话,就会把它们复制到原位,故此能够统计头顶非空气方块的数量。

然后我们需要改改 _initmain

  1. # rain:_init

  2. scoreboard objectives add stats dummy
  3. summon armor_stand ~ ~ ~ {Invisible:1b,Invulnerable:1b,CustomName:"FireDetector"}
复制代码
  1. # rain:main
  2. # 检查位置是否正确,不是的话就重新放置
  3. scoreboard players set @e[name=FireDetector] stats 0
  4. stats entity @e[name=FireDetector] set AffectedBlocks @s stats
  5. execute @e[name=FireDetector] ~ ~ ~ clone ~ ~ ~ ~ 255 ~ ~ ~ ~ masked force
  6. function rain:new_place if @e[name=FireDetector,score_stats_min=1]

  7. # 移除之前可能加上的tag
  8. scoreboard players tag @e[name=FireDetector] remove rain
  9. scoreboard players tag @e[name=FireDetector] add rain {Fire:**}
  10. entitydata @e[name=FireDetector] {Fire:15s}
复制代码



替换方块实体

如果我们希望把一些方块替换为一个方块实体,比如是箱子,我们会发现我们没法使用 fill 命令。

因为 fill 命令的 replace 模式,如果替换成的方块是一个方块实体,那么 replace 参数后的数据就是NBT而不是被替换方块的数据,故此是不可行的。
在这种情况下,我们可以采取间接的方法: fill 一些不能独立存在的方块(并且被破坏时会产生掉落物),**方块更新让它们掉落,然后 execute 那些掉落物设置方块。


产生掉落物
首先我们需要把目标地区(x1, y1, z1到x2, y2, z2,其中x1, y1, z1为区域内x, y, z最小的坐标)的指定方块,以石头为例子,替换为不能独立存在的方块,我们以半道门(就是一个木门方块,完整的木门需要两个方块)做替换的东西。

  1. fill x1 y1 z1 x2 y2 z2 minecraft:wooden_door 0 replace minecraft:stone 0
复制代码
然后我们需要产生方块更新。我们之前说过,我们可以使用 cloneforce 模式来为整个区域产生方块更新。

  1. clone x1 y1 z1 x2 y2 z2 x1 y1 z1 replace force
复制代码
然后我们就能产生掉落物了。


替换为箱子
这个就很简单了,我们首先 execute 掉落物设置方块,然后杀掉那些掉落物。

  1. scoreboard players tag @e[type=item] add temp {Item:{id:"minecraft:wooden_door"}}
  2. execute @e[type=item,tag=temp] ~ ~ ~ setblock ~ ~ ~ minecraft:chest
  3. kill @e[type=item,tag=temp]
复制代码



这例子看似很容易,其实还是挺难想出来的,而且其中搞方块更新的部分还是比较麻烦的。如果不特地产生一次方块更新,如果石头不是连续的,那么其中的一些木门就会没法获得更新,不会自我破坏,令整个检测不成功。


技能道具范例

前言
在 MC 中,所谓的技能,实际上只不过也是一个道具,通过某种方式触发(如按Q丢弃触发、F副手触发等)相应的事件达到想要的效果,就是我们平常所看到的“技能”效果。

这类系统一般分为三个部分。

  • 检测道具(技能)是否被触发,
  • 触发相应的事件,
  • 技能冷却和再补给。
我们将通过一个实例来讲解这三部分内容的组成。其中如果有错误,还请读者指正。

例如,在一个只有红队和蓝队PVP的小游戏中,我们希望通过F键触发一个技能,这个技能可以让我们瞬移到敌方一名玩家背后,同时触发1秒钟的力量加成效果。

所涉及到的记分板:

  • 名称: 技能1的冷却时间
  • Objective: skill1CD
  • Criteria: dummy`
本教程的命令都会放在一个高频里循环运行。


道具触发
就像上面说的那样,首先我们要检测道具的触发,那么我们先设定这样一个道具。我们使用染料作为技能图标,其带有一个{AssSkill:1}的标签(自定义NBT),以便我们检测。

当检测到副手的位置有这个技能的时候,视为触发。(F键为切换当前物品到副手)
副手Slot为-106

  1. scoreboard players tag @a[m=2] add skillSpell1 {Inventory:[{Slot:-106b,id:"minecraft:dye",Count:1b,Damage:5s,tag:{AssSkill:1}}]}
复制代码
从这里我们可以看到检测副手位的方法就是检测玩家的Inventory中-106b这个Slot,如果这个位置有技能道具,那么给玩家加上一个标签;然后,因为使用了技能,所以这个道具要被清掉。

  1. clear @a[tag=skillSpell1] dye 5 1 {AssSkill:1}
复制代码
到这里,我们就完成了第一步。接下来是第二部分。


技能事件
从前面的条件看出,我们要对红蓝两队分开讨论(因为我们要知道谁是敌方玩家,故此要分别比对)。每一个队伍中使用了这个技能的玩家,要标记一个不是自己队伍的附近玩家以便进行下一步操作。

  1. execute @a[tag=skillSpell1,team=red] ~ ~ ~ scoreboard players tag @p[r=15,team=!red,m=2] add SkillTag1
  2. execute @a[tag=skillSpell1,team=blue] ~ ~ ~ scoreboard players tag @p[r=15,team=!blue,m=2] add SkillTag1
复制代码
如果说有符合相应条件的玩家,那么现在就是触发技能的时候了。需要完成的效果是瞬移到对方背后。因此我们需要小小地动一下脑筋,分两步来完成,第一步是直接tp

  1. execute @a[tag=skillSpell1,team=red] ~ ~ ~ tp @s @p[team=!red,tag=SkillTag1]
  2. execute @a[tag=skillSpell1,team=blue] ~ ~ ~ tp @s @p[team=!blue,tag=SkillTag1]
  3. execute @a[tag=SkillTag1] ~ ~ ~ scoreboard players tag @p[r=0,tag=skillSpell1] add Skill1Success
复制代码
这一步的标记,表示第一步传送成功,如果成功了则执行后续的操作(因为稍微有点多呢)

紧接着是根据视角来向后tp,由于目前MC没有自带以视角建立坐标系的功能,所以要进行简单穷举面向,来实现这个功能。


点击展开命令
[spolier]
  1. tp @a[tag=Skill1Success,rym=-180,ry=-178] ~-0.0087262 ~ ~0.4999239
  2. tp @a[tag=Skill1Success,rym=-178,ry=-176] ~-0.02616798 ~ ~0.4993148
  3. tp @a[tag=Skill1Success,rym=-176,ry=-174] ~-0.04357787 ~ ~0.4980974
  4. tp @a[tag=Skill1Success,rym=-174,ry=-172] ~-0.06093467 ~ ~0.4962731
  5. tp @a[tag=Skill1Success,rym=-172,ry=-170] ~-0.07821723 ~ ~0.4938442
  6. tp @a[tag=Skill1Success,rym=-170,ry=-168] ~-0.0954045 ~ ~0.4908136
  7. tp @a[tag=Skill1Success,rym=-168,ry=-166] ~-0.1124755 ~ ~0.487185
  8. tp @a[tag=Skill1Success,rym=-166,ry=-164] ~-0.1294095 ~ ~0.4829629
  9. tp @a[tag=Skill1Success,rym=-164,ry=-162] ~-0.1461858 ~ ~0.4781524
  10. tp @a[tag=Skill1Success,rym=-162,ry=-160] ~-0.1627841 ~ ~0.4727593
  11. tp @a[tag=Skill1Success,rym=-160,ry=-158] ~-0.179184 ~ ~0.4667902
  12. tp @a[tag=Skill1Success,rym=-158,ry=-156] ~-0.1953656 ~ ~0.4602524
  13. tp @a[tag=Skill1Success,rym=-156,ry=-154] ~-0.2113091 ~ ~0.4531539
  14. tp @a[tag=Skill1Success,rym=-154,ry=-152] ~-0.2269952 ~ ~0.4455033
  15. tp @a[tag=Skill1Success,rym=-152,ry=-150] ~-0.2424048 ~ ~0.4373099
  16. tp @a[tag=Skill1Success,rym=-150,ry=-148] ~-0.257519 ~ ~0.4285837
  17. tp @a[tag=Skill1Success,rym=-148,ry=-146] ~-0.2723195 ~ ~0.4193353
  18. tp @a[tag=Skill1Success,rym=-146,ry=-144] ~-0.2867882 ~ ~0.409576
  19. tp @a[tag=Skill1Success,rym=-144,ry=-142] ~-0.3009075 ~ ~0.3993177
  20. tp @a[tag=Skill1Success,rym=-142,ry=-140] ~-0.3146602 ~ ~0.388573
  21. tp @a[tag=Skill1Success,rym=-140,ry=-138] ~-0.3280295 ~ ~0.3773548
  22. tp @a[tag=Skill1Success,rym=-138,ry=-136] ~-0.3409992 ~ ~0.3656769
  23. tp @a[tag=Skill1Success,rym=-136,ry=-134] ~-0.3535534 ~ ~0.3535534
  24. tp @a[tag=Skill1Success,rym=-134,ry=-132] ~-0.3656769 ~ ~0.3409992
  25. tp @a[tag=Skill1Success,rym=-132,ry=-130] ~-0.3773548 ~ ~0.3280295
  26. tp @a[tag=Skill1Success,rym=-130,ry=-128] ~-0.388573 ~ ~0.3146602
  27. tp @a[tag=Skill1Success,rym=-128,ry=-126] ~-0.3993177 ~ ~0.3009075
  28. tp @a[tag=Skill1Success,rym=-126,ry=-124] ~-0.409576 ~ ~0.2867882
  29. tp @a[tag=Skill1Success,rym=-124,ry=-122] ~-0.4193353 ~ ~0.2723195
  30. tp @a[tag=Skill1Success,rym=-122,ry=-120] ~-0.4285837 ~ ~0.257519
  31. tp @a[tag=Skill1Success,rym=-120,ry=-118] ~-0.4373099 ~ ~0.2424048
  32. tp @a[tag=Skill1Success,rym=-118,ry=-116] ~-0.4455033 ~ ~0.2269952
  33. tp @a[tag=Skill1Success,rym=-116,ry=-114] ~-0.4531539 ~ ~0.2113091
  34. tp @a[tag=Skill1Success,rym=-114,ry=-112] ~-0.4602524 ~ ~0.1953656
  35. tp @a[tag=Skill1Success,rym=-112,ry=-110] ~-0.4667902 ~ ~0.179184
  36. tp @a[tag=Skill1Success,rym=-110,ry=-108] ~-0.4727593 ~ ~0.1627841
  37. tp @a[tag=Skill1Success,rym=-108,ry=-106] ~-0.4781524 ~ ~0.1461858
  38. tp @a[tag=Skill1Success,rym=-106,ry=-104] ~-0.4829629 ~ ~0.1294095
  39. tp @a[tag=Skill1Success,rym=-104,ry=-102] ~-0.487185 ~ ~0.1124755
  40. tp @a[tag=Skill1Success,rym=-102,ry=-100] ~-0.4908136 ~ ~0.0954045
  41. tp @a[tag=Skill1Success,rym=-100,ry=-98] ~-0.4938442 ~ ~0.07821723
  42. tp @a[tag=Skill1Success,rym=-98,ry=-96] ~-0.4962731 ~ ~0.06093467
  43. tp @a[tag=Skill1Success,rym=-96,ry=-94] ~-0.4980974 ~ ~0.04357787
  44. tp @a[tag=Skill1Success,rym=-94,ry=-92] ~-0.4993148 ~ ~0.02616798
  45. tp @a[tag=Skill1Success,rym=-92,ry=-90] ~-0.4999239 ~ ~0.0087262
  46. tp @a[tag=Skill1Success,rym=-90,ry=-88] ~-0.4999239 ~ ~-0.0087262
  47. tp @a[tag=Skill1Success,rym=-88,ry=-86] ~-0.4993148 ~ ~-0.02616798
  48. tp @a[tag=Skill1Success,rym=-86,ry=-84] ~-0.4980974 ~ ~-0.04357787
  49. tp @a[tag=Skill1Success,rym=-84,ry=-82] ~-0.4962731 ~ ~-0.06093467
  50. tp @a[tag=Skill1Success,rym=-82,ry=-80] ~-0.4938442 ~ ~-0.07821723
  51. tp @a[tag=Skill1Success,rym=-80,ry=-78] ~-0.4908136 ~ ~-0.0954045
  52. tp @a[tag=Skill1Success,rym=-78,ry=-76] ~-0.487185 ~ ~-0.1124755
  53. tp @a[tag=Skill1Success,rym=-76,ry=-74] ~-0.4829629 ~ ~-0.1294095
  54. tp @a[tag=Skill1Success,rym=-74,ry=-72] ~-0.4781524 ~ ~-0.1461858
  55. tp @a[tag=Skill1Success,rym=-72,ry=-70] ~-0.4727593 ~ ~-0.1627841
  56. tp @a[tag=Skill1Success,rym=-70,ry=-68] ~-0.4667902 ~ ~-0.179184
  57. tp @a[tag=Skill1Success,rym=-68,ry=-66] ~-0.4602524 ~ ~-0.1953656
  58. tp @a[tag=Skill1Success,rym=-66,ry=-64] ~-0.4531539 ~ ~-0.2113091
  59. tp @a[tag=Skill1Success,rym=-64,ry=-62] ~-0.4455033 ~ ~-0.2269952
  60. tp @a[tag=Skill1Success,rym=-62,ry=-60] ~-0.4373099 ~ ~-0.2424048
  61. tp @a[tag=Skill1Success,rym=-60,ry=-58] ~-0.4285837 ~ ~-0.257519
  62. tp @a[tag=Skill1Success,rym=-58,ry=-56] ~-0.4193353 ~ ~-0.2723195
  63. tp @a[tag=Skill1Success,rym=-56,ry=-54] ~-0.409576 ~ ~-0.2867882
  64. tp @a[tag=Skill1Success,rym=-54,ry=-52] ~-0.3993177 ~ ~-0.3009075
  65. tp @a[tag=Skill1Success,rym=-52,ry=-50] ~-0.388573 ~ ~-0.3146602
  66. tp @a[tag=Skill1Success,rym=-50,ry=-48] ~-0.3773548 ~ ~-0.3280295
  67. tp @a[tag=Skill1Success,rym=-48,ry=-46] ~-0.3656769 ~ ~-0.3409992
  68. tp @a[tag=Skill1Success,rym=-46,ry=-44] ~-0.3535534 ~ ~-0.3535534
  69. tp @a[tag=Skill1Success,rym=-44,ry=-42] ~-0.3409992 ~ ~-0.3656769
  70. tp @a[tag=Skill1Success,rym=-42,ry=-40] ~-0.3280295 ~ ~-0.3773548
  71. tp @a[tag=Skill1Success,rym=-40,ry=-38] ~-0.3146602 ~ ~-0.388573
  72. tp @a[tag=Skill1Success,rym=-38,ry=-36] ~-0.3009075 ~ ~-0.3993177
  73. tp @a[tag=Skill1Success,rym=-36,ry=-34] ~-0.2867882 ~ ~-0.409576
  74. tp @a[tag=Skill1Success,rym=-34,ry=-32] ~-0.2723195 ~ ~-0.4193353
  75. tp @a[tag=Skill1Success,rym=-32,ry=-30] ~-0.257519 ~ ~-0.4285837
  76. tp @a[tag=Skill1Success,rym=-30,ry=-28] ~-0.2424048 ~ ~-0.4373099
  77. tp @a[tag=Skill1Success,rym=-28,ry=-26] ~-0.2269952 ~ ~-0.4455033
  78. tp @a[tag=Skill1Success,rym=-26,ry=-24] ~-0.2113091 ~ ~-0.4531539
  79. tp @a[tag=Skill1Success,rym=-24,ry=-22] ~-0.1953656 ~ ~-0.4602524
  80. tp @a[tag=Skill1Success,rym=-22,ry=-20] ~-0.179184 ~ ~-0.4667902
  81. tp @a[tag=Skill1Success,rym=-20,ry=-18] ~-0.1627841 ~ ~-0.4727593
  82. tp @a[tag=Skill1Success,rym=-18,ry=-16] ~-0.1461858 ~ ~-0.4781524
  83. tp @a[tag=Skill1Success,rym=-16,ry=-14] ~-0.1294095 ~ ~-0.4829629
  84. tp @a[tag=Skill1Success,rym=-14,ry=-12] ~-0.1124755 ~ ~-0.487185
  85. tp @a[tag=Skill1Success,rym=-12,ry=-10] ~-0.0954045 ~ ~-0.4908136
  86. tp @a[tag=Skill1Success,rym=-10,ry=-8] ~-0.07821723 ~ ~-0.4938442
  87. tp @a[tag=Skill1Success,rym=-8,ry=-6] ~-0.06093467 ~ ~-0.4962731
  88. tp @a[tag=Skill1Success,rym=-6,ry=-4] ~-0.04357787 ~ ~-0.4980974
  89. tp @a[tag=Skill1Success,rym=-4,ry=-2] ~-0.02616798 ~ ~-0.4993148
  90. tp @a[tag=Skill1Success,rym=-2,ry=0] ~-0.0087262 ~ ~-0.4999239
  91. tp @a[tag=Skill1Success,rym=0,ry=2] ~0.0087262 ~ ~-0.4999239
  92. tp @a[tag=Skill1Success,rym=2,ry=4] ~0.02616798 ~ ~-0.4993148
  93. tp @a[tag=Skill1Success,rym=4,ry=6] ~0.04357787 ~ ~-0.4980974
  94. tp @a[tag=Skill1Success,rym=6,ry=8] ~0.06093467 ~ ~-0.4962731
  95. tp @a[tag=Skill1Success,rym=8,ry=10] ~0.07821723 ~ ~-0.4938442
  96. tp @a[tag=Skill1Success,rym=10,ry=12] ~0.0954045 ~ ~-0.4908136
  97. tp @a[tag=Skill1Success,rym=12,ry=14] ~0.1124755 ~ ~-0.487185
  98. tp @a[tag=Skill1Success,rym=14,ry=16] ~0.1294095 ~ ~-0.4829629
  99. tp @a[tag=Skill1Success,rym=16,ry=18] ~0.1461858 ~ ~-0.4781524
  100. tp @a[tag=Skill1Success,rym=18,ry=20] ~0.1627841 ~ ~-0.4727593
  101. tp @a[tag=Skill1Success,rym=20,ry=22] ~0.179184 ~ ~-0.4667902
  102. tp @a[tag=Skill1Success,rym=22,ry=24] ~0.1953656 ~ ~-0.4602524
  103. tp @a[tag=Skill1Success,rym=24,ry=26] ~0.2113091 ~ ~-0.4531539
  104. tp @a[tag=Skill1Success,rym=26,ry=28] ~0.2269952 ~ ~-0.4455033
  105. tp @a[tag=Skill1Success,rym=28,ry=30] ~0.2424048 ~ ~-0.4373099
  106. tp @a[tag=Skill1Success,rym=30,ry=32] ~0.257519 ~ ~-0.4285837
  107. tp @a[tag=Skill1Success,rym=32,ry=34] ~0.2723195 ~ ~-0.4193353
  108. tp @a[tag=Skill1Success,rym=34,ry=36] ~0.2867882 ~ ~-0.409576
  109. tp @a[tag=Skill1Success,rym=36,ry=38] ~0.3009075 ~ ~-0.3993177
  110. tp @a[tag=Skill1Success,rym=38,ry=40] ~0.3146602 ~ ~-0.388573
  111. tp @a[tag=Skill1Success,rym=40,ry=42] ~0.3280295 ~ ~-0.3773548
  112. tp @a[tag=Skill1Success,rym=42,ry=44] ~0.3409992 ~ ~-0.3656769
  113. tp @a[tag=Skill1Success,rym=44,ry=46] ~0.3535534 ~ ~-0.3535534
  114. tp @a[tag=Skill1Success,rym=46,ry=48] ~0.3656769 ~ ~-0.3409992
  115. tp @a[tag=Skill1Success,rym=48,ry=50] ~0.3773548 ~ ~-0.3280295
  116. tp @a[tag=Skill1Success,rym=50,ry=52] ~0.388573 ~ ~-0.3146602
  117. tp @a[tag=Skill1Success,rym=52,ry=54] ~0.3993177 ~ ~-0.3009075
  118. tp @a[tag=Skill1Success,rym=54,ry=56] ~0.409576 ~ ~-0.2867882
  119. tp @a[tag=Skill1Success,rym=56,ry=58] ~0.4193353 ~ ~-0.2723195
  120. tp @a[tag=Skill1Success,rym=58,ry=60] ~0.4285837 ~ ~-0.257519
  121. tp @a[tag=Skill1Success,rym=60,ry=62] ~0.4373099 ~ ~-0.2424048
  122. tp @a[tag=Skill1Success,rym=62,ry=64] ~0.4455033 ~ ~-0.2269952
  123. tp @a[tag=Skill1Success,rym=64,ry=66] ~0.4531539 ~ ~-0.2113091
  124. tp @a[tag=Skill1Success,rym=66,ry=68] ~0.4602524 ~ ~-0.1953656
  125. tp @a[tag=Skill1Success,rym=68,ry=70] ~0.4667902 ~ ~-0.179184
  126. tp @a[tag=Skill1Success,rym=70,ry=72] ~0.4727593 ~ ~-0.1627841
  127. tp @a[tag=Skill1Success,rym=72,ry=74] ~0.4781524 ~ ~-0.1461858
  128. tp @a[tag=Skill1Success,rym=74,ry=76] ~0.4829629 ~ ~-0.1294095
  129. tp @a[tag=Skill1Success,rym=76,ry=78] ~0.487185 ~ ~-0.1124755
  130. tp @a[tag=Skill1Success,rym=78,ry=80] ~0.4908136 ~ ~-0.0954045
  131. tp @a[tag=Skill1Success,rym=80,ry=82] ~0.4938442 ~ ~-0.07821723
  132. tp @a[tag=Skill1Success,rym=82,ry=84] ~0.4962731 ~ ~-0.06093467
  133. tp @a[tag=Skill1Success,rym=84,ry=86] ~0.4980974 ~ ~-0.04357787
  134. tp @a[tag=Skill1Success,rym=86,ry=88] ~0.4993148 ~ ~-0.02616798
  135. tp @a[tag=Skill1Success,rym=88,ry=90] ~0.4999239 ~ ~-0.0087262
  136. tp @a[tag=Skill1Success,rym=90,ry=92] ~0.4999239 ~ ~0.0087262
  137. tp @a[tag=Skill1Success,rym=92,ry=94] ~0.4993148 ~ ~0.02616798
  138. tp @a[tag=Skill1Success,rym=94,ry=96] ~0.4980974 ~ ~0.04357787
  139. tp @a[tag=Skill1Success,rym=96,ry=98] ~0.4962731 ~ ~0.06093467
  140. tp @a[tag=Skill1Success,rym=98,ry=100] ~0.4938442 ~ ~0.07821723
  141. tp @a[tag=Skill1Success,rym=100,ry=102] ~0.4908136 ~ ~0.0954045
  142. tp @a[tag=Skill1Success,rym=102,ry=104] ~0.487185 ~ ~0.1124755
  143. tp @a[tag=Skill1Success,rym=104,ry=106] ~0.4829629 ~ ~0.1294095
  144. tp @a[tag=Skill1Success,rym=106,ry=108] ~0.4781524 ~ ~0.1461858
  145. tp @a[tag=Skill1Success,rym=108,ry=110] ~0.4727593 ~ ~0.1627841
  146. tp @a[tag=Skill1Success,rym=110,ry=112] ~0.4667902 ~ ~0.179184
  147. tp @a[tag=Skill1Success,rym=112,ry=114] ~0.4602524 ~ ~0.1953656
  148. tp @a[tag=Skill1Success,rym=114,ry=116] ~0.4531539 ~ ~0.2113091
  149. tp @a[tag=Skill1Success,rym=116,ry=118] ~0.4455033 ~ ~0.2269952
  150. tp @a[tag=Skill1Success,rym=118,ry=120] ~0.4373099 ~ ~0.2424048
  151. tp @a[tag=Skill1Success,rym=120,ry=122] ~0.4285837 ~ ~0.257519
  152. tp @a[tag=Skill1Success,rym=122,ry=124] ~0.4193353 ~ ~0.2723195
  153. tp @a[tag=Skill1Success,rym=124,ry=126] ~0.409576 ~ ~0.2867882
  154. tp @a[tag=Skill1Success,rym=126,ry=128] ~0.3993177 ~ ~0.3009075
  155. tp @a[tag=Skill1Success,rym=128,ry=130] ~0.388573 ~ ~0.3146602
  156. tp @a[tag=Skill1Success,rym=130,ry=132] ~0.3773548 ~ ~0.3280295
  157. tp @a[tag=Skill1Success,rym=132,ry=134] ~0.3656769 ~ ~0.3409992
  158. tp @a[tag=Skill1Success,rym=134,ry=136] ~0.3535534 ~ ~0.3535534
  159. tp @a[tag=Skill1Success,rym=136,ry=138] ~0.3409992 ~ ~0.3656769
  160. tp @a[tag=Skill1Success,rym=138,ry=140] ~0.3280295 ~ ~0.3773548
  161. tp @a[tag=Skill1Success,rym=140,ry=142] ~0.3146602 ~ ~0.388573
  162. tp @a[tag=Skill1Success,rym=142,ry=144] ~0.3009075 ~ ~0.3993177
  163. tp @a[tag=Skill1Success,rym=144,ry=146] ~0.2867882 ~ ~0.409576
  164. tp @a[tag=Skill1Success,rym=146,ry=148] ~0.2723195 ~ ~0.4193353
  165. tp @a[tag=Skill1Success,rym=148,ry=150] ~0.257519 ~ ~0.4285837
  166. tp @a[tag=Skill1Success,rym=150,ry=152] ~0.2424048 ~ ~0.4373099
  167. tp @a[tag=Skill1Success,rym=152,ry=154] ~0.2269952 ~ ~0.4455033
  168. tp @a[tag=Skill1Success,rym=154,ry=156] ~0.2113091 ~ ~0.4531539
  169. tp @a[tag=Skill1Success,rym=156,ry=158] ~0.1953656 ~ ~0.4602524
  170. tp @a[tag=Skill1Success,rym=158,ry=160] ~0.179184 ~ ~0.4667902
  171. tp @a[tag=Skill1Success,rym=160,ry=162] ~0.1627841 ~ ~0.4727593
  172. tp @a[tag=Skill1Success,rym=162,ry=164] ~0.1461858 ~ ~0.4781524
  173. tp @a[tag=Skill1Success,rym=164,ry=166] ~0.1294095 ~ ~0.4829629
  174. tp @a[tag=Skill1Success,rym=166,ry=168] ~0.1124755 ~ ~0.487185
  175. tp @a[tag=Skill1Success,rym=168,ry=170] ~0.0954045 ~ ~0.4908136
  176. tp @a[tag=Skill1Success,rym=170,ry=172] ~0.07821723 ~ ~0.4938442
  177. tp @a[tag=Skill1Success,rym=172,ry=174] ~0.06093467 ~ ~0.4962731
  178. tp @a[tag=Skill1Success,rym=174,ry=176] ~0.04357787 ~ ~0.4980974
  179. tp @a[tag=Skill1Success,rym=176,ry=178] ~0.02616798 ~ ~0.4993148
  180. tp @a[tag=Skill1Success,rym=178,ry=180] ~0.0087262 ~ ~0.4999239
复制代码
[/spoiler]到这里,我们就实现了TP的功能。实现完功能以后,我们需要清除掉整个事件中使用到的tag。当然了。在清除tag之前,我们先为玩家设定一下技能冷却,假如是15秒,换算成游戏刻则是300。

  1. scoreboard players set @a[tag=skillSpell1] skill1CD 300
复制代码
然后清tag

  1. scoreboard players tag @a[tag=skillSpell1] remove skillSpell1
  2. scoreboard players tag @a[tag=SkillTag1] remove SkillTag1
  3. scoreboard players tag @a[tag=Skill1Success] remove Skill1Success
复制代码
至此,第二部分已经完成。


技能冷却、补给
在前面我们已经为玩家设定好了CD时间那么只需要进行计时以及在需要的时间进行补给即可。

  1. scoreboard players remove @a[score_skill1CD_min=0] skill1CD 1
  2. replaceitem entity @a[score_skill1CD_min=0,score_skill1CD=0] slot.hotbar.2 dye 1 5 {AssSkill:1,display:{Name:"§b背刺",Lore:["§a神不知鬼不觉出现在敌人后面!","§c·将该技能置于副手瞬间触发","请勿修改道具快捷键","§c·冷却时间15秒"]},ench:[{id:0,lvl:0}],HideFlags:63}
复制代码
这就是第三部分的内容,非常简单。但是,如果你是一个细心的玩家,你一定会思考这样一个问题:如果玩家直接丢了技能或者进行了别的错误操作,该怎么办?
那么,你就需要特别处理额外的这些情况,这部分会教你如何锁住技能的位置,一旦发生异常,则会强行重置CD。

  1. # 在正确位置就给tag
  2. scoreboard players tag @a[m=2,score_skill1CD=-1] add withSkill1 {Inventory:[{Slot:2b,tag:{AssSkill:1}}]}
  3. scoreboard players tag @a[m=2,score_skill1CD=-1] add withSkill1 {Inventory:[{Slot:-106b,tag:{AssSkill:1}}]}

  4. # 没有指定的tag就代表不在指定位置,情况异常
  5. clear @a[m=2,score_skill1CD=-1,tag=!withSkill1] dye -1 64 {AssSkill:1}
  6. tellraw @a[m=2,score_skill1CD=-1,tag=!withSkill1] ["[§6提示§r]§b背刺§a道具异常,正在重新计算§b冷却时间§r"]
  7. scoreboard players set @a[m=2,score_skill1CD=-1,tag=!withSkill1] skill1CD 300
  8. scoreboard players tag @a[tag=withSkill1] remove withSkill1
复制代码
通过这个例子,我们可以发现,其实所谓的技能系统,并不复杂,只要你能够把系统拆分成我所说的这几个部分,并逐一完善,你就能写出很棒的技能系统来。



皇冠系统

前言
作者: 空白dalao。

首先我能被邀请参与大教程这个项目,我感到十分荣幸。pca领着一帮人克服重重困难能把教程写的如此完善,还克服了外界的种种压力,实在是可歌可泣。我也就在此做出一点微小的贡献,写一个有关系统制作的事例。这里为了方便,我就选择了燃烧的子弹中的皇冠系统来讲解。

说是地图中的一个模式,其实也是可以完全独立出来作为一个完整游戏的。因此我选择这个来作为事例,书写一下我平时做地图时的思路和想法。


核心目的
制作一个系统,首先要明确的就是你的目的,也就是要去用命令实现什么。而在皇冠模式这个系统中,有以下两个核心功能是要去首先实现的:

  • 玩家死亡掉落皇冠
  • 玩家捡起皇冠后带上皇冠
  • 玩家死亡后的重生
明确了目的,就可以制作核心玩法用来进行初步的测试了。只有做好了核心部分,其他协助它的部分才能根据核心的需要来撰写。下面是我具体实现的想法。以下这些内容在游戏进行过程中要高频执行。保存在system:loop下。


死亡掉落
对于有一定命令基础的玩家来说,这一个部分比较容易实现。

首先要判定玩家死亡。

建立记分板

  1. scoreboard objectives add playerDeath deathCount 玩家死亡次数
  2. scoreboard objectives add crown dummy 谁拥有皇冠
复制代码
一般玩家的crown分数为0,拥有皇冠者的分数则为1。

一看也觉得能够用tag代替,然而为何使用记分板变量呢?
其实因为tag在玩家离线的时候没法清除,后面有个模块用到了。

只要playerDeath大于1并且拥有皇冠,那么死亡的时候就会掉落一个皇冠。掉落后重置玩家的crown记分板。

  1. execute @a[score_playerDeath_min=1,score_crown_min=1] ~ ~ ~ summon item ~ ~ ~ {Item:{id:"minecraft:golden_helmet",Count:1,CustomName:"皇冠",CustomNameVisible:1}
  2. scoreboard players set @a[score_playerDeath_min=1,score_crown_min=1] crown 0
复制代码
可以注意到,核心部分制作的时候,一些细节先不用考虑。比如物品是否无敌、是否会消失等等NBT先不用考虑。甚至先弄一个默认不加nbt的都可以。因为现在只需要制作出一个原型即可,不需要加特技。


玩家捡起皇冠后带上
这一部分也同样不难,直接给命令。

建立记分板,这里用到了捡起物品的记分板判据

  1. scoreboard objectives add pickCrownUp stat.pickup.minecraft.golden_helmet 捡起皇冠
复制代码
清除捡起的皇冠并加分

  1. clear @a[score_pickCrownUp_min=1] golden_helmet
  2. scoreboard players set @a[score_pickCrownUp_min=1] crown 1
  3. scoreboard players set @a[score_pickCrownUp_min=1] pickCrownUp 0
复制代码

死亡重生
这一部分可能较为复杂,我会根据我的思路来讲解。

MC中的死亡是这样一种奇怪的操作:玩家死亡后,进入一个界面,可以点击按钮重生会出生点。但是如果玩家不点击按钮,那么玩家的位置仍然是在死亡的位置,并且用@a可以选择的到(@p则不行)。如果此时对玩家进行tp等移动位置的操作,在点击重生之后就不会有效果了。因此我们需要在玩家点击重生之后再开始对玩家死亡复活的计时。玩家点击重生按钮后,就会出现在他的出生点。因此可以用玩家出现在出生点作为玩家点击了重生按钮的判断依据。

首先建立记分板

  1. scoreboard objectives add spawnTime dummy 重生等待时间
复制代码
假设玩家的出生点在 @a[x=22,y=200,z=22,dx=30,dy=20,dz=30] 选择器的范围之内,玩家等待重生的小黑屋在 (12, 15, 12) 的位置,那么在玩家点击重生按钮后,将其传送至等待重生的小黑屋并开始计时。

  1. scoreboard players set @a[x=22,y=200,z=22,dx=30,dy=20,dz=30,score_playerDeath_min=1] spawnTime 1
  2. tp @a[x=22,y=200,z=22,dx=30,dy=20,dz=30,score_playerDeath_min=1] 15 12 15
复制代码
计时:

  1. scoreboard players add @a[score_spawnTime_min=1] spawnTime 1
复制代码
重生时间到了之后(假设5秒也就是100ticks),将玩家传送回场地(假设坐标为 (-32, 18, 32))。

  1. tp @a[score_spawnTime_min=100] -32 18 32
  2. scoreboard players set @a[score_spawnTime_min=100] spawnTime 0
复制代码
以上重生部分就完成了。


添加系统的开始与结束
核心功能完成之后,就要开始考虑系统是怎样开始与结束的。

以这个皇冠模式的系统为例,开始时要随机给一个玩家皇冠、传送玩家至场地、开始游戏计时。结束游戏的判定就是时间到,并且在游戏结束后重置各种记分板,将玩家传送回出生点。


游戏开始部分
以下内容仅在游戏开始时执行一次。保存在system:start下。首先建立记分板和一个实体用于计时:

  1. scoreboard objectives add system_time dummy 记录游戏时间
  2. summon area_effect_cloud 4 2 4 {Duration:999999,CustomName:"gaming"}
复制代码
设置游戏时间(假设3分钟也就是3600ticks)并随机给玩家一个皇冠。

  1. scoreboard players set @e[name=gaming] system_time 3600
  2. scoreboard players set @r[m=2] crown 1
复制代码
可以看到,由于核心部分的设定,只要将随机玩家的crown设为1,那么玩家在死亡之后就会掉落皇冠。同时由于要计时,所以要在系统高频的部分添加下面这条命令用来倒计时:

  1. scoreboard players remove @e[name=gaming] system_time 1
复制代码

游戏结束部分
以下内容仅在游戏结束时执行一次。

重置各种记分板、删除实体、传送玩家回出生点。保存在system:end下。

  1. scoreboard players reset *
  2. kill @e[type=!player]
  3. tp @a 22 200 22
复制代码

游戏结束的判定
这一部分很简单,就是判定游戏剩余时间是否为零。添加在system:loop结尾处。

  1. function system:end if @e[name=gaming,score_system_time=0]
复制代码

初步总结
现将以上全部命令集合在一起,将得到如下的文件结构:

  1. system
  2.   |-- _init.mcfunction
  3.   |-- loop.mcfunction
  4.   |-- start.mcfunction
  5.   |-- end.mcfunction
复制代码
所有命令函数文件如下:
_init 为初始化地图需要执行的命令函数。 loop 为系统高频执行的命令函数。)

  1. # system:_init

  2. scoreboard objectives add playerDeath deathCount 玩家死亡次数
  3. scoreboard objectives add crown dummy 谁拥有皇冠
  4. scoreboard objectives add pickCrownUp stat.pickup.minecraft.golden_helmet 捡起皇冠
  5. scoreboard objectives add spawnTime dummy 重生等待时间
  6. scoreboard objectives add system_time dummy 记录游戏时间
复制代码
  1. # system:loop

  2. #死亡掉落
  3. execute @a[score_playerDeath_min=1,score_crown_min=1] ~ ~ ~ summon item ~ ~ ~ {Item:{id:"minecraft:golden_helmet",Count:1,CustomName:"皇冠",CustomNameVisible:1}
  4. scoreboard players set @a[score_playerDeath_min=1,score_crown_min=1] crown 0

  5. #玩家捡起皇冠后带上
  6. clear @a[score_pickCrownUp_min=1] golden_helmet
  7. scoreboard players set @a[score_pickCrownUp_min=1] crown 1
  8. scoreboard players set @a[score_pickCrownUp_min=1] pickCrownUp 0

  9. #死亡重生
  10. scoreboard players set @a[x=22,y=200,z=22,dx=30,dy=20,dz=30,score_playerDeath_min=1] spawnTime 1
  11. tp @a[x=22,y=200,z=22,dx=30,dy=20,dz=30,score_playerDeath_min=1] 15 12 15
  12. scoreboard players add @a[score_spawnTime_min=1] spawnTime 1
  13. tp @a[score_spawnTime_min=100] -32 18 32
  14. scoreboard players set @a[score_spawnTime_min=100] spawnTime 0

  15. #增加时间
  16. scoreboard players remove @e[name=gaming] system_time 1

  17. #死亡判定
  18. function system:end if @e[name=gaming,score_system_time=0]
复制代码
  1. # system:start

  2. summon area_effect_cloud 4 2 4 {Duration:999999,CustomName:"gaming"}
  3. scoreboard players set @e[name=gaming] system_time 3600
  4. scoreboard players set @r[m=2] crown 1
复制代码
  1. # system:end

  2. scoreboard players reset *
  3. kill @e[type=!player]
  4. tp @a 22 200 22
复制代码

对系统内容和体验的优化
整个游戏系统可用之后,我们就可以增加一些花里胡哨的增加游戏体验的东西了。下面一个列表大致写了一下可以添加的内容。

  • 开始阶段
    • 开始游戏的title提示
    • 侧边栏显示剩余时间
    • tab栏显示玩家血量
  • 游戏阶段
    • 有皇冠的人带黄色高亮
    • 有皇冠的人头顶有一个向日葵
    • 皇冠掉地上之后有黄色高亮
    • 玩家捡到皇冠后全员提示
  • 结束阶段
    • title提示
    • subtitle提示谁是最后赢家或没有赢家
    • 全员tellraw提示每个人分别戴着皇冠多少秒


开始阶段
在system:start中添加几条命令即可

  1. scoreboard objectives setdisplay sidebar crown
  2. title @a time 10 100 10
  3. title @a subtitle ["拿到皇冠你才能变得更屌!"]
复制代码

游戏阶段

有皇冠的人带黄色高亮
新建一个队伍,并且改队伍颜色为黄色

  1. scoreboard teams add crownTeam 皇冠
  2. scoreboard teams option crownTeam color yellow
复制代码
在system:loop中添加以下命令,把戴着皇冠且不在crownTeam中的人加入队伍,让队伍里的人发光

  1. scoreboard teams join crownTeam @a[score_crown_min=1,team=!crownTeam]
  2. effect @a[score_crown_min=1] glowing 2 1 true
复制代码
在system:loop的玩家掉落皇冠部分加入以下命令,让掉落皇冠的人也退出队伍

  1. scoreboard teams leave @a[score_crown=0,team=crownTeam]
复制代码

有皇冠的人头顶有一个向日葵
在system:loop内加入以下命令

  1. scoreboard players add crown 0
  2. scoreboard players tag @a add haveCrown {Inventory:[{id:"minecraft:double_plant",Slot:103b}]}
  3. replaceitem entity @a[score_crown_min=1,tag=!haveCrown] slot.armor.head minecraft:double_plant
  4. replaceitem entity @p[tag=haveCrown,score_crown=0] slot.armor.head air
  5. scoreboard players tag @a[tag=haveCrown] remove haveCrown
复制代码
首先这段命令的目的是让有皇冠的(也就是score_crown_min=1的人)却没戴着向日葵的人带上向日葵,没皇冠的(也就是score_crown=0的人)戴上向日葵。tag=haveCrown表示玩家戴着向日葵。此时若玩家score_crown_min=1,但却没有haveCrown这个tag,就获得一个向日葵。如果玩家有haveCrown这个tag而crown=0,则去掉向日葵。这个是一种通过每一tick都添加再删除tag,来判定玩家是否穿装备的方法,简便易行。但是如果要判断的装备数量较大,则一般不使用这种方法。


皇冠掉地上有高亮
在system:loop中的掉落皇冠部分修改掉落的皇冠实体。修改后的命令如下:

  1. execute @a[score_playerDeath_min=1,score_respawnTime_min=1,score_respawnTime=1,score_crown_min=1] ~ ~ ~ summon item ~ ~ ~ {Item:{id:"minecraft:golden_helmet",Count:1},PickupDelay:20,Invulnerable:1,Glowing:1,Team:"crownTeam",CustomName:"皇冠",CustomNameVisible:1,Age:-32768}
复制代码

玩家捡到皇冠后全员提示
在system:loop的玩家掉落皇冠部分加入以下命令,通知全员

  1. execute @a[score_pickCrownUp_min=1] ~ ~ ~ tellraw @a [" >> ",{"selector":"@a[score_pickCrownUp_min=1]"}," 拾起了一顶皇冠"]
复制代码

结束阶段

title提示
在游戏结束模块加入以下命令

  1. title @a times 0 200 10
  2. title @a title ["游戏结束"]
  3. title @a subtitle ["你们都是笨蛋, 皇冠也能丢?"]
  4. execute @p[score_crown_min=1,r=1000] ~ ~ ~ title @a subtitle [{"selector":"@a[score_crown_min=1,r=1000]"},"特别的屌"]
复制代码
subtitle部分首先显示没人拿着皇冠的内容。如果有人拿着皇冠,第三条命令就会覆盖上去。


全员tellraw提示每个人分别戴着皇冠多少秒
首先添加记分板统计戴皇冠时间

  1. scoreboard objectives add crownTime dummy 持有皇冠时间
复制代码
在system:loop中加入以下命令统计戴皇冠时间

  1. scoreboard players add @a[score_crown_min=1] crownTime 1
复制代码
在游戏结束模块加入以下命令

  1. tellraw @a ["===================="]
  2. execute @a[m=2] ~ ~ ~ tellraw @a [{"selector":"@a[c=1]"}," 带着皇冠跑了",{"score":{"name":"@a[c=1]","objective":"crownTime"}}," 秒."]
  3. tellraw @a ["===================="]
复制代码
其中第二条命令表示,每个处在游戏中的玩家,也就是m=2的玩家都发送一条关于自己戴皇冠时间的消息。


二次总结
经过以上的美化处理,系统变得更完善了,同时也没有失去其易于维护、模块化的特性。下面是二次总结的命令。

  1. # system:_init

  2. scoreboard objectives add playerDeath deathCount 玩家死亡次数
  3. scoreboard objectives add crown dummy 谁拥有皇冠
  4. scoreboard objectives add pickCrownUp stat.pickup.minecraft.golden_helmet 捡起皇冠
  5. scoreboard objectives add spawnTime dummy 重生等待时间
  6. scoreboard objectives add system_time dummy 记录游戏时间
  7. scoreboard objectives add crownTime dummy 持有皇冠时间

  8. scoreboard teams add crownTeam 皇冠
  9. scoreboard teams option crownTeam color yellow
复制代码
  1. # system:loop

  2. #死亡掉落
  3. execute @a[score_playerDeath_min=1,score_respawnTime_min=1,score_respawnTime=1,score_crown_min=1] ~ ~ ~ summon item ~ ~ ~ {Item:{id:"minecraft:golden_helmet",Count:1},PickupDelay:20,Invulnerable:1,Glowing:1,Team:"crownTeam",CustomName:"皇冠",CustomNameVisible:1,Age:-32768}
  4. scoreboard players set @a[score_playerDeath_min=1,score_crown_min=1] crown 0
  5. scoreboard teams leave @a[score_crown=0,team=crownTeam]

  6. #玩家捡起皇冠后带上
  7. clear @a[score_pickCrownUp_min=1] golden_helmet
  8. scoreboard players set @a[score_pickCrownUp_min=1] crown 1
  9. execute @a[score_pickCrownUp_min=1] ~ ~ ~ tellraw @a [" >> ",{"selector":"@a[score_pickCrownUp_min=1]"}," 拾起了一顶皇冠"]
  10. scoreboard players set @a[score_pickCrownUp_min=1] pickCrownUp 0

  11. #死亡重生
  12. scoreboard players set @a[x=22,y=200,z=22,dx=30,dy=20,dz=30,score_playerDeath_min=1] spawnTime 1
  13. tp @a[x=22,y=200,z=22,dx=30,dy=20,dz=30,score_playerDeath_min=1] 15 12 15
  14. scoreboard players add @a[score_spawnTime_min=1] spawnTime 1
  15. tp @a[score_spawnTime_min=100] -32 18 32
  16. scoreboard players set @a[score_spawnTime_min=100] spawnTime 0

  17. #增加时间
  18. scoreboard players remove @e[name=gaming] system_time 1

  19. #死亡判定
  20. function system:end if @e[name=gaming,score_system_time=0]

  21. #玩家发光
  22. scoreboard teams join crownTeam @a[score_crown_min=1,team=!crownTeam]
  23. effect @a[score_crown_min=1] glowing 2 1 true

  24. #刷新向日葵
  25. scoreboard players add crown 0
  26. scoreboard players tag @a add haveCrown {Inventory:[{id:"minecraft:double_plant",Slot:103b}]}
  27. replaceitem entity @a[score_crown_min=1,tag=!haveCrown] slot.armor.head minecraft:double_plant
  28. replaceitem entity @p[tag=haveCrown,score_crown=0] slot.armor.head air
  29. scoreboard players tag @a[tag=haveCrown] remove haveCrown

  30. #统计戴皇冠时间
  31. scoreboard players add @a[score_crown_min=1] crownTime 1
复制代码
  1. # system:start

  2. summon area_effect_cloud 4 2 4 {Duration:999999,CustomName:"gaming"}
  3. scoreboard players set @e[name=gaming] system_time 3600
  4. scoreboard players set @r[m=2] crown 1
复制代码
  1. # system:end

  2. kill @e[type=!player]
  3. tp @a 22 200 22

  4. title @a times 0 200 10
  5. title @a title ["游戏结束"]
  6. title @a subtitle ["你们都是笨蛋, 皇冠也能丢?"]
  7. execute @p[score_crown_min=1,r=1000] ~ ~ ~ title @a subtitle [{"selector":"@a[score_crown_min=1,r=1000]"},"特别的屌"]

  8. tellraw @a ["===================="]
  9. execute @a[m=2] ~ ~ ~ tellraw @a [{"selector":"@a[c=1]"}," 带着皇冠跑了",{"score":{"name":"@a[c=1]","objective":"crownTime"}}," 秒."]
  10. tellraw @a ["===================="]

  11. scoreboard players reset *
复制代码

对系统瑕疵的优化
经过了上述三个步骤,可以说整个系统已经可以正常的开始、结束、游玩了,但是仍然有一些瑕疵。这些瑕疵或者说叫bug,是需要进行不断的测试来发现的。下面这一部分难度相比上面更加复杂,对于刚刚接触命令的新玩家可以略过。由于这部分面向较高水平玩家,我将讲解的比较概略。下面是我们在测试中发现的瑕疵,并不保证完全。

  • 玩家带着皇冠下线会导致游戏无法进行
  • 有皇冠的玩家死在岩浆里,导致皇冠落在岩浆里,致其他玩家无法捡起
解决以上两个问题,我的思路是计算玩家带的与地面上不在岩浆里的皇冠数量,如果小于1,那么就重新分配皇冠。以下是我的具体解决方案:


判定玩家是否掉线
首先将玩家掉落皇冠的皇冠中加入一个tag(crownItem)方便统计。修改后的命令如下:

  1. execute @a[score_playerDeath_min=1,score_respawnTime_min=1,score_respawnTime=1,score_crown_min=1] ~ ~ ~ summon item ~ ~ ~ {Item:{id:"minecraft:golden_helmet",Count:1},PickupDelay:20,Invulnerable:1,Glowing:1,Team:"crownTeam",CustomName:"皇冠",CustomNameVisible:1,Age:-32768,Tags:["crownItem"]}
复制代码
新建一个记分板用于保存假名的记分板值:

  1. scoreboard objectives add system dummy 系统
复制代码
在system:loop中加入以下命令来统计皇冠数量

  1. scoreboard players set @e[name=gaming] system 0
  2. execute @a[score_crown_min=1] ~ ~ ~ scoreboard players add @e[name=gaming] system 1
  3. execute @e[type=item,tag=crownItem] ~ ~ ~ scoreboard players add @e[name=gaming] system 1
复制代码
这样 @e[name=gaming] 的system记分板值就是玩家身上和地面上的皇冠数量了。若皇冠数量为0,则说明有玩家戴着皇冠掉线了。新建一个函数 system:leave_game 用于处理玩家掉线

  1. # system:leave_game

  2. scoreboard players reset * crown
  3. scoreboard teams empty crownTeam
  4. scoreboard players add @r[m=2,score_spawnTime=0] crown 1
  5. tellraw @a [" >> ","有个笨蛋戴着皇冠跑了!我们果断把皇冠抢了回来重新分配了一下"]
复制代码
前两条命令清空crown记分板和队伍,这两条无论被清空玩家在不在线,其数据都会被清空。然后再随机分配给另一个人(这个人不能处于复活进程中,也就是score_spawnTime=0)并提示。

最后在system:loop中加入以下命令来调用leave_game函数

  1. function system:leave_game if @e[name=gaming,score_system=0]
复制代码

判定皇冠是否在岩浆中
  1. execute @e[type=item,tag=crownItem] ~ ~ ~ detect ~ ~ ~ lava -1 scoreboard players set @s system 1
  2. execute @e[type=item,tag=crownItem] ~ ~ ~ detect ~ ~ ~ flowing_lava -1 scoreboard players set @s system 1
  3. execute @e[type=item,tag=crownItem,score_system_min=1] ~ ~ ~ scoreboard players add @r[m=2,score_spawnTime=0] crown 1
  4. execute @e[type=item,tag=crownItem,score_system_min=1] ~ ~ ~ tellraw @a [" >> ","皇冠进了岩浆, biu的一下跑到了一个玩家的身上"]
  5. kill @e[type=item,tag=crownItem,score_system_min=1]
复制代码
前两条命令判断皇冠是否在岩浆中(包括流动的),第三条随机分配给一个玩家,然后提示并删除皇冠实体。


最终总结
由上可见,完成一个完整的系统可以从易到难一步步分析完成。要在设想阶段把一个系统简单化,把大问题化成小问题来解决。这种模块化的思维有助于清醒头脑,把注意力集中在一个点上,更少的犯错误。

在细节的考虑方面,我们把握一个度,不能太粗犷,也不能太细致。比如在第三部分以后的部分,我们如果不考虑玩家掉线的情况(实际上这经常发生),那么其他玩家的游戏体验就会较差,对于这种小品游戏,这种情况还是要考虑到的。但是有心的玩家也能从我写的命令中看出问题,如果所有玩家都在重生,唯一戴皇冠活着的玩家掉线了,那么皇冠就无处分配,游戏也无法进行下去。但是这种情况实在是少之又少,因此此时对细节的考虑就不能太细致,否则会大大增加自己的工作量。

在系统的大小方面,我们也要控制一个度。由于每个模块之间的交互,越大的系统越难完成。举一个模块之间交互的例子,如果在玩家掉线后,需要注意皇冠不能分配给一个房间复活的人。本来两个不相干的模块,却有一个交叉点需要注意。模块越多,这样的交叉点就越多。我对这种情况的处理方式就是,写一个文档,描述每一个记分板,每一个tag的作用,甚至是一些关键的坐标,每次写命令之前都去看一看,尽量记住这些东西的作用,这样能最大限度减少交互方面带来的小bug。

最后,感谢各位读完这篇又臭又长的系统编写事例,希望不会让你一头雾水。下面是总结好的所有命令,请收下。

最后的最后,提出一个小小的**:使系统支持多个皇冠同时存在于场上。游戏开始分配一个以上的皇冠,死亡只掉落一个皇冠。在更多人游玩时多皇冠更加适合。正式的燃烧的子弹游戏里是支持多皇冠的(只是没有启用),有兴趣的读者可以翻翻function源码,看看和自己的构思是否一致。

  1. system
  2.   |-- _init.mcfunction
  3.   |-- loop.mcfunction
  4.   |-- start.mcfunction
  5.   |-- end.mcfunction
  6.   |-- leave_game.mcfunction
复制代码
  1. # system:_init

  2. scoreboard objectives add system dummy 系统
  3. scoreboard objectives add playerDeath deathCount 玩家死亡次数
  4. scoreboard objectives add crown dummy 谁拥有皇冠
  5. scoreboard objectives add pickCrownUp stat.pickup.minecraft.golden_helmet 捡起皇冠
  6. scoreboard objectives add spawnTime dummy 重生等待时间
  7. scoreboard objectives add system_time dummy 记录游戏时间
  8. scoreboard objectives add crownTime dummy 持有皇冠时间

  9. scoreboard teams add crownTeam 皇冠
  10. scoreboard teams option crownTeam color yellow
复制代码
  1. # system:loop

  2. #死亡掉落
  3. execute @a[score_playerDeath_min=1,score_respawnTime_min=1,score_respawnTime=1,score_crown_min=1] ~ ~ ~ summon item ~ ~ ~ {Item:{id:"minecraft:golden_helmet",Count:1},PickupDelay:20,Invulnerable:1,Glowing:1,Team:"crownTeam",CustomName:"皇冠",CustomNameVisible:1,Age:-32768,Tags:["crownItem"]}
  4. scoreboard players set @a[score_playerDeath_min=1,score_crown_min=1] crown 0
  5. scoreboard teams leave @a[score_crown=0,team=crownTeam]

  6. #玩家捡起皇冠后带上
  7. clear @a[score_pickCrownUp_min=1] golden_helmet
  8. scoreboard players set @a[score_pickCrownUp_min=1] crown 1
  9. execute @a[score_pickCrownUp_min=1] ~ ~ ~ tellraw @a [" >> ",{"selector":"@a[score_pickCrownUp_min=1]"}," 拾起了一顶皇冠"]
  10. scoreboard players set @a[score_pickCrownUp_min=1] pickCrownUp 0

  11. #死亡重生
  12. scoreboard players set @a[x=22,y=200,z=22,dx=30,dy=20,dz=30,score_playerDeath_min=1] spawnTime 1
  13. tp @a[x=22,y=200,z=22,dx=30,dy=20,dz=30,score_playerDeath_min=1] 15 12 15
  14. scoreboard players add @a[score_spawnTime_min=1] spawnTime 1
  15. tp @a[score_spawnTime_min=100] -32 18 32
  16. scoreboard players set @a[score_spawnTime_min=100] spawnTime 0

  17. #增加时间
  18. scoreboard players remove @e[name=gaming] system_time 1

  19. #死亡判定
  20. function system:end if @e[name=gaming,score_system_time=0]

  21. #玩家发光
  22. scoreboard teams join crownTeam @a[score_crown_min=1,team=!crownTeam]
  23. effect @a[score_crown_min=1] glowing 2 1 true

  24. #刷新向日葵
  25. scoreboard players add crown 0
  26. scoreboard players tag @a add haveCrown {Inventory:[{id:"minecraft:double_plant",Slot:103b}]}
  27. replaceitem entity @a[score_crown_min=1,tag=!haveCrown] slot.armor.head minecraft:double_plant
  28. replaceitem entity @p[tag=haveCrown,score_crown=0] slot.armor.head air
  29. scoreboard players tag @a[tag=haveCrown] remove haveCrown

  30. #统计戴皇冠时间
  31. scoreboard players add @a[score_crown_min=1] crownTime 1

  32. #判定是否有玩家掉线
  33. scoreboard players set @e[name=gaming] system 0
  34. execute @a[score_crown_min=1] ~ ~ ~ scoreboard players add @e[name=gaming] system 1
  35. execute @e[type=item,tag=crownItem] ~ ~ ~ scoreboard players add @e[name=gaming] system 1
  36. function system:leave_game if @e[name=gaming,score_system=0]

  37. #处理皇冠掉岩浆
  38. execute @e[type=item,tag=crownItem] ~ ~ ~ detect ~ ~ ~ lava -1 scoreboard players set @s system 1
  39. execute @e[type=item,tag=crownItem] ~ ~ ~ detect ~ ~ ~ flowing_lava -1 scoreboard players set @s system 1
  40. execute @e[type=item,tag=crownItem,score_system_min=1] ~ ~ ~ scoreboard players add @r[m=2,score_spawnTime=0] crown 1
  41. execute @e[type=item,tag=crownItem,score_system_min=1] ~ ~ ~ tellraw @a [" >> ","皇冠进了岩浆, biu的一下跑到了一个玩家的身上"]
  42. kill @e[type=item,tag=crownItem,score_system_min=1]
复制代码
  1. # system:start

  2. summon area_effect_cloud 4 2 4 {Duration:999999,CustomName:"gaming"}
  3. scoreboard players set @e[name=gaming] system_time 3600
  4. scoreboard players set @r[m=2] crown 1
复制代码
  1. # system:end

  2. kill @e[type=!player]
  3. tp @a 22 200 22

  4. title @a times 0 200 10
  5. title @a title ["游戏结束"]
  6. title @a subtitle ["你们都是笨蛋, 皇冠也能丢?"]
  7. execute @p[score_crown_min=1,r=1000] ~ ~ ~ title @a subtitle [{"selector":"@a[score_crown_min=1,r=1000]"},"特别的屌"]

  8. tellraw @a ["===================="]
  9. execute @a[m=2] ~ ~ ~ tellraw @a [{"selector":"@a[c=1]"}," 带着皇冠跑了",{"score":{"name":"@a[c=1]","objective":"crownTime"}}," 秒."]
  10. tellraw @a ["===================="]

  11. scoreboard players reset *
复制代码
  1. # system:leave_game

  2. scoreboard players reset * crown
  3. scoreboard teams empty crownTeam
  4. scoreboard players add @r[m=2,score_spawnTime=0] crown 1
  5. tellraw @a [" >> ","有个笨蛋戴着皇冠跑了!我们果断把皇冠抢了回来重新分配了一下"]
复制代码



黑洞
数学不好不会写,请dalao多多体谅

本题目出自CBL前新手群(现技术摸鱼**)问答比赛。

题目:

32*32的场地有一个确定的中点,范围内玩家均会受到值为每秒0.5格,方向对准这个中心点的速度分量。

目前已经有船黑科技,然而船黑科技比较难以控制速度(一层=0.2米,不可能0.5米),所以我们还是需要用别的方法的。

而我们选择的方法就是穷举。


穷举
我们首先要解决的问题就是,如何得知中心在哪个角度。

我们可以穷举不同坐标,穷举出每个坐标分别在中心的哪个方位,然后穷举传送的值。
然而这方法的精度不足(一个方块里也能有不同位置),我们不会使用这个方法。

我们可以在中心四周(圆形)放一大堆marker实体,每个代表一个相应角度,让玩家选择最近的那个。很难解释直接上图算了。



绿色代表黑洞中心。黑色圆形外框代表我们穷举生成的marker,注意marker的位置必须精准测量,间隔相同以及距离中心的位置相同。
ABCD这四点代表不同的玩家位置。红色的点代表玩家选择到最接近他们的marker。
可以看到玩家选择的离他们最近的marker,就代表他们的角度是θ。(解释啥的已经懒得写了)

我们要得知玩家当前角度的方法十分简单:把角度分数放进marker记分板里,然后赋值就行了。于是我们穷举的工作有这几个部分:

  • 每个角度:
    • 指定坐标(根据角度和中心位置进行计算)生成marker
    • 给新生成的marker一个角度分数
  • tp 指定分数(角度)的玩家一定距离(水平距离为0.5米)
    由于我们是向着圆心传送的,因此我们的角度是  180°-θ 。然而为了方便,我们直接把marker和圆心的距离设为0.5,然后tp的坐标就是  -marker和圆心的相对坐标  就行了。
穷举部分(直接上js代码,没人会手算。辣鸡代码请dalao手下留情):


穷举出的所有命令请参见这里

  1. //初始化的命令,在中心的位置执行
  2. var init = [];
  3. //main的命令
  4. var main = ["execute @a ~ ~ ~ scoreboard players operation @s angle = @e[c=1,tag=circle] angle"];

  5. //i为角度
  6. for (var i = 0; i < 360; i+=3) {
  7.     //x偏移
  8.     var x = 0.5 * Math.cos(i / 180 * Math.PI); //角度转rad
  9.     var z = 0.5 * Math.sin(i / 180 * Math.PI);

  10.     init.push('summon area_effect_cloud ~' + x.toFixed(3) + ' ~ ~' + z.toFixed(3) + ' {Duration:2147483647,Tags:[circle,new]}');
  11.     init.push('scoreboard players set @e[tag=new] angle ' + i);
  12.     init.push('scoreboard players tag @e[tag=new] remove new');
  13.     main.push('tp @a[score_angle_min=' + i + ',score_angle=' + i + '] ~' + (-x).toFixed(3) + ' ~ ~' + (-z).toFixed(3));
  14. }
复制代码



登录系统
我们这个例子会制作一个登录系统,也就是密码系统。
这个密码系统支持4种字符( abcd )。而且我只会写出重点的逻辑部分。(密码部分)
这限制是为了偷懒,并且缩减篇幅长度。要扩展是绝对可以实现的,并且会相当容易。

建议对记分板及命令函数递归机制有认识的读者阅读。
如果不懂这两方面的话还是先看看之前的章节吧...

这是作者在头疼的时候写出来的,或许会有点小遗漏,如果发现请告诉作者。


需求分析及系统设计
登录系统需要两部分:注册、验证。

注册,就是当玩家第一次进入该世界,就要求玩家输入密码,作为以后验证之用。
验证,就是当玩家之后进入该世界,就先禁止玩家行动,要求玩家输入密码,错误则需要重新输入密码,正确则容许行动。

禁止玩家行动啊,检查第一次进入世界啊,多次进入世界啊那些不是太简单就是曾经在之前记分板的例子说了,这里就不写了。
我们就写密码输入和验证系统。

由此可见我们需要几个系统:

  • 输入密码(注册)系统:  system:register
  • 输入密码(验证)系统:  system:login_password
  • 密码检测:  system:password_check

数据储存
由于我们需要输入的不是一般数字,我们需要使用分数以外的方式储存密码。
我的选择是使用一堆实体:每个实体代表密码中的一个字符( CustomName:password_char )。

每个实体有两个分数:

  • 字符类型(char)。比如1代表 a ,2代表 b ,3代表 c ,4代表 d
  • 字符位置(pos),比如 abcd 中的 a 的位置是1。
注册的时候,实体会被tp到位于出生点的marker那里,这是为了避免这些marker不被加载。
然后验证的时候我们会逐个字符检查,看看密码是否正确。

相信聪明的读者已经发现了:我们怎么分辨这些密码是谁的?
没错,我们根本无法分辨。因为我们设计的时候根本不知道密码系统竟然需要特别去分辨玩家。

没关系,我们可以现在想。
如何让储存密码的实体,和玩家之间弄一个连接呢?
我们可以使用分数:定义一个编号(ID),每个玩家的编号都是独一无二的,然后让储存密码的实体的编号和玩家一样,检查的时候检查编号是否相同即可。

所以我们目前有三个记分板变量:

  • char
  • pos
  • id

输入密码
我设计系统时喜欢从难的地方思考,因为如果发现根本没法搞就可以及早避免这个方法。如果从简单的地方开始设计,则发现没法搞的时候就已经太晚了。
而这系统里明显密码是最困难的,我们就先思考密码了。

输入密码可以分为三部分:

  • 输入一个字符
  • 删除一个字符
  • 完成输入

输入字符
先从输入一个字符开始思考。
我们输入一个字符需要干什么呢?

  • 生成实体
  • 设置char为指定分数
  • 设置id为玩家的编号
  • 设置pos为当前密码长度+1
看起来挺简单的对吧?然而我不喜欢,准确来说,我不喜欢第四个步骤,因为这太麻烦了。
在命令里要做到第四步是十分麻烦的,因为我们需要一个储存当前密码数量的marker,这marker需要生成,需要删除,需要和别的玩家的分开等等。

所以,我们不如改改我们pos的表达方式?
现在是1代表第一个字符,2代表第二个字符,如此类推。
如果我们改成1代表最后一个字符,2代表从后数上的第二个字符,如此类推。
这样我们的工作就会变为:

  • 生成实体(设置tag以得知这是新的)
  • 设置char为指定分数
  • 设置id为玩家的编号
  • 所有当前的密码实体的pos分数+1
这样第四个步骤就简单多了。


删除字符
然后我们需要看看删除一个字符得咋办。
删除一个字符需要的工作为:

  • 删除pos分数为1的当前密码实体(最后一个)
  • 所有当前的密码实体的pos分数-1
不错对吧?所以这个就不需要改了


完成输入
最后是完成输入。

  • 如果找不到任何当前密码实体就告诉玩家还没输入密码
  • 如果能找到当前密码实体的话,就继续

注册
注册就非常简单,加上个tag叫 raw 然后tp到出生点就好了。(注册是一开始就注册的,所以不应该会出现玩家到了别的世界才注册的情况...)


注册
验证其实就是逐个字符验证,也非常简单。
如果字符不相同就整个密码不同;如果密码长度不同就整个密码不同。


当前密码实体
好了,如果读者还是觉得没问题的话,那就让我问你们一个问题吧!
什么叫 当前的密码实体

很明显我们是没有定义这个的。
那么我们 当前的密码实体 是啥呢?

就是我们当前在输入密码的,属于当前这个玩家当前输入 的实体。因为我们的系统需要兼容多人,因此可能有多个人在输入密码。而且我们的系统需要有两组密码: 一组储存以供验证,一组检查,所以需要检测啥是当前输入的。

我们采取的方法是:

  • 所有当前输入(tag  password_input )密码实体的id分数-=当前玩家id分数
  • 处理一切
  • 加上当前玩家id分数

实现
写这个的时候突然想起我们是使用JSON按键的,也就是说会使用trigger,那么让玩家的char分数=要输入的字符分数就好了,那样就可以operation完成分数赋值,不需要分开不同符号。

  1. # input:add_char
  2. # 必须由玩家执行

  3. # 生成实体,有new tag是因为我们需要一个方法记认这个新生成的实体
  4. summon area_effect_cloud ~ ~ ~ {Duration:2147483647,CustomName:"password_char",Tags:["new","password_input"]}
  5. scoreboard players operation @e[tag=new,name=password_char] char = @s char
  6. scoreboard players operation @e[tag=new,name=password_char] id = @s id

  7. # 移除new tag
  8. scoreboard players tag @e[tag=new,name=password_char] remove new

  9. # 找出当前输入的密码字符
  10. scoreboard players operation @e[tag=password_input,name=password_char] id -= @s id
  11. # id = 0的就是当前输入的密码字符
  12. scoreboard players add @e[score_id=0,score_id_min=0,tag=password_input,name=password_char] pos 1
  13. # 重置id
  14. scoreboard players operation @e[tag=password_input,name=password_char] id += @s id
复制代码



  1. # input:delete
  2. # 还是需要由玩家执行

  3. # 找出当前输入的密码字符,id = 0的就是当前输入的密码字符
  4. scoreboard players operation @e[tag=password_input,name=password_char] id -= @s id
  5. # 移除pos=1的
  6. kill @e[score_id=0,score_id_min=0,tag=password_input,name=password_char,score_pos=1,score_pos_min=1]
  7. # 一起减分
  8. scoreboard players remove @e[score_id=0,score_id_min=0,tag=password_input,name=password_char] pos 1
  9. # 重置id
  10. scoreboard players operation @e[tag=password_input,name=password_char] id += @s id
复制代码



  1. # input:try_register

  2. # 找出当前输入的密码字符,id = 0的就是当前输入的密码字符
  3. scoreboard players operation @e[tag=password_input,name=password_char] id -= @s id

  4. # 如果存在当前密码实体才执行input:register_complete
  5. function input:register_complete if @e[score_id=0,score_id_min=0,tag=password_input]
复制代码
  1. # input:register_complete

  2. # 一早已经找出了当前输入的密码字符,所以id=0
  3. scoreboard players tag @e[score_id=0,score_id_min=0,tag=password_input] add raw
  4. # 先重置id,因为重置id依赖于password_input tag
  5. scoreboard players operation @e[tag=password_input,name=password_char] id += @s id
  6. # 然后移除那个tag
  7. scoreboard players tag @e[tag=raw] remove password_input

  8. # 先当出生点有个叫spawn_marker的marker好了
  9. tp @e[tag=raw] @e[name=spawn_marker]
复制代码



  1. # input:check_password

  2. # 这里我们会使用递归,如果成功则为玩家加上tag success

  3. # 先找出id为当前玩家的所有password_char。
  4. scoreboard players operation @e[name=password_char] id -= @s id

  5. # 调用check_char
  6. function input:check_char

  7. # 检查完毕,删除输入
  8. kill @e[score_id=0,score_id_min=0,tag=password_input]

  9. # 恢复id
  10. scoreboard players operation @e[name=password_char] id += @s id
复制代码
  1. # input:check_char

  2. # 逐个字符进行检查,先把最小的(最后的)拉到pos=0,进行检查
  3. scoreboard players remove @e[score_id=0,score_id_min=0] pos 1

  4. # 如果输入的char=0就代表相同
  5. scoreboard players operation @e[score_id=0,score_id_min=0,tag=password_input,score_pos=0,score_pos_min=0] char -= @e[score_id=0,score_id_min=0,tag=raw,score_pos=0,score_pos_min=0] char

  6. # 如果@e[score_id=0,score_id_min=0,tag=password_input,score_pos=0,score_pos_min=0,score_char=0,score_char_min=0]存在,那就代表还能继续检查下去,因为到目前为止的密码都是正确的
  7. function input:check_char if @e[score_id=0,score_id_min=0,tag=password_input,score_pos=0,score_pos_min=0,score_char=0,score_char_min=0]

  8. # 如果@e[score_id=0,score_id_min=0,tag=password_input,score_pos=0,score_pos_min=0,score_char=0,score_char_min=0]不存在,那可能代表几个情况:
  9. # * 不存在这个字符(输入)
  10. # * 不存在对应的字符(原始密码)
  11. # * 字符不同

  12. # 如果目前这个输入字符存在,则代表密码错误(无论是不存在对应的原始密码字符,还是字符不同) (这是错的,看下方Debug部分)
  13. function input:password_wrong if @e[score_id=0,score_id_min=0,tag=password_input,score_pos=0,score_pos_min=0,score_char=0,score_char_min=0]


  14. # 如果不存在这个输入字符,就去检查有没有相应的原始密码字符。如果都不存在就代表成功
  15. function input:check_raw unless @e[score_id=0,score_id_min=0,tag=password_input,score_pos=0,score_pos_min=0]

  16. # 递归完毕的时候每层加上pos
  17. scoreboard players add @e[score_id=0,score_id_min=0] pos 1
复制代码
  1. # input:check_raw

  2. function input:password_correct unless @e[score_id=0,score_id_min=0,tag=raw,score_pos_min=0]

  3. function input:password_wrong if @e[score_id=0,score_id_min=0,tag=raw,score_pos_min=0]
复制代码
  1. # input:password_wrong
  2. tellraw @s ["错误密码"]
复制代码
  1. # input:password_correct
  2. # 这也是错的,看下方debug部分
  3. tellraw @s["密码正确"]
  4. scoreboard players tag @s add success
  5. # 其他处理...
复制代码

Debug
测试的时候发现:

password_correct 里的tellraw写错了,应该是 tellraw @s ["密码正确"] ,写少了个空格

check_char 中的

  1. # 如果目前这个输入字符存在,则代表密码错误(无论是不存在对应的原始密码字符,还是字符不同)
  2. function input:password_wrong if @e[score_id=0,score_id_min=0,tag=password_input,score_pos=0,score_pos_min=0,score_char=0,score_char_min=0]
复制代码
是错的,不小心写成检查正确的(char = 0),应该是检查错误的(char != 0)。以下是改进

  1. # 如果目前这个输入字符存在,并且分数不等于0,则代表密码错误(无论是不存在对应的原始密码字符,还是字符不同)
  2. function input:password_wrong if @e[score_id=0,score_id_min=0,tag=password_input,score_pos=0,score_pos_min=0,score_char=-1]
  3. function input:password_wrong if @e[score_id=0,score_id_min=0,tag=password_input,score_pos=0,score_pos_min=0,score_char_min=1]
复制代码
所以各位记住,写了系统之后还得测试运行一次...
发现结果和想象中不同就猜测哪儿错了,然后看看是不是真的错了。


密码检查详解
我的这个写法或许会让很多人感到困惑,所以就在这里写一下那个的运作原理。

如果懂的话就不用看这里了,这里写得很差。

比如我的id是1,我的密码是 abc ,输入的也是 abc
其中比如a的 pos 就是3(从后数上来第四个), id 就是1(和我的 id 一样), char 就是1(a)。

然后我们来运行 input:check_password ,期间我们会这样做的:

所有属于我的( id =1) password_charid 变为0。然后调用 input:check_char

首先所有这些 password_charpos 会-1。现在 apos 为2, b 的为1, c 的为0。
然后我输入的那个 c (tag= password_input )的 char 分数(3)就会减去以前储存的那个 c (tag= raw )的 char 分数(3)。所以现在我输入的那个 cchar 分数就会是0。
由于这个东西存在并且分数为0,所以我们会再次调用 input:check_password

然后下一次基本上是 ba 的处理,和 c 的类近,因此就**细写了。

最后一次,我们发现连 pos 分数最大的 apos 分数也是负数了,没东西的分数是0,那么会发生什么情况呢?很简单,没法继续递归,并且落入下方的条件里。(因为我们不存在任何东西的 pos 分数为0)

  1. function input:check_raw unless @e[score_id=0,score_id_min=0,tag=password_input,score_pos=0,score_pos_min=0]
复制代码
然后到了 input:check_raw 了。因为我们原始密码(tag= raw )那里也没有一个的 pos 分数为0,因此就是密码正确。

完了!!??

不,还没完。因为递归完会回去之前的执行点,执行那些命令。

  1. # 递归完毕的时候每层加上pos
  2. scoreboard players add @e[score_id=0,score_id_min=0] pos 1
复制代码
这个就是用作重置 pos 的,每层+1回去,于是最后就会没有改变。

  1. # 检查完毕,删除输入
  2. kill @e[score_id=0,score_id_min=0,tag=password_input]

  3. # 恢复id
  4. scoreboard players operation @e[name=password_char] id += @s id
复制代码
这里就是删除输入和恢复id,非常清晰也不多说了。




或许读者会感到困惑,那么如果有字符不对咋办呢?
字符不对就调用不了递归,因为 char 分数不为0。
然后就掉入

  1. # 如果目前这个输入字符存在,并且分数不等于0,则代表密码错误(无论是不存在对应的原始密码字符,还是字符不同)
  2. function input:password_wrong if @e[score_id=0,score_id_min=0,tag=password_input,score_pos=0,score_pos_min=0,score_char=-1]
  3. function input:password_wrong if @e[score_id=0,score_id_min=0,tag=password_input,score_pos=0,score_pos_min=0,score_char_min=1]
复制代码
如果字符长度不对呢?
那就有两个可能:输入过长,原始密码过长。

假如是输入过长,那么就会发生没有原始密码字符配对的情况,也就是那个检查是否相同的scoreboard players operation命令不能执行,也就是说 char 分数不会改变。由于我们限制了 char 分数不能为0,因此没改变的话 char 分数不可能为0,故此也会如同上面一样当作错误密码处理。

如果是原始密码过长,那么就会发生没有输入字符的情况,就会调用 input:check_rawinput:check_raw 里会检查有没有剩余( pos  >= 0)的原始密码字符存在,这情况下是肯定存在的,因此就会报错。


最后
可能读者会觉得这例子十分乱,特别是对比之前的例子。
没错,这例子确实很乱,因为我几乎是直接往下写,不对之前的部分进行修改(逻辑)。
虽然这样会比较乱,但这也表示了我的思路:每一个部分详细思考,发现不足就增加然后进行修改。

这例子也展现了记分板以及是递归的一些特别应用,特别是处理一堆实体的时候应该怎么使用。

[groupid=546]Command Block Logic[/groupid]