В предыдущих сериях: я собрался написать туториал как создать игру на Unity с нуля и рассказал об этом тут, затем мы установили необходимые программы, создали проект и даже начали делать главное меню. Сегодня продолжаем.
Краткое содержание:
• Скроллинг текста
• Создание скрипта
• Скрипт работы кнопок главного меню
• Внешние ссылки
• Создание локализации с помощью JSON
Сейчас мы сделаем поле для блока "Дополнительно". В своей игре в этом блоке я расположил благодарности членам комьюнити ВК, которые активно срутся в беседе помогают развивать игру. Этот пункт меню в принципе не обязателен, однако на нём мы потренируемся работать с компонентом для скроллинга текста (это один из примархов нашей игры).
Создайте новое поле extrasList аналогично предыдущим. Затем добавьте ему дочерний пустой объект viewport, которому так же добавьте дочерний объект content. В итоге у нас получилась своеобразная матрёшка extrasList -> viewport -> content.
К extrasList прикрепляем компонент Scroll Rect. Эта важная курица будет контролировать работу скроллинга содержимого.
В качестве Content установите наш одноимённый объект content, уберите галочку с Horizontal (нам пока не требуется горизонтальный скроллинг), Movement Type --> Clamped, Viewport --> viewport.
• Movement Type отвечает за поведение контента при достижении нижней или верхней точки скроллинга. Unrestricted унесёт содержимое в дальние дали, Elastic даст ему плавно отскочить и вернуться назад, а Clamped просто резко остановит.
Не хватает только полосы прокрутки. Создайте для extrasList дочерний объект UI/Scrollbar. Как видите - у него есть два компонента (Image и Scrollbar) и два дочерних объекта. В первую очередь сделаем его вертикальным. Для этого в компоненте Scrollbar установите Direction --> Bottom to Top. Теперь нужно разместить его в правой части extrasList и растянуть на всю высоту экрана. Это, равно как и изменение его цветов вы теперь сможете сделать самостоятельно ;) Когда всё будет готово, назначьте объект Scrollbar в качестве Vertical Scrollbar в компоненте Scroll Rect.
Далее сделайте размеры viewport и content побольше - подстать extrasList - и добавьте текстовый компонент в content. Для корректного отображения скроллинга в content нужно добавить еще один компонент - Content Size Fitter и установить:
Horizontal Fit --> Unconstrained
Vertical Fit --> Preferred Size
• Обычно для конструкций скроллинга требуется установка компонента Mask для объекта viewport. Этот компонент будет скрывать текст за пределами границ viewport. Сейчас он нам не нужен, так как текст доходит до края экрана.
При желании попробовать добавьте компоненты Mask и Image для viewport и отключите у Mask галочку на Show Mask Graphic. Текст будет обрезаться на границах viewport.
Полюбуемся результатом.
• В правом нижнем углу можно разместить кнопки, ведущие в ваш Дискорд, ВК, сайт итд. Можете этого не делать, но я далее все равно покажу как это прописать в скрипте.
Каждую сцену нужно добавлять в билд через меню File - Build Settings. Причем сцена, зарегистрированная под номером 0 будет являться стартовой. Давайте добавим нашу сцену в билд.
Я долго откладывал этот момент, но больше тянуть кота за яйца смысла нет - будем писать код. Сейчас нам нужно заставить работать все (ну, почти) кнопки меню.
ВАЖНО! Я постараюсь максимально понятно и просто объяснить что и как работает, но знайте - мои объяснения не обучат вас C#. Вы просто научитесь пользоваться теми функциями, которые использую в своей игре я. Обязательно ищите дополнительную информацию, читайте справочники и учебники и шерстите форумы каждый раз, когда вам что-то непонятно.
Откройте Notepad++ или любой другой редактор кода. Измените кодировку на UTF-8, а синтаксис на C#. Затем сохраните файл как mainMenu.cs в папку Assets/Scripts.
В самом начале скрипта нам нужно указать необходимые пространства имен - говоря проще хранилища функций, которые мы будем использовать. Объяснение каждого из них займёт очень много места и времени, поэтому просто скопируйте их.
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;
using System.Linq;
using System.Text;
using UnityEngine.SceneManagement;
Затем создайте класс mainMenu - он должен называться так же как и сам файл. К классу допишите : MonoBehaviour - это означает наследование от базового класса Unity.
Подробнее о пространствах имен
Подробнее о клаcсах
Подробнее о MonoBehaviour
В первую очередь создадим метод для работы кнопки "Новая игра". По плану он должен открывать поле справа, где игрок сможет выбрать слот для новой игры и начать её.
Нам понадобится переменная типа GameObject, которая будет указывать на поле newGameList.
public class mainMenu : MonoBehaviour {
public GameObject newGameList;
}
Слово public перед переменной означает, что она будет доступна для других классов. Пока что все переменные и методы мы будем делать публичными.
Теперь создадим метод menuNewGame(), который будет показывать/скрывать newGameList. Фактически он проверяет активен ли объект newGameList в иерархии - если да, то скрывает, а если нет, то активирует.
public void menuNewGame() {
if (newGameList.activeInHierarchy == true) newGameList.SetActive(false);
else {
newGameList.SetActive(true);
}
}
Подробнее о конструкции if... else
Старайтесь всегда соблюдать отступы в коде и не лепить всё в одну кучу - это просто удобно для чтения.
Теперь сохраним файл и вернемся в Unity. Чтобы всё заработало как надо, мы должны прикрутить к кнопке newGame скрипт mainMenu в качестве компонента. Затем в этом компоненте в поле newGameList указать одноимённый объект. В компоненте Button в событии On Click () укажите нашу кнопку и выберите из списка наш новый метод menuNewGame().
Теперь можно запустить игру и посмотреть как работает кнопка. Если не работает - ищите где что пропустили :)
Аналогичную операцию с переменной, методом и назначением скрипта нужно проделать с кнопками "Загрузить игру", "Настройки" и "Дополнительно".
• Тут есть одна важная деталь. Каждый раз прикручивая скрипт к кнопке, нам придется назначать в инспекторе переменные. Это особенность редактора Unity. Можно, конечно, делать отдельный скрипт и класс для каждой кнопки, но это шило на мыло. А расписывать всё только через скрипт мы не будем (пока что...).
Обратите особое внимание на то, чтобы в каждом методе активация одного поля меню приводила к деактивации другого.
Как видите, я постоянно делаю пометки красным цветом. Это не очень удобно в скриптах - там для этого есть отдельный инструмент комментариев. Если вы поставите двойной слеш //, то вся остальная часть строки будет превращена в комментарий, который никак не будет учитываться компилятором. В комментарии можно заключать сразу целые блоки кода с помощью /* закомментированный блок */.
Всегда комментируйте свой код - причем делайте это так, чтобы ваши записи были понятны другому человеку. Это облегчает работу, понимание структуры, поможет вспомнить свой код. Да и просто это правило хорошего тона.
Я даже специально скриншот для этого сделаю.
Конечно же нет смысла комментировать каждую строку - это слишком громоздко. Я сделал это в качестве примера. Однако, указывать вкратце что и как делает очередной метод стоит. Не перенебрегайте этим! Спасибо мне потом скажете.
Теперь сделаем скрипт для кнопок "Выход" и ссылок на соцсети. Для выхода из приложения используется простая команда Application.Quit(); Её нужно добавить в метод exitGame() и описанным ранее способом привязать к кнопке exit.
Для перехода по ссылке используем команду Application.OpenURL("адрес ссылки"); аналогичным образом.
• Экономьте время и силы. Прикрепите скрипт mainMenu к одному объекту, сделайте все привязки, затем скопируйте компонент. Выделите все необходимые объекты и массово вставьте скопированный компонент со всеми привязками.
Это далеко не единственный способ программировать кнопки. Программирование - это достаточно гибкая штука. Способов, которыми можно реализовать выполнение задачи множество и ни один из них не может быть универсальным идеалом. Ваша задача - найти самый удобный для себя.
Сейчас мы просто обязаны немного поговорить о тексте. Как его хранить в игре?
1. Прописывать прямо в скрипт. Это наихудший вариант из всех, которые только можно представить. Громоздкие файлы, низкая читабельность кода, презрение коллег по цеху, низкая самооценка. Никогда не пишите текст в ваших скриптовых файлах! Ни единого слова!
2. Хранить его в отдельных текстовых файлах, а затем импортировать с помощью библиотек LINQ. Это хороший способ - я так поначалу и делал. На каждую сцену у меня был свой текстовый документ со строками необходимого текста. Проблема наступила, когда его стало слишком много и я начал теряться в структуре. Вторая проблема наступила, когда я решил добавить несколько языков в локализацию - это создало необходимость делать более громоздкий код и в разы больше файлов.
3. Хранить текст в JSON. Этот вариант на данный момент идеален для меня, так как я готовлю локализацию с 11 языками. Этот формат позволяет мне поддерживать хранение текста сразу блоками, а не строками и структурировать его. Подробнее про формат JSON можно почитать тут.
Вот этот формат мы и будем использовать на протяжении всего туториала. Я крайне редко встречал уроки (возможно даже и не встречал) и курсы, где с самого начала ученику предлагают хранить информацию в JSON, так что это будет своего рода эксперимент.
Схема будет такая:
• Создаем базу данных текста в Excel или Google Таблицы
• Создаем класс под эту базу данных в проекте
• Импортируем БД в JSON и добавляем в проект
• Создаем скрипт, распаковывающий JSON, и расставляющий значения переменных текста
Благодаря возможностям JSON, мы сразу получаем возможность делать локализацию своей игры - переводить её на другие языки. Не вижу смысла откладывать эту тему на потом. Такие серьезные моменты лучше решать сразу.
Я буду объяснять на примере Гугл Таблиц (Google Spreadsheets), но тут с Excel различий мало. Создайте новую таблицу.
• В программировании первая цифра, первый элемент, первый объект - это всегда ноль. Всё начинается с нуля. Привыкайте считать с нуля и тогда вы не словите когнитивный диссонанс при написании кода.
Поле id нам нужно по большей мере для себя - чтобы не путаться, а также для повышения производительности. Оно будет нумерально совпадать с индексом элемента в списке, который мы создадим позднее. Начинайте его всегда с нуля и по порядку.
Key - это поле нам нужно для упрощенного обращения к элементу. Мы будем "вызывать" элемент по Key и требовать "какие ваши доказательства" предоставить необходимое значение.
• Не забудьте внести в таблицу весь текст главного меню - кнопки меню, заголовки настроек, текст из "Дополнительно".
• Обратите внимание, что названия языков мы не вносим в таблицу. Они отображаются в родном начертании в компоненте Dropdown объекта optionsList - language - Dropdown.
Хорошо, сделали - что дальше? Как запихать это в игру? Для начала нам нужно превраить нашу таблицу в json. Сохраните её в xlsx - стандартный формат экселя, который поддерживается и гугл таблицами. Далее зайдите на сайт, сохраните его, создайте его закладку, выбейте татуировку ссылки на левой руке - Beautifytools (https://beautifytools.com/excel-to-json-converter.php). Откройте с помощью него нашу таблицу и экспортируйте в json.
Нам понадобится новая папка для хранения текстовых баз данных (а у нас уже именно база данных, представьте себе). Assets/Resources/textDB
Там нам понадобится новый файл с кодировкой UTF-16 LE с BOM и синтаксисом JSON.
Добавляем конвертированную в джейсон таблицу в файл и получаем следующий результат.
• Если вам непонятно почему это так расположено и что это за знаки, то почитайте про синтаксис JSON
Теперь всё это нужно аккуратно запихать в игру. Для начала нам потребуется новый скриптовый файл dataBase.cs. Добавьте ему такие же пространства имен, как и файлу mainMenu. Этот файл будет содержать классы для импорта различных таблиц в формате JSON.
Импорт будет происходить в список List<> - этот класс в языке C# позволяет хранить объекты одного типа, добавлять/удалять элементы, сортировать и прочие прелести жизни. Каждый блок из файла json будет рассматриваться как отдельный объект и импортироваться в список, откуда мы его с лёгкостью будем использовать в игре.
Создадим в скрипте dataBase.cs класс TextDB, в котором объявим список List<Textdata>. Textdata - это класс, который описывает наши текстовые объекты.
• Разъяснение. Текстовым объектом в нашем случае является каждая строка таблицы со своим id, Key и блоками russian, english, japanese.
Так, класс Textdata мы уже начали использовать, однако еще не объявили. Какое нахальство! Давайте скорее это сделаем, пока компилятор не надавал нам по рукам. В этом же скрипте объявите класс Textdata. В нём нам потребуются переменные согласно столбцам таблицы.
public int id; // int - это 32-битный формат целых чисел от -2147483648 до 2147483647
public string Key; // string - это строковый формат. Хранит одну строку текста
public string russian;
public string english;
public string japanese;
"Одна строка" - понятие растяжимое. В языке программирования знак переноса строки ( '\n') - это такой же символ как и буква. Переменная формата string может содержать очень много текста - до 2^31 символов (2 147 483 648).
Подробнее о типах данных
Осталась "мелочь" - написать в скрипте mainMenu.cs команду на импорт базы текста и подставить значения в зависимости от языка. Классы, которые наследуют от MonoBehaviour могут использовать специальные методы движка Unity. В нашем случае потребуется метод Start() который выполняется сразу после инициализации объекта. В него мы и добавим инструкции по импорту базы. Также понадобится объявить переменную textDatabase класса TextDB.
public TextDB textDatabase;
void Start() {
TextAsset asset0 = Resources.Load("textDB/mainMenu") as TextAsset;
textDatabase = JsonUtility.FromJson<TextDB>(asset0.text);
}
Для того, чтобы видеть в инспекторе нашу базу данных, добавьте в скрипте dataBase.cs перед классами строку [System.Serializable].
Теперь, если вы запустите игру и выберете объект, к которму прикреплён скрипт mainMenu, вы увидите список импортированных тесктовых объектов.
Прекрасно! Следующий шаг - это настроить функцию выбора языка. Язык мы выбираем при помощи всплывающего списка Dropdown в разделе настроек. Давайте создадим для него переменную public Dropdown langDropdown, чтобы было проще к нему обращаться. Также в скрипте mainMenu создадим новый метод setLanguage(). Прикрепите скрипт mainMenu к объекту Dropdown и в поле Lang Dropdown укажите этот объект. В разделе On Value Changed прикрепите метод setLanguage().
Что будет происходить? Каждый раз, когда вы будете выбирать язык во всплывающем меню, будет меняться числовое значение Dropdown и вызываться метод setLanguage(). У каждого языка получится свой порядковый номер: русский - 0, английский - 1, японский - 2. На основании этого номера мы и будем менять текстовые блоки.
Для быстрого доступа к текстовым блокам создадим для каждого из низ переменную класса Text и назначим их соответствующим полям скрипта mainMenu в инспекторе.
public Text newGameText;
public Text loadGameText;
...
Однотипные переменные можно объявлять через запятую.
public Text newGameText, loadGameText, optionsText, extraText, exitText, langText, audioText, musicText, creditsText;
Теперь напишем смену блоков текста при помощи оператора switch. Этот оператор похож на if...else. Также мы применим цикл foreach, который будет перебирать элементы списка с текстом и выбирать нужный согласно критерию. Критерием будет значение Key. Значение из Dropdown получим с помощью запроса langDropdown.GetComponent<Dropdown>().value
Подробнее об операторе switch можно почитать здесь
Подробнее о циклах и цикле foreach
public void setLanguage() {
switch (langDropdown.GetComponent<Dropdown>().value) {
case 0:
foreach (Textdata p in textDatabase.text) {
if (p.Key == "newgame") newGameText.text = p.russian;
else if (p.Key == "loadgame") loadGameText.text = p.russian;
else if (p.Key == "options") optionsText.text = p.russian;
else if (p.Key == "extra") extraText.text = p.russian;
else if (p.Key == "exit") exitText.text = p.russian;
else if (p.Key == "language") langText.text = p.russian;
else if (p.Key == "ambience") audioText.text = p.russian;
else if (p.Key == "music") musicText.text = p.russian;
else if (p.Key == "credits") creditsText.text = p.russian;
}
break;
}
}
В конструкции switch нам также необходимо прописать блоки для case 1: и case 2:
• Надписи "свободный слот" в меню новой игры и загрузки можно пока не трогать - для них припасен отдельный урок ;)
Язык игры - это настройка, а настройка должна сохраняться. Нет смысла сохранять настройки непосредственно в файле сохранения самой игры - для этого нам отлично подойдёт реестр. С помощью функции
PlayerPrefs мы можем сохранять числа и текст. Для этого нам нужно создать запись в реестре и передать ей значение - в нашем случае номер языка - а затем сохранить.
PlayerPrefs.SetInt("language", langDropdown.GetComponent<Dropdown>().value);
PlayerPrefs.Save();
Запись с именем "language" будет создана автоматически, а если она уже существует, то поменяет своё значение. Добавьте эти строки в метод setLanguage() до или после конструкции switch.
Теперь нам нужно прописать код, который проведёт операции с локализацией при старте игры.
Что нам нужно:
• Если игра запускается впервые - установить язык как на устройстве пользователя или английский (иначе японец не сможет прочитать надписи на русском и поменять язык)
• Если игра уже запускалась и язык сохранён, то установить этот язык
• Установить значение value объекта Dropdown согласно текущему языку
Нам понадобится новый метод setLangAtStart(), в котором мы определим язык устройства и установим его или английский.
public void setLangAtStart() {
if (!PlayerPrefs.HasKey("language"))
{
if (Application.systemLanguage == SystemLanguage.Russian) PlayerPrefs.SetInt("language", 0);
else if (Application.systemLanguage == SystemLanguage.English) PlayerPrefs.SetInt("language", 1);
else if (Application.systemLanguage == SystemLanguage.Japanese) PlayerPrefs.SetInt("language", 2);
else PlayerPrefs.SetInt("language", 1);
PlayerPrefs.Save();
}
langDropdown.GetComponent<Dropdown>().value = PlayerPrefs.GetInt("language");
setLanguage();
}
Метод проверяет сохранен ли язык в настройках, если нет, то устанавливает как у пользователя или английский. Далее он вызывает метод setLanguage() и, для того, чтобы не возникало ошибок, меняет значение value компонента Dropdown на номер текущего языка. Теперь добавьте метод setLangAtStart() в метод Start(), чтобы он автоматически запускался при старте игры.
Теперь внесём небольшой корректив в конструкцию switch и заменим условие (langDropdown.GetComponent<Dropdown>().value) на (PlayerPrefs.GetInt("language")). Теперь игра будет запускаться с тем языком, который вы установили в прошлый сеанс.
Дополнительно можно сделать так, чтобы менялось название игры при смене языка. Фактически нам нужно просто подменить изображение. Создадим для этого переменную logo класса Image и новый метод changeLogo(). В нём мы используем команду Resources.Load<Sprite>("имя файла"); для обращения к содержимому папки Resources и загрузке оттуда необходимого изображения.
public void changeLogo() {
if (PlayerPrefs.GetInt("language") == 0) logo.sprite = Resources.Load<Sprite>("Images/game_name");
else logo.sprite = Resources.Load<Sprite>("Images/game_name_eng");
}
Единственное, что пока осталось без нашего внимания - это настройки музыки и звуков. К ним мы вернемся позднее отдельной статьей. В следующих постах мы наконец-то создадим свою первую игровую сцену, настроим переходы и научимся сохранять игру. Всем добра и пёсиков!
Материалы из поста
Содержание туториала (Google Docs с ссылками на Пикабу)
Игра "Питерский Квест" в Google Play
Архив версий apk и Windows