Разработка чистого / гибкого способа для «персонажа» разыгрывать различные заклинания в ролевой игре - PullRequest
11 голосов
/ 04 февраля 2010

Я создаю ролевую игру для развлечения и обучения. Я нахожусь в точке, где мой персонаж (волшебник) использует заклинания. Я использую шаблон стратегии, чтобы установить заклинание, которое они собираются разыграть, перед тем, как читать заклинание. Причина, по которой я выбрал этот подход, заключается в том, что я хочу иметь возможность добавлять различные типы заклинаний в дальнейшем без необходимости возиться с классом персонажа / волшебника.

Мой вопрос - это плохой дизайн? Есть ли лучший / чище / проще подход для этого?

Я стараюсь держаться подальше от того "парня", который старается, чтобы все вписывалось в шаблон дизайна. Но в этом случае я чувствую, что это приличная подгонка.

Вот как выглядит мой код с двумя заклинаниями

public class Wizard : Creature
{
   public List<Spell> Spells { get; set; }

   public void Cast(Spell spell, Creature targetCreature)
   {
      spell.Cast(this, targetCreature);
   }
}

public abstract class Spell
{
   public string Name { get; set; }
   public int ManaCost { get; set; }
   public Spell(string name, int manaCost)
   {
      Name = name;
      ManaCost = manaCost;
   }
   public void Cast(Creature caster, Creature targetCreature)
   {
      caster.SubtractMana(ManaCost);
      ApplySpell(caster, targetCreature);
   }
   public abstract void ApplySpell(Creature caster, Creature targetCreature);
}

// increases the target's armor by 4
public class MageArmor : Spell
{
   public MageArmor() : base("Mage Armor", 4);
   public override void ApplySpell(caster, targetCreature)
   {
      targetCreature.AddAC(4);
   }
}

// target takes 7 damage
public class FireBall : Spell
{
   public FireBall() : base("Fire Ball", 5);
   public override void ApplySpell(caster, targetCreature)
   {
      targetCreature.SubtractHealth(7);
   }
}

Теперь, чтобы разыграть заклинание, мы делаем что-то вроде этого:

Wizard wizard = new Wizard();
wizard.Cast(new Spell.MageArmor(), wizard); // i am buffing myself 

ОБНОВЛЕНИЕ: обновленный код с некоторыми предложениями из ответов ниже

Ответы [ 11 ]

6 голосов
/ 04 февраля 2010

Следуя тому, что сказал Willcodejavaforfood, вы можете создать класс SpellEffect, который описывает один эффект, который может оказать ваше заклинание. Вы можете создать «словарь» для описания:

Атрибуты заклинания:

  • Имя
  • Стоимость маны
  • Целевое ограничение заклинания целом (игрок, NPC, монстр, ...)
  • Общая продолжительность заклинания (наибольшая из длительностей эффекта заклинания) (10 секунд, 5 тактов, ...)
  • Время каста
  • Дальность заклинаний (5 метров, 65 единиц, ...)
  • Процент отказов (5%, 90%)
  • Время ожидания, прежде чем это заклинание может быть снова использовано (Время перемотки)
  • Время ожидания, прежде чем ЛЮБОЕ заклинание может быть снова использовано (Время восстановления)
  • и т.д ...

Атрибуты для эффекта заклинания:

  • Тип эффекта (защита, нападение, бафф, дебафф, ...)
  • Цель Эффект (само, партия, цель, область вокруг цели, линия к цели, ...)
  • Свойство или характеристики, на которые действует эффект (л.с., мана, макс. Л.с., сила, скорость атаки, ...)
  • Насколько эффект меняет стат (+10, -500, 5%, ...)
  • Как долго длится эффект (10 секунд, 5 тиков, ...)
  • и т.д.

Я бы предположил, что ваш словарный запас (слова в скобках выше) будет определен в виде набора перечислений. Также может быть целесообразно создать иерархию классов для представления типов SpellEffect вместо использования перечисления для этого конкретного атрибута, поскольку может быть типом SpellEffect, которому не нужны все эти атрибуты или, возможно, существует какая-то нестандартная логика для каждого базового типа SpellEffect, о которой я не думаю. Но это также может усложнить ситуацию. Принцип поцелуя =).

В любом случае, дело в том, что вы извлекаете конкретную информацию об эффекте заклинания в отдельную структуру данных. Прелесть этого в том, что вы можете создать 1 Spell класс и сделать так, чтобы он содержал список эффектов заклинаний для применения при активации. Затем заклинание может выполнять несколько функций (наносить урон врагу и лечить игрока, также называемое «спасение жизни») за один выстрел. Вы создаете новый экземпляр заклинания для каждого заклинания. Конечно, в какой-то момент вам придется на самом деле создавать заклинаний. Вы можете легко собрать утилиту редактирования заклинаний, чтобы сделать это проще.

Кроме того, каждый заданный вами эффект SpellEffect может очень легко записываться и загружаться из XML с помощью класса XmlSerializer System.Xml.Serialization. Легко использовать на простых классах данных, таких как SpellEffect. Вы даже можете просто сериализовать ваш окончательный список заклинаний в XML тоже. Например:

<?xml header-blah-blah?>
<Spells>
  <Spell Name="Light Healing" Restriction="Player" Cost="100" Duration="0s"
         CastTime="2s" Range="0" FailRate="5%" Recast="10s" Recovery="5s">
    <SpellEffect Type="Heal" Target="Self" Stat="Hp" Degree="500" Duration="0s"/>
  </Spell>
  <Spell Name="Steal Haste" Restriction="NPC" Cost="500" Duration="120s"
         CastTime="10s" Range="100" FailRate="10%" Recast="15s" Recovery="8s">
    <SpellEffect Type="Buff" Target="Self" Stat="AttackSpeed" Degree="20%" Duration="120s"/>
    <SpellEffect Type="Debuff" Target="Target" Stat="AttackSpeed" Degree="-20%" Duration="60s"/>
  </Spell>
  ...
</Spells>

Вы также можете поместить свои данные в базу данных вместо xml. Sqlite будет маленьким, быстрым, легким и бесплатным. Вы также можете использовать LINQ для запроса данных заклинаний из xml или sqlite.

Конечно, вы могли бы сделать что-то подобное для своих монстров и тому подобное - по крайней мере, для их данных. Я не уверен насчет логики.

Если вы используете систему такого типа, вы можете получить дополнительное преимущество от возможности использовать свою систему Существо / Заклинание для других игр. Вы не можете сделать это, если вы «жестко закодируете» свои заклинания. Это также позволит вам изменить заклинания (балансировка классов, ошибки, что угодно) без без необходимости перестраивать и перераспределять ваш исполняемый файл игры. Просто простой XML-файл.

Святая корова! Я действительно взволнован вашим проектом сейчас и тем, как что-то подобное я мог бы реализовать. Если вам нужна помощь, дайте мне знать !!

3 голосов
/ 04 февраля 2010

Не совсем понятно, почему вы хотите, чтобы это был двухэтапный процесс, если только он не будет представлен в пользовательском интерфейсе (т. Е. Если пользователь установит «загруженное заклинание» и может позже передумать).

Кроме того, если вы собираетесь иметь свойство, а не просто wizard.Cast (новый Spell.MageArmor(), wizard), метод SetSpell немного странен - ​​почему бы просто не сделать свойство LoadedSpell общедоступным

Наконец, заклинания действительно имеют какое-либо изменяемое состояние? Не могли бы вы иметь фиксированный набор экземпляров (шаблон flyweight / enum)? Я не думаю об использовании памяти здесь (которая является нормальной причиной для модели веса), а просто о ее концептуальной природе. Такое ощущение, что вы хотите что-то, что действительно похоже на перечисление Java - набор значений с пользовательским поведением. Сложнее сделать это в C #, потому что нет прямой языковой поддержки, но это все еще возможно.

Фактическая структура внутри заклинания (наличие заклинателя и цели) кажется разумной, хотя вы можете обнаружить, что она становится негибкой, если вы хотите иметь возможность использовать заклинания с эффектом области (с целью, а не с конкретным существом) или заклинания, которые проклинают / благословляют предметы и т. д. Вам также может понадобиться пройти в остальной части состояния игрового мира - например, если у вас есть заклинание для создания миньонов.

2 голосов
/ 04 февраля 2010

Естественно инкапсулировать «Заклинания» с помощью шаблона команд (что в основном и делается). Но вы сталкиваетесь с двумя проблемами: -

1) Вы должны перекомпилировать, чтобы добавить больше заклинаний

  • Можно перечислять все возможные действие возможно для заклинания взять, а затем определить заклинания в некоторых внешний формат (XML, база данных), который загружается в ваше приложение на запускать. Западные RPG имеют тенденцию быть закодированы как это - "заклинание" состоит из "Применить эффект заклинания # 1234 с параметром 1000 ", "Воспроизвести анимацию # 2345" и т. д.

  • Вы можете выставить свое игровое состояние в сценарий язык, и сценарий ваших заклинаний (вы также можете объединить это с первой идеей, так что в большинстве случаи, когда ваши заклинания в сценарии просто вызывают заранее определенные эффекты в коде). Дуэль Странников (игра M: TG на X-Box 360) была написана широко этот подход

  • Или вы можете просто жить с этим (я знаю ...)

2) Что происходит, когда ваша цель заклинания не является существом?

  • Если вы подвергаете свое состояние игры сценариям заклинаний, это не проблема, потому что ваши сценарии могут делать все, что угодно, в контексте того, что вы выставляете.

  • В противном случае вам лучше всего создать универсальный тип.

Обычно я делаю что-то вроде следующего (и не только в играх, я использую этот тип паттерна для представления поведения в системах с несколькими агентами): -

public interface IEffect<TContext>
{
  public void Apply(TContext context);
}

public class SingleTargetContext
{
  public Creature Target { get; set; }
}
public class AoEContext
{
  public Point Target { get; set; }
}
// etc.

Преимущество этого паттерна заключается в том, что он действительно гибок для выполнения тех «странных» вещей, которые, как вы часто ожидаете, могут выполнять заклинания, на которые не способны более фиксированные модели. Вы можете делать такие вещи, как связать их вместе. Вы можете иметь эффект, который добавляет TriggeredEffect к вашей цели - хорошо для того, чтобы делать что-то вроде Thorns Aura. Вы можете иметь IReversibleEffect (с дополнительным методом Unapply), подходящий для представления баффов.

Тем не менее, эта статья о дуэли Planeswalker'ов действительно прекрасна для чтения. Так хорошо, я свяжу это дважды!

2 голосов
/ 04 февраля 2010

Я бы не стал использовать здесь подклассы для каждого заклинания.Я бы попытался записать его на диск с помощью XML или JSON и создать их динамически.

- Изменить, чтобы уточнить (надеюсь) -

Этот подход потребует реального планирования заранеекак можно больше.Вы должны были бы определить атрибуты как:

  • Имя
  • Описание
  • Продолжительность
  • Цель (само, область, другое)
  • Тип (бонус, урон, проклятие)
  • Эффект (например: 1d6 урон от магии льда, +2 класс брони, -5 Сопротивление урону)

Обертывание всего этого поведения в общемкласс заклинаний должен сделать его действительно гибким и более простым для тестирования.

1 голос
/ 05 февраля 2010

Я думаю, что ваш дизайн выглядит хорошо. Поскольку каждый класс Spell по сути является оберткой вокруг функции (это, скорее, шаблон Command, а не Strategy), вы можете полностью избавиться от классов заклинаний и просто использовать функции с небольшим количеством отражения, чтобы найти методы заклинаний и добавить некоторые метаданные им. Как:

public delegate void Spell(Creature caster, Creature targetCreature);

public static class Spells
{
    [Spell("Mage Armor", 4)]
    public static void MageArmor(Creature caster, Creature targetCreature)
    {
        targetCreature.AddAC(4);
    }

    [Spell("Fire Ball", 5)]
    public static void FireBall(Creature caster, Creature targetCreature)
    {
        targetCreature.SubtractHealth(7);
    }
}
1 голос
/ 04 февраля 2010

Самая большая проблема, которую я вижу с этим шаблоном, состоит в том, что все заклинания должны помнить, чтобы вычесть их мана-стоимость. Как насчет:

public abstract class Spell
{
   public string Name { get; set; }
   public int ManaCost { get; set; }
   public Spell(string name, int manaCost)
   {
      Name = name;
      ManaCost = manaCost;
   }

   public void Cast(Creature caster, Creature targetCreature)
   {
       caster.SubtractMana(ManaCost); //might throw NotEnoughManaException? 
       ApplySpell(caster, targetCreature);
   }

   protected abstract void ApplySpell(Creature caster, Creature targetCreature);
}

Кроме того, должен ли Wizard расширить PlayerCharacter, что расширило бы Существо?

1 голос
/ 04 февраля 2010

По какой-то причине «заклинания» для меня больше напоминают командный паттерн.Но я никогда не разрабатывал игру, так что ...

0 голосов
/ 04 февраля 2010

Я склонен считать, что ваши заклинания и предметы должны быть не классами, а композицией эффектов.

Вот мое мнение, не стесняйтесь расширяться. В основном это комбинированный подход и двухэтапная оценка эффектов заклинаний, поэтому каждый класс может добавить определенное сопротивление.

[Serializable]
class Spell
{
    string Name { get; set; }
    Dictionary<PowerSource, double> PowerCost { get; set; }
    Dictionary<PowerSource, TimeSpan> CoolDown { get; set; }
    ActionProperty[] Properties { get; set; }
    ActionEffect Apply(Wizzard entity)
    {
        // evaluate
        var effect = new ActionEffect();
        foreach (var property in Properties)
        {
            entity.Defend(property,effect);
        }

        // then apply
        entity.Apply(effect);

        // return the spell total effects for pretty printing
        return effect;
    }
}

internal class ActionEffect
{
    public Dictionary<DamageKind,double> DamageByKind{ get; set;}       
    public Dictionary<string,TimeSpan> NeutralizedActions{ get; set;}       
    public Dictionary<string,double> EquipmentDamage{ get; set;}
    public Location EntityLocation{ get; set;} // resulting entity location
    public Location ActionLocation{ get; set;} // source action location (could be deflected for example)
}

[Serializable]
class ActionProperty
{
    public DamageKind DamageKind { get;  set; }
    public double? DamageValue { get; set;}
    public int? Range{ get; set;}
    public TimeSpan? duration { get; set; }
    public string Effect{ get; set}
}

[Serializable]
class Wizzard
{
    public virtual void Defend(ActionProperty property,ActionEffect totalEffect)
    {
        // no defence   
    }
    public void Apply(ActionEffect effect)
    {
        // self damage
        foreach (var byKind in effect.DamageByKind)
        {
            this.hp -= byKind.Value;
        }
        // let's say we can't move for X seconds
        foreach (var neutralized in effect.NeutralizedActions)
        {
            Actions[neutralized.Key].NextAvailable += neutralized.Value;
        }

        // armor damage?
        foreach (var equipmentDamage in effect.EquipmentDamage)
        {
            equipment[equipmentDamage.Key].Damage += equipmentDamage.Value;
        }
    }
}

[Serializable]
class RinceWind:Wizzard
{
    public override void Defend(ActionProperty property, ActionEffect totalEffect)
    {
        // we have resist magic !
        if(property.DamageKind==DamageKind.Magic)
        {
            log("resited magic!");
            double dmg = property.DamageValue - MagicResistance;
            ActionProperty resistedProperty=new ActionProperty(property);
            resistedProperty.DamageValue = Math.Min(0,dmg);                
            return;
        }           
        base.Receive(property, totalEffect);
    }
}
0 голосов
/ 04 февраля 2010

Как выглядят ваши юнит-тесты?

Позволяет ли дизайн вам писать нужные тесты?

0 голосов
/ 04 февраля 2010

Я мог бы что-то упустить, но трио WizardSpells, LoadedSpell, SetSpell кажется, что это можно уточнить. В частности, я не вижу, чтобы список использовался в вашем коде до сих пор. Возможно, я бы добавил в список заклинания, доступные для мастера, с помощью LearnNewSpell (Spell newSpell) и проверил, что LoadSpell использует заклинание из этого списка. Кроме того, вы можете в какой-то момент добавить дополнительную информацию о типе заклинателя в Заклинании, если у вас будет несколько типов заклинателей.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...