Шаблоны реализации динамически расширяемой архитектуры игровой логики

1

В играх с кодировкой есть много случаев, когда вам нужно динамически вводить свою логику в существующий класс и не создавать ненужных зависимостей.

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

class Rabbit
{
    public bool CanJump { get; set; }

    void Jump()
    {
        if (!CanJump) return;
        ...
    }
}

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

Другое решение?

class Rabbit
{
    public bool Frozen { get; set; }
    public bool InWater { get; set; }
    bool CanJump { get { return !Frozen && !InWater; } }
}

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

Можно ли сделать стек значений bool для свойства CanJump? Нет, потому что способности можно деактивировать не в том порядке, в котором они были активированы.

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

Одним из возможных решений для этого является специальная проверка:

class Rabbit
{
    class CheckJumpEventArgs : EventArgs
    {
        public bool Veto { get; set; }
    }

    public event EventHandler<CheckJumpEvent> OnCheckJump;

    void Jump()
    {
        var args = new CheckJumpEventArgs();
        if (OnCheckJump != null) OnCheckJump(this, args);
        if (!args.Veto) return;
        ...
    }
}

Но это много кода! У настоящего класса Кролика было бы много свойств, подобных этому (атрибуты здоровья и скорости и т.д.).

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

class FreezeAbility
{
   void ActivateAbility() 
   {
       _rabbit.CanJump.Push(ReturnFalse);
   }

   void DeactivateAbility() 
   {
       _rabbit.CanJump.Remove(ReturnFalse);
   }

   // should be implemented as instance member
   // so it can be "unsubscribed"
   bool ReturnFalse(bool previousValue)
   {
       return false;
   }
}

Этот подход хорош? Что мне также следует учитывать? Каковы другие подходящие варианты и шаблоны? Любые готовые к использованию решения?


ОБНОВИТЬ

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

Теги:
design-patterns
mvvm

5 ответов

0
Лучший ответ

РЕДАКТИРОВАТЬ
Извините, что неправильно прочитал вопрос. Мое новое понимание целей.

У нас есть животное с поведением (скажем, прыжок), которое мы хотим изменить, исходя из состояния животного (скажем, "Замороженного") или состояния окружающей среды (скажем, гравитации или температуры)

Мы не хотим продолжать распространять животное, но хотели бы его настроить.

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

Нам нужны две новые концепции

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

IStateful/IState
Нам нужны некоторые критерии для тестирования. Вместо добавления к интерфейсу мы можем определить животное как включающее в себя сумку состояний, к которой мы можем добавить или удалить.

public interface IEnvironment {
    double Gravity { get; set; }
}   

public interface IState {
    string Name { get; }
    bool Is(object target);
}

public interface IStateful {
    void SetState(string  name);
    void SetState(string name, Func<object, bool> test);
    void ClearState(string name);
    bool IsInState(string name);
}

У нас все еще есть (в основном) те же классы WithBehaviours, мы просто добавляем состояния и экстернализируем конфигурацию

public abstract class WithBehaviors : IStateful {

    private readonly List<IState> _states;
    private readonly List<Behavior> _behaviors;
    private readonly IEnvironment _environment;

    protected WithBehaviors(IEnvironment environment) {
        _environment = environment;
        _behaviors = new List<Behavior>();
        _states = new List<IState>();
    }

    #region IStateful


    public void SetState(string name) {
        SetState(name , o=> true);
    }

    public void SetState(string name, Func<object, bool> test) {
        if (_states.Any(s => Match(s, name))) {
        throw new ArgumentException();
        }
        _states.Add(new State(name, test));
    }

    public void ClearState(string name) {
        _states.RemoveAll(s => Match(s, name));
    }

    public bool IsInState(string name) {
        var theState = _states.FirstOrDefault(s => Match(s, name));
        return theState != null && theState.Is(this);
    }

    private static bool Match(IState state, string name) {
        return state.Name.Equals(name, StringComparison.CurrentCultureIgnoreCase);
    }

    #endregion

    public void RegisterBehaviour(string name, Action<object> defaultAction) {
        _behaviors.Add(new Behavior(name, defaultAction));
    }

    public void RegisterBehaviorModifier(
        string name, 
        Func<IEnvironment, IStateful, bool> check,
        Action<object> replacementAction = null
    ) {
        ActOn(name, 
            b => b.BehaviorActions.Add(new BehaviorAction(check, 
                                                          replacementAction ?? (o =>{}))));
    }


    public void Invoke(string behaviourName) {
        ActOn(behaviourName, behavior => {
            var replacement = behavior.BehaviorActions.FirstOrDefault(b => b.Check(_environment, this));
            if (replacement == null) {
                behavior.DefaultAction(this);
            } else {
                replacement.Action(this);
            }
        });
    }

    private void ActOn(string name, Action<Behavior> action) {
        var behavior = _behaviors.FirstOrDefault(b => name.Equals(b.Name, StringComparison.CurrentCultureIgnoreCase));
        if (behavior != null) {
            action(behavior);
        }
    }

    private class Behavior {
        public Behavior(string name, Action<object> defaultAction) {
            Name = name;
            DefaultAction = defaultAction;
            BehaviorActions = new   List<BehaviorAction>();
        }

        public string Name { get; private set; }
        public Action<object> DefaultAction { get; private set; }
        public List<BehaviorAction> BehaviorActions { get; private set; }
    }

    private class BehaviorAction {

        public BehaviorAction(Func<IEnvironment, IStateful, bool> check, Action<object> action) {
            Check = check;
            Action = action;
        }

        public Func<IEnvironment, IStateful, bool> Check { get; private set; }
        public Action<object> Action { get; private set; }

    }

}

public class WithBehaviors<T> : WithBehaviors where T : class {

    public WithBehaviors(IEnvironment environment) : base(environment) {}

    public void RegisterBehaviour(string name, Action<T> defaultAction) {
        base.RegisterBehaviour(name, obj => defaultAction((T)obj));
    }

    public void RegisterBehaviorModifier(
        string name,
        Func<IEnvironment, IStateful, bool> check,
        Action<T> replacementAction = null
    ) {
        base.RegisterBehaviorModifier(name, 
                                      check,
                                      (replacementAction != null)
                                          ? (Action<object>)(o => replacementAction((T)o))
                                          : null);
    }

}


public  class Rabbit : WithBehaviors<Rabbit> {
    public Rabbit(IEnvironment environment) : base(environment){}

    public int XVal { get; set; }

}

public class State : IState {

    private readonly Func<object, bool> _test;

    public State(string name, Func<object, bool> test = null) {
        Name = name;
        _test = test;
    }

    public string Name { get; private set; }

    public bool Is(object target) {
        return _test(target);
    }
}

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

[TestClass]
public class BehaviorsFixture {

    #region Setup

    private Mock<IEnvironment> _mockEnvironment;

    private Rabbit CreateRabbit() {

        var buggs = new Rabbit(_mockEnvironment.Object);
        buggs.RegisterBehaviour(Behaviors.Jump, r => r.XVal += 10);

        // gravity GTE 30, cannot jump
        buggs.RegisterBehaviorModifier(Behaviors.Jump, (e, o) => e.Gravity >= 30);

        // gravity GTE 20, (but LT 30), jumps 5
        buggs.RegisterBehaviorModifier(Behaviors.Jump, (e, o) => e.Gravity >= 20, r => r.XVal += 5);

        // if the rabbit is frozen it cannot jump
        buggs.RegisterBehaviorModifier(Behaviors.Jump, (e, o) => o.IsInState(States.Frozen));

        // if the rabbit is chilled it can only jump 2
        buggs.RegisterBehaviorModifier(Behaviors.Jump, (e, o) => o.IsInState(States.Chilled), r => r.XVal += 2);

        return buggs;
    }

    #endregion

    [TestInitialize]
    public void TestInitialize() {
    _mockEnvironment = new Mock<IEnvironment>();
    _mockEnvironment.SetupProperty(mk => mk.Gravity, 9.81);
    }


    [TestMethod]
    public void JumpingInGravity() {

        var buggs = CreateRabbit();

        Assert.AreEqual(0, buggs.XVal);

        buggs.Invoke(Behaviors.Jump);
        buggs.Invoke(Behaviors.Jump);

        Assert.AreEqual(20, buggs.XVal);

        // higher gravity means can only jump 5
        _mockEnvironment.Object.Gravity = 20.0;

        buggs.Invoke(Behaviors.Jump);

        Assert.AreEqual(25, buggs.XVal);


        // even higher gravity, cannot jump
        _mockEnvironment.Object.Gravity = 30.0;

        buggs.Invoke(Behaviors.Jump);

        Assert.AreEqual(25, buggs.XVal);


        // set gravity back to normal - can jump
        _mockEnvironment.Object.Gravity = 9.81;

        buggs.Invoke(Behaviors.Jump);

        Assert.AreEqual(35, buggs.XVal);

    }

    [TestMethod]
    public void JumpingWhenCold() {

        var buggs = CreateRabbit();

        Assert.AreEqual(0, buggs.XVal);

        buggs.Invoke(Behaviors.Jump);

        Assert.AreEqual(10, buggs.XVal);

        // if frozen, cannot jump
        buggs.SetState(States.Frozen);

        buggs.Invoke(Behaviors.Jump);

        Assert.AreEqual(10, buggs.XVal);

        // remove, can jump again
        buggs.ClearState(States.Frozen);

        buggs.Invoke(Behaviors.Jump);

        Assert.AreEqual(20, buggs.XVal);

        // if chilled, can jump a bit
        buggs.SetState(States.Chilled);

        buggs.Invoke(Behaviors.Jump);

        Assert.AreEqual(22, buggs.XVal);

        // remove, can jump again
        buggs.ClearState(States.Chilled);

        buggs.Invoke(Behaviors.Jump);

        Assert.AreEqual(32, buggs.XVal);

        }
    }

}

Выдающиеся проблемы связаны с

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

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

  • 0
    Ваш ответ о динамическом поведении (может быть зарегистрирован во время выполнения вместо статических интерфейсов). Это не то, что я спрашиваю. Вы упомянули «можно зарегистрировать чек как функцию, которая проверяет состояние кролика, прежде чем разрешить» - это как раз тот вопрос. Но мне нужно расширить не только свойство «IsEnabled», но также JumpHeight, JumpSpeed, CrouchVisibilityFactor и другие свойства. Вопрос о таком механизме расширения.
0

вы можете использовать шаблон " Цепь ответственности", где класс Кролика расширяется обработчиками FrozenRabbit и HeavyRabbit, которые изменяют расстояние перехода (или любой другой фактор Кролика, на который влияет фактор, за который отвечает обработчик).

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

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

0

Это, похоже, требует шаблон декоратора, где вы можете либо переопределить, либо улучшить поведение класса, украсив его классом того же типа и повторить по мере необходимости. Внешний класс Rabbit контролирует конечное поведение, поэтому потребительский класс не задается вопросом, что он делает, он просто вызывает метод. Метод ShedThisBehavior() возвращает кролика с удаленным внешним удалением.

public class Rabbit
{
    protected Rabbit _innerRabbit;
    public virtual void Jump()
    {
        //do something jumpy
    }
    public Rabbit ShedThisBehavior()//aka Get My Inner Rabbit
    {
        if (_innerRabbit == null)
        {
            //this will only happen when the most inner rabbit is reached
            return this; 
        }
        return _innerRabbit;
    }
}

//override the base behavior and don't jump because the rabbit is frozen
public class FrozenRabbit:Rabbit
{

    public FrozenRabbit(Rabbit innerRabbit)
    {
        _innerRabbit = innerRabbit;
    }

    public override void Jump()
    {
        //don't jump
    }
}

//override the base behavior and don't jump because the rabbit is wet
public class WetRabbit : Rabbit
{

    public WetRabbit(Rabbit innerRabbit)
    {
        _innerRabbit = innerRabbit;
    }

    public override void Jump()
    {
        //don't jump
    }
}

//ignore the inner rabit, and jump twice
public class VeryJumpyRabbit : Rabbit
{

    public VeryJumpyRabbit(Rabbit innerRabbit)
    {
        _innerRabbit = innerRabbit;
    }

    public override void Jump()
    {
        base.Jump();
        base.Jump();
    }
}

//do whatever the inner rabbit does for jumping
public class OtherAttributeRabbit : Rabbit
{

    public OtherAttributeRabbit(Rabbit innerRabbit)
    {
        _innerRabbit = innerRabbit;
    }

    public override void Jump()
    {
        _innerRabbit.Jump();
    }
}
  • 0
    Что, если у кого-то уже есть ссылка на кролика? Я думал об использовании Decorator, но это не похоже на решение. Мне нужно динамически влиять на свойства кролика во время выполнения. Конечно, я мог бы создать промежуточный прокси-объект, который будет хранить актуальную ссылку на украшенного кролика, но он кажется слишком сложным.
  • 0
    Может быть, Государство было бы более подходящим здесь, и если бы я украсил его ... Но все же я не могу отменить примененные изменения, потому что некоторые другие изменения могут быть поверх этого.
Показать ещё 5 комментариев
0

Очевидный способ

public interface ICanJump
{
    public void Jump();
}
public Rabbit: Animal, ICanJump
{
    public void Jump() { ... }
}

Animal animal = new Rabbit();
if(animal is ICanJump)
    animal.Jump();

Поведение

abstract class Animal
{
    abstract int Y { get; set; }
}
public class JumpingAnimal
{
    Animal _instance;
    public JumpingAnimal(Animal animal) { _instance = animal; }

    public void Jump()
    {
        _instance.Y += 10;
    }
}

Animal animal = new Rabbit();
var jump = new JumpingAnimal(animal);
jump.Jump();

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

Очистить?

  • 0
    Хорошо, вы разделили его на два класса - само животное и поведение при прыжке. Если у меня есть способность, которая влияет на прыжок, его поведение должно наследовать JumpingAnimal и переопределять Jump с пустой реализацией. Чем он должен быть назначен вместо оригинального. Как вы отмените одно из таких поведений (может быть, не последнее)?
  • 0
    В том-то и дело, что вы ничего не отменяете. Поведение знает кое-что об объекте, чего достаточно для того, чтобы выполнять это поведение . Объект ничего не знает о его возможном поведении.
Показать ещё 2 комментария
0

Это звучит как пример шаблона спецификации (http://en.wikipedia.org/wiki/Specification_pattern)

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

Таким образом, бизнес-логика, решающая, может ли ваш кролик прыгать, инкапсулирована в ее спецификации, таким образом удаляя это от кролика.

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

Вы можете дополнительно привязать спецификации к более сложным спецификациям, выполнив составную спецификацию (также описанную в статье wikipedia).

  • 0
    Но это не применимо для числовых значений, только для логических значений?
  • 0
    Также возникает проблема, когда вам нужно отменить один из экземпляров спецификации, потому что он может быть перенесен неизвестным другим.
Показать ещё 1 комментарий

Ещё вопросы

Сообщество Overcoder
Наверх
Меню