유연한 버프 / 디버프 시스템을 구현하는 방법은 무엇입니까?


66

개요 :

RPG와 같은 통계를 사용하는 많은 게임은 단순한 "딜 25 % 추가 데미지"에서 "적중시 공격자에게 15 번 피해를 입히는 것"과 같은 더 복잡한 것에 이르기까지 캐릭터 "버프"를 허용합니다.

버프의 각 유형의 세부 사항은 실제로 관련이 없습니다. 임의의 버프를 처리하는 (아마도 객체 지향) 방법을 찾고 있습니다.

세부:

내 경우에는 턴 기반 전투 환경에 여러 캐릭터가 있으므로 "OnTurnStart", "OnReceiveDamage"등과 같은 이벤트에 버프가 연결되는 것을 상상했습니다. 아마도 각 버프는 기본 버프 추상 클래스의 하위 클래스 일 것입니다. 관련 이벤트 만 오버로드됩니다. 그러면 각 캐릭터는 현재 적용되는 버프 벡터를 가질 수 있습니다.

이 솔루션이 의미가 있습니까? 필자는 수십 가지 이벤트 유형이 필요하다는 것을 확실히 알 수 있으며, 각 버프마다 새로운 서브 클래스를 만드는 것은 과잉이며 버프 "상호 작용"을 허용하지 않는 것 같습니다. 즉, 데미지 부스트에 캡을 적용하여 25 %의 추가 피해를주는 10 개의 버프가 있어도 추가로 250 %가 아닌 100 % 만 추가 할 수 있습니다.

이상적으로 내가 통제 할 수있는 더 복잡한 상황이 있습니다. 더 정교한 버프가 잠재적으로 게임 개발자로서 원하지 않는 방식으로 서로 상호 작용할 수있는 방법에 대한 예를 모두가 확신 할 수 있습니다.

비교적 경험이없는 C ++ 프로그래머 (일반적으로 임베디드 시스템에서 C를 사용했습니다)로서 솔루션이 단순하고 객체 지향 언어를 최대한 활용하지 않는 것 같습니다.

생각? 여기 누구도 전에 상당히 강력한 버프 시스템을 설계 했습니까?

편집 : 답변에 관하여 :

나는 기본적으로 좋은 세부 사항과 내가 묻는 질문에 대한 확실한 대답을 바탕으로 답변을 선택했지만 답변을 읽으면 좀 더 통찰력을 얻었습니다.

아마도 다른 시스템이나 조정 된 시스템이 특정 상황에 더 잘 적용되는 것 같습니다. 내 게임에 가장 적합한 시스템은 적용 할 버프의 유형, 편차 및 버프 수에 따라 다릅니다.

거의 모든 장비가 버프의 강도를 변경할 수있는 Diablo 3 (아래 언급)과 같은 게임의 경우 버프는 캐릭터 통계 시스템 일뿐 입니다.

내가 속한 턴제 상황의 경우 이벤트 기반 접근법이 더 적합 할 수 있습니다.

어쨌든 누군가 여전히 버프 당 +2 이동 거리 , 공격자 버프 에 입힌 피해의 50 % 를 적용 할 수있는 멋진 "OO"마법의 총알이 나올 것으로 기대합니다. +5 강도 버프를 자체 서브 클래스로 바꾸지 않고 단일 시스템 에서 3 개 이상의 타일 어웨이 버프를 공격하면 근처 타일자동 순간 이동합니다 .

가장 가까운 것은 내가 표시 한 답이지만 바닥은 여전히 ​​열려 있습니다. 의견을 보내 주셔서 감사합니다.


나는 단지 브레인 스토밍하면서 이것을 답변으로 게시하지는 않지만 버프 목록은 어떻습니까? 각 버프에는 상수와 계수 수정자가 있습니다. + 10 %의 데미지, + 10 %의 데미지 부스트의 경우 1.10입니다. 데미지 계산에서 모든 버프를 반복하고 전체 수정자를 얻은 다음 원하는 제한을 적용합니다. 모든 종류의 수정 가능한 속성에 대해이 작업을 수행합니다. 복잡한 일을 위해서는 특별한 경우가 필요합니다.
William Mariager 2016 년

