Как связать скрипты в unity

Обновлено: 26.04.2024

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

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

Сразу определимся с терминологией: здесь и далее под скриптами я буду подразумевать не те скрипты, которые MonoBehaviour из Unity, а именно скриптинг - то есть скрипты которые описывают контент игры.

В целом скриптинг работает так. В основном движке разрабатывается абстрактный класс (назовем его ScriptingBase) который содержит набор методов для взаимодействия скриптов с основным движком игры. ScriptingBase вынесем в отдельный namespace.

Например это может выглядеть так:

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

Далее в движке пишем простой контроллер, который может показывать текст игроку. И еще пишем MonoBehaviour класс PluginsController который будет заниматься загрузкой и выполнением скриптов.

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

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

Ситуация

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

Ранее для хранения данных я бы просто использовала некий объект-контроллер, но с выгрузкой сцены он перестаёт существовать.

Передача данных (static class)

Оказалось, что Unity превосходно умеет работать с кодом, даже если он мирно лежит файликом в папке скриптов и не прикреплён компонентом к объекту на сцене (это было не очевидно для новичка). Например, таким файлом может быть статический класс такого вида:

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

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

Мультисценность (SceneManagement)

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

Подгружаю игровую сцену контроллером меню (сцена с меню и объектом рекламы остаётся загруженной тоже):


По завершению уровня, выгружаю сцену игры игровым контроллером (чтобы не висела в памяти):


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

Проблемы

К сожалению, даже при аддитивной загрузке сцен, не удастся вволю покопаться в объектах одной сцены из другой. Ссылки на объекты придётся передавать через некий «медиатор» (в моём случае использовался тот самый статический класс).

Будьте внимательны при инстанцировании префабов, если активны несколько сцен — у меня они все решили затолкаться в неправильную сцену (об этом в другой раз).

Я не много знаю о тонкостях работы Unity, так как не занимаюсь этим профессионально, а как хобби. Обычно я просто изучаю все необходимое по мере надобности, поэтому эта статья ориентирована на таких же как я.

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

Порывшись в разных статьях я нашел несколько различных вариантов реализации этой системы:

В большинстве статей практически нет информации по быстродействию тех или иных подходов, их сравнению и прочее. Обычно встречается только такое упоминание о быстродействии "Используйте SendMessage только в крайних случаях, а лучше не используйте вообще"

Окей, у этого подхода, видимо, есть существенные проблемы со скоростью, но как тогда обстоят дела у других?

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

Сравнивать решил эти 3 подхода, а так же обычный прямой вызов функции на объекте по его ссылке.
И как бонус — посмотрим наглядно, как медленно работает Find при поиске объекта каждый Update (о чем кричат все гайды для новичков) Погнали.

Подготовка скриптов

Для теста нам потребуется создать на сцене 2 объекта:

  • Отправитель, назовем его Sender, создадим и прикрепим на него скрипт Sender.cs
  • Получатель, назовем его Receiver, создадим и прикрепим на него скрипт Receiver.cs

Начнем с получателя Receiver.cs, т.к. тут будет меньше всего кода.
По правде говоря, сначала я думал ограничиться просто пустой функцией, которая будет вызываться извне. И тогда этот файл выглядел бы просто:

Для этого нам понадобится 4 переменные :

И дописываем функцию TestFunction так, что бы она могла считать за какое время она выполнилась testIterations раз и выплюнуть эту инфу в консоль. В аргументах будем принимать строку testName, в которой будет приходить имя тестируемого способа, т.к. сама функция не знает кто ее будет вызывать. Эту информацию так же добавляем к выводу в консоль. В итоге мы получаем:

Напишем нашу функцию DirectCallTest, которая будет заготовкой для всех остальных функций теста:

В каждой итерации мы вызываем на получателе нашу TestFunction и передаем название теста.

Теперь осталось сделать вывод в консоль и запуск этого теста, поэтому добавим в Start() строчку:

И строчку в Start():

Пишем новую функцию UnityEventTest:

Получается такой код:

Не забываем в UnityEventTest() заменить testEvent на testStringEvent.

Теперь подписываемся на событие в получателе Receiver.cs:

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

Запускаем. Все работает, отлично! Переходим к следующему тесту.

Пишем новую функцию EventDelegateTest:

Теперь подписываемся на событие в получателе Receiver.cs:

Запускаем и проверяем. Отлично, все тесты готовы.

Бонус

Добавим ради интереса копии методов DirectCallTest и SendMessageTest, где в каждой итерации будем искать объект на сцене, перед обращением к нему, что бы новички могли понять насколько дорого совершать такие ошибки:

Анализ результатов

