Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей

в 10:49, , рубрики: game development, Gamedev, unity, unity3d, ооп, полиморфизм, метки: , , , , , ,

Unity3D. ИСПОЛЬЗУЕМ ПРЕИМУЩЕСТВА ООП. ПОЛИМОРФИЗМ

Доброго времени суток. В этом уроке мы рассмотрим несколько важных приемов программирования на движке Unity 3D. Думаю, статья поможет как начинающим, так и опытным игроделам.

Целью статьи ставлю осознание мощи и безграничной удобности использования всех прелестей ООП. В частности ПОЛИМОРФИЗМ. Кроме того затронем несколько других важных вопросов.

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

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

Статья рассчитана на людей имеющих представление Unity3D. Если вы этого представления не имеете, настоятельно рекомендую прочитать эти статьи:

1) Очень хорошие статьи: habrahabr.ru/post/112287/, habrahabr.ru/post/113859/
Во второй статье примеры скриптов на JS. Думаю что проблем перевести на C# не будет, даже если вы новичок и в JS и в С#. Единственное скажу что строка на JS:

bull_clone = Instantiate(bullet, transform.position, transform.rotation);

На С# будет выглядеть так:

bull_clone = (GameObject)Instantiate(bullet, transform.position, transform.rotation);

А такая строка на JS:

var ml = GameObject.Find("Player").GetComponent(MouseLook);

На С# будет выглядеть так:

GameObject ml = GameObject.Find("Player").GetComponent<MouseLook>();

2)Статья: habrahabr.ru/post/141362/ Там же на сайте есть продолжение этой статьи. Называются они «Unity3d. Уроки от Unity 3D Student (B00-B16)» Всего 4 статьи. На данный момент, по крайней мере. Там справа вверху есть поисковик на сайте. Думаю, справитесь.

3) habrahabr.ru/post/148410/ Тоже необходимо прочесть и осознать. Есть еще вторая часть этой статьи. Опять вам поможет поисковик на сайте.

4) Это что-то типа справочника habrahabr.ru/post/128711/, habrahabr.ru/post/128948/.

5) Много на этом форуме интересных статей. Поищите сами, если хотите.
Итак. Вы усвоили все что прочитали, поэкспериментировали, возможно, уже сделали маленькую игру. Возможно не маленькую. Плохо если вы только прочитали эти статьи, но ничего сами не написали. Практика и еще раз практика

Но сначала рассмотрим парочку второстепенных вопросов. (На всякий случай)

Первая половина статьи носит демонстрационный характер. Переписывать код не надо.
Я употребляю следующие слова как синонимы: скрипт-класс

КЛАССЫ, ЭКЗЕМПЛЯРЫ КЛАССОВ

Мы занимаемся объектно-ориентированным программированием (ООП). Существуют Классы и экземпляры классов. Класс – это как бы формочка или чертёж объекта. А отлитая фигурка по формочке – это экземпляр класса. Все автобусы являются экземплярами класса Автобус. Автобус Васи, вашего соседа, это экземпляр класса Автобус. У Васиного автобуса есть наклейка на левом борту, царапины по всему корпусу, спущено правое переднее колесо, пол бака бензина, разбито одно стекло, не работает одна из дверей. Этот автобус обладает многими индивидуальными свойствами. У другого вашего соседа тоже есть автобус. У него тоже есть царапины, наклейки, но нет разбитых окон, колеса не спущены и еще много индивидуальных особенностей именно этого автобуса. Оба автобуса экземпляры класса Автобус. Класс автобус – это чертеж. Чертеж — это еще не созданный автобус и на нем нельзя покататься. У него нет никаких царапин и спущенных колёс. Без чертежа нельзя создать ни одного автобуса. Без описания класса мы не сможем создать ни один экземпляр класса. Класс служит не только для того чтобы создавать экземпляры, по нему можно например узнать длину автобуса. Но вот покататься не чертеже нельзя. В Unity имена классов пишутся с большой буквы (GameObject, Transform, Animation и т.п.), а имена экземпляров классов с маленькой.
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Имеет смысл все имена переменных начинать с маленькой буквы, чтобы отличать их от имен классов. Так диктует нам Unity.

КЭШИРОВАНИЕ

Из википедии: Кэширование результатов работы

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

Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Рассмотрим этот скрипт. Что в нем плохого? Вроде все хорошо. Некий объект движется в Лево с постоянной скоростью 10 единиц в секунду. Именно благодаря Time.deltaTime происходит равномерное движение, не зависящее от текущей производительности на конкретном компьютере.

Плохо в этом скрипте то, что каждый кадр вызывается transform.position, а точнее плохо, что вызывается transform. На самом деле transform написанный у нас в Update() означает gameObject.transform. А это в свою очередь то же самое что gameObject.GetComponent ( ). А вот это уже означает что каждый кадр ваш скрипт ищет среди всего что повешено на gameObject именно transform. А зачем каждый раз искать, когда можно найти один раз и запомнить(кэшировать) (обратите внимание gameObject написано с маленькой буквы. То есть это экземпляр класса GameObject)
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Казалось бы, ничего не изменилось. Но на самом деле это гораздо рациональнее использует ресурсы машины.
Не верите мне. Откройте документацию Unity и там сказано кэшировать НАДО!!!

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

