« June 2024 | Main | August 2024 »

July 25, 2024

工人任务分配系统

在矮人要塞 like 的游戏中,都有一套基于工人的任务分发系统。玩家通常不能像 RTS 中那样直接操作工人去工作,而是对要做的事情下达任务,等着工人自主去完成。

由于任务数量通常远多于工人数量,这个任务分发系统中大多配有优先级设置,可以让诸多任务有条不紊的进行。调整优先级变成玩家主动操控的渠道。初玩这类游戏,会有点不习惯:感觉难以在微观层面直接做自己像做的事情。像捡块石头放进指定仓库这件事,无法像玩 RTS 游戏那样,先点选工人,再针对石头发出拾取指令…… 但习惯之后,恐怕又回不去了。比如我在玩 Ratopia 时,就对操控鼠王直接干活烦躁不已。

这类游戏,我玩的时间比较长的有三个,按时长排序为:缺氧 (ONI) 、边缘世界 (Rimworld)、矮人要塞 (DF)。其它如 Songs of Syx 、Prison Architect 等很多也有所涉猎。其实,这些游戏在设计工人任务系统的细节上也有所不同。

以我游戏时长最长的缺氧和边缘世界相比较,同样是提供玩家主动操控的能力:Rimworld 可以给工人的任务队列直接下达指令(这更接近 RTS 的玩法),而 ONI 则是通过给单个任务本身排优先级实现的。ONI 设计了警报级任务,可以越过一切优先级设定,强制立刻完成。虽然 ONI 也保留了指挥单个小人移动到指定位置,但实际游戏中几乎没什么用。

对于拾取物品,Rimworld 可以封禁、解禁单个物品,而 ONI 没有这个设计。ONI 的工人几乎不会主动把地上的东西搬入仓库,除非下达清扫指令。

这些细节的不同,可能来源于作者设计时的思维轨迹,很大程度上也取决于游戏的其他玩法。例如 Rimworld 偏重手控成分很重的战斗,而 ONI 没有战斗成分。Rimworld 强调人物之间的情感联系,ONI 里的都是工具人。

我比较喜欢 ONI 的系统,打算用这个规则打底设计自己的游戏。下面是设计的草稿:

  1. 游戏场景中代做的事情全部被视为任务,任务需要由工人完成。

  2. 任务构成要素主要由对象和行为构成。对象大多为场景中的建筑,也可以是其它一些活动角色,例如某个工人或敌人。

  3. 行为决定了任务的类型,而每种任务类型有一个预设的“类型权重”;玩家可以对任务所属对象设置一个“对象权重”。

  4. 每个工人有自己的任务队列。工人可以设置对任务类型的“偏好权重”,任务对象在场景中的位置和工人之间的距离决定了任务的“位置权重”。将每个任务的所有权重相乘,得到任务分配给每个工人的最终权重。同一个任务会分别进入每个工人的任务队列中。

  5. 有些任务是分配给特定工人的。例如,工人需要周期进食,氧气不足时会就近补充氧气,等等。也会排入对应的任务队列。

  6. 各个任务队列定期刷新,将归属的任务以权重排序。工人从高到低依次完成任务。因为一个任务可以被分配到多个队列中,所以,可以出现工人当前任务被取消的情况。如果扩展战斗系统,而攻击敌人也属于任务的话,同一个任务也可以被并行执行。

比较特殊的是搬运任务,它通常是建造任务的一个环节,即给建造任务提供原料。它需要把原料从一个地点搬运到建造蓝图的地方。这种搬运任务有两个地点。但给建造蓝图供料时,原料可以有很多候选。我想到两种实现方法:

第一,当一个建造任务被发布后,所有可用的原料均被发布一个供应的子任务,根据和原料和建造任务的距离,给予不同的权重。这样,工人再根据自身的位置,如果开始就近执行一个搬运任务,就立刻把其它搬运任务取消。第二,不考虑不同原料的位置,只要原料可达,就发布一个供应任务,每个工人在考量建造任务权重时,只考虑自己和建造工地的距离。

