Подтвердить что ты не робот

Проектирование чистого/гибкого способа "персонажа", чтобы различать заклинания в ролевой игре

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

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

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

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

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 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 

UPDATE: обновленный код с некоторыми предложениями из нижеприведенных ответов

4b9b3361

Ответ 1

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

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

  • Имя
  • Стоимость маны
  • Целевое ограничение заклинания целое (игрок, npc, монстр,...)
  • Общая продолжительность заклинания (максимальная длительность SpellEffect) (10 секунд, 5 тиков,...)
  • Время исполнения
  • Диапазон заклинаний (5 метров, 65 единиц,...)
  • Отказоустойчивость (5%, 90%)
  • Время ожидания до того, как это заклинание может быть снова запущено (время перепрограммирования)
  • Время ожидания до того, как любое заклинание может быть снова запущено (время восстановления)
  • и т.д...

Атрибуты для SpellEffect:

  • Тип эффекта (защита, нарушение, бафф, дебафф,...)
  • Цель эффекта (self, party, target, area around target, line to target,...)
  • Свойство или стат действует (hp, mana, max hp, strength, скорость атаки,...)
  • Насколько изменяется эффект stat (+10, -500, 5%,...)
  • Как долго длится эффект (10 секунд, 5 тиков,...)
  • и др.

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

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

Кроме того, каждый определенный вами SpellEffect может очень легко записываться и загружаться из XML с помощью класса System.Xml.Serialization XmlSerializer. Это легкий ветерок для использования на простых классах данных, таких как 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.

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

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

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

Ответ 2

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

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

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

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

Ответ 3

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

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

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

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

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

Ответ 4

Естественно инкапсулировать "Заклинания" с помощью Command Pattern (в основном это вы сделали). Но у вас возникают две проблемы: -

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

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

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

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

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

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

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

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

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 к вашей цели - хорошо для выполнения чего-то вроде Ауры Шипов. У вас может быть IReversibleEffect (с дополнительным методом Unapply), который хорош для представления баффов.

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

Ответ 5

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

Ответ 6

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

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, который расширяет Существо?

Ответ 7

Я думаю, что ваш дизайн выглядит отлично. Так как каждый класс заклинаний в основном представляет собой оболочку вокруг функции (это скорее шаблон 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);
    }
}

Ответ 8

Прежде всего: всегда есть лучший/более чистый/легкий подход для всего.

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

Ответ 9

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

Ответ 10

Как выглядят ваши тесты устройств?

Легко ли вам написать дизайн тестов, которые вы хотите?

Ответ 11

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

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

[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 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);
    }
}