우연히 나는 장비와 무기를위한 시스템을 만들 때 Stats 객체에 대해 이와 비슷한 것을 이미 구현했습니다. 당신이 말했듯이, 그것은 기존 속성 만 수정하는 버프에 대한 적절한 솔루션이지만 물론 X가 회전 한 후에 특정 버프가 만료되고 Y 효과가 발생하면 다른 버프가 만료되기를 원할 것입니다. 이미 오래 걸리기 때문에 주요 질문에서 이것을 언급하십시오.
gkimsey 2016 년

1
메시징 시스템에 의해 또는 수동으로 또는 다른 방법으로 호출되는 "onReceiveDamage"메소드가있는 경우 누구 / 손상을 입 었는지에 대한 참조를 포함하기가 쉬워야합니다. 그럼 당신은 당신의 버프에이 정보 만들 수

저는 추상 버프 클래스의 각 이벤트 템플릿에 이와 관련된 매개 변수가 포함될 것으로 예상했습니다. 확실히 작동하지만 확장 성이 좋지 않은 느낌이 들기 때문에 주저합니다. 수백 가지의 버프가있는 MMORPG를 상상하는 데 어려움을 겪고 있습니다. 버프마다 별도의 클래스가 정의되어 있으며 백 가지 이벤트에서 선택합니다. 나는 많은 버프 (아마도 30에 가까운)를 만들고 있지만, 더 간단하고 우아하거나 유연한 시스템이 있다면 그것을 사용하고 싶습니다. 보다 유연한 시스템 = 더 흥미로운 버프 / 능력.
gkimsey 2016 년

4
이것은 상호 작용 문제에 대한 좋은 대답은 아니지만 데코레이터 패턴이 여기에 잘 적용되는 것 같습니다. 서로 더 많은 버프 (장식)를 적용하십시오. 버프를 "병합"하여 상호 작용을 처리하는 시스템이있을 수 있습니다 (예 : 10x 25 %가 하나의 100 % 버프로 병합).
ashes999 2016 년

답변:


32

이 문제는 복잡한 문제입니다. 요즘에는 '버프'로 묶여있는 몇 가지 다른 것들에 대해 이야기하고 있기 때문입니다.

  • 플레이어 속성에 대한 수정 자
  • 특정 이벤트에서 발생하는 특수 효과
  • 위의 조합.

나는 항상 특정 캐릭터에 대한 활성 효과 목록으로 첫 번째를 구현합니다. 기간을 기준으로하거나 명시 적으로 목록에서 제거하는 것은 매우 사소한 일이므로 여기서 다루지 않습니다. 각 이펙트에는 속성 수정 자 목록이 포함되어 있으며 간단한 곱셈을 통해 기본 값에 적용 할 수 있습니다.

그런 다음 수정 된 속성에 액세스하는 함수로 래핑합니다. 예 :

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

이를 통해 곱셈 효과를 쉽게 적용 할 수 있습니다. 추가 효과가 필요한 경우 적용 할 순서를 결정하고 (아마 마지막 추가) 목록을 두 번 실행하십시오. (아마도 Effect에 별도의 수정 자 목록이있을 것입니다. 하나는 곱하기위한 것이고 다른 하나는 첨가제입니다.)

기준 값은 "+ 20 % vs Undead"를 구현하는 것입니다. Effect에 UNDEAD 값을 설정하고 UNDEAD 값을 get_current_attribute_value()언데드 적에 대한 데미지 롤을 계산할 때에 만 전달하십시오 .