看起来,第一个方案看起来会有更聪明的表现。因为会倾向于让离原料近的工人就近拿到原料开始搬运。但从实现上看,第二个方案更简单。因为它没有把搬运任务做特殊处理。只在工人执行任务时,再寻找原料。寻早原料变成执行任务的过程,而不在计算权重和分配任务阶段进行。


关于寻路

昨天我实现了基本的寻路模块。一开始,我认为需要的基本功能是:标记出场景中从 A 点到 B 点的路径。由于场景是玩家一点点搭建起来,随时变化,所以很难对场景做充分的预处理。这样一个寻路模块的时间及空间复杂度都不会太低,而在游戏中恐怕不会太低频使用它。我实现了两版都不是很满意,所以又回头来回顾需求。

结合任务系统来考虑,我突然发现,其实真正需要的不是找到任务 A B 两点间的通行路径,而是找到从同一个点出发,到场景中很多点的路径。因为,任务本身是有位置属性的,这个位置属性决定了它在每个工人的队列中的不同权重;或者从工人角度看,他同时会面对多个不同位置的任务,需要根据距离远近排序。所以,更合理的基础功能应该是:针对每个工人的当前位置,计算出距离可达区域每个位置的行动路线,这用几行代码就可以生成。同理,如果像找到一个建筑任务的所有原料供应点的远近,也可以使用相同的算法。

工人只在开始准备一个新任务时,才需要做一次计算。而一旦他处在执行一个任务的过程中,就不再需要实时计算距离其它任务地的路径。

July 16, 2024

飞船建设部分的设计草案

像异星工厂、缺氧、边缘世界都有大量的 mod 。可以通过大改游戏机制,把本体游戏改造成完全体验不同的游戏。这些好玩的 mod 几乎都是一个人完成的。所以我觉得,固然游戏的外层玩法决定了整体体验,但其实开发的总工作量并不大。而且,一旦玩法不满意,也容易修改。

我的游戏开发计划是先完成一些底层基础系统,再考虑完整游戏的全貌。没有上层玩法支撑,光有底层系统玩起来一定寡淡无味,但我认为它们是设计和开发中最重要的一环。

在进一步实现 demo 之前,我设计了一下飞船的建造系统。

游戏世界中的物质分为三种:建筑,零件,原料。

摆放在场景中(飞船)的是建筑,墙体、门、机器、家具这些属于建筑,一旦修建出来就不可任意移动位置。建筑是由零件构成,拆毁建筑会 100% 返还零件。建筑修建是可逆的。

零件是由原料在机器中合成出来,这部分和异星工厂相同。并不一定存在逆向的合成配方。例如,铁板可以合成铁丝,但铁丝并不一定存在变回铁板的方法。所以和建筑不同,加工零件可能是一个不可逆的过程。

飞船(场景)是由四边形网格构成。每个格子永远只能容纳一种物质,要么是建筑的一部分,要么是一种零件。而原料无法直接存在于场景格中,它们必须存在于容器里。建筑和工人都可以是容器。

工人可以在场景格的过道中移动,工人之间不设阻挡。没有建筑的格天然称为过道,部分建筑会占据场景格,成为障碍物。有些建筑将所占据的部分场景格保留为过道。例如门。

每种零件在同一场景格内有一堆叠上限,超过上限就无法容纳在同一格内。有些建筑拥有容器格,容器格和场景格相同,但不存在于场景中(只附属于建筑或工人)。容器还可以存放原料。

每次建设建筑的时候:

  1. 把所占场景格清空。
  2. 把所需零件堆在所占用的场景格上。
  3. 工人在建组所占格的建设邻接位(每个格子有 8 个建设邻接位)工作。
  4. 工人必须通过过道接近建筑蓝图,工人在过道中只能以四个邻接向移动。

注:由多种零件构成的建筑,所占格的数量必须大于等于零件种类。换句话说,如果一格建筑是 1x1 格大小,它就只可能由一种零件构成。

当拆建筑时,也需要工人站在建筑所占格的建设邻接位工作。建筑拆卸后,完全返还零件,置于所占空地。

有些建筑带有不只一格的容器,例如货架,这类建筑会扩展场景的收纳能力。但,拆卸这种建筑必须先排空容器,才能进入拆卸环节。一旦建筑损坏,容器内的物品就不可取出。

加工机器通常带有几格容器,用于保存原料和产品。

工人可以在场景中移动,工人身上带有一或多个容器格。用于建设建筑和安装配件。配件是一种特殊的建筑,存在于工人行动层之外,例如电线和管道,它们可以和建筑同个场景格,拆卸不会回到场景格内,而是进入工人身上的容器中。


游戏的建设部分会类似异星工厂和边缘世界的混合体。和异星工厂不同,建设需要把零件运送到场景格上,并由工人消耗工时修建。一旦修好,不可以在场景上移动位置,但可以无损拆卸。零件加工又比较像异星工厂的加工机制,由合成配方决定了生产过程。但又不允许原料直接放在地板上。

我的设计思路是:游戏里的东西尽量具象化,每个东西就是一个视觉上可见的东西,而不是表单上的一个数字。自动化像矮人要塞或缺氧那样,由工人的工作系统驱动完成。不排除后面加上传送带和自动装卸机,但可能把传送带机制和流体管道合并成同一机制(类似缺氧)。

July 10, 2024

室内空气流动模拟

在设计飞船建造的游戏时,我想做一个空气流动的模拟系统。这里有一个初步的方案,不知道好不好,先记录一下:

  1. 空间是由二维的正方形格子构成的,有些格子如墙体会阻止空气流动。每个格子有温度属性及气体质量的记录。不同气体可以同时存在于同一个内,质量单独记录。温度传导系统另外设计。
  2. 每个游戏 tick 对每个格子做一次独立计算,根据温度值给出一个概率,这个概率决定了该格气体向周围扩展的比例。
  3. 温度越高,越可能将更大比例的气体平均扩散到最多邻接的 8 格空间。
  4. 当格子因为建墙而导致空间变化时,把该格内气体全部扩散到临接格,当极端情况下无法做到时,空气锁定在墙体内,不会消失,等待以后满足条件后再扩散出去。

为什么要做基于格子而不是基于舱室的空气模拟?例如, FTL 的氧气气压模拟就是基于房间的。

因为,这个模拟需要结合游戏本身其它玩法的需要。预计之后会做气闸舱,真空室等设定。需要有一个更微观的空气流动模拟,简单的给船舱加上气压的数值是不够的。和 FTL 不同,我希望做一个基地建设风格的游戏,舱室的结构是动态的,难以以房间为单位计算。

目前有气体流动模拟的游戏中,最流行是缺氧。为什么不直接使用缺氧的模拟系统?

缺氧的基本规则是:每个格子只能有一种物质,这种物质可能有三态:固态、液态、气态。物质会因为温度发生三态转换。不同形态的物质属于不同的东西,每一格不能混装。特殊情况下,气体会直接消失。气体倾向于从高压格流向临近的低压格,并受重力影响向下运动。

缺氧的流体系统是和其它游戏系统相结合的:动植物需要在不同的气体环境下生长,小人会对环境气体尤其是有毒气体做出反应。这些玩法我应该不会做。尤其是,我的游戏地图是平面的,和缺氧不同,没有上下之分,所以不考虑气体重力分层。

对于缺氧来说,它的核心玩法之一是治理无序的自然空间。在玩家拓展空间时,必须考虑原有空间的环境。配合这个设定,才有衍生玩法。而我暂时不想在我的游戏中设计这些。我需要和气体模拟相配合的玩法主要是,根据环境气压影响人物或动植物的状态。

我希望模拟系统简单、准确、符合直觉。

简单:每个格子可以单独运算,不涉及复杂的公式。

直观:尽可能符合玩家的预期。例如,空气从高温区流向低温区,从高气压处流向低气压处,直到平衡。如果空气中混入一点毒气,也会缓慢的随时间扩展,稀释。

准确:规则上可以保证,不会因为模拟计算的误差而出现气体总量的变换。


下面还有一些对玩法的初步想法,尚未推敲细节:

  1. 飞船内需要部署制氧机。制氧机只能通过管道和其它设备连接,不直接向环境排放气体。
  2. 氧气需要通过特定设备输入通风管道,通风管道需要通过排气口向环境排放。
  3. 船舱中可部署一些氧气面罩供应点,它们由管道供气。
  4. 角色随身携带一格氧气面罩,当环境气压在阈值附近变化时自动戴上或取下。氧气消耗完毕后,会自动寻找临近的面罩更换。

July 02, 2024

角色动画系统

Ant 引擎的角色动画系统还需要完善。

之前我们用 Ant 引擎开发的游戏以机械装置为主,所以并不需要人型角色动画。对于人物角色动作的动画控制,最好有更多的引擎支持。

通常,角色逻辑上的属性和动画表现存在一个映射关系。一个角色,它逻辑上的基本属性可能只有在空间中的坐标。我们编写代码控制它时,只关心它在哪里。但是,在做画面表现时,则需要根据空间坐标这组简单属性,转换为动画播放:如果角色静止不动,就播放 idle 动画,如果正在运动,就播放 walk 动画。

在引擎底层,每帧只通过坐标这个属性来计算角色该播放哪个动画以及怎样播放,信息是不够的。通常还需要结合过去时间线上的状态变化来推算出当前状态;或是通过几个独立的逻辑属性的组合,得到动画需要的信息。

例如,可以通过空间坐标的变化过程计算出速度;根据是否处于战斗状态决定是警戒行动还是自由行动……

这种从逻辑属性数据到表现用数据的映射关系,一般使用状态机来实现。Unity 文档对此有一个很好的描述。通过这个状态机,可以生成动画系统底层每帧需要的数据,而开发者只需要简单修改逻辑上的基本属性即可。

动画系统底层看到的是 Entity 当前状态下用于动画渲染的基础数据:一个动画片段的当前帧,或是几个动画片段的加权混合。而状态机只用运行当前帧的当前状态关联的转换逻辑去加工那些基本属性输入。

因为状态机永远处于单一状态,但对于动画来说,从一种状态到另一种状态通常有一个表现过程,所以,状态机中的状态和表现上的状态是有区别的。Unity 把两者区分开,idle ,walk 这种叫 state ,从 idle 到 walk 的过渡期叫 state transition 。从状态机的实现角度看,其实它们都是状态机的节点 node 。

对于非帧动画来说,处于 state 时,通常只有一个动画片段;在 state transition 阶段,则为它连接的两个 state 的动画片段的混合。state 可以触发特定的行为 behaviour ,开发者可以围绕每个特定的 state 来编写逻辑;而 state transition 通常由几个参数控制,对开发者是透明的。

动画状态机的 transition 和动画表现上的多个动画片段混合,是不同的两个东西。

从走路到跑步的过渡阶段可以直接切换两个做好的动画片段,如果是帧动画,动画片段有若干帧构成,通常几个可以衔接在一起的片段,每个片段的最后一帧可以和第一帧衔接在一起,保证循环播放,不同动画片段的第一帧是相同的,允许片段间切换。那么,状态机的 transition 要做的工作一般是,保持上一个状态的动画片段序列播放到最后一帧,可以顺利切换到下一个状态。

以走路过度到跑步为例,状态机在更新时,如果处于走路状态,一旦发现移动速度超过跑步的阈值,就可以切换到 walk to run 这个 transition ,在这个新的 node 中,状态机会继续保持走路的帧动画片段提供给动画底层,直到一个片段周期结束,然后切换到 run 这个新 state ,新的 state 会使用跑步的动画片段从头播放。

对于骨骼动画,往往不受单一动画片段的限制。它可以将多个动画片段混合在一起。在 transition 中,可以逐帧调整多个动画片段的混合权重。

关于人物运动怎样用多个动画片段混合,这里有一篇论文 做了非常详细的解释。

如果是简单的两组动画混合,例如运动方向不变的走路到跑步的过度,使用一维的混合即可。即逐步降低前一个动画片段的权重,同时增加目标动画的权重。

而如果要考虑人物在移动过程中转向,则需要二维的插值。通常使用 Gradient Band Interpolation 。当需要考虑速度变化时,把插值放在极坐标系下效果更好。虽然这需要 O(n2) 的算法复杂度,但通过一张预运算的表,就可以减少到常数时间。

btw, 即使是在 state 中,也可以用到动画的混合。比如,负伤走路的角色和正常走路的角色表现不一样。负伤走路的动画可以是由负伤动画和行走动画叠加混合而来。