什么是实现灵活的增益/减益系统的方法?


66

概述:

许多具有类似RPG的统计数据的游戏都允许角色“ buff”,从简单的“造成25%的额外伤害造成的伤害”到更复杂的事情(如“在受到打击时造成15伤害的伤害回击攻击者”)。

每种类型的增益的细节并没有真正的关系。我正在寻找一种处理任意爱好者的方法(大概是面向对象的)。

细节:

在我的特殊情况下,我在回合制战斗环境中有多个角色,因此我设想了buff与“ OnTurnStart”,“ OnReceiveDamage”等事件相关联。也许每个buff是主Buff抽象类的子类,其中仅相关事件超载。然后,每个角色可以具有当前施加的增益向量。

这个解决方案有意义吗?我当然可以看到数十种事件类型是必要的,感觉为每个buff创建一个新的子类是过大的,而且似乎不允许任何buff“交互”。也就是说,如果我想对伤害提升设置上限,以使即使您有10个不同的增益都会造成25%的额外伤害,则您只会额外执行100%,而不是额外执行250%。

理想情况下,我可以控制更复杂的情况。我敢肯定,每个人都能拿出一些例子,说明如何以更复杂的方式以潜在的方式相互交互,而这可能是我作为游戏开发人员所不希望的。

作为一个相对缺乏经验的C ++程序员(我通常在嵌入式系统中使用过C),我觉得我的解决方案过于简单,并且可能没有充分利用面向对象的语言。

有什么想法吗?这里有没有人设计过一个相当强大的抛光系统?

编辑:关于答案:

我选择答案的主要依据是详细信息和对所提问题的扎实答案,但阅读答案可以使我获得更多见解。

也许不足为奇,不同的系统或经过调整的系统似乎更好地适用于某些情况。哪种系统最适合我的游戏,取决于我打算能够应用的增益类型,方差和数量。

对于像《暗黑破坏神3》(下文所述)这样的游戏,几乎任何设备都可以改变增益效果的力量,增益效果只是角色统计系统,只要有可能,都是个好主意。

对于我所处的基于转弯的情况,基于事件的方法可能更合适。

无论如何,我仍然希望有人能带上花哨的“ OO”魔法子弹,这会让我每转 buff 施加+2移动距离,对攻击者 buff 造成50%的伤害,并且一个由3层或更多的瓷砖掉攻击时自动瞬移到附近的瓷砖爱好者在一个系统中,而不转动+5强度的buff到它自己的子类。

我认为最接近的答案是我标记的答案,但是地板仍然空着。感谢大家的投入。


我并没有集思广益,因为我只是在集思广益,但是一个爱好者列表呢?每个增益都有一个常数和一个系数修改器。常数是+10伤害,系数是1.10,伤害增加+ 10%。在您的伤害计算中,您要遍历所有增益,以获得总修正值,然后施加所需的任何限制。您可以针对任何类型的可修改属性执行此操作。但是,您需要一种特殊情况的方法来处理复杂的事情。
William Mariager 2012年

顺便说一句,当我制作适用于武器和配件的系统时,我已经为Stats对象实现了类似的功能。就像您说的那样,对于buff来说,这是一个足够不错的解决方案,它只能修改现有属性,但是即使那样,我还是希望某些buff在X转之后过期,其他buff在效果发生Y次后过期,等等。我没有在主要问题中提到这一点,因为它已经很长了。
gkimsey 2012年

1
如果您有一个“ onReceiveDamage”方法被消息传递系统调用,或者通过手动或其他方式调用,则应该很容易包含对您造成伤害的对象/对象的引用。因此,您可以将该信息提供给您的buff

是的,我曾期望抽象Buff类的每个事件模板都将包含类似的相关参数。当然可以,但是我很犹豫,因为它感觉缩放效果不好。我很难想象拥有数百个不同增益的MMORPG为每个增益定义了一个单独的类,是从一百个不同的事件中选取的。并不是说我要增加很多增益(可能接近30),但是如果有一个更简单,更优雅或更灵活的系统,我想使用它。更灵活的系统=更有趣的增益/能力。
gkimsey 2012年

4
这不是解决交互问题的好方法,但是在我看来装饰器模式在这里适用得很好。只需在彼此之上施加更多的增益(装饰器)即可。也许有一个通过“合并” buff来处理交互的系统(例如10x 25%合并为一个100%buff)。
ashes999 2012年

Answers:


32

这是一个复杂的问题,因为您正在谈论的是(这些天)被归类为“ buff”的一些不同的事物:

  • 玩家属性的修饰符
  • 在某些事件上发生的特殊效果
  • 以上的组合。