덧붙여서, 나는 기본 속성 값에 직접 값을 적용하고 적용하지 않는 시스템을 시도하고 작성하려고하지 않을 것입니다. 결국 결과는 오류로 인해 속성이 의도 한 값에서 벗어날 수 있습니다. (예를 들어, 무언가에 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 클래스에는 모든 유형의 이벤트에 대한 이벤트 핸들러가 있으며 각 경우에 필요한만큼 핸들러 함수를 지정할 수 있습니다. 각 효과는 포함 된 속성 수정 자 및 이벤트 핸들러의 구성으로 정의되므로 Effect를 서브 클래스화할 필요가 없습니다. (아마도 이름, 기간 등이 포함될 것입니다.)


2
탁월한 디테일을 위해 +1 이것은 내가 본 것처럼 공식적으로 내 질문에 대답하는 가장 가까운 응답입니다. 여기의 기본 설정은 많은 유연성을 제공하고 지저분한 게임 로직이 될 수있는 것의 작은 추상화를 허용하는 것으로 보입니다. 당신이 말했듯이, 더 펑키 한 효과는 여전히 자신의 클래스가 필요하지만, 이것은 전형적인 "버프"시스템의 요구를 대부분 처리합니다.
gkimsey 2016 년

여기에 숨겨진 개념적 차이를 지적 해 +1. 그들 모두가 동일한 이벤트 기반 업데이트 로직으로 작동하지는 않습니다. 완전히 다른 응용 프로그램은 @Ross의 답변을 참조하십시오. 둘 다 서로 옆에 존재해야합니다.
ctietze

22

친구와 함께 수업을 진행 한 게임에서 사용자가 키 큰 잔디와 속도 향상 타일에 갇히거나 그렇지 않은 것, 그리고 출혈과 독과 같은 사소한 것들에 대한 버프 / 디버프 시스템을 만들었습니다.

아이디어는 간단했고 우리가 파이썬에 적용하는 동안 오히려 효과적이었습니다.

기본적으로 다음과 같이 진행되었습니다.

  • 사용자는 현재 적용된 버프와 디버프 목록을 가졌습니다 (버프와 디버프는 상대적으로 동일합니다. 결과가 다른 결과 일뿐입니다)
  • 버프에는 정보를 표시하기위한 기간, 이름 및 텍스트, 유효 시간 등 다양한 속성이 있습니다. 중요한 것은 살아있는 시간, 지속 시간 및이 버프가 적용되는 액터에 대한 참조입니다.
  • 버프의 경우 player.apply (buff / debuff)를 통해 플레이어에 연결된 경우 start () 메서드를 호출하면 속도 증가 또는 속도 저하와 같이 플레이어에 중요한 변경 사항이 적용됩니다.
  • 그런 다음 업데이트 루프에서 각 버프를 반복하고 버프가 업데이트되므로 시간이 늘어납니다. 서브 클래스는 플레이어를 독살하고 시간이 지남에 따라 플레이어에게 HP를주는 등의 것을 구현합니다.
  • 버프가 timeAlive> = 지속 시간을 의미하는 것으로 완료되면, 업데이트 로직은 버프를 제거하고 finish () 메소드를 호출합니다.이 메소드는 플레이어의 속도 제한을 제거하는 것에서 작은 반경을 만드는 것 (폭탄 효과에 대한 생각)까지 다양합니다. DoT 후)

이제 세계에서 버프를 실제로 적용하는 방법은 다른 이야기입니다. 생각할 음식은 여기 있습니다.


1
이것은 위에서 설명하려는 것에 대한 더 나은 설명처럼 들립니다. 비교적 간단하고 이해하기 쉽습니다. 당신은 본질적으로 세 가지 "이벤트"(OnApply, OnTimeTick, OnExpired)를 언급하여 내 생각과 관련이 있습니다. 그대로, 그것은 때리거나 등을 때 피해를 반환하는 등의 지원하지 않지만 많은 버프에 대한 확장 성이 더 좋습니다. 버프가 수행 할 수있는 작업을 제한하지는 않지만 (메인 게임 로직에서 호출해야하는 이벤트 수를 제한) 버프 확장 성이 더 중요 할 수 있습니다. 입력 해 주셔서 감사합니다!
gkimsey 2016 년

예, 우리는 그런 것을 구현하지 않았습니다. 정말 깔끔하고 훌륭한 개념입니다 (Torns 버프와 같은 종류).
Ross

@gkimsey Thorns 및 기타 수동 버프와 같은 경우, Mob 클래스의 논리를 손상 또는 건강과 유사한 수동 통계로 구현하고 버프를 적용 할 때이 통계를 증가시킵니다. 이 단순화 많이 당신이 여러 가시 애호가이 경우뿐만 아니라 (10 개 버프는 1 개 반환 손상보다는 10을 보여줄 것) 깨끗한 인터페이스를 유지하고 버프 시스템은 간단한 남아 있습니다.
3Doubloons

이것은 거의 반 직관적으로 간단한 접근 방법이지만, 디아블로 3를 플레이 할 때 나 자신에 대해 생각하기 시작했습니다. 나는 삶의 도둑질, 명중률, 근접 공격자에 대한 피해 등이 캐릭터 창에서 모두 자신의 능력치라는 것을 알았습니다. 물론, D3는 세계에서 가장 복잡한 버핑 시스템이나 상호 작용이 없지만 사소한 것은 아닙니다. 이것은 많은 의미가 있습니다. 여전히 15 가지 버프가있을 수 있으며 12 가지 효과가 있습니다. 캐릭터 통계 시트를 이상하게 패딩하는 것 같습니다.
gkimsey

11

나는 아직도 당신이 이것을 읽고 있는지 확실하지 않지만 오랫동안 이런 종류의 문제로 어려움을 겪었습니다.

다양한 유형의 영향 시스템을 설계했습니다. 이제 간단히 살펴 보겠습니다. 이것은 모두 내 경험에 근거한 것입니다. 나는 모든 답을 알고 있다고 주장하지 않습니다.


정적 수정 자

이 유형의 시스템은 대부분 수정을 결정하기 위해 간단한 정수에 의존합니다. 예를 들어 +100은 최대 HP, +10은 공격 등입니다. 이 시스템은 퍼센트도 처리 할 수 ​​있습니다. 스태킹이 제어 범위를 벗어나지 않도록해야합니다.

이 유형의 시스템에 대해 생성 된 값을 실제로 캐시하지 않았습니다. 예를 들어, 최대 건강 상태를 표시하려는 경우 그 자리에서 값을 생성합니다. 이를 통해 오류가 발생하기 쉬우 며 관련된 모든 사람이 이해하기 쉬워졌습니다.

(저는 Java 기반으로 작업하므로 Java 기반이지만 다른 언어에서는 일부 수정 작업을 수행해야합니다.)이 시스템은 수정 유형에 대해 열거 형을 사용한 다음 정수를 사용하여 쉽게 수행 할 수 있습니다. 최종 결과는 키, 값 순서 쌍이있는 일종의 컬렉션에 배치 할 수 있습니다. 빠른 조회 및 계산이 가능하므로 성능이 매우 좋습니다.

전반적으로 평평한 정적 수정 자와 함께 잘 작동합니다. 그러나 수정자가 사용될 적절한 위치에 코드가 있어야합니다 (getAttack, getMaxHP, getMeleeDamage 등).

이 방법이 실패하는 곳은 버프 간의 매우 복잡한 상호 작용입니다. 게토를 조금만 더하는 것 외에는 상호 작용을하는 쉬운 방법이 없습니다. 간단한 상호 작용 가능성이 있습니다. 그렇게하려면 정적 수정자를 저장하는 방식을 수정해야합니다. 열거 형을 키로 사용하는 대신 문자열을 사용합니다. 이 문자열은 Enum 이름 + 추가 변수입니다. 10에서 9 번, 추가 변수는 사용되지 않으므로 열거 이름을 키로 유지합니다.

간단한 예를 들어 보자 : 언데드 생물에 대한 피해를 수정하려면 다음과 같은 순서 쌍을 가질 수 있습니다 : (DAMAGE_Undead, 10) DAMAGE는 열거 형이고 언데드는 추가 변수입니다. 따라서 전투 중에 다음과 같은 작업을 수행 할 수 있습니다.

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
  • 제거
  • checkForInteraction <--- 중요

설명 자체를 적용하고 제거하십시오 (백분율과 같은 경우 영향은 입었을 때 HP가 얼마나 많이 증가했는지 추적하여 추가 된 양만 제거합니다). 그것이 옳은지 확인하는 데 오랜 시간이 걸렸습니다. 나는 여전히 그것에 대해 좋은 느낌을 얻지 못했습니다.)

checkForInteraction 메소드는 엄청나게 복잡한 코드 조각이었습니다. 각 영향 (예 : ChangeHP) 클래스에는 입력 영향으로 수정해야하는지 여부를 결정하는 코드가 있습니다. 예를 들어, 당신이 같은 것을 가지고 있다면 ...

  • 버프 1 : 공격시 10의 화염 피해를 입 힙니다.
  • 버프 2 : 모든 화염 피해가 25 % 증가합니다.
  • 버프 3 : 모든 화염 공격력이 15 증가합니다.

checkForInteraction 메소드는 이러한 모든 영향을 처리합니다. 이를 위해 근처에있는 모든 플레이어에 대한 각 영향을 확인해야했습니다 !! 이것은 내가 여러 지역에 걸쳐 여러 플레이어를 상대 한 영향의 유형 때문입니다. 이것은 코드에 위와 같은 특별한 진술이 없었 음을 의미합니다. 이 시스템은 적시에 자동으로 올바르게 처리합니다.

