Как решить, какой GameObject должен обрабатывать столкновение?

28

В любом столкновении участвуют два объекта GameObject, верно? Я хочу знать, как мне решить, какой объект должен содержать мой OnCollision*?

В качестве примера, давайте предположим, что у меня есть объект Player и объект Spike. Моя первая мысль - поставить скрипт на плеер, который содержит такой код:

OnCollisionEnter(Collision coll) {
    if (coll.gameObject.compareTag("Spike")) {
        Destroy(gameObject);
    }
}

Конечно, та же самая функциональность может быть достигнута, вместо этого имея скрипт на объекте Spike, который содержит такой код:

OnCollisionEnter(Collision coll) {
    if (coll.gameObject.compareTag("Player")) {
        Destroy(coll.gameObject);
    }
}

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

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

OnCollisionEnter(Collision coll) {
    GameObject other = coll.gameObject;
    if (other.compareTag("Spike") || other.compareTag("Lava") || other.compareTag("Enemy")) {
        Destroy(gameObject);
    }
}

Принимая во внимание, что в случае, когда скрипт был на Spike, все, что вам нужно было бы сделать, это добавить этот же скрипт ко всем другим объектам, которые могут убить Player, и назвать скрипт как-то так KillPlayerOnContact.

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

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


Любое понимание высоко ценится! Спасибо за ваше время :)

Redtama
источник
Во-первых, почему строковые литералы для тегов? Перечисление намного лучше, потому что опечатка превратится в ошибку компиляции, а не в трудно отслеживаемую ошибку (почему мой шип не убивает меня?).
фрик с трещоткой
Потому что я следовал учебникам по Unity, и это то, что они сделали. Итак, у вас просто есть публичное перечисление с именем Tag, которое содержит все значения ваших тегов? Так было бы Tag.SPIKEвместо этого?
Редтама
Чтобы получить лучшее из обоих миров, рассмотрите возможность использования структуры с перечислением внутри, а также статических методов ToString и FromString
zcabjro
Очень связанные: объекты с нулевым поведением в ООП - моя дилемма дизайна и серия «Волшебники и воины» Эрика Липперта: ericlippert.com/2015/04/27/wizards-and-warriors-part-one

Ответы:

48

Я лично предпочитаю проектировать системы таким образом, чтобы ни один объект, участвующий в столкновении, не обрабатывал его, а вместо этого некоторый прокси-сервер обработки столкновений обрабатывал его с помощью функции обратного вызова HandleCollisionBetween(GameObject A, GameObject B). Таким образом, мне не нужно думать о том, какая логика должна быть, где.

Если это не вариант, например, когда вы не контролируете архитектуру столкновения-отклика, все становится немного неясным. Обычно ответ «это зависит».

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

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

Например, объект игрока и объект пули. Возможно, «получение урона при столкновении с пулями» имеет логический смысл как часть игрока. «Наносить урон тому, что поражает», имеет логический смысл как часть пули. Однако пуля может нанести урон большему количеству объектов, чем игроку (в большинстве игр). Игрок не будет получать урон от блоков местности или неигровых персонажей, но пуля все еще может наносить урон. Так что я бы предпочел в этом случае использовать обработку столкновений для нанесения ущерба пуле.

мистифицировать
источник
1
Я нашел ответы на все вопросы очень полезными, но для ясности должен был принять ваши. Ваш последний абзац действительно подводит итог для меня :) Очень полезно! Спасибо большое!
Редтама
12

TL; DR Лучшее место для установки детектора коллизий - это место, где вы считаете, что это активный элемент в коллизии, а не пассивный элемент в коллизии, потому что, возможно, вам потребуется дополнительное поведение для выполнения в такой активной логике (и, следуя хорошим правилам ООП, таким как SOLID, ответственность лежит на активном элементе).

Подробно :

Посмотрим ...

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

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

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

Представьте себе это поведение:

  1. Поведение для определения объекта как стека на земле. РПГ игры используют это для предметов в земле, которые можно подобрать.

    public class Treasure : MonoBehaviour {
        public InventoryObject inventoryObject = null;
        public uint amount = 1;
    }
    
  2. Поведение для указания объекта, способного нанести урон. Это может быть лава, кислота, любое токсичное вещество или летящий снаряд.

    public class DamageDealer : MonoBehaviour {
        public Faction facton; //let's use it as well..
        public DamageType damageType = DamageType.BLUNT;
        public uint damage = 1;
    }
    

В этом смысле считают DamageTypeи InventoryObjectперечисления.

Эти поведения не активны , так как находясь на земле или летая, они не действуют сами по себе. Они ничего не делают. Например, если у вас есть огненный шар, летящий в пустом пространстве, он не готов наносить урон (возможно, другие действия следует считать активными в огненном шаре, например, огненный шар, поглощающий его силу во время путешествия и уменьшающий его мощность урона в процессе, но даже когда потенциальное FuelingOutповедение может быть разработано, это все же другое поведение, а не то же самое поведение DamageProducer), и если вы видите объект в земле, это не проверяет целых пользователей и думает, что они могут выбрать меня?

Для этого нам нужно активное поведение. Аналогами будут:

  1. Одно поведение, чтобы получить урон (Это потребует поведения, чтобы справиться с жизнью и смертью, поскольку снижение жизни и получение урона обычно являются отдельными действиями, и потому что я хочу сохранить эти примеры как можно более простыми) и, возможно, противостоять урону.

    public class DamageReceiver : MonoBehaviour {
        public DamageType[] weakness;
        public DamageType[] halfResistance;
        public DamageType[] fullResistance;
        public Faction facton; //let's use it as well..
    
        private List<DamageDealer> dealers = new List<DamageDealer>(); 
    
        public void OnCollisionEnter(Collision col) {
            DamageDealer dealer = col.gameObject.GetComponent<DamageDealer>();
            if (dealer != null && !this.dealers.Contains(dealer)) {
                this.dealers.Add(dealer);
            }
        }
    
        public void OnCollisionLeave(Collision col) {
            DamageDealer dealer = col.gameObject.GetComponent<DamageDealer>();
            if (dealer != null && this.dealers.Contains(dealer)) {
                this.dealers.Remove(dealer);
            }
        }
    
        public void Update() {
            // actually, this should not run on EVERY iteration, but this is just an example
            foreach (DamageDealer element in this.dealers)
            {
                if (this.faction != element.faction) {
                    if (this.weakness.Contains(element)) {
                        this.SubtractLives(2 * element.damage);
                    } else if (this.halfResistance.Contains(element)) {
                        this.SubtractLives(0.5 * element.damage);
                    } else if (!this.fullResistance.Contains(element)) {
                        this.SubtractLives(element.damage);
                    }
                }
            }
        }
    
        private void SubtractLives(uint lives) {
            // implement it later
        }
    }
    

    Этот код не проверен и должен работать как концепция . Какова цель? Ущерб - это то, что кто-то чувствует , и связан с повреждением объекта (или живой формы), как вы считаете. Это пример: в обычных играх RPG меч повреждает вас , в то время как в других RPG, реализующих его (например, Summon Night Swordcraft Story I и II ), меч также может получать урон, ударяя по твердой части или блокируя броню, и может нуждаться в ремонт . Таким образом, поскольку логика повреждения относится к получателю, а не к отправителю, мы помещаем только соответствующие данные в отправителя, а логику в получателе.

  2. То же самое относится и к владельцу инвентаря (другое требуемое поведение будет связано с тем, что объект находится на земле, и возможностью удалить его оттуда). Возможно, это подходящее поведение:

    public class Inventory : MonoBehaviour {
        private Dictionary<InventoryObject, uint> = new Dictionary<InventoryObject, uint>();
        private List<Treasure> pickables = new List<Treasure>();
    
        public void OnCollisionEnter(Collision col) {
            Treasure treasure = col.gameObject.GetComponent<Treasure>();
            if (treasure != null && !this.pickables.Contains(treasure)) {
                this.pickables.Add(treasure);
            }
        }
    
        public void OnCollisionLeave(Collision col) {
            DamageDealer treasure = col.gameObject.GetComponent<DamageDealer>();
            if (treasure != null && this.pickables.Contains(treasure)) {
                this.pickables.Remove(treasure);
            }
        }
    
        public void Update() {
            // when certain key is pressed, pick all the objects and add them to the inventory if you have a free slot for them. also remove them from the ground.
        }
    }
    
Луис Масуэлли
источник
7

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

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

Итак, у вас есть шипы, которые наносят урон игроку. Спроси себя:

  • Есть ли другие вещи, кроме игрока, которые шипы могут захотеть повредить?
  • Есть ли другие вещи, кроме шипов, от которых игрок должен получить урон при столкновении?
  • Будут ли у каждого уровня с игроком шипы? Будет ли на каждом уровне с шипами игрок?
  • Могут ли у вас быть вариации на шипы, которые должны делать разные вещи? (например, разные суммы ущерба / типы ущерба?)

Очевидно, что мы не хотим увлекаться этим и создавать сверхпроектированную систему, которая может обрабатывать миллион случаев, а не только один случай, который нам нужен. Но если это одинаковая работа в любом случае (т.е. поместите эти 4 строки в класс A против класса B), но одну лучше масштабировать, это хорошее правило для принятия решения.

Для этого примера, в частности, в Unity, я бы склонялся к тому, чтобы поместить логику нанесения ущерба в шипы , в форме чего-то вроде CollisionHazardкомпонента, который при столкновении вызывает метод получения ущерба в Damageableкомпоненте. Вот почему:

  • Вам не нужно выделять тег для каждого участника столкновения, которого вы хотите различить.
  • Когда вы добавляете больше разновидностей взаимодействующих взаимодействий (например, лавы, пуль, пружин, бонусов ...), ваш класс игрока (или компонент Damageable) не накапливает гигантский набор вариантов if / else / switch для обработки каждого из них.
  • Если половина ваших уровней заканчивается отсутствием пиков, вы не теряете время в обработчике столкновений игроков, проверяя, был ли спайк задействован. Они стоят вам только тогда, когда они вовлечены в столкновение.
  • Если позже вы решите, что шипы должны убивать врагов и подпорки, вам не нужно дублировать код из класса, специфичного для игрока, просто прикрепите к ним Damageableкомпонент (если вам нужно, чтобы некоторые из них были невосприимчивы только к шипам, вы можете добавить типы повреждений и пусть Damageableкомпонент справится с иммунитетами)
  • Если вы работаете в команде, человек, которому нужно изменить поведение шипа и проверить класс / активы опасности, не блокирует всех, кому нужно обновить взаимодействие с игроком. Для нового члена команды (особенно для дизайнеров уровней) также интуитивно понятно, какой сценарий им нужен для изменения поведения шипов - это тот, который прикреплен к шипам!
Д.М.Григорий
источник
Система Entity-Component существует, чтобы избежать подобных IF. Эти IF всегда неправильны (только те, если). Кроме того, если шип движется (или является снарядом), обработчик столкновения сработает, несмотря на то, что сталкивающийся объект является игроком или нет.
Луис Масуэлли
2

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

В своих играх я пытаюсь выбрать то, GameObjectчто «делает» что-то с другим объектом, как тот, который контролирует столкновение. IE Spike наносит урон игроку, поэтому он справляется со столкновением. Когда оба объекта «делают» что-то друг с другом, пусть каждый из них выполняет свое действие над другим объектом.

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

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

public abstract class DamageController
{
    public Faction faction; //GOOD or BAD
    public abstract void ApplyDamage(float damage);
}

Вы можете создать подкласс DamageControllerв своем игроке GameObject, чтобы обработать ущерб, а затем в Spikeкоде столкновения

OnCollisionEnter(Collision coll) {
    DamageController controller = coll.gameObject.GetComponent<DamageController>();
    if(controller){
        if(controller.faction == Faction.GOOD){
          controller.ApplyDamage(spikeDamage);
        }
    }
}
spectacularbob
источник
2

У меня была такая же проблема, когда я только начинал, так что вот так: в данном конкретном случае используйте врага. Вы обычно хотите, чтобы Столкновение было обработано Объектом, который выполняет действие (в данном случае - противником, но если у вашего игрока было оружие, тогда оружие должно справиться со столкновением), потому что, если вы хотите настроить атрибуты (например, урон противника) вы определенно хотите изменить его в сценарии врага, а не в сценарии игрока (особенно если у вас есть несколько игроков). Аналогично, если вы хотите изменить урон любого оружия, которое использует Игрок, вы определенно не хотите менять его у каждого врага в вашей игре.

Treeton
источник
2

Оба объекта будут обрабатывать столкновение.

Некоторые объекты будут немедленно деформированы, разбиты или разрушены в результате столкновения. (пули, стрелы, водяные шары)

Другие просто отскочили бы и откатились бы в сторону. (камни) Они все еще могут получить урон, если вещь, которую они ударили, была достаточно твердой Скалы не получают повреждений от ударов человека, но они могут быть от кувалды.

Некоторые мягкие объекты, такие как люди, будут получать урон.

Другие будут связаны друг с другом. (магниты)

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

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

Например, меч может иметь тег для Melee, который переводится в интерфейс с именем Melee, который может расширять интерфейс DamageGiving, который имеет свойства для типа урона и величину урона, определяемую либо с помощью фиксированного значения или диапазонов, и т. Д. И бомба может иметь тег Explosive, чей интерфейс Explosive также расширяет интерфейс DamageGiving, который имеет дополнительное свойство для радиуса и, возможно, скорость распространения повреждения.

Тогда ваш игровой движок будет кодировать интерфейсы, а не конкретные типы объектов.

Рик Райкер
источник