我总是用一个特定角色的活动效果列表来实现第一个。从列表中删除,无论是基于持续时间还是显式地删除,都是微不足道的,因此在此不再赘述。每个效果都包含一个属性修饰符列表,并且可以通过简单的乘法将其应用于基础值。

然后,我用函数包装它以访问修改后的属性。例如。:

def get_current_attribute_value(attribute_id, criteria):
    val = character.raw_attribute_value[attribute_id]
    # Accumulate the modifiers
    for effect in character.all_effects:
        val = effect.apply_attribute_modifier(attribute_id, val, criteria)
    # Make sure it doesn't exceed game design boundaries
    val = apply_capping_to_final_value(val)
    return val

class Effect():
    def apply_attribute_modifier(attribute_id, val, criteria):
        if attribute_id in self.modifier_list:
            modifier = self.modifier_list[attribute_id]
            # Does the modifier apply at this time?
            if modifier.criteria == criteria:
                # Apply multiplicative modifier
                return val * modifier.amount
        else:
            return val

class Modifier():
    amount = 1.0 # default that has no effect
    criteria = None # applies all of the time

这样一来,您就可以轻松轻松地应用乘法效果。如果您还需要加法效果,请决定要按什么顺序应用它们(可能最后加法),然后遍历该列表两次。(我可能在“效果”中有单独的修饰符列表,一个用于乘法运算,一个用于加法运算)。

标准值是让您实现“ + 20%vs亡灵”-在效果上设置UNDEAD值,并且仅get_current_attribute_value()在计算针对亡灵敌人的伤害掷骰时将UNDEAD值传递给。

顺便说一句,我不会尝试编写一个将值直接应用和取消应用到基础属性值的系统-最终结果是,由于错误,您的属性很可能偏离预期值。(例如,如果您将某物乘以2,然后将其上限,则再次将其除以2时,该值将低于开始时的值。)

至于基于事件的效果,例如“击中攻击者会造成15点伤害伤害”,您可以为此在Effect类上添加方法。但是,如果您想要截然不同的行为(例如,上述事件的某些影响可能会反映出损害,某些可能会治愈您,或者可能使您随机离开,无论如何),则需要自定义函数或类来处理它。您可以将功能分配给效果上的事件处理程序,然后可以仅在任何活动效果上调用事件处理程序。

# This is a method on a Character, called during combat
def on_receive_damage(damage_info):
    for effect in character.all_effects:
        effect.on_receive_damage(character, damage_info)

class Effect():
    self.on_receive_damage_handler = DoNothing # a default function that does nothing
    def on_receive_damage(character, damage_info):
        self.on_receive_damage_handler(character, damage_info)

def reflect_damage(character, damage_info):
    damage_info.attacker.receive_damage(15)

reflect_damage_effect = new Effect()
reflect_damage_effect.on_receive_damage_handler = reflect_damage
my_character.all_effects.add(reflect_damage_effect)

显然,您的Effect类将为每种类型的事件提供一个事件处理程序,并且在每种情况下,您都可以根据需要分配处理程序功能。您不需要将效果子类化,因为每个效果都是由它包含的属性修饰符和事件处理程序的组合定义的。(它可能还会包含名称,持续时间等)


2
+1提供出色的细节。正如我所看到的,这是对正式回答我的问题的最接近的回应。这里的基本设置似乎具有很大的灵活性,并且对可能是混乱的游戏逻辑的内容进行了小的抽象。正如您所说,更时髦的效果仍然需要自己的类,但是我认为这可以满足典型的“ buff”系统的大部分需求。
gkimsey 2012年

+1指出此处隐藏的概念差异。并非所有人都可以使用相同的基于事件的更新逻辑。有关完全不同的应用程序,请参见@Ross的答案。两者必须彼此相邻存在。
ctietze

22

在与朋友一起上课的游戏中,我们制作了一个buff / debuff系统,用于当用户被高高的草丛和加速砖所困时,以及诸如流血和毒药之类的小事时,被困住。

这个想法很简单,并且当我们在Python中应用它时,它非常有效。

基本上,这是怎么回事:

  • 用户有一个当前应用的buff和debuff列表(请注意,buff和debuff相对相同,只是效果具有不同的结果)
  • 增益具有多种属性,例如持续时间,名称和用于显示信息的文本以及生存时间。重要的是生存时间,持续时间以及对应用该增益的演员的引用。
  • 对于Buff,当它通过player.apply(buff / debuff)连接到播放器时,它将调用start()方法,这会将关键的更改应用于播放器,例如提高速度或放慢速度。
  • 然后,我们将在更新循环中遍历每个buff,buff将更新,这将增加其生存时间。子类将实现诸如毒害玩家,随着时间的流逝赋予玩家HP等功能。
  • 当buff完成后(即timeAlive> =持续时间)完成时,更新逻辑将删除buff并调用finish()方法,该方法的变化范围从消除玩家的速度限制到引起较小的半径(例如炸弹效果) DoT之后)

现在,如何实际应用来自世界各地的buff是另一回事了。不过这是我值得深思的地方。


1
这听起来像是我在上面试图描述的更好的解释。它相对简单,当然很容易理解。您实质上提到了三个“事件”(OnApply,OnTimeTick,OnExpired),以进一步将其与我的想法联系起来。照原样,它不支持击中时返回伤害之类的东西,但它确实可以更好地缩放许多增益。我宁愿不限制buff的功能(这=限制我想出的事件数量,必须由主要游戏逻辑调用),但是buff的可伸缩性可能更为重要。感谢您的输入!
gkimsey 2012年

是的,我们没有实现那样的东西。听起来真的很整洁,是个很棒的概念(有点像Thorns buff)。
罗斯

@gkimsey对于荆棘和其他被动增益之类的东西,我会在Mob类中将逻辑实现为类似于伤害或生命值的被动状态,并在应用增益时增加此状态。当您有多个刺式抛光器并保持界面清洁时,这简化了很多情况(10个抛光器将显示1次返回伤害,而不是10个),并使抛光器系统保持简单。
3Doubloons,2012年

这几乎是一种违反直觉的简单方法,但是当我玩《暗黑破坏神3》时,我开始思考自己。我注意到角色窗口中的抢命,命中率,近战攻击者的伤害等都是他们自己的属性。诚然,D3没有世界上最复杂的抛光系统或交互作用,但它并非微不足道。这很有道理。尽管如此,仍然可能有15种不同的增益效果以及12种不同的效果。似乎不可思议填充出的字符统计片....
gkimsey

11

我不确定您是否仍在阅读此书,但长期以来我一直在努力解决此类问题。

我设计了许多不同类型的影响系统。现在,我将简要介绍它们。这一切都是基于我的经验。我并不是声称知道所有答案。


静态修饰符

这种类型的系统主要依靠简单的整数来确定任何修改。例如,最大生命值+100,攻击最大+10,依此类推。该系统也可以处理百分比。您只需要确保堆叠不会失控即可。

我从未真正缓存过此类系统的生成值。例如,如果我想显示某物的最大健康状况,则可以在现场生成该值。这样可以防止事情容易出错,并且每个参与人员都更容易理解。

(我使用Java进行工作,因此下面的内容基于Java,但应该对其他语言进行一些修改)。可以使用枚举的修改类型,然后使用整数来轻松完成此系统。可以将最终结果放入具有键值对的某种集合中。这将是快速的查找和计算,因此性能非常好。

总体而言,仅使用静态修饰符,效果很好。但是,必须在适当的位置放置代码才能使用修饰符:getAttack,getMaxHP,getMeleeDamage等。

这种方法失败的地方(对我而言)是buff之间非常复杂的交互。没有互动的真正简单方法,只能是贫民窟。它确实具有一些简单的交互可能性。为此,必须对存储静态修饰符的方式进行修改。您可以使用String而不是枚举作为键。该字符串将是Enum名称+额外变量。在10中有9次中,没有使用多余的变量,因此您仍然保留枚举名称作为键。

让我们举一个简单的例子:如果您希望能够修改对不死生物的伤害,则可以有一个这样的有序对:(DAMAGE_Undead,10)DAMAGE是枚举,而Undead是多余的变量。因此,在战斗中,您可以执行以下操作:

dam += attacker.getMod(Mod.DAMAGE + npc.getRaceFamily()); //in this case the race family would be undead

无论如何,它运行良好且速度很快。但是它在复杂的交互以及到处都有“特殊”代码的情况下失败了。例如,考虑“死亡时有25%的机会传送”的情况。这是一个“相当”复杂的过程。上面的系统可以处理它,但是不容易,因为您需要以下条件:

  1. 确定玩家是否拥有此模组。
  2. 如果成功的话,在某个地方,有一些代码可以执行隐形传送。此代码的位置本身就是一个讨论!
  3. 从Mod映射中获取正确的数据。该值是什么意思?他们也在传送房间吗?如果玩家身上有两个瞬移模组怎么办?金额不会加在一起吗?????? 失败!

因此,这使我进入了下一个:


终极复杂增益系统

我曾经尝试自己编写2D MMORPG。这是一个可怕的错误,但我学到了很多东西!

我重写了情感系统3次。第一个使用了上述功能的一种不太强大的变体。第二个是我要谈论的。

对于每个修改,该系统都有一系列类,例如:ChangeHP,ChangeMaxHP,ChangeHPByPercent,ChangeMaxByPercent。我有一百万个家伙,甚至像TeleportOnDeath这样的家伙。

我的课堂上有要做以下事情的事情:

  • applyAffect
  • removeAffect
  • checkForInteraction <-重要

套用并删除自己的解释(尽管对于百分比之类的东西,该影响会跟踪它增加了多少HP,以确保该影响消失时,它只会删除其添加的量。这是错误的,大声笑和我花了很长时间来确保它是正确的。对此我仍然感觉不太好。

checkForInteraction方法是一段非常复杂的代码。在每个情感(即ChangeHP)类中,它将具有代码来确定是否应通过输入情感对其进行修改。例如,如果您有类似...

  • Buff 1:攻击时造成10点火焰伤害
  • Buff 2:火焰伤害提高25%。
  • Buff 3:火焰伤害提高15点。

checkForInteraction方法将处理所有这些影响。为了做到这一点,必须检查对附近所有玩家的影响!!这是因为我在一个区域的跨度中与多个玩家打交道时产生的影响类型。这意味着该代码永远不会像上面那样发生任何特殊的陈述-“如果我们刚刚死亡,我们应该检查死亡的传送能力”。该系统将在正确的时间自动正确处理它。

尝试编写这个系统花了我大约2个月的时间,使头部爆炸了好几次。但是,它确实功能强大,可以完成大量的工作-尤其是当您考虑以下两个事实时,我的游戏能力:1.他们具有目标范围(即:单个,自我,仅团体,PB AE自我) ,PB AE目标,目标AE等)。2.能力可能对他们造成1以上的影响。

正如我上面提到的,这是该游戏的第三影响系统的第二。我为什么要离开这个?

该系统的性能是我见过的最差的!这太慢了,因为它必须对发生的每件事进行大量检查。我试图改进它,但认为它是失败的。

所以我们来看看我的第三个版本(以及另一种类型的buff系统):


具有处理程序的复杂影响类

因此,这几乎是前两者的组合:我们可以在Affect类中包含静态变量,该类包含许多功能和额外数据。然后,当我们想做某事时,只需调用处理程序(对我来说,是一些静态实用程序方法,而不是用于特定操作的子类。但是我敢肯定,如果您愿意的话,可以将子类用于操作)。

Affect类将拥有所有多汁的好东西,例如目标类型,持续时间,使用次数,执行机会等等。

我们仍然必须添加特殊代码来处理这种情况,例如,死亡时传送。我们仍然需要在战斗代码中手动检查该代码,然后如果存在,我们将获得一个影响列表。此影响列表包含处理死亡时传送的玩家当前所有已应用的影响。然后,我们将仅查看每个对象,并检查它是否已执行且是否成功(我们将在第一个成功的对象处停止)。它成功了,我们只需要调用处理程序来解决这个问题。

如果需要,也可以进行交互。只需编写代码即可在播放器/ etc上查找特定的buff。因为它具有良好的性能(请参见下文),所以这样做应该相当有效。它只需要更复杂的处理程序等等。