Разумеется, кэшировать есть смысл, только если то, что мы собираемся кэшировать будет много раз использоваться. (Часто это более одного раза). Кроме того стоит понимать если мы удалим у нашего gameObject компонент который предварительно кэшировали в переменную “A”, а потом снова добавили, в «А» уже ничего не будет. Поэтому нет смысла кэшировать то, что постоянно удаляется и снова добавляется. Хотя можно кэшировать сразу после очередного добавления. Кэшировать можно только Экземпляры класса (может ещё что-нибудь врать не буду), но вот кэшировать типы данных и структуры не надо.
Например, это можно кэшировать
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей

Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Там справа написано, что это класс(class), да еще и значок слева тоже особенный. (Красным обведено)
А вот это кэшировать нельзя
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Так как position это не экземпляр класса. Это структура Vector3
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
И опять Visual Studio нам показывает, что Vector3 это структура. И значок у неё тоже другой.
Итак. Кэшировать только классы. Переменные всякие типа float, byte, vector3 и т.п. не кэшировать! Ну сами понимаете что:

Public Vector3  aaa;
 aaa = transform.position;  //просто сохранит текущее значение   transform.position. А когда оно измениться в aaa останется старое значение transform.position.  А кэширование класса (экземпляра класса) обеспечивает постоянный доступ к нему, свеженькому. Мы как бы сохраняем ссылку на нужный нам объект.

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

ОБЩАЯ КОНЦЕПЦИЯ МОБА (ПЕРСОНАЖА)

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

  • Поведение в режиме ожидания(Игрока нету рядом и моб ничего не делает(или делает. Это зависит уже от конкретной идеи игры)). Еще можно назвать этот режим «Поиск цели»
  • Поведение в режиме «цель вижу, могу убить, сближаюсь»(Бежим на новую цель)
  • Поведение в режиме «цель рядом надо атаковать»(Атакуем)

Конечно, можно усложнить схему. Например, добавив Поведение в режиме «цель вижу, убить не могу, убегаю» (Убегаем от цели)
Реализация каждого Поведения в своем скрипте. Главный скрипт понятия не имеет о том, как реализовано конкретное поведение. Его задача включить его в нужное время. Например, скрипт Поведение в режиме «цель вижу, могу убить, сближаюсь» можно включать, когда игрок приблизился к мобу на определённое расстояние или первым атаковал моба. Если нам нужно два почти одинаковых моба, то мы не переделываем скрипт целиком, а меняем один из скриптов Поведения. Это можно делать не только на стадии разработки, но и во время выполнения. (для этого нужен полиморфизм)

Все эти скрипты могут управлять передвижением персонажа (моба). Управляют не сами, а через промежуточный скрипт называющийся МОТОРОМ. Тесть у Мотора есть несколько методов необходимых для управления персонажем. Например:
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Зачем это надо? Есть две основные причины. Во-первых это просто логично и в случае если вам захочется что-то поменять в Моторе вам не придётся менять код в разных скриптах которые двигают персонажа. Они все делают это через один скрипт. Измените код в Моторе и все. Кроме того запуск анимации при движении тоже осуществляется из Мотора. Особенно это важно, если проект разрабатывается командой. Человек, который делает мотор, понятия не имеет, откуда будут вызываться методы мотора. А человек делающий скрипт Поведения понятия не имеет, как реализован метод MobMotor.MoveLeft() главное, что он двигает персонажа влево. Это все что ему нужно. Таким образом, для слаженной работы им надо знать только два простых факта. Моб умеет ходить влево и вправо. Все! Ничего лишнего. Во вторых…… самое главное во вторых. Но мы еще не знаем что такое полиморфизм. Вернемся к этому преимуществу позже.

Продолжаем. У нас есть главный скрипт, который следит за обстановкой и включает нужные скрипты когда надо. А те перемещают персонаж только через Мотор. Есть скрипт, который отвечает за Здоровье персонажа. Например:
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей

Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Кстати напомню Public — означает, что объявленная переменная(метод и т.п.) будет доступна всем скриптам вообще. Private-доступно только внутри самого скрипта

Рассмотрим скрипт MobHp подробно. Персонаж появляется и сразу имеет здоровье 100 единиц. Когда персонаж получает дамаг(получение дамага отслеживает главный скрипт а не сам MobHp), главный скрипт вызывает метод MobHp.SetDamage(float damage). Этот метод возвращает либо true либо false. Если вернулось true, значит моб пережил получение дамага. И главный скрипт продолжает свое функционирование. Если вернулось false, моб умер. Что происходит с мобом после смерти главному скрипту все равно. Он этого не знает. В нашем случае при смерти воспроизведется звуковой файл (звук умирания персонажа) и через 5 секунд персонаж исчезнет(см. картинку). Роль главного скрипта проста. Он в случае получения false отключает все скрипты Поведения. Отключает Мотор персонажа. Он отключит все. То есть моб замрет, проиграется звук умирания моба, а затем он исчезнет. Конечно, можно в скрипт MobHp добавить генерацию осколков, воспроизведение анимации падения на землю персонажа. Все что захотите. Прелесть в том, что скрипт MobHp полностью независим. Если основной скрипт и MobHp будут программировать разные люди, то все что им понадобится знать для слаженной работы, это то, что персонаж получает дамаг через метод MobHp.SetDamage(float dfamage). И если Метод вернет false, значит, моб умер. Все! Никакой лишней информации. И если создатель MobHp захочет изменить содержимое скрипта, это ни как не касается основного скрипта!

Его величество ПОЛИМОРФИЗМ

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

Представьте себе выключатель. Обычный выключатель. Который включает (выключает) свет у вас в комнате. А может он включает вентилятор? А может он запускает токарный станок. А может ли обычный выключатель запустить ядерную бомбу. Пожалуй, очевидно, что выключателю все равно, что включать (выключать). Он просто размыкает контакт. Он пускает энергию куда-либо. Ему все равно куда.

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

Вернемся к нашим скриптам. У нас есть метод MobHp.SetDamage(float damage). Гипотетический танковый снаряд, упавший радом с персонажем, должен вызвать этот метод, чтобы нанести урон персонажу. Однако по нашей концепции метод MobHp.SetDamage(float damage) должен вызываться из Главного скрипта. Таким образом, Снаряд из танка должен взаимодействовать именно с главным скриптом. Допустим, мы создадим глобальный метод у Главного скрипта. И Снаряд при падение будет проверять не находится ли персонаж(моб) в радиусе взрыва и вызывать этот метод если надо. Пусть у нас много абсолютно одинаковых мобов (персонажей) на карте. Снаряд при падение должен перебрать их всех, выбрать из них те что в радиусе поражения и нанести им дамаг(вызвать соответствующий метод у главного скрипта каждого из мобов). В Unity это можно реализовать так: Всем мобам присваиваем тег Mob
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Получить массив всех объектов с определенным тегом можно так

GameObject[] allMobs=  GameObject.FindGameObjectsWithTag ("Mob" ); //получили массив всех мобов
//пока не зависимо от расстояния

Для определенности пусть наш Главный скрипт называется MyBehaviour

И теперь перебираем всех мобов и наносим им дамаг(Обратите внимание. Мы представили что у главного скрипта(MyBehaviour) есть метод MyBehaviour.SetDamage(float damage), который в свою очередь вызывает метод MobHp.SetDamage(float damage) у скрипта MobHp. Оба скрипта висят на персонаже(мобе))
Код в скрипте на снаряде.

foreach ( GameObject mob in allMob ) //перебираем по одному из массива мобов
        {
            mob.GetComponent<MyBehaviour>().SetDamage(50); //у каждого моба вызываем   метод SetDamage
        }

В данном примере Снаряд наносит 50 единиц дамага
Все прекрасно! Это будет работать. Но если мы хотим много мобов ДВУХ типов? У второго типа Основной скрипт может быть реализован по-другому. Например, робот и пушка. У них совсем разные модели поведения. У них по разному реализовано Поведение в режиме «цель вижу, могу убить, сближаюсь»(Бежим на новую цель). У пушки его вообще нету. Таким образом у башни главный скрипт будет называться MyBehaviour2 (разные скрипты обязаны по-разному называться), и по-другому называются все скрипты Поведения. И даже MobHp у пушки другой. MobHp2 например в нем по своему реализовано «умирание пушки»(дело может не ограничится только заменой звука и анимации умирания). Как теперь должен Вести себя танковый снаряд, чтобы нанести урон всем мобам и башням?

Надо вводить новый тег для башни «tower». А скрипт самого снаряда примет вид:

//РАБОТАЕМ С МОБАМИ
GameObject[] allMobs=  GameObject.FindGameObjectsWithTag ("Mob" ); //получили массив всех мобов
//пока не зависимо от расстояния
foreach ( GameObject mob in allMob )//перебираем по одному всех роботов
        {
            mob.GetComponent<MyBehaviour>().SetDamage(50); //у каждого вызываем нужный метод
        }

//РАБОТАЕМ С БАШНЯМИ
GameObject[] allTower=  GameObject.FindGameObjectsWithTag ("Tower" );
foreach ( GameObject tower in allTower )//перебираем по одному все башни
        {
            tower.GetComponent<MyBehaviour2>().SetDamage(50); //у каждого вызываем нужный метод
        }

Какие минусы у такого подхода. Очевидно, что скрипт Снаряда зависит от количества видов мобов. Таким образом, если вы захотите добавить один вид моба, вам придётся залезть в скрипт Снаряда и модернизировать его. Теперь представим что в игре 500 видов мобов. Скрипт снаряда будет иметь порядка 1500 строк кода. Стоит учесть, что в игре наверняка будет несколько десятков видов снарядов. Считайте сами. Если скрипт Снаряда будет делать один человек, а основные скрипты мобов и пушек и других персонажей (MyBehaviour, MyBehaviour2, MyBehaviour3 и т.д.) другой человек, то для слаженной работы им потребуется знать количество мобов и точные названия(а может и содержание) их основных скриптов (представьте что названия будут не вида MyBehaviourN, а RobotBehaviour, TowerBehaviour, MonsterBehaviour, TigerBehaviour и т.д.)

Вернемся к выключателю, которому все равно, что он включает. Если у вас в игре 5 видов дверей, то для каждого нужен будет свой выключатель (кнопка). И если вы захотите добавить один вид двери, то вам придётся добавлять кнопку, либо модернизировать имеющиеся. И если кнопки и двери будут программировать разные люди, то им придется знать все названия (а может и содержание) всех скриптов всех дверей и кнопок.

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

Конечно, есть способы решения этой проблемы. Но самый изящный и простой в понимании, на мой взгляд, это ПОЛИМОРФИЗМ. Не буду писать строгих определений, вы их сами можете найти.

Наследование, инкапсуляция и полиморфизм — три слона ООП.
Далее идут не определения! Скорее приблизительные пояснения
Инкапсуляция — это способность класса скрывать свои кишки от внешнего кода. Мы не можем обратиться к переменным (методам, процедурам и др.), объявленным c ключевым словом Private,Protected из внешнего, по отношению к классу кода, и это хорошо.
Наследование — способность класса наследовать (копировать) методы, переменные и др. у своего родителя.
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Видите! MobHp — наследник MonoBehaviour. MonoBehaviour — родитель MobHp. Через двоеточие пишется родитель.
А теперь фокус. Создадим новый пустой скрипт, назовем его NewMobHp и вместо MonoBehaviour(в самом скрипте) напишем MobHp(То есть мы указали, что родитель у этого нового NewMobHp будет не MonoBehaviour, а MobHp)(Смотри картинку ниже).
Объявим в скрипте NewMobHp переменную типа NewMobHp с именем newMobHp(С маленькой буквы). Не пугайтесь что в скрипте NewMobHp («скрипт» и «класс» — это синонимы для нас) мы объявили переменную того же типа что и сам класс. Да класс может содержать переменные того же типа что и сам класс. Напишем «newMobHp» в Start() и нажмем точку. Если у вас нормальный редактор кода, то он покажет все доступные вам методы, свойства и др. в данном месте кода. Начнем писать SetDa… Магия
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
И магия!!! У нашего класса NewMobHp есть метод NewMobHp .SetDamage(float damage) прям как у класса MobHp. Хотя скрипт только создан и нет в нем объявления метода SetDamage(float damage), но зато этот метод объявлен у класса-родителя. Вот это и есть НАСЛЕДОВАНИЕ. Класс NewMobHp унаследовал метод SetDamage(float damage) так как в MobHp он был объявлен с ключевым словом Public.

Кстати в чем разница между Protected и Private? Все что Private не наследуется. Все что Protected наследуется. Итак если хотим унаследовать Глобально объявленный(Public) метод или переменную и др., то ничего делать не надо. Она и так наследуется. Если хотим унаследовать локальную для данного класса переменную, то вместо Private надо в родительском классе написать Protected.
Итак с этого момента даю команду, КОДИТЬ! Все что дальше будет написано мною, должно быть написано вами. Экспериментируйте, разбирайтесь, набивайте ручки.

NewMobHp и другие скрипты нам мне пригодятся удалим их(если сделали). Кстати класс NewMobHp является наследником не только MobHp, но и MonoBehaviour. Так как MobHp является наследником класса MonoBehaviour.

Вот так: MonoBehaviour--> MobHp--> NewMobHp.

MobHp непосредственный(ближний) родитель для NewMobHp.
Открывайте Unity, мы начинаем кодить! Возможно, кое что будет не понятно, но когда доберемся до создания маленькой игры все станет ясно.

Создадим папочку MyScript. В ней создадим скрипт MyBehaviour. Это будет родитель для всех главных скриптов всех мобов(вообще всего что умеет получать дамаг). Мы можем создать в нем метод SetDamage. И в дальнейшем, новые главные скрипты будут наследовать его. И у всех мобов будет этот метод. Но у каждого вида моба процесс получения дамага может быть реализован по-разному. Как сделать, чтобы метод реализовывался по своему, для каждого скрипта?

ПОЛИМОРФИЗМ — не знаю, как дать ему определение. Может вы сами потом его дадите. Мы лучше рассмотрим все на примере. Напишем в MyBehaviour такое:
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Пустой метод SetDamage. Зачем он нужен? Да он нам не нужен. Обратите внимание на ключевое слово Virtual (смотри рисунок). Это означает, что данный метод может быть переопределен в других скриптах, которые будут наследниками скрипта MyBehaviour. То есть они его наследуют, но реализуют по-своему. Теперь сделаем скрипт MobBehaviour:
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
И так разберем, что тут происходит:
1- Мы явно указали, что MobBehaviour наследник класса MyBehaviour
2- Пишу слово Override, ставлю пробел и Visual Studio сама подсказывает, что мы можем ПЕРЕОПРЕДЕЛИТЬ. Как видите там есть SetDamage(float damage), он то нам и нужен. Все остальные методы, это методы объявленные в Родительских скриптах более дальних чем MyBehaviour. Например ToString(). Мы можем переопределить ToString()и запрограммировать по своему работу этого метода для нашего класса MobBehaviour. Но нам это не надо. Выберем SetDamage(float damage)и видим (на рисунке цифра 3) что данный метод изначально определен в MyBehaviour(Тоесть он унаследован от класса MyBehaviou). Можете посмотреть где определены ToString() и другие. Теперь выберем строку SetDamage(float damage), нажмем Enter и вот что Visual Studio нарисовала сама:
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Base.SetDamage(damage); — здесь Base- это как рас родитель(класс) в котором был определен метод SetDamage(float damage). Или если сказать по-другому, ссылка на класс, от которого был унаследован этот метод. У нас он определен в MyBehaviour. Он пуст, если вы не забыли. То есть можно что — нибидь туда добавить. И тогда Base.SetDamage(damage); будет вызывать этот код, а после можно что-нибудь добавить для конкретного наследника. Это нужно если у вех наследников MyBehaviour в методе SetDamage(float damage) будет что-то общее. Но нам это не нужно. Пока удалите все лишнее из MobBehaviour. Оставьте так
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Отвлечемся от MobBehaviour. Создадим, новый скрипт MyHp
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Это скрипт, от которого будут наследоваться все Скрипты, отвечающие за здоровье персонажа
Создадим еще скрипт. Назовем его MobHp. Мы рассматривали его выше
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Все, как и в прошлый раз, только есть слово Override в пере объявлении метода SetDamage(float damage). И указано что класс является наследником класса MyHp. Еще немного изменено умирание моба. Нет никаких звуков, моб просто увеличится при смерти в 3 раза.

Снова вспоминаем о MobBehaviour. Сделаем его таким:
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Мы пере объявили метод SetDamfge(float damage). Обратите внимание на то, что thisHp объявлена как MyHp, хотя Скрипт, отвечающий за здоровье, называется MobHp. Вот он ПОЛИМОРФИЗМ. Мы можем работать с MobHp так же как с MyHp. Мы можем переменной типа MyHp присвоить экземпляр класса MobHp, так как он является наследником MyHp. Таким образом, скрипту MobBehaviour все равно какой наследник класса MyHp лежит в переменной thisHp. Главное что у thisHp есть метод SetDamfge(float damage). И его можно использовать! Мы можем поменять скрипт MobHp на другой (тоже наследник MyHp). И все будет работать.

Теперь сделаем нашего моба в виде шара (увеличим его размер в два раза от начального). Сделаем землю (я в виде плоского прямоугольника делаю), и свет какой-нибудь. У меня так получилось:
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Этот шар будет нашим первым мобом. Повесим на него скрипты MobBehaviour и MobHp.
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Обратите внимание на This Hp в скрипте MobBehaviour(Смотри рисунок). Сейчас там ничего не присвоено(None). Однако на старте (в процедуре Start()в MobBehaviour) будет найден MyHp(или его наследник)
thisHp = GetComponent();
Итак, у нас есть моб которого можно убить. Правда называется он «Sphere», но это не важно. Надо бы создать игрока, который будет стрелять шариками по мобам. Займемся этим.
Удалим имеющуюся камеру. Создадим First Person Controller(он там, в папочках есть, вспомните уроки, на которые есть ссылки в начале статьи). Создадим шарик (маленький), прицепим к нему Rigibody. Назовем его Bullet и перетащим в новую папку с названием MyPrefab
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Это наш будущий патрон. Кстати чтобы он не пролетал сквозь стены на больших скоростях, сделаем так
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Теперь физический движок будет интерполировать движение патрона. Это наверняка довольно сложный алгоритм, поэтому не стоит всему подряд ставить Interpolate. Только быстро движущимся объектам (Очень быстрым. Тем, которые за один кадр могут пролететь от полуметра и более). Почитайте на википедии что такое интерполяция и экстраполяция.
Создадим новый скрипт BulletScript. Это скрипт патрона.
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
При ударе обо что-нибудь данный скрипт получает у этого чего-нибудь компонент MyBehaviour(или любой его наследник). И вызывает (если у этого чего-то есть MyBehaviour ) метод SetDamfge(float damage) с damage=30. Значит, один патрон нанесет 30 единиц урона. Благодаря ПОЛИМОРФИЗМУ патрону всё равно у чего именно, и какой именно наследник MyBehaviour он хочет атаковать. Даже если после написания этого скрипта мы добавим в игру новый тип моба, со своим наследником MyBehaviour(например, TowerBehaviour), патрон все равно при попадании в этого нового моба будет наносить дамаг. Вспомните пример с Танковым снарядом. Не забудьте перетянуть этот скрипт на префаб патрона.
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Осталось только заставить игрока стрелять этими патронами. Создадим новый скрипт PlayerShoot
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Подобный скрипт был создан и подробно описан в одной из статей, ссылка на которые есть в начале статьи. Только там он был на JS. Остановимся лишь на основных моментах:
1-префаб патрона, которым будем стрелять,
2-начальная скорость патрона,
3-пауза между выстрелами,
4-трансформ камеры, ведь именно она крутится, когда мы передвигаем мышку,
5-время последнего выстрела,
6-кэшируем трансформ камеры. Обратите внимание, как мы это сделали. Этот скрипт будет висеть на нашем FirstPersonController. Поэтому под transform(если написать это слово в скрипте) подразумевается трансформ FirstPersonController`а, а не камеры. Transform камеры является под трансформом трансформа FirstPersonController`а
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Обратите внимание на масштаб. First Person Controller у меня имеет масштаб 1,1,1
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Если у вас там будет 100,100,100, то вы можете не заметить ни моба, ни его будущего движения.

Мы будем искать среди под трансформов (можно сказать дочерних трансформов. Но не в смысле наследников как с клссами) трансформ камеры с помощью метода transform.FindChild(“Main Camera”). Тесть этот метод найдет среди подобъектов FirstPersonController`а объект с именем “Main Camera” и достанет из него Transform. И мы этот Transform кэшируем в переменную cameraTransform.
7-если с последнего выстрела прошло больше чем shootPause, то можно делать выстрел.
8-Создаем копию патрона.
9-Получаем rigiBody патрона.
10-Добавляем скорость патрону.
11-Запоминаем время выстрела.
Кидаем этот скрипт на игрока. Перетягиваем префаб патрона куда надо
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Запускаем игру. Находим моба. Стреляем в него, используя левую кнопку мыши. Считаем выстрелы. Он должен разбухнуть с 4 попадания. В консоли появилось «Моб умер!!!». Моб разбухает и через 5 секунд исчезает.
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Моб разбух так как в скрипте MobHp мы написали thisTransform.localScale = new Vector3 ( 3, 3, 3 ). Если у вас моб не достаточно разбух, измените троечки на большие значения. Ну, вот мы сделали то, что вы и так, наверное, могли сделать. В чем же прелесть полиморфизма? А вот в чем. Создадим еще одного моба. Тоже шарик. Повесим на него новый скрипт MonsterHp:
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Как видите, он почти не отличается от скрипта MobHp. Все отличия подчеркнуты красным цветом. Мы кэшировали материал, и меняем его цвет в зависимости от здоровья. Если вы не знаете, как создать цвет, который хотите, посмотрите в интернетах по поводу RGB цветовой модели.

Сделаем у нашего главного героя тег «Player»
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Теперь создадим скрипт MonsterBehaviour:
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей

Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Это аналог скрипта MobBehaviour. Но моб обладающий этим скриптом может ещё и бегать за игроком, если тот достаточно близко. Вообще методы движения моба должны быть в Моторе (мы уже обсуждали почему) но чтобы не захламлять пример и не взрывать ваш мозг, мы сделаем это в самом MonsterBehaviour. Обратите внимание на то, что метод SetDamage(float damage) реализован не так как в MobHp. В нем добавилось IsLive=false; и изменился текст в Debud.Log();. Разумеется, можно дописать еще какой-нибудь код. Хочу обратить ваше внимание на то что метод FindGameObjectWithTag ( «Player» ) возвращает только один первый попавшийся gameObject с тегом «Player». Но у нас и так он один. В случае если нам надо будет получить все объекты, нужен будет метод FindGameObjectsWithTag.

FindGameObjectWithTag("Tag")   //Вернет один gameObject
FindGameObjectsWithTag("Tag")// Вернет массив gameObject[]     Всего одной буковкой отличаются. А какая большая разница. 

Обратите внимание, мы кэшировали transform игрока, а не весь gameObject. Мы будем обращаться именно к трансформу, а через gameObject это бы выглядело так: player.transform что эквивалентно player.getComponent(). А зачем это каждый раз делать, когда можно один раз и сразу кэшировать. Конечно, от пары десятков лишних getComponent ничего особо не поменяется. Но если проект очень большой. И каждый программист будет тыкать, где попало getComponent и GameObject.Find(…) и GameObject.FindGameObjectWithTag (…)и т.п. Это в совокупности может замедлить выполнение программы. Понизить FPS. А вам оно надо?

Повесим скрипты MonsterBehaviour и MonsterHp на нашего нового моба (Новый шарик). У меня получилась такая сцена:
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Маленький шарик — это патрон(Bullet). Его можно удалить, все равно он уже в префабах (папка MyPrefab). Обратите внимание, у меня оба моба называются одинаково(Shape). Можно поменять. Назовем Нового моба Monster, а старого Tower.
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Запускаем игру. Подходим к новому мобу поближе. Он начинает двигаться в нашу сторону. Отбегаем подальше, он останавливается. Стреляем в моба, он меняет цвет. Стреляем еще, он краснеет и уже не бегает за нами. Через 5 секунд исчезает. Можно пострелять в старого моба и убедиться, что он все еще способен получать дамаг и умирать.
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Итак, в чем прелесть ПОЛИМОРФИЗМА. Мы добавили моба и написали ему скрипт. Но мы ничего не писали в скрипте игрока и в скрипте патрона.

Опять вспомним наш танковый снаряд. Как в нашем случае мог бы выглядеть его скрипт? Очень просто. Но сначала добавим нашим мобам тег “Mob”
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Обоим мобам! Теперь сделаем из нашего патрона Танковый разрывной снаряд. Мы не будем делать осколки, взрыв и т.п. это не имеет отношения к нашему уроку. Удалите со сцены висячий патрон (если еще не удалили). Он должен остаться только в папке MyPrefab. Создадим новый скрипт BigBulletScript
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей

Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Это аналог скрипта BulletScript. Это скрипт Танкового патрона. При соударении с чем-либо он при помощи GameObject.FindGameObjectsWithTag ( «Mob» ) получает массив всех игровых объектов с тегом «Mob». Проверяет, есть ли у этого объекта компонент MyBehaviour(или его наследник) и если есть, то проверяет расстояние до этого объекта. И если это расстояние меньше чем atacDistanse, то этому объекту, через интерфейс (метод SetDamage) MyBehaviour`а (или любого его наследника) наносится дамаг.