이 시스템을 작성하는 데 2 ​​개월이 걸렸으며 머리로 여러 번 폭발했습니다. 그러나, 그것은 정말 강력하고 미쳤던 양의 일을 할 수 있습니다. 특히 내 게임의 능력에 대해 다음 두 가지 사실을 고려할 때 : 1. 목표 범위를 가졌습니다. , PB AE 타겟, 타겟 AE 등). 2. 능력은 그들에게 하나 이상의 영향을 미칠 수 있습니다.

위에서 언급했듯이, 이것은이 게임의 2 차 3 차 영향 시스템이었습니다. 나는 왜 이것을 멀리 했습니까?

이 시스템은 내가 본 것 중 최악의 성능이었습니다! 그것이 진행되는 각각의 일에 대해 너무 많은 점검을해야했기 때문에 그것은 너무 느 렸습니다. 나는 그것을 개선하려고 시도했지만 실패로 간주했습니다.

그래서 우리는 내 세 번째 버전 (그리고 다른 유형의 버프 시스템)에 왔습니다.


핸들러가있는 복잡한 영향 클래스

따라서 이것은 처음 두 가지의 조합입니다. 많은 기능과 추가 데이터를 포함하는 Affect 클래스에 정적 변수를 가질 수 있습니다. 그런 다음 처리기를 호출하면 특정 작업에 대한 하위 클래스 대신 정적 유틸리티 메서드가 거의 호출됩니다. 그러나 원하는 경우 작업을 위해 하위 클래스와 함께 갈 수 있다고 확신합니다.

Affect 클래스는 대상 유형, 지속 시간, 사용 횟수, 실행 기회 등과 같은 수분이 많은 좋은 것들을 모두 갖습니다.

순간 이동 등의 상황을 처리하기 위해 여전히 특수 코드를 추가해야합니다. 우리는 여전히 전투 코드에서 수동으로 이것을 확인해야하고, 존재한다면 영향 목록을 얻을 것입니다. 이 영향 목록에는 사망시 순간 이동을 처리 한 플레이어에게 현재 적용된 모든 영향이 포함됩니다. 그런 다음 각 항목을보고 실행되어 성공했는지 확인합니다 (첫 번째 성공한 단계에서 중지 함). 성공 했으므로 처리기를 호출하여 처리합니다.

원하는 경우 상호 작용을 수행 할 수 있습니다. 플레이어 등의 특정 버프를 찾기 위해 코드를 작성해야합니다. 성능이 우수하기 때문에 (아래 참조) 그렇게하는 것이 상당히 효율적입니다. 더 복잡한 처리기 등이 필요합니다.

따라서 첫 번째 시스템의 많은 성능과 두 번째 시스템과 같은 많은 복잡성을 갖습니다 (그러나 AS는 아님). Java에서는 최소한 대부분의 경우 거의 첫 번째 성능을 얻기 위해 까다로운 작업을 수행 할 수 있습니다 (예 : 열거 형 맵 ( http://docs.oracle.com/javase/6/docs/api/java) /util/EnumMap.html )을 키로 사용하고 ArrayList의 값을 값으로 사용하면 [목록이 0이거나 맵에 열거 형이 없기 때문에] 영향을 빠르게 받는지 여부를 확인할 수 있습니다. 이유없이 플레이어의 영향 목록을 계속 반복합니다.이 시점에서 필요에 따라 영향을 반복하는 것은 괜찮습니다. 문제가되면 나중에 최적화하겠습니다.)

2005 년에 끝난 MUD를 현재 재개하고 있는데 (원래 있던 FastROM 코드 기반 대신 Java로 게임을 다시 작성하고 있음) MUD를 최근에 버프 시스템을 어떻게 구현하고 싶었습니까? 이 시스템은 이전에 실패한 게임에서 잘 작동했기 때문에 사용할 것입니다.

글쎄, 누군가가 어딘가에서 이러한 통찰력 중 일부가 유용하다는 것을 알게 될 것입니다.


6

해당 버프의 동작이 서로 다른 경우 각 버프에 대해 다른 클래스 (또는 주소 지정 가능한 함수)는 과도하지 않습니다. 한 가지는 + 10 % 또는 + 20 %의 버프 (물론 같은 클래스의 두 객체로 더 잘 표현 될 것임) 일 것이고, 다른 하나는 어쨌든 커스텀 코드를 필요로하는 매우 다른 효과를 구현하는 것입니다. 그러나 나는 각 버프가 원하는 것을 무엇이든 할 수있게하는 대신 게임 로직을 사용자 정의하는 표준 방법을 사용하는 것이 좋습니다 (그리고 예기치 않은 방식으로 서로 방해하여 게임 밸런스를 방해 할 수 있음).

각 "공격주기"를 단계로 나누는 것이 좋습니다. 여기서 각 단계에는 기본 값, 해당 값에 적용 할 수있는 정렬 된 수정 목록 (최소 한도) 및 최종 한도가 있습니다. 각 수정에는 기본적으로 ID 변환이 있으며 0 개 이상의 버프 / 디버프에 의해 영향을받을 수 있습니다. 각 수정의 세부 사항은 적용된 단계에 따라 다릅니다. 주기를 구현하는 방법은 사용자가 결정합니다 (토론 한 이벤트 중심 아키텍처의 옵션 포함).

공격주기의 한 예는 다음과 같습니다.

  • 플레이어 공격 계산 (기본 + 모드);
  • 상대 방어 계산 (기본 + 개조);
  • 차이를 수행하고 개조를 적용하고 기본 피해를 결정하십시오.
  • 모든 패리 / 갑옷 효과 (기본 피해 수정)를 계산하고 피해를 적용합니다.
  • 반동 효과 (기본 피해 수정)를 계산하고 공격자에게 적용합니다.

중요한 것은 사이클 의 초기 에 버프가 적용 되면 결과에 더 많은 영향을 미친다는 것입니다 . 따라서,보다 "전술적 인"전투 (플레이어 스킬이 캐릭터 레벨보다 중요)를 원한다면 기본 스탯에 많은 버프 / 디버프를 생성하십시오. 진행률을 제한하기 위해 레벨이 더 중요한 MMOG에서 더 "균형화 된"전투를 원한다면주기 후반에 버프 / 디버프 만 사용하십시오.

앞에서 언급 한 "수정"과 "버프"의 구별은 목적이 있습니다. 규칙과 균형에 대한 결정은 전자에 대해 구현 될 수 있으므로, 그에 대한 모든 변경은 후자의 모든 클래스에 대한 변경에 반영 할 필요가 없습니다. OTOH, 버프의 수와 종류는 상상력에 의해서만 제한됩니다. 각 버프는 다른 사람과의 상호 작용을 고려하지 않고 원하는 행동을 표현할 수 있기 때문에 (또는 다른 사람의 존재조차도) 상상할 수 없습니다.

따라서 질문에 대답하십시오 : 각 버프에 대한 클래스를 만들지 말고 각 유형의 수정에 대해 클래스를 만들고 수정을 캐릭터가 아닌 공격주기에 묶으십시오. 버프는 단순히 (수정, 키, 값) 튜플의 목록 일 수 있으며, 버프를 캐릭터의 버프 세트에 추가 / 제거함으로써 버프를 캐릭터에 적용 할 수 있습니다. 버프를 적용 할 때 캐릭터의 통계 를 전혀 변경할 필요가 없기 때문에 오류 발생 시간도 줄어 듭니다 (버프가 만료 된 후 통계를 잘못된 값으로 복원 할 위험이 적습니다).


이것은 내가 생각한 두 가지 구현 사이에 있습니다. 즉, 버프를 상당히 간단한 통계 및 결과 손상 수정 자로 제한하거나 모든 것을 처리 할 수있는 매우 강력하지만 오버 헤드 시스템을 만드는 것입니다. 이것은 단순한 인터페이스를 유지하면서 가시를 허용하는 전자의 확장입니다. 필자가 그것이 필요한 것에 대한 마법의 총알이라고 생각하지는 않지만 다른 접근 방식보다 훨씬 쉽게 균형을 잡는 것처럼 보일 수 있으므로 갈 길입니다. 입력 해 주셔서 감사합니다!
gkimsey 2016 년

3

아직 읽고 있는지 모르겠지만 여기에 지금 어떻게하는지 (코드는 UE4 및 C ++을 기반으로 함)입니다. 2 주 이상 문제에 대해 숙고 한 후 (!!) 마침내 나는 이것을 발견했다.

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, 구조체로 감싸 짐), 그 효과는이 효과와 상호 작용하기위한 수단으로, 그리고 그 효과가 다음과 같은지 추적합니다. 여전히 활성화되어 있습니다. 효과가 제거되면 핸들이 관심있는 모든 객체에 브로드 캐스트됩니다.

이것의 진짜 중요한 부분은 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;
}

이 기능은 효과가 추가되거나 제거 될 때마다 보너스의 전체 스택을 다시 계산해야합니다. 기능은 여전히 ​​완료되지는 않았지만 일반적인 아이디어를 얻을 수 있습니다.

