Альтернативы множественному наследованию для моей архитектуры (NPC в стратегии в реальном времени)?

11

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

Поэтому я хочу создать архитектуру для неигровых персонажей в видеоигре. Это стратегия в реальном времени, такая как Starcraft, Age of Empires, Command & Conquers и т. Д. И т. Д. Так что у меня будут разные виды NPC. NPC может иметь один ко многим способностям (методы) этим: Build(), Farm()и Attack().

Примеры:
рабочий может Build()и Farm()
воин может Attack()
гражданин может Build(), Farm()а Attack()
рыбак может Farm()иAttack()

Я надеюсь, что пока все ясно.

Так что теперь у меня есть типы NPC и их способности. Но давайте перейдем к техническому / программному аспекту.
Какой будет хорошая программная архитектура для моих разных типов NPC?

Хорошо, я мог бы иметь базовый класс. На самом деле я думаю, что это хороший способ придерживаться принципа СУХОЙ . Таким образом, у меня могут быть методы, как WalkTo(x,y)в моем базовом классе, так как каждый NPC сможет двигаться. Но теперь давайте подойдем к настоящей проблеме. Где я могу реализовать свои способности? (помните: Build(), Farm()а Attack())

Поскольку способности будут состоять из одной и той же логики, было бы досадно / нарушать принцип СУХОГО, чтобы реализовать их для каждого NPC (Рабочий, Воин, ..).

Хорошо, я мог бы реализовать способности в базовом классе. Это потребует какой - то логики , которая проверяет , если NPC может использовать способность X. IsBuilder, CanBuild.. Я думаю, ясно , что я хочу выразить.
Но я не очень хорошо себя чувствую с этой идеей. Это звучит как раздутый базовый класс со слишком большой функциональностью.

Я использую C # в качестве языка программирования. Так что множественное наследование здесь не вариант. Означает: наличие дополнительных базовых классов вроде Fisherman : Farmer, Attackerне сработает.

Brettetete
источник
3
Я смотрел на этот пример, который кажется решением этой проблемы: en.wikipedia.org/wiki/Composition_over_inheritance
Шон Маклин,
4
Этот вопрос будет лучше подходить для gamedev.stackexchange.com
Филипп

Ответы:

14

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

Все, что вы описали выше, начинающееся со слова «может», - это возможность, которая может быть представлена ​​интерфейсом, например ICanBuild, или ICanFarm. Вы можете наследовать столько интерфейсов, сколько считаете нужным.

public class Worker: ICanBuild, ICanFarm
{
    #region ICanBuild Implementation
    // Build
    #endregion

    #region ICanFarm Implementation
    // Farm
    #endregion
}

Вы можете передавать в «общие» строительные и сельскохозяйственные объекты через конструктора вашего класса. Вот где приходит композиция.

Роберт Харви
источник
Просто думать вслух: The «отношения» будет (внутренне) быть имеет вместо IS (работник имеет строитель вместо работник Builder). Пример / шаблон, который вы объяснили, имеет смысл и звучит как хороший развязанный способ решения моей проблемы. Но мне трудно получить эти отношения.
Brettetete
3
Это ортогонально решению Роберта, но вы должны отдавать предпочтение неизменности (даже больше, чем обычно), когда интенсивно используете композицию, чтобы избежать создания сложных сетей побочных эффектов. В идеале ваши реализации сборки / фермы / атаки принимают и выплевывают данные, не затрагивая ничего другого.
Довал
1
Рабочий все еще строитель, потому что вы унаследовали от ICanBuild. То, что у вас также есть инструменты для сборки, имеет второстепенное значение в иерархии объектов.
Роберт Харви
Имеет смысл. Я попробую этот принцип. Спасибо за ваш дружелюбный и наглядный ответ. Я действительно ценю это. Я
оставлю
4
@Brettetete Это может помочь представить реализации Build, Farm, Attack, Hunt и т. Д. Как навыки , которыми обладают ваши персонажи. BuildSkill, FarmSkill, AttackSkill и т. Д. Тогда состав имеет гораздо больше смысла.
Эрик Кинг,
4

Как упоминали другие авторы, компонентная архитектура может быть решением этой проблемы.

class component // Some generic base component structure
{
  void update() {} // With an empty update function
} 

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

class build : component
{
  override void update()
  { 
    // Check if the right object is selected, resources, etc
  }
}

Повторение контейнера компонента и обновление каждого компонента позволяет применять определенные функциональные возможности каждого компонента.

class npc
{
  void update()
  {
    foreach(component c in components)
      c.update();
  }

  list<component> components;
}

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

npc worker;
worker.add_component<build>();
worker.add_component<walk>();
worker.add_component<attack>();

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

Коннор Холлис
источник
3

Если вы определяете интерфейсы как, ICanBuildто ваша игровая система должна проверять каждого NPC по типу, который обычно не одобряется в ООП. Например: if (npc is ICanBuild).

Вам лучше с:

interface INPC
{
    bool CanBuild { get; }
    void Build(...);
    bool CanFarm { get; }
    void Farm(...);
    ... etc.
}

Затем:

class Worker : INPC
{
    private readonly IBuildStrategy buildStrategy;
    public Worker(IBuildStrategy buildStrategy)
    {
        this.buildStrategy = buildStrategy;
    }

    public bool CanBuild { get { return true; } }
    public void Build(...)
    {
        this.buildStrategy.Build(...);
    }
}

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

var workerType = NPC.Builder
    .Builds(new WorkerBuildBehavior())
    .Farms(new WorkerFarmBehavior())
    .Build();

var workerFactory = new NPCFactory(workerType);

var worker = workerFactory.Build();

Строитель NPC может реализовать функцию, DefaultWalkBehaviorкоторая может быть переопределена в случае NPC, который летает, но не может ходить и т. Д.

Скотт Уитлок
источник
Ну, просто снова подумать: на самом деле нет большой разницы между if(npc is ICanBuild)и if(npc.CanBuild). Даже когда я предпочел бы npc.CanBuildвыход из моего нынешнего отношения.
Бреттетета
3
@Brettetete - Вы можете увидеть в этом вопросе, почему некоторым программистам не нравится проверять тип.
Скотт Уитлок
0

Я бы поместил все способности в отдельные классы, которые наследуются от Способностей. Затем в классе типов юнитов есть список способностей и других вещей, таких как атака, защита, максимальное здоровье и т. Д. Также есть класс юнитов, который имеет текущее здоровье, местоположение и т. Д. И имеет тип юнита в качестве переменной-члена.

Определите все типы модулей в конфигурационном файле или базе данных, а не жестко их кодируйте.

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

user3970295
источник