Теперь удалим с префаба патрона скрипт BulletScript, а взамен повесим скрипт BigBulletScript.
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Сделайте так чтобы ваши мобы на сцене стояли не далеко друг от друга. Запускаем игру. Стреляем так, чтобы патрон упал между мобами. Четыре выстрела и оба моба готовы. Мы заменили полностью скрипт на патроне, но мы никак не затрагивали скрипты мобов и игрока. Скрипт BigBullrtScript не зависит от моба в которого он попадает. Если мобов будет делать один человек, а патроны другой, то всё что им надо знать для слаженной работы — это то, что все мобы (вообще все что может получать дамаг) несут на себе какой-либо (мы не знаем какой, то есть любой) наследник класса MyBehaviour. И что у этого наследника есть метод SetDamage(float damage). Это очень удобно. Таким образом можно наклепать 100500 видов мобов, не думая об остальном коде проекта.

ПОДВЕДЕМ ИТОГИ

В данном простом проекте мы использовали Полиморфизм в двух местах.

1) У любого моба можно удалить MyHp и повесить новый (другой наследник MyHp). Особенно это удобно при коллективной разработке. Допустим, некий глава проекта подбирает оптимальный скрипт поведения (или скрипт, отвечающий за здоровье в нашем случае). Он меняет предложенные ему варианты. А они сразу без переделок работают с Главным скриптом. Глава проекта не знает, как работают эти скрипты, и он не сможет быстро найти в них объявление нужной переменной и изменить её тип. Но если мы используем полиморфизм, этого делать и не надо. Можно редактировать и добавлять новые наследники скрипта MyHp и они прекрасно вольются в проект. Самое главное эти скрипты можно менять не только на стадии разработки, но и во время выполнения.

2) Патрон может атаковать любого моба. Даже созданного после создания самого патрона. Можно создать новый патрон, и не менять скрипты мобов.

Вот ссылка на урок полиморфизма от создателей Unity unity3d.com/learn/tutorials/modules/intermediate/scripting/polymorphism. На мой взгляд, он отвратительный и не понятный. Я уже не говорю про то, что он на английском. Более подробно о полиморфизме можете почитать тут sharp-generation.narod.ru/C_Sharp/methods.html. Но для начала прочтите первые пару глав этого самоучителя (на который ссылка дана), а то не понятно будет.

Переосмыслим командный подход к созданию крупных проектов.

Проектирование можно начинать с разных концов. Но лучше начать сверху. То есть сначала определиться, что вообще будет в игре. Какие мобы, какое оружие будет доступно игроку и т.д. Разбиваем все, что навыдумывали на классы и подклассы. Например:
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
На рисунке под овалами подразумеваются не сами объекты, а скрипты на них повешенные. Например
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Игрок-это некий скрипт, который будет висеть на игроке. Итак, на картинке есть знакомый нам MyHp и несколько его наследников. Как видите, у разрушаемых предметов тоже есть собственный наследник MyHp, так как они реагируют на наносимый дамаг. В схеме есть класс «Динамические» (участвуют тем или иным способом). Если в сцене найти всех наследников этого класса (кроме привязанных к игроку) и отключить их (метод отключения будет публично объявлен в «Динамические» (участвуют тем или иным способом) ), то все в игре замрет, кроме самого игрока. Так можно просто реализовать эффект остановки времени. Хотя можно сделать это иначе и не создавать класс «Динамические» — начать с более низких классов. «Способен получать дамаг» — это скрипт (класс), который будет висеть на всем что умеет получать дамаг. Мы с вами его уже реализовали и назвали MyBehaviour, но у нас MyBehaviour является наследником MonoBehaviour, а не «Динамические» т.к. нам он («Динамические») не нужен. Наша схема выглядит так:
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
У нас все скрипты (классы) являются наследниками MonoBehaviour. Так диктует нам Unity.