지금 가장 큰 문제는 전체 스택을 다시 계산하지 않고 손상 / 치유 속성을 처리하는 것입니다.

어쨌든 속성은 다음과 같이 정의됩니다 (+ 언리얼 매크로, 여기서 생략) :

FGAAttributeBase Health;
FGAAttributeBase Energy;

기타

또한 속성의 CurrentValue를 처리하는 것에 대해 100 % 확신하지 못하지만 작동해야합니다. 그들은 지금 그대로입니다.

어쨌든 나는 그것이 사람들에게 헤드 캐시를 절약하기를 희망합니다. 이것이 최선인지 또는 좋은 해결책인지 확실하지 않지만 속성과 독립적으로 효과를 추적하는 것보다 더 좋아합니다. 이 경우 각 속성을 고유 한 상태로 추적하는 것이 훨씬 쉬우 며 오류가 덜 발생합니다. 본질적으로 단 하나의 실패 지점이 있으며 이는 상당히 짧고 간단한 클래스입니다.


귀하의 작업에 대한 링크와 설명에 감사드립니다! 나는 당신이 본질적으로 내가 요구 한 것을 향해 나아가고 있다고 생각합니다. 염두에 두어야 할 몇 가지 작업 순서 (예 : 동일한 속성에 대한 "추가"효과 3 개와 "곱하기"효과 2 개, 먼저 발생해야합니까?)가 순전히 속성 지원입니다. 또한 "충격시 손실 1 AP"효과 유형과 같은 트리거 개념도 있지만 별도의 조사가 필요할 수 있습니다.
gkimsey

속성의 보너스를 계산하는 것이 쉬운 경우 작업 순서. 내가 거기 있고 전환했다는 것을 여기서 볼 수 있습니다. 모든 현재 보너스를 반복하고 (추가, 빼기, 곱하기, 나누기 등) 보너스를 누적하십시오. BonusValue = (BonusValue * MultiplyBonus + AddBonus-SubtractBonus) / DivideBonus와 같은 작업을 수행하지만이 방정식을보고 싶습니다. 단일 진입 점으로 실험하기가 쉽습니다. 방아쇠에 관해서는, 그것에 대해 쓰지 않았습니다. 왜냐하면 그것이 내가 고민하는 또 다른 문제이기 때문에 이미 3-4 (한도)를 시도했습니다.
Łukasz Baran

솔루션 중 어느 것도 내가 원하는 방식으로 작동하지 않았습니다 (내 주요 목표는 디자이너 친화적 인 것입니다). 내 일반적인 아이디어는 태그를 사용하고 태그에 대해 들어오는 효과를 확인하는 것입니다. 태그가 일치하면 효과가 다른 효과를 유발할 수 있습니다. (태그는 Damage.Fire, Attack.Physical 등과 같은 간단한 사람이 읽을 수있는 이름입니다.) 핵심은 매우 쉬운 개념이며, 문제는 데이터를 구성하고, 쉽게 액세스 할 수 있고 (검색이 빠름) 새로운 효과를 쉽게 추가 할 수 있도록하는 것입니다. 당신은 여기에 코드를 확인할 수 있습니다 github.com/iniside/ActionRPGGame을 (GameAttributes 당신이 관심을 가질 것입니다 모듈입니다)
의 Łukasz 바란

2

나는 작은 MMO에서 일했고 모든 아이템, 힘, 버프 등에는 '효과'가있었습니다. 효과는 'AddDefense', 'InstantDamage', 'HealHP'등에 대한 변수가있는 클래스였습니다. 위력, 아이템 등은 해당 효과의 지속 시간을 처리합니다.

파워를 시전하거나 아이템을 착용하면 지정된 지속 시간 동안 캐릭터에게 효과가 적용됩니다. 그런 다음 주 공격 등의 계산에 적용된 효과가 고려됩니다.

예를 들어 방어력을 강화하는 버프가 있습니다. 해당 버프에는 최소한 EffectID와 Duration이 있습니다. 캐스팅 할 때 지정된 지속 시간 동안 캐릭터에 EffectID를 적용합니다.

항목에 대한 다른 예는 동일한 필드를 갖습니다. 그러나 지속 시간은 무한하거나 캐릭터가 아이템을 제거하여 효과가 제거 될 때까지 지속됩니다.