因此,它具有第一个系统的很多性能,并且仍然像第二个系统一样具有很多复杂性(但不多)。至少在Java中,您可以做一些棘手的事情来获得MOST情况下几乎第一个的性能(即:拥有一个枚举映射(http://docs.oracle.com/javase/6/docs/api/java /util/EnumMap.html),其中以Enums作为键,而将ArrayList of Effects作为值。这使您可以快速查看是否有影响[因为列表为0或地图没有枚举],并且没有以无缘无故地不断迭代玩家的情感列表。我不介意目前是否需要迭代情感。如果出现问题,我会在以后进行优化)。

我目前正在重新开放(用Java代替原来的FastROM代码库重写游戏)到2005年结束的MUD,最近我遇到了如何实现buff系统的问题?我将使用此系统,因为它在我之前的失败游戏中能很好地工作。

好吧,希望有人能从中找到一些有用的见解。


6

如果每个增益的行为彼此不同,则每个增益的不同类(或可寻址函数)也不过分。一件事将具有+ 10%或+ 20%的增益(当然,最好将其表示为同一类的两个对象),另一件事将实现千差万别的效果,这些效果始终需要自定义代码。但是,我认为最好有一种标准的方法来自定义游戏逻辑,而不是让每个buff尽其所能(并且可能以无法预料的方式互相干扰,从而扰乱游戏平衡)。

我建议将每个“攻击周期”划分为多个步骤,其中每个步骤都有一个基本值,可以应用于该值的修改的有序列表(可能有上限)和最终上限。每个修改默认都具有一个标识转换,并且可能会受到零个或多个buff / debuff的影响。每个修改的细节将取决于所应用的步骤。如何实现周期取决于您自己(包括您一直在讨论的事件驱动架构的选项)。

攻击周期的一个示例可能是:

  • 计算玩家的攻击(基础+ MOD);
  • 计算对手防御(基础+ MOD);
  • 进行区别(并应用mod)并确定基本伤害;
  • 计算任何格挡/护甲效果(基础伤害修正)并施加伤害;
  • 计算任何后坐力效果(基础伤害修正)并应用于攻击者。

要注意的重要一点是,在周期中越早应用抛光效果,将对结果产生更大的影响。因此,如果您想进行更具“战术性”的战斗(玩家的技能比角色水平更重要),请在基本属性上创建许多增益/减益效果。如果您想要更“平衡”的战斗(级别更重要-在MMOG中很重要,以限制进度),则只能在该周期的后期使用buff / debuff。

我前面提到的“修改”和“增益”之间的区别是有目的的:关于规则和平衡的决定可以在前者上执行,因此对规则和平衡的任何更改都不需要反映后者的每个类别。OTOH,buff的数量和种类仅受您的想象力限制,因为它们中的每个可以表达自己想要的行为,而无需考虑它们之间的任何可能的相互作用(甚至根本没有其他存在)。

因此,回答这个问题:不要为每个Buff创建一个类,而是为每个(类型)修改创建一个类,并将修改与攻击周期(而不是角色)联系在一起。buff可以只是(Modification,key,value)元组的列表,您可以通过简单地将buff添加到角色的buff集中来将buff应用于角色。这也减少了错误的发生时间,因为在应用增益效果时根本不需要更改角色的属性(因此在增益值过期后将属性恢复为错误值的风险较小)。


这是一种有趣的方法,因为它介于我考虑过的两个实现之间–即,仅将增益限制在相当简单的状态和结果伤害修改器上,或者制作一个非常健壮但开销很高的系统,可以处理任何事情。这是对前者的一种扩展,以允许“刺”同时保持简单的界面。虽然我不认为这是灵丹妙药我需要什么,它肯定看起来像它使得平衡得多比其他方法更简单,所以它可能是要走的路。感谢您的输入!
gkimsey 2012年

3

我不知道您是否还在阅读它,但是这就是我现在正在做的事情(代码基于UE4和C ++)。在对问题进行了两个多星期的思考之后(!!),我终于找到了:

http://gamedevelopment.tutsplus.com/tutorials/using-the-composite-design-pattern-for-an-rpg-attributes-system--gamedev-243

而且我认为,将类属性封装在类/结构中毕竟不是一个坏主意。但是请记住,我确实利用了UE4构建在代码反射系统中的巨大优势,因此如果不做一些重做,这可能并不适合所有地方。

无论如何,我从将属性包装到单个结构开始:

USTRUCT(BlueprintType)
struct GAMEATTRIBUTES_API FGAAttributeBase
{
    GENERATED_USTRUCT_BODY()
public:
    UPROPERTY()
        FName AttributeName;
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Value")
        float BaseValue;
    /*
        This is maxmum value of this attribute.
    */
    UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Value")
        float ClampValue;
protected:
    float BonusValue;
    //float OldCurrentValue;
    float CurrentValue;
    float ChangedValue;

    //map of modifiers.
    //It could be TArray, but map seems easier to use in this case
    //we need to keep track of added/removed effects, and see 
    //if this effect affected this attribute.
    TMap<FGAEffectHandle, FGAModifier> Modifiers;

public:

    inline float GetFinalValue(){ return BaseValue + BonusValue; };
    inline float GetCurrentValue(){ return CurrentValue; };
    void UpdateAttribute();

    void Add(float ValueIn);
    void Subtract(float ValueIn);

    //inline float GetCurrentValue()
    //{
    //  return FMath::Clamp<float>(BaseValue + BonusValue + AccumulatedBonus, 0, GetFinalValue());;
    //}

    void AddBonus(const FGAModifier& ModifiersIn, const FGAEffectHandle& Handle);
    void RemoveBonus(const FGAEffectHandle& Handle);

    void InitializeAttribute();

    void CalculateBonus();

    inline bool operator== (const FGAAttributeBase& OtherAttribute) const
    {
        return (OtherAttribute.AttributeName == AttributeName);
    }

    inline bool operator!= (const FGAAttributeBase& OtherAttribute) const
    {
        return (OtherAttribute.AttributeName != AttributeName);
    }

    inline bool IsValid() const
    {
        return !AttributeName.IsNone();
    }
    friend uint32 GetTypeHash(const FGAAttributeBase& AttributeIn)
    {
        return AttributeIn.AttributeName.GetComparisonIndex();
    }
};

它仍未完成,但基本思想是,该结构跟踪其内部状态。只能通过效果修改属性。直接修改它们是不安全的,不会暴露给设计人员。我假设所有可以与属性交互的东西都是效果。包括项目的固定奖金。装备新物品时,会创建新效果(以及手柄),并将其添加到专用地图中,该地图可以处理无限时长奖励(必须由玩家手动移除的奖励)。应用新的效果时,会为其创建新的句柄(句柄只是int,并用struct包装),然后将该句柄作为与该效果进行交互的一种方式进行传递,并跟踪效果是否为仍然活跃。移除效果后,其句柄将广播到所有感兴趣的对象,

真正重要的部分是TMap(TMap是哈希图)。FGAModifier是非常简单的结构:

struct FGAModifier
{
    EGAAttributeOp AttributeMod;
    float Value;
};

它包含修改的类型:

UENUM()
enum class EGAAttributeOp : uint8
{
    Add,
    Subtract,
    Multiply,
    Divide,
    Set,
    Precentage,

    Invalid
};

而Value是我们将应用于属性的最终计算值。

我们使用简单的函数添加新效果,然后调用:

void FGAAttributeBase::CalculateBonus()
{
    float AdditiveBonus = 0;
    auto ModIt = Modifiers.CreateConstIterator();
    for (ModIt; ModIt; ++ModIt)
    {
        switch (ModIt->Value.AttributeMod)
        {
        case EGAAttributeOp::Add:
            AdditiveBonus += ModIt->Value.Value;
                break;
            default:
                break;
        }
    }
    float OldBonus = BonusValue;
    //calculate final bonus from modifiers values.
    //we don't handle stacking here. It's checked and handled before effect is added.
    BonusValue = AdditiveBonus; 
    //this is absolute maximum (not clamped right now).
    float addValue = BonusValue - OldBonus;
    //reset to max = 200
    CurrentValue = CurrentValue + addValue;
}

每次添加或删除效果时,此功能都应重新计算整个奖金堆栈。功能仍未完成(如您所见),但是您可以了解一般想法。

我现在最大的麻烦是处理“损坏/修复”属性(不涉及重新计算整个堆栈),我想我已经解决了一些问题,但是仍然需要进行更多测试才能达到100%。

无论如何,属性都是这样定义的(+虚幻宏,这里省略):

FGAAttributeBase Health;
FGAAttributeBase Energy;

等等

我也不是100%确定处理CurrentValue属性,但是应该可以。他们现在是这样。

无论如何,我希望它可以节省一些人的头缓存,不确定这是最佳还是什至是好的解决方案,但是我更喜欢它,而不是独立于属性来跟踪效果。在这种情况下,使每个属性跟踪其自己的状态要容易得多,并且应减少出错的可能性。基本上只有一个故障点,这是相当简短的类。


感谢您的工作链接和解释!我认为您正在朝着我所要的方向发展。我想到的是操作顺序(例如,对同一属性的3个“加”效果和2个“乘”效果,应该首先发生?),这纯粹是属性支持。也有触发的概念(例如“击中时丢失1 AP”类型的效果)可以解决,但这很可能是一个单独的研究。
gkimsey 2015年

如果只计算属性的加成,操作顺序很容易做到。您可以在这里看到我在那里进行切换。要遍历当前所有奖金(可以加,减,乘,除等),然后累积即可。您可以执行类似BonusValue =(BonusValue * MultiplyBonus + AddBonus-SubtractBonus)/ DivideBonus的操作,或者您也可以看一下此等式。由于单点进入,因此很容易进行试验。至于触发器,我没有写它,因为那是另一个问题,我思考过来,我已经尝试过3-4(极限)
卢卡斯巴兰

解决方案,没有一个能按照我想要的方式工作(我的主要目标是让他们变得对设计师友好)。我的一般想法是使用标签,并对照标签检查传入的效果。如果标签匹配,则效果可以触发其他效果。(标记是人类易读的简单名称,例如Damage.Fire,Attack.Physical等)。从根本上讲,这是一个非常简单的概念,问题在于组织数据,使其易于访问(可快速搜索)并易于添加新效果。您可以点击这里代码github.com/iniside/ActionRPGGame(GameAttributes是模块,你会有兴趣)
卢卡斯巴兰

2

我在一个小型MMO上工作,所有物品,力量,增益等都有“效果”。效果是一类具有“ AddDefense”,“ InstantDamage”,“ HealHP”等变量的类。力量,物品等将处理该效果的持续时间。

当您施放能量或戴上物品时,会在指定的持续时间内对角色施加效果。然后,主要攻击等计算将考虑所应用的效果。

例如,您有一个增强防御的buff。该buff至少有一个EffectID和持续时间。投射时,它将在指定的持续时间内将EffectID应用于角色。

项目的另一个示例将具有相同的字段。但是持续时间将是无限的,或者直到通过将项目从角色上移除来消除效果为止。

此方法使您可以迭代当前应用的效果列表。

希望我足够清楚地解释了这种方法。


据我最少的经验所了解,这是在RPG游戏中实现stat mods的传统方式。它运作良好,易于理解和实施。不利的一面是,它似乎没有给我腾出任何空间来做诸如“荆棘”爱好者,更高级或情境更深的东西。从历史上看,这也是RPG中某些漏洞利用的起因,尽管它们很少见,并且由于我是在制作单人游戏,所以如果有人发现了漏洞利用,我并不那么担心。感谢您的输入。
gkimsey 2012年

2
  1. 如果您是一个团结的用户,则可以从以下内容开始:http : //www.stevegargolinski.com/armory-a-free-and-unfinished-stat-inventory-and-buffdebuff-framework-for-unity/

我使用ScriptableOjects作为增益/咒语/天赋

public class Spell : ScriptableObject 
{
    public SpellType SpellType = SpellType.Ability;
    public SpellTargetType SpellTargetType = SpellTargetType.SingleTarget;
    public SpellCategory SpellCategory = SpellCategory.Ability;
    public MagicSchools MagicSchool = MagicSchools.Physical;
    public CharacterClass CharacterClass = CharacterClass.None;
    public string Description = "no description available";
    public SpellDragType DragType = SpellDragType.Active; 
    public bool Active = false;
    public int TargetCount = 1;
    public float CastTime = 0;
    public uint EffectRange = 3;
    public int RequiredLevel = 1;
    public virtual void OnGUI()
    {
    }
}

使用UnityEngine; 使用System.Collections.Generic;

公共枚举BuffType {Buff,Debuff} [System.Serializable]公共类BuffStat {public Stat Stat = Stat.Strength; 公共浮动ModValueInPercent = 0.1f; }

public class Buff : Spell
{
    public BuffType BuffType = BuffType.Buff;
    public BuffStat[] ModStats;
    public bool PersistsThroughDeath = false;
    public int AmountPerTick = 3;
    public bool UseTickTimer = false;
    public float TickTime = 1.5f;
    [HideInInspector]
    public float Ticktimer = 0;
    public float Duration = 360; // in seconds
    public float ModifierPerStack = 1.1f;
    [HideInInspector]
    public float Timer = 0;
    public int Stack = 1;
    public int MaxStack = 1;
}

BuffModul:

using System;
using RPGCore;
using UnityEngine;

public class Buff_Modul : MonoBehaviour
{
    private Unit _unit;

    // Use this for initialization
    private void Awake()
    {
        _unit = GetComponent<Unit>();
    }

    #region BUFF MODUL

    public virtual void RUN_BUFF_MODUL()
    {
        try
        {
            foreach (var buff in _unit.Attr.Buffs)
            {
                CeckBuff(buff);
            }
        }
        catch(Exception e) {throw new Exception(e.ToString());}
    }

    #endregion BUFF MODUL

    public void ClearBuffs()
    {
        _unit.Attr.Buffs.Clear();
    }

    public void AddBuff(string buffName)
    {
        var buff = Instantiate(Resources.Load("Scriptable/Buff/" + buffName, typeof(Buff))) as Buff;
        if (buff == null) return;
        buff.name = buffName;
        buff.Timer = buff.Duration;
        _unit.Attr.Buffs.Add(buff);
        foreach (var buffStat in buff.ModStats)
        {
            switch (buff.BuffType)
            {
                case BuffType.Buff:
                    _unit.Attr.AddBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] + _unit.Attr.StatsItem[buffStat.Stat]) * buffStat.ModValueInPercent));
                    break;
                case BuffType.Debuff:
                    _unit.Attr.RemoveBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] /*+ unit.character.StatsItem[_stat.stat]*/) * buffStat.ModValueInPercent));
                    break;
            }
            Core.StatController(_unit.Attr, buffStat.Stat);
        }
    }

    public void RemoveBuff(Buff buff)
    {
        foreach (var buffStat in buff.ModStats)
        {
            switch (buff.BuffType)
            {
                case BuffType.Buff:
                    _unit.Attr.RemoveBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat] + _unit.Attr.StatsItem[buffStat.Stat]) * buffStat.ModValueInPercent));
                    break;
                case BuffType.Debuff:
                    _unit.Attr.AddBuffStatValue(buffStat.Stat, Mathf.RoundToInt((_unit.Attr.StatsBase[buffStat.Stat]  /*+ unit.character.StatsItem[_stat.stat]*/) * buffStat.ModValueInPercent));
                    break;
            }
            Core.StatController(_unit.Attr, buffStat.Stat);
        }
        _unit.Attr.Buffs.Remove(buff);
    }

    void CeckBuff(Buff buff)
    {
        buff.Timer -= Time.deltaTime;
        if (!_unit.IsAlive && !buff.PersistsThroughDeath)
        {
            if (buff.ModStats != null)
                foreach (var stat in buff.ModStats)
                {
                    _unit.Attr.StatsBuff[stat.Stat] = 0;
                }

            RemoveBuff(buff);
        }
        if (_unit.IsAlive && buff.Timer <= 0)
        {
            RemoveBuff(buff);
        }
    }
}

0

对我来说这是一个实际的问题。我对此有一个想法。

  1. 如前所述,我们需要Buff为buff 实现一个列表和一个逻辑更新器。
  2. 然后,我们需要在Buff该类的子类中的每一帧更改所有特定的播放器设置。
  3. 然后,我们从可更改的设置字段中获取当前的播放器设置。

class Player {
  settings: AllPlayerStats;

  private buffs: Array<Buff> = [];
  private baseSettings: AllPlayerStats;

  constructor(settings: AllPlayerStats) {
    this.baseSettings = settings;
    this.resetSettings();
  }

  addBuff(buff: Buff): void {
    this.buffs.push(buff);
    buff.start(this);
  }

  findBuff(predcate(buff: Buff) => boolean): Buff {...}

  removeBuff(buff: Buff): void {...}

  update(dt: number): void {
    this.resetSettings();
    this.buffs.forEach((item) => item.update(dt));
  }

  private resetSettings(): void {
    //some way to copy base to settings
    this.settings = this.baseSettings.copy();
  }
}

class Buff {
    private owner: Player;        

    start(owner: Player) { this.owner = owner; }

    update(dt: number): void {
      //here we change anything we want in subclasses like
      this.owner.settings.hp += 15;
      //if we need base value, just make owner.baseSettings public but don't change it! only read

      //also here logic for removal buff by time or something
    }
}