Вернемся к большой схеме. «Действие» — это класс, наследники которого получают несколько методов, необходимых для реализации событий. Например, это могут быть методы StartAction(), Open(), Close(). Наследниками класса «Действие» являются все сюжетные скрипты, скрипты дверей, люков и т.п. Такой подход позволяет создать универсальный триггер (кнопку и т.п.), который может вызвать любой из этих методов (или сразу несколько методов) у массива скриптов «Действие». Это очень удобно. В Unity даже есть стандартный скрипт для такого триггера. Только он немного усовершенствован. Благодаря полиморфизму каждый наследник класса «Действие» по-своему реализует методы StartAction(), Open(), Close(). Разные двери по-разному открываются, разные сюжетные скрипты по-разному реализуют StartAction(). Пример с дверью: игрок заходит в универсальный триггер, к триггеру привязана дверь (точнее наследник класса «Действие» от этой двери).Триггер вызывает метод Open(). Настройка триггера заключается в том, чтобы поставить галочку, какой метод именно надо вызвать у указанного наследника класса «Действие». То есть не надо к каждому классу двери делать свой класс кнопки. Кнопка универсальная. А в ней объявлена публичная переменная:

Public ActionScript action; //Переменная для хранения наследника класса ActionScript
Public bool callStartAction=true; //Флаги означающие, что именно надо вызвать
Public bool callOpen =false;
Public bool callClose =false;

Далее в коде кнопки (триггера и т.п.) при определенных обстоятельствах (нажатие кнопки или вхождение в триггер нужного объекта) вызывается нужный метод объекта action

If(callStartAction) { action. StartAction(); }
If(callOpen) { action. Open(); }
If(callClose) { action. Close (); }

Снова смотрим на большую схему. Находим там Моторы игрока, зомби, и пушки. Мы уже обсуждали концепцию мотора. Но теперь есть Главный мотор, от которого все остальные наследуют методы. Будет очень хорошо, если у всех моторов будут одинаковые методы. Например, могут быть такие методы Jump(), MoveDirection(Vector3 direction), Run(bool run),Stop(), LookTo(Vector3 direction), GoToPoint(Vector3 point). Все методы кроме последнего нужны Игроку. А вот последний для мобов. Он может содержать в себе алгоритм поиска пути и т.п. Благодаря полиморфизму каждый наследник главного мотора сможет по-своему реализовать эти методы, а так же можно будет менять мотор с одного на другой. Они полностью взаимозаменяемы.

Теперь рассмотрим Скрипты поведения. «Зомби-не занят», «Зомби-атака» и др. Они так же являются наследниками одного скрипта. Можно выделить общий метод. Например, SetTarget(Transform target). Через этот метод можно передать скрипту поведения текущую цель моба. Ведь именно наследник класса MyBehaviour занимается анализом обстановки, следовательно, он должен при включении нужного скрипта поведения передать ему необходимую информацию. В реальной игре этих методов, возможно, будет больше.

3) После составления подобной схемы приступаем к её реализации. Сначала делаем скрипты «родители». Все остальные будут наследоваться от них. Например, скрипт «родитель» мотора можно сделать таким:
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Красным подчеркнуто ключевое слово, которое позволит наследникам этого класса пере определить эти методы по-своему. Пере объявление будет выглядеть так:
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Тогда в MonsterBehaviour заведём переменную типа MyMotor, в которой будет храниться какой-либо наследник скрипта MyMotor, повешенный на этого моба. Конечно, моб не прыгает в нашей игре. Это просто пример. Для мобов можно придумать еще один метод у класса MyMotor и назвать его SetTarget(Transform target). И когда надо двигаться за игроком (или другим мобом) использовать этот метод. А Мотор моба сам будет принимать решение о направлении движения прыжках и т.п.
На последнем примере хорошо видно: все, что нужно для реализации полиморфизма — это ключевые слова Virtual и Override, а так же явное указание родителя класса
Правильно программируем. Используем полиморфизм. Общая логика игровых персонажей
Вот и все. Несколько «слов» в коде сделают наши скрипты независимыми и взаимозаменяемыми. Вообще надо делать все скрипты не зависимыми друг от друга.

Продолжим использовать полиморфизм и другие прелести ООП в следующей статье.

Автор: Yabloko9393

Источник

Поделиться

* - обязательные к заполнению поля