Разработка чистого/гибкого пути к “символу” для кастинга различных написаний в ролевой игре играющего

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

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

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

Вот то, на что мой код похож с 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'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 

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

9
задан mikedev 4 February 2010 в 09:01
поделиться

11 ответов

Как выглядят ваши модульные тесты?

Облегчает ли дизайн создание необходимых вам тестов?

{{1 }}
0
ответ дан 27 October 2019 в 07:26
поделиться

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

- Отредактируйте, чтобы прояснить (надеюсь) -

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

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

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

2
ответ дан 4 December 2019 в 13:01
поделиться

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

1
ответ дан 4 December 2019 в 13:01
поделиться

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

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

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

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

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

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

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

Кроме того, каждый определяемый вами 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-файл.

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

6
ответ дан 4 December 2019 в 13:01
поделиться

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

0
ответ дан 4 December 2019 в 13:01
поделиться

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

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

[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
ответ дан 4 December 2019 в 13:01
поделиться

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

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

1
ответ дан 4 December 2019 в 13:01
поделиться

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

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
ответ дан 4 December 2019 в 13:01
поделиться

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

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

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

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

3
ответ дан 4 December 2019 в 13:01
поделиться

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

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

0
ответ дан 4 December 2019 в 13:01
поделиться

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

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

  • Вы можете перечислить все возможные действия, которые может совершить заклинание. , затем определите заклинания в некотором внешнем формате (XML, База данных), который загружается в ваше приложение при запуске . Западные ролевые игры, как правило, имеют такой код: заклинание состоит из «Применить эффект заклинания № 1234 с параметром 1000», «воспроизвести анимацию № 2345» и т. Д.

  • Вы можете открыть свое игровое состояние для языка сценариев и написать сценарии для своих заклинаний (вы также можете объединить это с первой идеей, чтобы в большинстве случаев ваши сценарии заклинаний просто вызывают предопределенные эффекты в коде). Duel of the Planeswalkers (игра 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), подходящий для представления баффов.

А вот статья о Duel of the Planeswalker-а действительно отличное чтение. Так хорошо, я дважды свяжу!

2
ответ дан 4 December 2019 в 13:01
поделиться
Другие вопросы по тегам:

Похожие вопросы: