Як вирішити, який GameObject повинен керувати зіткненням?


28

У будь-якому зіткненні задіяно два GameObjects? Що я хочу знати, як я вирішую, який об’єкт повинен містити мій 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, все, що вам потрібно було б зробити, це додати цей самий сценарій до всіх інших об'єктів, які можуть вбити програвача та назвати сценарій чимось подібним KillPlayerOnContact.

Крім того, якщо у вас зіткнення між програвачем і ворогом, ви, ймовірно, захочете виконати дію і над обома . Тож у такому випадку, який об’єкт повинен керувати зіткненням? Або обидва повинні керуватися зіткненням та виконувати різні дії?

Я ніколи раніше не будував гру будь-якого розумного розміру, і мені цікаво, чи може код стати безладним і складним у підтримці, оскільки він зростає, якщо на початку ви помиляєтесь. А може всі способи дійсні, і це насправді не має значення?


Будь-яке розуміння дуже цінується! Спасибі за ваш час :)


Спочатку чому рядкові літерали для тегів? Перерахунок набагато кращий, тому що помилка друку перетвориться на помилку компіляції замість важкого відстежувати помилку (чому мене не вбив колосок?).
храповик урод

Тому що я слідкував за навчальними посібниками Unity, і це те, що вони зробили. Отже, чи просто у вас з'явиться публічний перелік під назвою Tag, який містить усі значення вашого тегу? Так було б Tag.SPIKEзамість цього?
Редтама

Щоб отримати найкраще з обох світів, подумайте про використання структури з перерахунком всередині, а також статичних методів ToString і FromString
zcabjro

Дуже пов’язані: об’єкти нульової поведінки в OOP - моя дизайнерська дилема та серія «Чарівники та воїни Еріка

Відповіді:


48

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

Якщо це не варіант, наприклад, коли ви не керуєте архітектурою реакцій зіткнення, речі стають трохи нечіткими. Як правило, відповідь "це залежить".

Оскільки обидва об'єкти отримають зворотний виклик зіткнення, я би вклав код в обидва, але тільки код, що відповідає ролі цього об'єкта в ігровому світі , при цьому намагаючись максимально зберегти розширюваність. Це означає, що якщо якась логіка має однаковий сенс в будь-якому об'єкті, віддайте перевагу розміщенню її в об'єкті, що, швидше за все, призведе до меншої кількості змін коду в довгостроковій перспективі, коли буде додана нова функціональність.

По-іншому, між будь-якими двома типами об'єктів, які можуть стикатися, один із цих типів об'єктів, як правило, має такий же вплив на більше типів об'єктів, ніж інший. Введення коду для цього ефекту у тип, який впливає на більше інших типів, означає менший рівень обслуговування в довгостроковій перспективі.

Наприклад, об’єкт гравця та об’єкт кулі. Можливо, "нанесення шкоди при зіткненні з кулями" має логічний сенс як частина гравця. "Нанесення шкоди речі, в яку вона потрапила" має логічний сенс як частина кулі. Однак куля, ймовірно, завдає шкоди більш різним типам предметів, ніж гравцеві (у більшості ігор). Гравець не зазнає пошкоджень від блоків місцевості або NPC, але куля все одно може завдати шкоди цим. Тож я вважаю за краще поставити навантаження на зіткнення для нанесення пошкодження кулі в такому випадку.


1
Я знайшов відповіді всіх справді корисними, але мушу прийняти ваші для її ясності. Ваш останній абзац справді підсумовує мене :) Надзвичайно корисно! Дуже дякую!
Редтама

12

TL; DR Найкращим місцем для встановлення детектора зіткнення є місце, де ви вважаєте, що це активний елемент у зіткненні, а не пасивний елемент у зіткненні, тому що, можливо, вам знадобиться додаткова поведінка для виконання такої активної логіки (і, слідуючи правильним рекомендаціям OOP, як 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 рядки в клас А проти класу В), але один масштаб краще, це хороше правило для прийняття рішення.