이 방법을 사용하면 현재 적용된 효과 목록을 반복 할 수 있습니다.

이 방법이 충분히 명확하게 설명 되었기를 바랍니다.


최소한의 경험으로 이해하면 RPG 게임에서 스탯 모드를 구현하는 전통적인 방법입니다. 잘 작동하고 이해하고 구현하기 쉽습니다. 단점은 "가시"버프와 같은 일이나 더 발전된 상황이나 상황에 맞는 일을 저에게 맡길 수 없다는 것입니다. RPG의 일부 익스플로잇은 역사적으로도 드문 일이지만, 싱글 플레이어 게임을 만들고 있기 때문에 누군가 익스플로잇을 발견하면 그다지 걱정하지 않습니다. 입력 해 주셔서 감사합니다.
gkimsey 2016 년

2
  1. 단일 사용자 인 경우 여기에 시작해야 할 것이 있습니다. http://www.stevegargolinski.com/armory-a-free-and-unfinished-stat-inventory-and-buffdebuff-framework-for-unity/

ScriptableOjects를 buffs / spells / talents로 사용하고 있습니다.

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 사용;

public enum BuffType {버프, 디버프} [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;
}

버프 모듈 :

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이 버프에 대한 목록과 로직 업데이터 를 구현해야합니다 .
  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

나는 이것이 상당히 오래되었다는 것을 알고 있지만 새로운 게시물에 연결되어 있으며 공유하고 싶은 생각이 있습니다. 불행히도 나는 현재 나와 함께 메모를하지 않았기 때문에 내가 이야기하고있는 것에 대한 일반적인 개요를 제공하려고 노력할 것이며 세부 사항과 예제 코드를 편집 할 것입니다. 나를.

첫째, 디자인 관점에서 볼 때 대부분의 사람들은 버프 유형과 적용 방법 및 객체 지향 프로그래밍의 기본 원칙을 잊어 버리는 방법에 너무 익숙해 있다고 생각합니다.

무슨 뜻이야? 무언가가 버프인지 디버프인지는 중요하지 않으며 , 긍정적이거나 부정적인 방식으로 무언가 에 영향을 미치는 수정 자입니다 . 코드는 어느 것을 신경 쓰지 않습니다. 그 문제에 대해 통계를 추가하거나 곱하는 것이 궁극적으로 중요하지는 않습니다. 다른 연산자 일뿐이며 코드는 어느 것을 신경 쓰지 않습니다.

그럼 내가 어디로 갈까? 좋은 (읽기 : 단순하고 우아한) 버프 / 디버프 클래스를 디자인하는 것이 그렇게 어렵지는 않습니다. 게임 상태를 계산하고 유지하는 시스템을 설계하는 것은 어렵습니다.

버프 / 디버프 시스템을 설계하는 경우 다음과 같은 사항을 고려해야합니다.

  • 효과 자체를 나타내는 버프 / 디버프 클래스입니다.
  • 버프의 영향 및 방법에 대한 정보를 포함하는 버프 / 디버프 유형 클래스입니다.
  • 문자, 아이템 및 위치는 모두 버프 및 디버프를 포함하기 위해 list 또는 collection 속성을 가져야합니다.

어떤 버프 / 디버프 유형에 포함되어야하는 몇 가지 세부 사항 :

  • 적용 대상 / 대상, IE : 플레이어, 몬스터, 위치, 아이템 등
  • 그것이 효과의 유형 (긍정적, 부정적), 그것이 곱셈이든 부가 적이든, 그리고 어떤 유형의 영향을 받는지, IE : 공격, 방어, 이동 등
  • 점검해야 할 때 (전투, 시간 등)
  • 제거 할 수 있는지 여부와 제거 할 수있는 방법

그것은 시작에 불과하지만 거기에서 당신은 당신이 원하는 것을 정의하고 정상적인 게임 상태를 사용하여 행동합니다. 예를 들어 이동 속도를 줄이는 저주받은 아이템을 만들고 싶다고 가정 해보십시오.

적절한 유형을 제 위치에 놓으면 다음과 같은 버프 레코드를 만드는 것이 간단합니다.

  • 유형 : 저주
  • 개체 유형 : 항목
  • StatCategory : 유틸리티
  • 영향을받는 : MovementSpeed
  • 기간 : 무한
  • 방아쇠 : OnEquip

그리고 버프를 만들 때 BuffType of Curse를 지정하면 모든 것이 엔진에 달려 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.