这样,可以很容易地添加新的玩家统计信息,而无需更改Buff子类的逻辑。


0

我知道这已经很老了,但是它被链接到一个新帖子中,我对此有一些想法想分享。不幸的是,我目前没有备忘,所以我将尝试概述我所谈论的内容,并且在我将其放在前面时会在细节和示例代码中进行编辑。我。

首先,我认为从设计的角度来看,大多数人都太着迷于可以创建什么类型的buff,如何应用它们,而忘记了面向对象编程的基本原理。

我什么意思 无论是buff还是debuff都没关系,它们都是修饰符,它们以正面或负面的方式影响事物。该代码不在乎是哪个。因此,无论是添加统计信息还是乘以统计信息,最终都无关紧要,它们只是不同的运算符,代码也不在乎哪个是哪个。

那我要去哪里呢?设计一个好的(阅读:简单,优雅)buff / debuff类并不是那么困难,难的是设计计算和维护游戏状态的系统。

如果我在设计抛光/除尘系统,则需要考虑以下几点:

  • 表示效果本身的buff / debuff类。
  • buff / debuff类型类,包含有关buff影响什么以及如何影响的信息。
  • 角色,物品以及可能的位置都需要具有列表或集合属性,以包含增益和减益。

有关buff / debuff类型应包含的一些细节:

  • IE的适用对象/对象:玩家,怪物,位置,物品等。
  • 它是什么类型的影响(正的,负面的),它是乘性的还是累加的,以及它影响的统计类型,即IE:攻击,防御,移动等。
  • 应检查的时间(战斗,一天中的时间等)。
  • 是否可以将其删除,以及是否可以将其删除。

这只是一个开始,但是从那里您可以定义所需的内容,并使用正常的游戏状态对其进行操作。例如,假设您要创建一个诅咒物品,以降低移动速度...

只要我放置了正确的类型,就可以很容易地创建一条buff记录,内容如下:

  • 类型:诅咒
  • ObjectType:项目
  • StatCategory:实用程序
  • 受影响的状态:移动速度
  • 持续时间:无限
  • 触发条件:OnEquip

依此类推,当我创建一个buff时,我就为其分配了Curse的BuffType,其他所有内容都取决于引擎...

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.