(图片准备中,暂时只有文字捏)
在这篇笔记开始之前,我想再次感谢共同参与本次活动制作的大家。即使是现在,我依然对活动能够顺利落地怀有一些庆幸。哪怕早在圣诞期间我们就启动了制作,即便这已经是一套经受验证的成熟系统,但直到活动开启前,大大小小的问题与临时添加的需求依旧是没让我们过个轻松的新年假期。
但也正因这些付出,我们制作了喵毛历来最为精致的活动之一。量身定制的地图,精心设计的角色,经久不衰的玩法,外加那增添了一抹奇幻色彩的故事背景,这些都是制作组成员们倾注的心血。可惜,短短的三天活动,也许对参与的玩家而言真的只是浮光掠影。于是,我们希望通过这篇笔记,向大家介绍更多的内容细节,讲述制作的幕后故事,分享活动的举办经验。如果你碰巧正在基于MC制作或者开发一些游戏内容,我们由衷的希望这篇笔记能对你有所帮助。
另外,这篇笔记将借由活动组的各个成员视角,由成员们(当然目前只有三位核心成员)共同编写。借此大家也能了解到不同成员之间的想法差异(从而看出来他们比我写的要好多了)。总之下面就是笔记的正文了!
制作团队
你想不想加入我们邪恶东手帮? —— Ran_nano
说实话,在这次活动开始制作之前,我也不太注重制作团队的招募。就我个人而言,作为单人开发活动的经验者,我对自己的能力范围有着明确的把握。对于以往的小型活动,我可以包揽整个流程中除了开服务器(这个还是得劳烦伟大的凤凰卷sama)以外的所有内容。虽然“一人役”能够省下一些沟通交流的时间,对于小规模的活动能够显著提高效率,但对大规模的活动就难以做到面面俱到了。因此在这次的活动制作,我们开始对制作团队的建设展开了探索。
团队组建:确定玩法和制作需求
就正常的认知来说,在团队组建时,我们需要明确活动的需求,并根据需求来招募团队。当然有时这一部分也离不开机缘巧合。早在十一月份左右,吐司带着满腔热情找到了我,提出了复刻职业死斗玩法的想法。当时正巧一同在聊天的还有AB,听闻制作新活动的消息,不由分说的也想要加入进来。虽然是复刻活动,但吐司对于活动内容有很多设想,包括故事背景,地图设计,职业设计等等。作为专业MC建筑师与模型师的AB与吐司探讨了很多地图与模型的制作的方案。而我作为一个配置插件和写命令方块的,也和吐司交换了一些关于游戏机制与武器系统上的想法。我们最后确定了复刻活动需要制作的内容:整体基于原先的游戏系统但稍作调整;重新制作一批职业,为每个职业制作一套武器与头饰模型;基于背景故事制作一张新的地图,并针对PVP玩法作尽可能的优化。
制作开始:安排分工与合理交流
既然选择了团队制作,就必定离不开分工合作。我们此次活动的分工如下:吐司作为活动总策划,负责设计活动流程,设计职业与数值,撰写设定和背景;AB则负责制作材质包,规划并牵头制作整张地图;我负责游戏玩法的实现,包括了核心数据包制作,插件配置以及道具的制作。对于大多数的活动而言,这种三角式的分工是非常合理的。主负责人牵动活动开发的整体走向,并在一些关键性问题上进行定夺。而在美术和程序负责人则各自做好内容的规划。
安排了分工以后,我们需要极力避免的是自闭式的开发。这在一些团队项目中并不少见:各人坐着各人的事情,别人做成什么样也不管不问。我们需要有意的安排一些整体交流,让大家分享各自的进展。这种交流没有必要做成工作汇报那样正式,但它也是有一定的目的性的。第一在于让每个人能够有机会向大家展示自己的成果,通过大家的认可保持自己的制作热情与动力。第二在于让成员们对活动整体的进度有所把握,了解到当前的制作全貌,这样对于后续的开发能够更好的提出自己的意见。说的具体一点,我们可以在制作组中确定每天或者每数天的某个时间点,把大家都叫上线,在游戏里实际展示各自的内容进度,一些功能也可以趁着这个机会进行测试。
集体感与团队建设
如何提高一个团队的凝聚力是一件复杂的事。虽然这也是老生常谈的事了,毕竟没有人喜欢每天板着脸来做事。想要搞好成员之间的关系,更多需要依靠各自的才艺发挥,这里我只想分享一些个人的经验和故事。
例如,从项目讨论的第一刻开始,我一定会做的一件事是建立一个语音频道。且不谈语音相比文字而言的交流效率如何,语音聊天也能够在成员中营造亲切感,让大家尽快熟悉,并建立起必要的信任关系。比起文字聊天,语音聊天也更适合各种整活,包括但不限于各种抽象发言和发癫。不过为了彼此之间的融洽关系,在有人吟唱一些中二咒语的时候,最好还是不要录音了,咳咳。
另外我们需要明白,虽说活动制作是正经事,但这毕竟也不是上班。对于一些“不做正事”的行为,我们应该保持一定的宽容度。在我们的活动开发中期,AB花了一晚上把原本的大房间控制室拆了,进行了重建和精装修。当时我看到这个充满了奇幻工业风的控制室,半天说不出一句话来。虽然这对于活动进展完全没有帮助,甚至还额外花了我老大的功夫,额外找了一片放命令方块的地方。
如果某天完全没有精神来做活动,也可以拉着大家一起玩玩小游戏。对于MC开发而言,我非常推荐大家去玩玩各种团队制作的MC小游戏。不仅仅是从玩家角度,更是从开发角度去评价这些小游戏,从中吸取各种正面或者反面的经验。我们非常喜欢的一个小游戏就是目前正开放在hana服务器上的Arbalist游戏。这个小游戏的系统设计的非常精巧:玩法容易上手但拥有深度,通过一些收集性的内容还能提供重复游玩的动力。这个小游戏也为后续我们一些活动的设计提供了宝贵的参考。
活动玩法
求求你们快去看我写的小说吧 —— ToastBrand
贯穿活动的故事背景
也许是出于和多数人的成长环境不大相同的原因,我从小时候起就热衷于冒险,去发现新奇事物。废弃的建筑物、偏僻的小树林,都是我儿时最爱的游乐场。如今,我仍然怀揣着这种好奇心观察这个世界,了解不同的人的生活方式与价值观。我也会时常幻想自己能够成为一名剑与魔法的世界中的旅行者,去探索种种神秘,未知与可能性。
现在,我试图通过写作的方式,表达出自己对现实世界的理解,并描绘出自己幻想中的那个美好又神奇的世界。本次活动的故事,便是我为此迈出的第一步。
这篇故事的大致情节,是在活动策划案起草之前便构思好了的。在活动正式开工前夕,我发布了故事的开头,介绍神秘的旋涡事件,期望能够使更多的读者关注这个故事的后续。活动已有雏形后,故事的主人公们也恰好全部登场,并开始为揭开事件的真相以及活动的举办做下铺垫。当故事进展到众人迎敌,准备战斗时,活动的正式预告也恰好发布。而活动结束后,我则着重采用笔墨为这个故事收尾。
这个故事并不存在高潮的战斗部分,这是我有意为之,也是在试图取巧的地方。一方面这是出于对文笔的不自信,我并不认为自己现在就能写出一个酣畅淋漓、引人入胜的战斗场面;另一方面,我认为这个故事的高潮,正是由亲身经历这次活动的玩家们所描绘出来的。
如果你阅读过我的故事与游戏内道具的介绍文案,你便会发现游戏内容是与这些文字是息息相关的。从这个活动的根本机制来说,你和你的队友正是在扮演故事中的主角探索幻境,而你所面对的敌人——野怪与其他敌对玩家则是在幻境的影响下失去理智的人们。一些细节同样有所对应:比如不同职业的魔力值恢复方式遵循着这个架空世界关于魔法的设定;再比如地图内的一些位置与故事里的描述是一致的……等等。
以上种种的设计,都是希望每一个参与到这个活动中的玩家同样能够沉浸在一个幻想世界中,去感受除了操作游戏本身之外的乐趣——或者说,在如今快节奏的生活中静下心来,试着去欣赏、体验那些包含美的事物。也许这次的尝试局限于我的个人能力并不是很成功,但我仍然会努力用各种不同的方式去构建那个心目中那个美好的世界。
——正如我之前所写的,也许在未来的某一刻,你的道路会再次与那些冒险者们汇聚。
核心玩法的优化思路
作为一个姑且算得上是老玩家的人,我参加了毛线的绝大多数活动,其中一些的举办也包含了我的些许贡献。因此,我对以往活动的优缺点和毛线玩家的喜好也有一定的了解。我认为创作一个东西,首先要量力而行,其次是要考虑好你的受众。所以出于积累活动策划经验的目的,我选择了制作一个优化版的职业死斗——虽然pvp对部分玩家并不友好,但这个活动更简单、轻量,并且已经有了成熟的制作方案,是我认为自己目前能够驾驭的重量级。
整个活动的设计思路无非两个重点:一是尽可能脱离mc原版战斗机制,让可游玩内容变多;二是让轻度玩家同样能够在pvp中获得较好的体验。比如“锐利”机制,一方面用于平衡各职业的强度,另一方面也为精通pvp的玩家提供了不同的进攻思路。不同的魔力回复机制,则让一些职业比起操作更注重决策,又不至于整场只重复着同一个操作无事可做。野怪的加入则意味着当你实在无法与所向披靡的队伍抗衡的时候,仍在游戏中能找到属于自己的发挥空间。
着重要指出的是,无论如何都不要忘记一个游戏最基本的初衷:好玩。作为一个活动的策划,一定要自己试着从一个玩家的角度去体验它,自己能够觉得有趣是把它完成的大前提。其余的种种,建筑、资源包、剧情......都是在一个玩法本身足够有趣的基础之上锦上添花的事物。切莫舍本逐末,在某些不必要的地方耗费过多的精力。
这里要感谢蓝瓜制作的插件,实现了高度可自定的位移技能、暗影刺客的完全隐身以及有趣的跳板。还要感谢ab精心设计了兼顾美观与游戏性的地图,并且不厌其烦地倾听我的要求,为这次活动定制了非常精美的资源包。
职业技能与数值设定
职业的设计的基本思路,一方面参考了《守望先锋:归来》的1/2/2位置分配,另一方面则是以“近战/远程”,“简单/复杂”两个变量进行排列组合,期望每个玩家都可以找到最适合自己的职业。
每个职业都被试图设计得独特且有趣。一个职业设计的流程是:先敲定其大致定位,随后决定它的设定及其特色机制,最后在机制的强度基础上设定数值的强度。很多职业的设计都参考了一些我自己玩过的其他游戏。比如破阵剑士的原型是《荣耀战魂》中的看守者;自然法师在概念上参考了Fate系列的伊莉雅以及《公主连结》中的可可萝;暗影刺客和炼金术师则是借鉴了《英雄联盟》中阿卡丽和辛吉德的设计思路。我始终认为,伟大的作品都是建立于前人立下的基础之上的。因此在创作时,我的建议是尽可能拓宽思路,多接触新事物,并且在有所创新的基础上,不要避讳借鉴其他人。
对于平衡性,大体上来说是期望每个位置都具有一定作战能力,并在此基础之上突出其位置的特点。每个职业的在设计初期都对理论上的伤害输出和等效生命值进行了粗略的计算,使相同位置的职业的性能大致相等。此外,技能的有效范围和冷却时间被设置了一个固定的梯度标准,以便于进行性能的平衡调整,并使得职业设计时拥有一套固定的模板。例如大招的冷却时间被设定了“短/中/长”三个层级,狂战士的大招性能偏弱,则被设置在了“短”这一级别,作为斩杀单个敌人的手段,暗影刺客和精灵弓手的冷却则为“中”,会对整个团战造成影响的坚守者、圣职者则为“长”——顺便一提,大家记忆犹新的“奥能爆破”的冷却时间被特别地设置为了“超长”。(笑)
在这要特别感谢nano。在制作的过程中,nano几乎独自承担了全部我的称得上是折磨的,对技能机制、粒子特效和音效的各种苛刻要求,同时还提供了大量的宝贵的意见和非常新奇的点子。如果没有nano的帮助,我绝对无法完成这样一个精美的活动。
地图与美术
一切都要看起来高级 —— ABraHam_Sid_
能来参与这次活动的开发完全是意外,当时我只是抱着看乐子的心态进了李德二的语音,然后就被骗去搞美术了。在当时李德二和我说吐司想要做一格PVP活动,看内容大概是以前团队死斗的翻版,使用的是和以往不同的写实风格的武器,而且工作量也不大(大嘘)。于是我心动了,决定做完这批模型我就开溜。但是我没溜掉(悲),所以在这次活动里除了武器的设计外,我还负责了所有的头盔设计、材质包方面的调整和地图设计
同时这也是我第一次在没有曲奇的帮助下独立研究这些材质包相关的东西,包括弩、弓箭些有动画的材质的绑定方法。而这次团队死斗新加入的头饰,也是我在和各位团员经过商量提出的,我们认为相比于以前的靴子,我们需要一个更加明显的方式区分玩家的职业,并且需要明显到玩家能在远处就能看出对方的职业配置,所以在各种方案中,我们选择了制作玩家头部的模型,这种模型需要很好地反应出职业的特征。当然,这次的职业能这么好地做出区分,也要感谢吐司那简单粗暴的人物故事设定。
而地图方面,介于这次活动的定位是重置整个团队死斗,我认为重置整张PVP地图也是非常有必要的,最初,我也只有在保留前作地图地形简单更换前作地图设计风格的想法,毕竟这样是最快速,也是最稳妥的方法,而且前作的地图在某种程度上非常的平衡且适合pvp。但是相比于前作的虚空大圆盘地图,我们需要一个更好的pvp地图,这张地图需要有明显的高低差,多种的战斗地形,不论是室内还是室外都需要为战斗专门设计。所以我还是决定从头到尾设计一张新的pvp地图
材质包的制作要领
这次活动的视觉效果,很大部分得益于材质包的使用。毕竟想要在mc中增加不存在的物品,只能靠材质包。
CustomModelData是材质包中一个很重要的功能,只要在合理的范围内,玩家能够在mc中加入几乎无限多的新模型,当然这些功能我也在之前的多次活动中展示过,在这一条中,最重要的便是,制作者加入材质包的模型尽量要在风格上不偏离原本mc的风格,过高像素过于精细的模型都会产生很强的割裂感,同时我也不建议使用magicavoxel(以下简称mv)去制作mc中的模型,虽然mv是一款很好且容易上手的体素软件,但是它对mc的优化并不好,通常用mv制作的模型就算在优化后,也会有高达几百个方块数量,并且mv制作的模型最终也要导入BlockBench进行调整。所以我认为,不如直接尝试使用BlockBench(以下简称bb)制作模型。
而材质包还能额外更改很多mc原生的素材,比如这次活动的死亡音效和额外加入的背景音乐,或是通过修改MC的翻译使mc的原本不可修改的文本内展示出你想要展示出的内容
其实材质包中还有很多看似诡异但是很好的用的技巧,如果有学习材质包制作的想法的话,我建议前期先去找B站的教程,目前B站有大量的教程会教如何在mc中添加自定义模型或是单纯修改某张贴图。而如果想学其他的奇技淫巧,并且自己有一定的材质包基础,我这边建议去解包一些商业服的材质,虽然这些商业服整不出什么好活,但是他们买来的服务端却整合目前国内最高级(不知道怎么形容)的mc技术
地图的设计和规划
这次的地图虽然是完全重置之前的PVP地图,但是我仍然保留了之前地图的大圆盘设计,并且秉持着能省一点力就省一点力的原则,我将从前从未使用过的建筑素材放入了地图内,并且使用了早在2020年研究出来的,能够短时间建造大量建筑群的建筑1方式。所以作为一名合格的建筑师,有一套完整的建筑素材和一套万金油建筑模板是非常有必要的。这样就算工期再短,也能快速地完成地图。虽然容易被玩家指责说一套老本吃到死,但是这也是非常有必要的。
这次地图的除了中心供玩家游玩的圆盘部分,在圆盘之外还制作了大小为700X700的山脉与海岛地形,这些地形玩家虽然无法进入,但是能够防止玩家一眼看到区域外的虚空,影响玩家的代入感。
在建造建筑时,严格按照步骤进行建造是非常有必要的,不可在某一部分死扣细节。而这次建造建筑的大致步骤为:
寻找地图中心点 - 划定战斗区域 - 放置核心建筑 - 设计高低差地台 - 划分战斗风格区域 - 规划路网 - 规划城镇建筑位置 - 建造大型建筑及内饰&城镇建筑大体建造 - 增加建筑外围细节 - 铺设植被
在这些过程中,玩家需要严格遵照建筑步骤,同时在完成建筑后,需要与其他建筑成员沟通,来交换当前进度
这次建筑的重头戏便是对整体地图的优化,在当前地图建筑密集且有大量细节的情况下,玩家必然产生卡顿,这时候优化地图是十分有必要的。而这次活动,我们主要采用的优化方法有:在地图内尽量不使用展示框、盔甲架等实体来装饰,不使用带字的牌子和过多的旗帜,不使用屏障作为地图边界,将树叶方块替换为颜色相近的实心方块
美工团队的领导
作为一名美工,如果你认为自己有强大的美术功底,那么尽量选择参与符合自己审美的活动项目或是加入自己能有较大话语权的项目,如果这个活动在一开始就不和你的心意,那么你就不要霍霍别人了
团队中的美工大部分时候要尽量按照策划的想法进行制作,但是当策划的想法和设计过于单一或老土并且达不到你的审美要求的时候,要尽快且尽早向策划提出,并主动要求按照自己的想法去修改美术资源。当然这一点在目前喵毛美工稀缺且美工比策划在美术方面有更大话语权的情况下是完全可行的
同时美工团队中,因为每一名美工都有自己独特的美术风格,所以需要一名有美工经验的人员来完全主导美工部分的工作,这名美术总监需要管理该项目中所有的和美术有关的事项,保证目前所有的美术资源的风格都能维持在自己统一的一个风格之内,并且在必要的时候需要对手底下的美工进行教学
数据包与插件
再也不想用RGI做道具了 —— Ran_nano
核心功能与数据包
在这次活动里,我依旧是干了自己的老本行——做玩法,做道具。团队死斗的核心功能并不复杂。在多年前最早期的版本中,我一向是使用命令方块制作的。随着后面功能需求的不断增加,命令方块编辑困难的弊端逐渐明显,我开始转向使用数据包函数进行制作。整个活动的完整数据包可以在GitHub查看:
这个数据包涵盖了从活动初始化配置到开放管理的所有功能。其中一些简单的部分,例如游戏规则设定(死亡不掉落等)我就不在这里详细解释了。关于活动服务器基ß础框架的搭建也可以参考我在2018编写的这篇笔记(虽然写的很早而且很简单,但里面大部分内容都没有过时)。这里我想主要讲解一下这个数据包中,各种功能模块的函数实现基本方式。这其中最为基础的模块是击杀积分与死亡复活,这两个模块都使用了高频检测这种最为常见的计分板应用方式。首先定义一个计分项,其准则用来检测你所希望检测的玩家数据,例如玩家击杀数:
scoreboard objectives add kill playerKillCount
随后,使用高频检查每个玩家在该计分项的数值,其值一旦发生变动,则进行对应的操作。在这里,我们需要对于玩家的每个击杀,为其队伍添加对应的分数,因此我们需要高频执行以下内容:
execute as @a[team=1,scores={kill=1..}] run scoreboard players operation Team1 score += @s kill
execute as @a[team=2,scores={kill=1..}] run scoreboard players operation Team2 score += @s kill
execute as @a[team=3,scores={kill=1..}] run scoreboard players operation Team3 score += @s kill
# ...
scoreboard players reset @a kill
这里由于队伍与分数均由字符串而非编号定义,因此必须对每个队伍单独使用一条指令添加积分。在完成了积分操作的最后,我们还需要清空用于检测的计分项。
以上是一个基础的例子。实际的功能实现中会基于高频检测作各种的变化,以实现诸如连杀奖励,悬赏等功能。对于死亡复活,除了检测玩家死亡,我们还需要利用会在每个tick自增的计分项进行计时,由此来达到死亡倒计时的效果。同样的,我们首先添加一个计分项用于检测玩家死亡,并添加一个用作计时器的计分项:
scoreboard objectives add death deathCount
scoreboard objectives add deathTimer dummy
高频执行以下内容,让死亡的玩家获得一个持续600tick的计时器:
execute as @a[scores={death=1..}] run scoreboard players set @s deathTimer -601
scoreboard players reset @a death
同样高频执行以下内容,让计时器跑动起来,并当其到达0时,触发某些效果(这里以调用函数,随机复活在地图中为例)
scoreboard players add @a deathTimer 1
execute as @a[scores={deathTimer=-1}] at @s run function careerpvp:respawn/randomtp
上述的两个基础模块使得游戏能够正常运作。剩下的则需要处理各种特殊情况以及额外功能。在多人活动中,最需要处理的一种特殊情况是玩家掉线的问题。对于掉线问题,我们的处理方案如下。首先使用essentials插件进行两项基础的配置:
- 将玩家的登录位置改为出生点而非下线地点
- 玩家每次登陆时,游戏模式切换回默认的冒险模式
这两项配置都是帮助我们更好的管理掉线玩家。现在,我们只需要对于出现在出生点的玩家进行操作即可。出现在出生点的玩家可能有以下几种情况,我们需要一一对其进行区分:
- 在本局游戏中死亡的玩家
- 在本局游戏中掉线的参与玩家
- 在本局游戏中掉线的观战玩家
- 未参与本局游戏的玩家(可能是新加入的玩家,也可能是上一局的掉线玩家)
利用玩家是否有死亡统计,我们可以区分死亡与掉线的玩家;通过检查玩家是否在某个队伍中(队伍会在局间清空重置),我们可以区分玩家是否有参与到游戏中;对于观战的玩家,我们额外使用了一个计分项纪录其观战状态(这个计分项也会在局间删除重置,不留下上一局的信息),可以用来区分玩家是否在本局的开始就加入了观战。使用上述条件区分每一类玩家后,我们就可以进行对应的操作了。
玩家掉线问题还会影响到一点的是队伍和职业的选择。最早期的版本中,分队都需要管理员现场指挥,每个队伍只有固定的职业可选,且不可更换。随着新职业的开发,组队和职业系统也急需进一步完善。在组队中,因为每个队伍存在人数上限,所以在加入队伍时,我们需要就当前人数进行判定。MC的队伍会将离线的玩家也计入其中,因此我们可以实时检查队伍的人数,而不用单开一个计分项来统计离线玩家了。
# 这里以玩家加入1队为例
# 首先获取当前队伍人数。队伍人数需要减去2因为其中包含了两个非玩家成员
execute store result score Team.1 member run team list 1
scoreboard players remove Team.1 member 2
# 根据不同情况判断是否加入这个队伍,并提示玩家当前状况
tellraw @p[distance=..3,team=1] "你已经在这个队伍里了!"
execute if score Team.1 member matches 4.. run tellraw @p[distance=..3,team=!1] "该队伍已满!请选择其他队伍加入!"
execute if score Team.1 member matches ..3 run tellraw @p[distance=..3,team=] "您已经成功加入队伍 1 "
execute if score Team.1 member matches ..3 run tellraw @p[distance=..3,team=!,team=!1] "您已经成功更换至队伍 1 "
# 如果可以加入队伍,则执行退出队伍的函数(包括清空职业等等),并加入队伍
execute if score Team.1 member matches ..3 run execute as @p[distance=..3,team=!1] at @s run function careerpvp:team/leave_team
execute if score Team.1 member matches ..3 run team join 1 @p[distance=..3,team=]
# 更新队伍的人数
execute store result score Team.1 member run team list 1
scoreboard players remove Team.1 member 2
建立在组队系统之上,更复杂的是职业选择系统。我们的职业系统与OW相同,除了不能重复以外,每一种类型的职业还存在上限。由于玩家可能出现的离线状况,我们无法通过实时检查每个玩家的职业进行判断。因此在这里,我为每个队伍生成了一个盔甲架,用来记录这个队伍的职业选择情况。每有一个职业被选择,盔甲架会被加上对应的tag。后续玩家在选择职业时,都会与盔甲架上的tag进行比对,由此来判断本次选择的合法性。
# 这里以玩家选择职业1为例
# 踩下踏板后,为最近玩家添加tag,之后针对这个tag进行操作
tag @p[distance=..3,gamemode=!spectator] add choose_1
# 如果选择了重复职业,则删除这个tag,中断操作
execute as @p[distance=..20,tag=choose_1 ] at @s unless score @s career matches 1 run execute as @e[tag=team] if score @s team = @p team if entity @s[tag=chosen_1] run tellraw @p "不可以选择与队友重复的职业"
execute as @p[distance=..20,tag=choose_1 ] at @s unless score @s career matches 1 run execute as @e[tag=team] if score @s team = @p team if entity @s[tag=chosen_1] run tag @p remove choose_1
# 清空盔甲架上原职业的tag
execute as @p[distance=..20,tag=choose_1,scores={career=2} ] at @s run execute as @e[tag=team] if score @s team = @p team run tag @s remove chosen_2
execute as @p[distance=..20,tag=choose_1,scores={career=3} ] at @s run execute as @e[tag=team] if score @s team = @p team run tag @s remove chosen_3
#...
# 为盔甲架添加新职业的tag
execute as @p[distance=..20,tag=choose_1 ] at @s run execute as @e[tag=team] if score @s team = @p team run tag @s add chosen_1
# 这里是一系列选择职业需要进行的操作,包括清空背包、发放物品、记录职业编号、重置充能、重置title
clear @p[distance=..20,tag=choose_1 ]
scoreboard players set @p[distance=..20,tag=choose_1 ] give_item 1
scoreboard players set @p[distance=..20,tag=choose_1 ] career 1
scoreboard players set @p[distance=..20,tag=choose_1 ] all_ultcharge 0
title @p[distance=..20,tag=choose_1 ] times 0 10 5
title @p[distance=..20,tag=choose_1 ] subtitle ""
title @p[distance=..20,tag=choose_1 ] title ""
# 最后,清除这个操作tag
tag @a remove choose_1
最后,我们还需要为玩家提供清空职业选择和退出队伍的选项,方便玩家互换职业或者退出队伍。这里不再单独举例。
最后,我还想要简单介绍一下随机复活点的功能。虽然大部分情况下,玩家都会选择复活在队友身边。但如果当队友全部阵亡,第一个复活的玩家只能选择在地图的随机地点重生。与以往不同的是,我们这次不再使用 /spreadplayes
来传送玩家,而是专门定义了一些特定的复活点,这使得玩家在室内复活成为了可能。这些复活点并非通过盔甲架的位置定义,而是直接保存了坐标,这是考虑到了区块加载的问题(玩家无法传送到未加载区块的实体,但可以传送到特定的坐标)。受原版指令参数传递的限制,为了实现特定坐标的读取和传送,我需要用到一些函数的递归。这也是目前数据包开发的局限性所在了。
RPGItems道具制作
显然,到现在为止也没有第二款足够方便的插件能够帮助我们制作道具,我们也只能勉为其难的继续使用RPGItems来制作武器。这个插件我已经使用了数年的时间,也参与了其中一个阶段的功能开发。虽然插件目前已经没有功能性的更新,但好在1.18.2的兼容性更新没有出现任何问题。这个插件对于制作简单功能的小道具来说确实足够方便,但来到我们设计的这套职业系统下,其局限性还是暴露了出来。
在我们的职业系统中,存在很多的自定义持续效果、状态切换,以及对目标的敌友判断。这些都是RPGItems难以支持的方向。一直以来,结合插件与命令方块都是职业技能制作唯一方案。一个比较通用的技能模板如下:首先创建一个dummy的power,用来统一管理某一个trigger的条件、冷却时间,以及可能的耐久消耗。随后,直接使用一个scoreboard的power修改玩家的计分项,剩下的则交给命令方块,通过检查计分项进行后续的技能效果处理。
# 添加计分项res_effect,用来记录这个技能的持续时间
scoreboard objectives add res_effect minecraft.custom:minecraft.time_since_death
# 为道具添加一个dummy的技能,用于管理左键的冷却时间。当冷却时间未达到时,直接取消后续的左键技能。这里没有设置耐久消耗,也没有添加执行条件。
rpgitem power add test_item dummy trigger:LEFT_CLICK cooldown:200 cooldownResult:ABORT cooldownKey:left
# 接着为道具添加一个scoreboard技能,用于修改玩家的计分项。这里将res_effect的值设置为-100。我们无需管理这个技能的消耗、冷却和条件,因为这些可以被前面的dummy技能统一接管。
rpgitem power add test_item scoreboard trigger:LEFT_CLICK objective:res_effect scoreOperation:SET_SCORE value:-100
对于持续或延迟效果,一般会使用一个准则为minecraft.custom:minecraft.time_since_death
的计分项。这个计分项会在每个游戏刻自增,因此方便作为一个内置的计时器。如果我们需要制作某个持续的效果,可以将这个计分项的值设为持续时间的负值,并使用高频命令方块,在其值小于0时持续触发一些效果,例如给附近的队友添加抗性提升效果:
# 下面的高频将在技能生效时为附近10格范围内的队友提供抗性提升I(通过比较team的分数是否与自身相同,判断玩家是否为队友)
execute as @a[scores={res_effect=..-1}] at @s run execute as @a[distance=..10] if score @s team = @p team run effect give @s minecraft:resistance 0 1 true
如果是某个延迟的效果,则依旧可以将这个计分项的值设为延迟时间的负值,并使用高频命令方块,在其值回到0时持续触发效果,这里不再做举例。结合命令方块和RGI插件,我们基本能够完成所有需求效果的制作。
插件与附加系统
一些技能的效果仅仅依靠RGI和原版数据包是无法实现的,因此我们需要额外的插件提供这些功能。其中的一个功能需求是玩家位移,我们使用了蓝瓜开发的ActSensors插件。这个插件将原版命令无法修改的数值以一个个计分项的形式展示出来,只需要修改这些计分项的数值,就可以实现对玩家数值的操作。这些数值包括了玩家的运动、饥饿值、生命值、氧气值、寒冷值等等。我们还利用这个插件制作了一个简单的跳板功能,方便玩家在拥有高低落差的环境中灵活作战。
另外,我们使用了蓝瓜开发的EntityHider插件,用来实现我们梦寐以求的反玩家雷达功能。当非本队玩家B处于玩家A一定范围外的掩体后时,插件将会取消玩家B的数据包发送,玩家A在本地将完全无法得知玩家B的信息,直到玩家B重新出现在玩家A的视野中。由于从发包层面隐藏了对方玩家,安装在本地的任何mod都无法获得这一信息,我们从根源上避免了玩家雷达为PVP游戏带来的不公平性。同时,这个功能也帮助我们实现了刺客职业“完全隐身”的效果。
关于这两个插件的更多代码细节,可以前往GitHub页面查看:
测试与调试
不是,这和我有什么关系 —— LuYiiiiiyo
因为lyy在忙着做新的活动和剪视频,所以这一部分由nano来代写了。欢迎lyy回帖谈一谈自己是怎么测出这么多bug的。
问题测试与平衡性调整
由于后期的时间安排较为紧凑,活动的测试基本与开发同步进行。由于不想过早透露太多的游戏内容,我们没有进行测试人员的招募。但我们在一些内部试玩中,仍然排查到了绝大部分可能会造成活动事故的重大问题。在这里,我们必须承认拥有奇思妙想的Lyy在测试中发挥的关键性作用。
临近活动档期时,我们参照当前的开发进度,认为我们已经不具备大规模测试的时间条件,我们因此决定将活动开放时间确定为连续的数天。在保证基本功能完善后直接开放活动,并根据前一日的情况,在后续的几天进行问题修复与平衡性调整。这可能也是大部分玩家可能觉得首日的活动表现不尽如人意的原因。
性能优化
在测试与正式活动期间,我们和所有玩家一样,都注意到了严重的性能问题。这一性能问题在客户端和服务端都十分突出。客户端方面主要体现在帧数不足与区块边界的卡死的问题。在优化帧数方面,我们首先去除了地图中的大部分旗帜,并将树叶尽可能的替换为绿色的实体方块。这将会为客户端渲染降低不少的压力。而对于区块边界的卡顿,经过我们一天的排查,最后将问题锁定在原计划用来防止玩家出界的大量屏障方块上。我们移除了所有的屏障,并改用提示和伤害来限制玩家出界的行为,问题也随之修复。
服务端的性能优化则相对棘手一些。在首日的活动中,有超过60位玩家同时在场地中游玩,服务器的tps一度保持在极低的水平。这其实是大大出乎我的意料的。要知道,同规模甚至更大规模的职业死斗活动已经举行过数次,没有一次出现过如此大规模的卡顿状况。在发现性能问题的第一时间,我们采取了一些临时措施,包括但不限于重启服务器、限制视距、限制最大参与玩家数等。同时开始使用spark插件对服务器进行性能分析,而结果显示RPGItems和命令方块的性能消耗远超过其他。此时的我们除了对道具和命令方块进行性能优化,已经别无选择。
首先是对数据包的优化。因为早期的开发需要快速实现功能,我直接将命令方块的书写方式套用在了数据包之中。为了节约性能考虑,我们其实可以利用函数调用的方式,大幅减少循环命令中实体选择器和条件判断的次数。例如在上面对于玩家击杀的判断,我们需要如下的高频命令:
execute as @a[team=1,scores={kill=1..}] run scoreboard players operation Team1 score += @s kill
execute as @a[team=2,scores={kill=1..}] run scoreboard players operation Team2 score += @s kill
execute as @a[team=3,scores={kill=1..}] run scoreboard players operation Team3 score += @s kill
# ...
scoreboard players reset @a kill
我们一共有16个队伍,这也意味着我们需要在每一刻进行16次选择器判断。但事实上玩家击杀的频率很低,保持这个高频运作是很消耗性能的。注意到这些命令都有一些共同的条件,因此我们可以将这部分条件提取出来,转化为一个函数调用:
# 高频运行
execute as @a[scores={kill=1..}] run function careerpvp:playerkill
## playerkill.mcfunction
# 玩家进入函数后判断其队伍
execute if score @s team matches 1 run scoreboard players operation Team1 score += @s kill
execute if score @s team matches 2 run scoreboard players operation Team2 score += @s kill
execute if score @s team matches 3 run scoreboard players operation Team3 score += @s kill
# ...
# 重置击杀数
scoreboard players reset @s kill
经过如上的调整,高频的部分只会在每一刻运行一条指令,用来判断是否有玩家达成了击杀。在某个玩家满足击杀条件后,才会进入到playerkill.mcfunction
的函数,继续进行玩家队伍的判断和分数计算,且后续的条件判断只对特定玩家自己进行,不需要使用@a
选择器。如此就可以省下绝大部分的命令方块性能。
而对于RPGItems的优化则比较不直观。一些比较容易理解的优化是降低power beam中的粒子密度以减少递归次数,再就是删除职业condition,减少条件判断的次数(在实现了职业套装自动发放和穿戴后,只要控制玩家无法丢弃物品和交互箱子,就不需要考虑职业道具混拿的问题了)。经过对spark的仔细分析,我们发现使用power attachment充能终极技能会造成严重的性能问题,所以我也花了一天时间,将终极技能的充能改为了基于命令方块和计分板的系统。这套系统大致如下:首先创建用来记录终极技能充能的计分项,并在游戏开始后,对每个玩家的终极技能以不同的速度进行充能。
scoreboard objectives add ult_charge dummy
# 高频运行,如果游戏进行中,则为参加的玩家增加终极技能充能
execute if score ingame internal_var matches 1.. run scoreboard players add @a[team=!] ult_charge 1
对于终极技能的RPGItems道具,使用一个scoreboardcondition进行终极技能是否完成的条件判断,并使用power dummy进行条件管理:
# 创建一个scoreboard条件,用于判断终极技能是否完成充能。如果条件不满足,会取消该触发下的所有后续技能
rpgitem condition add ult_test scoreboardcondition score:`ult_charge:3600,` id:ult_charged isCritical:true isStatic:true
# 创建一个dummy技能,用于判断右键触发是否满足条件
rpgitem power add ult_test dummy trigger:RIGHT_CLICK condition:ult_charged
# 创建一个scoreboard技能,用于清空终极技能充能
rpgitem power add ult_test scoreboard trigger:RIGHT_CLICK objective:ult_charge scoreOperation:SET_SCORE value:0
# 剩余的终极技能效果...
与此同时,我们也使用了一些材质包的功能,将终极技能的充能显示从物品栏上移到了actionbar,而原本这个位置的终极技能提示调整到了聊天栏。经过了这一系列的调整,我们终于在第三天活动的开放前,大幅优化了指令和RGI插件的性能。从第三天的游玩数据来看,我们基本达到了支持40人同时流畅游玩的水平。
展望未来
我要把我的插件重构一遍 —— Asakura_kukki
由于kukki在忙他的课题,所以这一部分依旧由nano代写了。虽然kukki没有为我们的活动提供很多内容帮助,但他为我们的团队建设和未来规划提供了充分的指导。让我们一起感谢kukki!
团队的未来走向
虽然算不上圆满,但这次职业死斗的复刻活动也算是平稳落地了。在这两个月的制作时间中,抛开活动本身,更重要的是我们形成了一个优势互补的团队。我们有一名热爱交流,能够提供丰富创意的策划师兼建筑师;一名对材质包制作有着丰富经验,并负责地图美术与规划的大画家;一名熟悉活动运作,了解各种原版机制与数据包编写的技术员;一位熟知游戏原理,精通程序设计,并为活动插件开辟道路的全能程序员;还有一名充满奇思妙想,热爱探索和发掘游戏深度的LYY。在此之外,我们也欢迎热爱Minecraft开发的玩家的加入,一同进行更大规模的内容制作。
新活动的开发前瞻
最后,在这里悄悄放出一些正在进行制作的活动企划,大家敬请期待吧!

别乱讲,不是刚新建的文件夹!