在绝大多数包含战斗要素的游戏中,技能系统是相对比较复杂和关键的系统,尤其是在某些强调丰富的技能、种类繁多的武器装备以及爽快的打击感为主的游戏中,如何让玩家体验到震撼的攻击场面,技能系统设计的好坏直接会影响游戏的游玩体验。从宏观来看,游戏技能系统就是程序、美术、音效、策划数值的综合表现。但如何设计出一个易于扩展、安全健壮的技能系统,却又是一个复杂而又系统的工程。如何通过技能系统实现《鬼泣》、《猎天使魔女》这种爽快打击感的ACT游戏,如何通过技能系统实现《DOTA2》、《英雄联盟》这类易于扩展的MOBA游戏的技能和装备系统,如何实现《星际争霸》、《命令与征服》这类RTS游戏中大规模群体战斗的技能机制?
Dota2:数据驱动类技能
得益于Dota2自带的编辑器,通过玩家的手在DOTA2的创意工坊中有很多有趣有好玩的Mod,比如《进化岛》、《大乱斗》等等,这其中更是诞生了风靡全球的《DOTA自走棋》。这些Mod的诞生也从另一个角度证明了DOta2的技能系统和编辑器设计的优秀。
从DOTA2的开发者文档中可以看出其技能系统采用的是”数据驱动“的方式,数据驱动技能是一组键值KeyValue数据,这是一种简单的树状结构,来存储包含了一组组可嵌套的键及其对应值的数据。Dota2通过C++编写的接口来读取技能描述文件,这些描述文件是一种KV(Key-Values)结构的数据,开发者通过编写这些KV(Key-Values)结构的数据来实现不同技能效果的组合。
Dota2中定义的基本键值如下:
"datadriven_skeleton"
{
// General
// ----------------------------------------------------------------------------------------
"BaseClass" "ability_datadriven"
"AbilityBehavior" "DOTA_ABILITY_BEHAVIOR_UNIT_TARGET"
"AbilityTextureName" "spellicon"
"AbilityUnitTargetTeam" "DOTA_UNIT_TARGET_TEAM_ENEMY"
"AbilityUnitTargetType" "DOTA_UNIT_TARGET_HERO | DOTA_UNIT_TARGET_BASIC"
"AbilityUnitTargetFlags" "DOTA_UNIT_TARGET_FLAG_MAGIC_IMMUNE_ENEMIES"
"AbilityUnitDamageType" "DAMAGE_TYPE_MAGICAL"
"AbilityType" "DOTA_ABILITY_TYPE_BASIC"
"MaxLevel" "7"
"RequiredLevel" "-4"
"LevelsBetweenUpgrades" "7"
"AbilityCastPoint" "0.0"
"AbilityCastAnimation" "ACT_DOTA_ATTACK"
"AnimationPlaybackRate" "1"
"AnimationIgnoresModelScale" "1"
// Stats
//----------------------------------------------------------------------------------------
"AbilityDamage" "0 0 0 0"
"AbilityManaCost" "0 0 0 0"
"AbilityCooldown" "0.0 0.0 0.0 0.0"
"AbilityCastRange" "0"
"AbilityCastRangeBuffer" "250"
"AbilityChannelTime" "0.0 0.0 0.0 0.0"
"AbilityChannelledManaCostPerSecond" "30 35 40 45"
"AbilityDuration" "0.0 0.0 0.0 0.0"
"AoERadius" "250"
// ...
}
- Base Class:基类必须是任意原版dota技能或者"ability_datadriven"
- AbilityBehavior :技能行为,技能类型描述了技能如何生效,生效后具体如何执行。 可以同时使用多个不同类型,使用空格和 | 符号分割。
- AbilityTextureName:技能图标,用于在UI上展示。
- AbilityUnitTargetTeam :目标队伍,全部、友军、敌人等等。
- AbilityUnitTargetType:目标类型,隐藏单位、召唤单位、建筑等等。
- AbilityUnitTargetFlags:目标标签,标签允许对默认被忽略的目标单位 (例如魔法免疫敌人)施法, 或者忽略特定的单位类型 (例如远古单位和魔免友军)来允许对其施法。
- AbilityUnitDamageType:伤害类型,魔法伤害、物理伤害、纯粹伤害。
- AbilityType:技能类型,用于区分“普通技能”、“终极技能”、“奖励技能”等等。
- MaxLevel:最大等级。
- RequiredLevell:第一次可以学习技能的英雄等级。这个值设置为负值可以让技能在任意等级开始被学习。
- LevelsBetweenUpgrades:升级下一级技能需要提升多少英雄等级。
- AbilityCastPoint:技能释放的中心点。
- AbilityCastAnimation:技能释放动画。
- AnimationPlaybackRate:动画播放速度。
- AnimationIgnoresModelScale:
- AbilityDamage:技能伤害,对应每一个提升等级。
- AbilityManaCost:魔法消耗,对应每一个提升等级。
- AbilityCooldown:冷却时间,对应每一个提升等级。
- AbilityCastRange:技能释放范围。
- AbilityCastRangeBuffer:技能释放范围缓冲,当目标离开释放范围+这个值时,法术就会取消。
- AbilityChannelTime:施法时间,
- AbilityChannelledManaCostPerSecond:每秒魔法消耗,用于在持续施法时添加一个额外的魔法消耗。
- AbilityDuration:持续时间。
- AoERadius:AOE范围。
- 其他一级键值:技能共享冷却时间AbilitySharedCooldown、队友共享技能AbilitySharedWithTeammates、技能黄金消耗AbilityGoldCost、技能升级黄金消耗AbilityUpgradeGoldCost,等等。
以下是一个简单的Dota2技能,技能被添加之后,将会等到拥有者死亡之后触发,当拥有者死亡的时候,一个带有酸雾特效和一个降低目标区域的单位护甲并施加伤害的计时器将会被创建。
//=================================================================================================================
// Creature: Acid Spray
//=================================================================================================================
"creature_acid_spray"
{
// General
//-------------------------------------------------------------------------------------------------------------
"BaseClass" "ability_datadriven"
"AbilityBehavior" "DOTA_ABILITY_BEHAVIOR_AOE"
"AbilityUnitDamageType" "DAMAGE_TYPE_COMPOSITE"
"AbilityTextureName" "alchemist_acid_spray"
// Casting
//-------------------------------------------------------------------------------------------------------------
"AbilityCastPoint" "0.2"
"AbilityCastRange" "900"
"OnOwnerDied"
{
"CreateThinker"
{
"ModifierName" "creature_acid_spray_thinker"
"Target" "CASTER"
}
}
"Modifiers"
{
"creature_acid_spray_thinker"
{
"Aura" "create_acid_spray_armor_reduction_aura"
"Aura_Radius" "%radius"
"Aura_Teams" "DOTA_UNIT_TARGET_TEAM_ENEMY"
"Aura_Types" "DOTA_UNIT_TARGET_HERO | DOTA_UNIT_TARGET_CREEP | DOTA_UNIT_TARGET_MECHANICAL"
"Aura_Flags" "DOTA_UNIT_TARGET_FLAG_MAGIC_IMMUNE_ENEMIES"
"Duration" "%duration"
"OnCreated"
{
"AttachEffect"
{
"EffectName" "alchemist_acid_spray"
"EffectAttachType" "follow_origin"
"Target" "TARGET"
"ControlPoints"
{
"00" "0 0 0"
"01" "%radius 1 1"
}
}
}
}
"create_acid_spray_armor_reduction_aura"
{
"IsDebuff" "1"
"IsPurgable" "0"
"EffectName" "alchemist_acid_spray_debuff"
"ThinkInterval" "%tick_rate"
"OnIntervalThink"
{
"Damage"
{
"Type" "DAMAGE_TYPE_COMPOSITE"
"Damage" "%damage"
"Target" "TARGET"
}
}
"Properties"
{
"MODIFIER_PROPERTY_PHYSICAL_ARMOR_BONUS" "%armor_reduction"
}
}
}
// Special
//-------------------------------------------------------------------------------------------------------------
"AbilitySpecial"
{
"01"
{
"var_type" "FIELD_INTEGER"
"radius" "250"
}
"02"
{
"var_type" "FIELD_FLOAT"
"duration" "16.0"
}
"03"
{
"var_type" "FIELD_INTEGER"
"damage" "118 128 138 158"
}
"04"
{
"var_type" "FIELD_INTEGER"
"armor_reduction" "-3 -4 -5 -6"
}
"05"
{
"var_type" "FIELD_FLOAT"
"tick_rate" "1.0"
}
}
}
从以上代码中可以看出,一个典型的DOTA2技能数据包括以下几个部分:
- 技能行为:使技能有不用的释放方式,如:持续性施法、被动技能、指向性技能等等能。
- 技能触发的事件和操作:技能可以有各种游戏中的事件,这些事件可以触发操作,如:当技能施法开始、当持续施法完成、当施法者死亡等等。
- 操作目标:技能释放的目标,如:单体目标、多目标等等。
- Modifiers:该属性能够改变对应Modifier影响的单位的游戏相关数值,如:所有魔法攻击无效、所有物理攻击无效、修改基础攻击力、修改攻击速度等等。
- AbilitySpecial:技能特殊定义,这个部分有两个作用:1. 定义随着技能升级改变的值,基于"%value";2. 格式化的文本提示,可以显示此处定义的值,用于鼠标悬停时的提示信息。
其中Modifiers、AbilitySpecial并不是必须的,有些技能并没有这两块的定义。通过Dota2定义好的技能键值和开放出来的API,玩家可以使用Lua重新设计Dota2的技能和英雄行为。不仅是Mod作者可以利用这些方法制作出各种地图,Dota2本身的许多技能和英雄也是使用这种方式制作的。
星际争霸2:ABE结构的技能系统
《星际争霸2》作为一款风靡一时的RTS游戏,其自带的地图编辑器也同样出名,这款编辑器也称为“银河引擎”,得益于暴雪专门为其开发了一套脚本语言,称为基于C的Galaxy,似乎所有 GUI 界面上的编辑器操作,都可以完整的对应成一段Galaxy脚本。通过这款编辑器玩家甚至还原了“暗黑破坏神”,这其中也诞生了《沙漠风暴》这样拥有一大批忠实玩家的地图。
星际2的整个数据对象都是通过XML进行配置的,编辑器读取这些XML文件,在视图界面以树状形式。
一个典型的星际2技能包含以下几个部分:
- Abilities(能力):Ability就是一个单位可以做的事情。比如:攻击, 移动, 建造之类。
- Effects(效果):Effect是让一件事情发生。它是Aiblity幕后的实现,可以增加Buff,产生伤害,治疗单位等。
- Behaviors(行为):效果产生的行为,常见的是“Buff”,是附加到单位并以某种方式影响它的东西。诸如:提高移动速度,禁用武器,有机会阻挡传入的伤害等等。
Abilities中定义了技能的基础数据,这些基础数据最终提供给Effects,而Effects又会产生其他的Effects或Behaviors,可以看出星际2的技能是一种树状结构。
UE4:Gameplay Ability System
对于大多数RPG或MOBA类游戏游戏,无论是Dota2中释放一个技能,然后该技能会击中单位,造成一定量的伤害,并对目标半径内的所有单位随时间造成伤害,同时,释放技能的的玩家会失去一些法力并处于冷却状态;还是在星际争霸2中高阶圣堂释放灵能风暴对某一区域持续性产生伤害,该伤害又会产生其他效果。这些都需要一个高效灵活的技能框架,而UE4中的Gameplay Ability System正是为此而设计的,EPIC的多款3A级的游戏中都得到过应用,如Paragon和Fortnite等。
以下是官方解释:
Gameplay技能系统是一个高度灵活的框架,可用于构建你可能会在RPG或MOBA游戏中看到的技能和属性类型。你可以构建可供游戏中的角色使用的动作或被动技能,使这些动作导致各种属性累积或损耗的状态效果,实现约束这些动作使用的“冷却”计时器或资源消耗,更改技能等级及每个技能等级的技能效果,激活粒子或音效,等等。
该插件主要有以下几个类:
- GameplayAbility:能力,定义C++代码或蓝图脚本中技能的实际作用,并建立处理技能的元素,如复制和实例化行为。
- GameplayTask:任务,在定义技能的逻辑过程中,gameplay技能中的逻辑通常会调用一系列被称为技能任务异步编译块。技能任务衍生自抽象UAbilityTask类,以C++编写。其完成操作时,会基于最终结果频繁调用委托(C++中)或输出执行引脚(蓝图中)(例如,需要目标的技能需进行“瞄准”任务,将调用一个委托或输出引脚确认目标,并调用另一引脚取消技能)。
- GameplayAttribute:属性,Gameplay属性是存储在FGameplayAttribute结构中的“浮点”值,将对游戏或Actor产生影响;其通常为生命值、体力、跳跃高度、攻击速度等值。
- GameplayEffect:效果,Gameplay效果可即时或随时间改变Gameplay属性(通常称为“增益和减益”)。例如,施魔法时减少魔法值,激活“冲刺”技能后提升移动速度,或在治疗药物的效力周期内逐渐恢复生命值
该插件在单人和多人游戏中提供了开箱即用的解决方案:
- 通过可选的消耗和冷却时间来实施基于等级的角色能力或技能(GameplayAbilities)
- 处理Attributes中的数值到Actors(Attributes)
- 将GameplayEffects应用于Actors(GameplayEffects)
- 应用于GameplayTags应用于Actors(GameplayTags)
- 产生视觉或声音效果(GameplayCues)
- 网络同步,将数据同步到其他客户端
在多人游戏中,GAS支持以下方面的客户端预测:
- 激活能力:激活某一个技能。
- 播放动画蒙太奇:播放技能动画。
- 改变Attributes:改变角色身上的属性,如:增加攻击、增加血量。
- 应用GameplayTags:展示技能释放后的状态,如:冷却时间、魔法消耗。
- 生成GameplayCues:显示技能释放后的表现,如:施法特效、声音。
- 通过RootMotionSource连接到带有CharacterMovementComponent功能的Actor控制其移动。
以下是GAS的UML图,可以看出其结构还是很清晰的,主要功能需要在C++中设置,GameplayAbilities和GameplayEffects可以通过蓝图扩展。
参考文档:
- Valve Developer Community:https://developer.valvesoftware.com/wiki/Dota_2_Workshop_Tools:zh-cn/Scripting:zh-cn/Abilities_Data_Driven:zh-cn
- Dota2 Wiki-资料整理版:http://nobuoldman.github.io/#Accessing_the_DOTA_2_Scripting_API_from_Lua
- Unity3D自学笔记——星际2技能系统分析(三)Ability(Effect):https://blog.csdn.net/alistair_chow/article/details/53325404
- GASDocumentation:https://github.com/tranek/GASDocumentation#concepts-asc