Как избежать приведения из интерфейса в класс - PullRequest
9 голосов
/ 25 сентября 2011

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

interface ICollideable
{
    Sprite Sprite { get; }
    int    Damage { get; }

    void HandleCollision(ICollideable collidedWith);
}

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

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

void HandleCollision(ICollideable collidedWith)
{
    if (!(collidedWith is Player)) { // do stuff }
}

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

Второй вопрос, далее по аналогии с первым. В целях оценки, если враг уничтожен снарядом, кто-то должен знать члена-владельца класса «Снаряд». Однако ни одно из моих других сопоставимых элементов не имеет и не нуждается в этом свойстве, поэтому я обнаружил, что хочу сделать это (в Enemy HandleCollision):

void HandleCollision(ICollideable collidedWith)
{
    if (collidedWith is Projectile) {
        Health -= collidedWith.Damage;

        if (Health <= 0) {
            Player whoDestroyedMe = (collidedWith as Projectile).FiredBy
            // ...
        }
    }
}

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

РЕДАКТИРОВАТЬ: Я хотел сосредоточиться на втором вопросе, потому что моя интуиция говорит мне, как решить эту проблему, решит первый вопрос. Что касается первого вопроса, я подумал о способе абстрагировать это поведение. Я мог бы определить перечисление:

enum Team
{
    Player,
    Enemy,
    Neither
}

и ICollideables реализует это свойство. Тогда детектор столкновений просто не регистрирует столкновения между объектами столкновения в одной и той же «Команде». Таким образом, Снаряды Игрока и Игрока будут в одной команде, Снаряды врага и Врага - в другой, а окружение (которое может повредить оба) не может быть ни в одном. Это не должно быть перечислением, может быть целым числом, строкой или чем-то другим, с той идеей, что объекты с одинаковым значением для этого свойства не сталкиваются друг с другом.

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

Ответы [ 5 ]

3 голосов
/ 26 сентября 2011

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

Вот как я мог бы справиться с этим, хотя это означает использование объекта для каждого стиля столкновения (Корабль противКорабль, Корабль против Снаряда, Корабль против Астероида и т. Д.) Вам может быть удобнее поместить всю эту логику в один объект или даже в один метод для этого объекта.

public interface ICollisionHandler
{
    bool HandleCollision(Entity first, Entity second);
}

public class PlayerShipVsProjectile : ICollisionHandler
{
    private GameOptions options;
    public PlayersOwnShipHandler(GameOptions options)
    {
        this.options = options;
    }

    public bool HandleCollision(Entity first, Entity second)
    {
        // Exactly how you go about doing this line, whether using the object types
        // or using a Type property, or some other method, is not really that important.
        // You have so much more important things to worry about than these little
        // code design details.
        if ((!first is Ship) || (!second is Projectile)) return false;
        Ship ship = (Ship)first;
        Projectile projectile = (Projectile)second;

        // Because we've decided to put this logic in it's own class, we can easily
        // use a constructor parameter to get access to the game options. Here, we
        // can have access to whether friendly fire is turned on or not.
        if (ship.Owner.IsFriendlyWith(projectile.Shooter) &&
              !this.options.FriendlyFire) {
            return false;
        }

        if (!ship.InvulnerableTypes.Contains(InvulnerableTypes.PROJECTILE))
        {
             ship.DoDamage(projectile.Damage);
        }

        return true;
    }
}

Таким образом, вы можетезатем сделайте ...

// Somewhere in the setup...
CollisionMapper mapper = new CollisionMapper();
mapper.AddHandler(new ShipVsProjectile(gameOptions));
mapper.AddHandler(new ShipVsShip(gameOptions));

// Somewhere in your collision handling...
mapper.Resolve(entityOne, entityTwo);

Реализация CollisionMapper оставлена ​​в качестве упражнения для читателя.Помните, что вам может потребоваться, чтобы Resolve дважды вызывал метод «Обработка» ICollisionHandler, при этом во второй раз сторнируются сущности (в противном случае вашим объектам-обработчикам коллизий потребуется проверить обратную ситуацию, что также может быть в порядке).

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

1 голос
/ 18 октября 2014

Я думаю, что это хороший вопрос @idlewire, и я должен сказать, что я не думаю, что в вашем первоначальном решении есть что-то принципиально неправильное. В вопросе о том, следует ли разрешить объекту Foo разыгрывать ICollideable для Bar, важен только один вопрос: нежелательно ли, чтобы Foo вообще что-либо знал о Bar? Если ответ «нет», потому что Foo уже знает о Bars (для поведения, отличного от столкновений, возможно), тогда я не вижу проблем в приведении и, как вы говорите, это позволяет вам лучше инкапсулировать поведение обоих.

Когда вам нужно быть осторожным, это только тогда, когда это вводит зависимость между двумя вещами, которые вы хотели бы отделить друг от друга - что делает невозможным повторное использование одного без другого (например, в другом игровом приложении). Там вы можете захотеть иметь более специфичные подчиненные интерфейсы от ICollideable (например, IElastic и IInelastic) или использовать свойства интерфейса, как вы предложили с Enum.

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

1 голос
/ 26 сентября 2011

Как насчет этого?

Collidable.cs

abstract class Collidable
{
    public Sprite Sprite { get; protected set;  }
    public int Damage { get; protected set; }

    protected delegate void CollisionAction(Collidable with);

    protected Dictionary<Type, CollisionAction> collisionTypes = new Dictionary<Type, CollisionAction>();

    public void HandleCollision(Collidable with)
    {
        Type collisionTargetType = with.GetType();

        CollisionAction action;

        bool keyFound = collisionTypes.TryGetValue(collisionTargetType, out action);

        if (keyFound)
        {
            action(with);
        }
    }
}

Bullet.cs

class Bullet: Collidable
{
    public Bullet()
    {
        collisionTypes.Add(typeof(Player), HandleBulletPlayerCollision);
        collisionTypes.Add(typeof(Bullet), HandleBulletBulletCollision);
    }

    private void HandleBulletPlayerCollision(Collidable with)
    {
        Console.WriteLine("Bullet collided with {0}", with.ToString());
    }

    private void HandleBulletBulletCollision(Collidable with)
    {
        Console.WriteLine("Bullet collided with {0}.", with.ToString());
    }
}

Player.cs

class Player : Collidable
{
    public Player()
    {
        collisionTypes.Add(typeof(Bullet), HandlePlayerBulletCollision);
        collisionTypes.Add(typeof(Player), HandlePlayerPlayerCollision);
    }

    private void HandlePlayerBulletCollision(Collidable with)
    {
        Console.WriteLine("Player collided with {0}.", with.ToString());
    }

    private void HandlePlayerPlayerCollision(Collidable with)
    {
        Console.WriteLine("Player collided with {0}.", with.ToString());
    }
}
1 голос
/ 25 сентября 2011

Для первого случая я бы добавил следующий дополнительный метод в ICollidable:

bool CanCollideWith(ICollidable collidedWith)

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

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

0 голосов
/ 11 февраля 2016

Иногда самый простой метод - лучший метод.Если вы не хотите разделить взаимодействие при столкновении на многочисленные подтипы, вы можете вместо этого поместить свойство bool IsPlayer в Интерфейс.

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

If (isplayer == true)
{
      Handlethisway;
}

Недостатком является то, что вам все еще приходится выполнять какую-то проверку состояния, но это более эффективно.

Чтобы избежать каких-либо проверок состояния, вам необходимо сделать следующее: Создать интерфейс ICollidablePlayer, который принимает универсальный Icollideable и обрабатывает их по-разному.Поскольку Icollideable является вашей внедренной зависимостью, зависимости ICollideablePlayer являются неотъемлемыми.Объекты Icollideable не будут знать об этом отдельном процессе и будут взаимодействовать друг с другом одинаково.

ICollideablePlayer:ICollideable
{
   //DependenciesHere

   HandlePlayerCollision(ICollideable)
   { 
       HandleDifferently
       {
       }

       ICollideable
       {
           //DependenciesHere
           HandleCollision(ICollideable)
       }
    }
}

Во взаимодействии ICollideable будет обрабатывать игрока как любого другого ICollideable, но ICollideablePlayerотклонит взаимодействие, когда выполнит саму проверку.

Для таких вещей, как щиты и все такое, вы говорите об изменениях состояния, которые подразумевают, что такие вещи должны быть свойствами в любом из этих интерфейсов, что-то вродеbool ColliderOff для временного изменения состояния.

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