Для цього прикладу в Unity спеціально я схиляюся до того, щоб розмістити логіку нанесення шкоди в шипах у вигляді чогось на зразок CollisionHazardкомпонента, який при зіткненні називає метод ушкодження в Damageableкомпоненті. Ось чому:

  • Не потрібно відкладати тег для кожного іншого учасника зіткнення, якого ви хочете розрізнити.
  • Коли ви додаєте більше різновидів стисливих взаємодій (наприклад, лава, кулі, пружини, бонуси ...), ваш клас гравців (або пошкоджуваний компонент) не накопичує гігантський набір випадків if / else / перемикання для обробки кожного з них.
  • Якщо половина ваших рівнів не має шипів у них, ви не витрачаєте час на обробника зіткнення гравця, перевіряючи, чи не був задіяний шип. Вони коштують вам лише тоді, коли вони беруть участь у зіткненні.
  • Якщо пізніше ви вирішите, що шипи також повинні вбивати ворогів і реквізитів, вам не потрібно дублювати код із класу, визначеного для гравця, просто наклейте Damageableна них компонент (якщо вам потрібно, щоб хтось не був захищений від шипів, ви можете додати типи пошкоджень і нехай Damageableкомпонент обробляє імунітет)
  • Якщо ви працюєте над командою, людина, якій потрібно змінити поведінку шипа і перевірити клас / активність небезпеки, не блокує всіх, кому потрібно оновити взаємодію з гравцем. Також інтуїтивно зрозуміло новому члену команди (особливо дизайнерів рівня), який сценарій їм потрібно переглянути, щоб змінити поведінку шипа - це той, який прикріплений до шипів!

Система Entity-Component існує, щоб уникнути подібних ІФ. Ці ІФ завжди помиляються (лише ті, якщо). Крім того, якщо шип рухається (або є снарядом), обробник зіткнення буде спрацьовувати, незважаючи на те, що об'єкт, що стикається, чи не грає.
Луїс Масуеллі

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);
        }
    }
}

2

У мене була така ж проблема, коли я починав свою роботу, так ось що: У цьому конкретному випадку використовуйте Ворога. Зазвичай ви хочете, щоб зіткнення оброблялося об'єктом, який виконує дію (в даному випадку - ворог, але якщо у гравця була зброя, то зброя повинна керувати зіткненням), тому що якщо ви хочете налаштувати атрибути (наприклад, пошкодження ворога) Ви обов'язково хочете змінити його в eneyscript, а не в програму програвання (особливо якщо у вас є кілька гравців). Так само якщо ви хочете змінити пошкодження будь-якої зброї, яку програвач використовує, ви точно не хочете змінювати його у кожного ворога вашої гри.


2

Обидва об'єкти впоралися б зіткненням.

Деякі об'єкти будуть негайно деформовані, розбиті або знищені в результаті зіткнення. (кулі, стріли, водяні кулі)

Інші просто відскакують і скочуються в бік. (скелі) Вони все ще можуть завдати шкоди, якщо річ, в яку вони потрапили, буде досить важкою. Скелі не завдають шкоди від удару людини, але вони можуть бути від кувалди.

Деякі колючі предмети, як люди, завдадуть шкоди.

Інші були б пов'язані один з одним. (магніти)

Обидва зіткнення розміщуватимуться на черзі подій для актора, який діє на об'єкт у певний час та місце (та швидкість). Просто пам’ятайте, що обробник повинен мати справу лише з тим, що відбувається з об’єктом. Навіть можливо, обробник розмістить іншого чергового на черзі, щоб обробити додаткові ефекти зіткнення, виходячи з властивостей актора чи об'єкта, на який діяли. (Бомба підірвалася, і вибух, що виникла, тепер має перевірити на зіткнення з іншими об'єктами.)

Під час створення сценаріїв використовуйте теги із властивостями, які були б переведені у реалізацію інтерфейсу за допомогою вашого коду. Потім посилайтеся на інтерфейси у вашому коді обробки.

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

Потім ваш ігровий движок буде кодувати проти інтерфейсів, а не конкретних типів об'єктів.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.