Запускаем все тесты по 10000 итераций каждый и получаем такие результаты (я сразу отсортирую по времени выполнения цикла на нашем отправителе (Sender), т.к. на этом этапе я уже выяснил опытным путем, что время теста на получателе сильно отличалось из-за одного вызова Debug.Log, который выполнялся в 2 раза дольше чем сам цикл вызовов!


Название теста Время теста на отправителе
DirectCallTest 0.0001518726
EventDelegateTest 0.0001523495
UnityEventTest 0.002335191
SendMessageTest 0.003899455
DirectCallWithGettingComponentTest 0.007876277
SendMessageTestWithGettingComponentTest 0.01255739

Для наглядности визуализируем данные (по вертикали время исполнения всех итераций, по горизонтали названия тестов)




Давайте теперь повысим точность наших тестов и повысим количество итераций до 10млн.


Название теста Время теста на отправителе
DirectCallTest 0.1496105
EventDelegateTest 0.1647663
UnityEventTest 1.689937
SendMessageTest 3.842893
DirectCallWithGettingComponentTest 8.068002
SendMessageTestWithGettingComponentTest 12.79391

Два последних столбца, я думаю, навсегда отучат использовать поиск объекта в цикле/апдейте.




Заключение

Надеюсь кому то это будет полезно как маленькое исследование или как небольшой гайд по системам событий.

Полный код получившихся файлов:

(в конце я выложу весь итоговый код)

Итак, изучая полученные данные можно сказать, что отличие Time.realtimeSinceStartup от System.Diagnostics.Stopwatch не существенные в нашем случае и их можно списать на погрешность (На самом деле в тесте DirectCallWithGettingComponentTest есть отличие почти в 4 раза в дебаг версии, но в релизе все соотношение опять возвращается. Увидите это в итоговой таблице). Как я и говорил — вид графика не изменился:




Подводим итоги — хотя, скорость выполнения существенно изменилась в релизной версии с новым способом измерения времени, но соотношение времени выполнения тестов не изменилось. DirectCall и EventDelegate все так же в лидерах и быстрее UnityEvent в 10раз. А вот DirectCall с поиском объекта в каждой итерации обогнал даже обычный SendMessage..

Так же из этого апдейта мы узнали, что версия в релизе работает быстрее чем в редакторе в 3-10 раз.

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

Есть 2 скрипта первый Mole, второй Score и из скрипта Mole мне нужен public void Hide(); Вроде все правильно вписал, ошибок нету, но при компиляции в консоли появляется ошибка типа NullReferenceException и указывает именно на ту строку в которой я пытаюсь сослаться на метод в первом скрипте

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


Взаимодействие между скриптами
Как мне использовать переменную из одного скрипта в другом?


Реализация взаимодействия между скриптами)
У меня небольшой вопрос возник есть скрипт с таймерами и скрипт с респавном мобов как можно.

Как влияет на производительность связь между скриптами
Здраствуйте меня интересует такой вопрос как влияет на производительность связь между скриптами. И.

хотябы скрипт покажи
или ощибку
или хоть чтото

Добавлено через 33 секунды

Steroid,
Вот такая ошибка:
NullReferenceException: Object reference not set to an instance of an object
Score.OnPointerClick (UnityEngine.EventSystems.PointerEventData eventData) (at Assets/Scripts/Score.cs:20)

Как задается значение moles?

Добавлено через 1 минуту
А я знаю, никак)

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

а что если сделать public static NewXYPosition в class Mole. а потом вызвать ее в class Score -- Mole = Mole.NewXYPosition

Сама ошибка:
NullReferenceException
Mole.HideMole () (at Assets/Scripts/Mole.cs:43)

я немного в тупике

Добавлено через 7 минут
DreamForest, Пробовал, но там весь код будет в ошибках

Player_Zhero, Все ошибки пропали, но почему-то от void HideMole(); попросту никакой реакции, что я делаю не так(

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

Добавлено через 6 минут
Если тебе нужно чтобы он возвращал Vector3 то нужно написать так:

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

public void HideMole()
NewXYPosition =
new Vector3(transform.localPosition.x, HiddenY, transform.localPosition.z);
>

Player_Zhero, Ну вот смотри весь код:

Короче этот скрипт висит на нескольких объектах, и они поочередно поднимаются и опускаются, и я хочу сделать что после клика игровой объект сразу принял свою координату по Y, если я вызову в этом скрипте метод Hide(); то объект сразу опустится, но если я ссылаюсь на этот метод в другом скрипте, то не работает, попробовал твои варианты, но разницы никакой

Steroid,
первый:

Решение

Навигация между скриптами
Добрый вечер. есть такой код $(document).ready(function() < .

Конфликт между скриптами
Доброго времени суток! помогите плз! подключаю 2 скрипта: для плавного перехода между картинками в.

Передача переменной между скриптами
Есть 3 файла: 1.php : в нем лежит форма (одно текстовое поле ввода) 2.php : в нем 2 фрейма (3.php.

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


Передача переменных между скриптами perl и shell
Есть shell-скрипт, который вызывает скрипт на perl (в нём разбирается эл.письмо со вложенным.


Передать значение bool из одного скрипта к другому скрипту
как передать значение bool, которое находиться в методе update на другой скрипт в метод update.

Как из одного скрипта изменить переменную (int) другого скрипта?
У меня есть два скрипта (money который отвечает за общее количество денег и CarBuy который отвечает.

Как правильно менять спрайт префаба благодаря скрипта, который прикреплён к другому GameObject?
Я новичок в Unity и недавно начал создавать 2D игру, в которой надо стрелять в обычные мишени из.

Возврат прогресса из одного скрипта другому
Прив всем :) Возникла такая задача: Есть скрипт на питоне, выполняется достаточно долго (до 5.

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

Читайте также: