Рубрика развивается при поддержке

Hyper Casual игра на Unity с нуля. #1 Начало

Всем привет. Хочу написать цикл обучающих статей на тему разработки Hyper Casual игры на Unity под мобильные платформы. С нуля и до полной готовности.

В закладки
Аудио

Чтобы немного упростить, не будем придумывать новую игру, а сделаем клон популярной игры Roller Splat от Voodoo (Google Play, App Store).

Примерный список того, что будем делать:

  • архитектура проекта;
  • геймплей;
  • UI;
  • редактор уровней;
  • локализации;
  • тесты;
  • реклама, аналитика, инапы.

Переходим к делу

Разработку будем вести на последней версии Unity (на текущий момент 2019.2.4f1). Качаем Unity Hub, устанавливаем, создаем новый проект.

Подготовим базовую структуру папок (будет меняться в процессе разработки).

В папке Assets создадим две папки "_" и "AwesomeCompany". Вы можете назвать их по-другому. В первой мы будем хранить все файлы текущего проекта. Во вторую вынесем общие скрипты, которые можно будет использовать в других проектах.

Resources - важно назвать именно так, из этой папки потом мы сможем загружать данные во время игры. Ознакомиться с зарезервированными именами папок Unity можно здесь.

Resources/DynamicData - здесь будем хранить различные динамические данные (уровни, локализации и т.д.).

Resources/Gameplay - здесь объекты, которые используются непосредственно в геймплее (префабы шаров, стен, пола).

Resources/UI/Windows - очевидно, интерфейс.

Scenes - сцены. Скорее всего она будет одна.

Scripts - скрипты. Внутри будет деление по папкам.

Sprites - изображения, которые используются в интерфейсе.

Пишем скрипты

AwesomeCompany/Common/Singleton.cs - Базовый класс для всех синглтонов в проекте. Подробнее можно почитать, например, здесь.

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

using UnityEngine; namespace AwesomeCompany.Common { public class Singleton<T> : MonoBehaviour where T : Component { private static T _instance; private static bool _isApplicationQuitting; public static T instance { get { if (_isApplicationQuitting) return null; if (_instance != null) return _instance; _instance = FindObjectOfType<T>(); if (_instance != null) return _instance; var obj = new GameObject { name = typeof(T).Name }; _instance = obj.AddComponent<T>(); return _instance; } } public virtual void Touch() { } protected virtual void Awake() { if (_instance == null) { _instance = this as T; DontDestroyOnLoad(gameObject); } else Destroy(gameObject); } protected virtual void OnApplicationQuit() { _isApplicationQuitting = true; } } }

AwesomeCompany/Common/UI/WindowManager.cs - менеджер, к которому мы будем обращаться чтобы открывать или закрывать окна интерфейса. Загружает окна из папки Resources и добавляет на сцену, каждое следующее окно будет поверх предыдущего.

using System.Collections.Generic; using System.Linq; using UnityEngine; namespace AwesomeCompany.Common.UI { public class WindowManager : MonoBehaviour { public List<WindowBase> windows { get; } private int _sortingOrder; public WindowManager() { windows = new List<WindowBase>(); } public T Show<T>() where T : WindowBase { _sortingOrder++; var existWindow = windows.FirstOrDefault(w => w is T); if (existWindow != null) { existWindow.canvas.sortingOrder = _sortingOrder; return existWindow as T; } var type = typeof(T); var windowPrefab = Resources.Load<T>($"UI/Windows/{type.Name}"); var window = Instantiate(windowPrefab, transform, false); window.Bind(this).canvas.sortingOrder = _sortingOrder; windows.Add(window); return window; } public T Get<T>() where T : WindowBase { var existWindow = windows.FirstOrDefault(w => w is T); if (existWindow == null) return default; return existWindow as T; } public void Close(WindowBase window) { var lastWindow = windows.LastOrDefault(); if (lastWindow == window) _sortingOrder--; DestroyWindow(window); } public void Close<T>() where T : WindowBase { var lastWindow = windows.LastOrDefault(); if (lastWindow is T) { _sortingOrder--; DestroyWindow(lastWindow); return; } DestroyWindow(windows.LastOrDefault(w => w is T)); } public void CloseAll() { foreach (var window in windows) Destroy(window.gameObject); windows.Clear(); _sortingOrder = 0; } private void DestroyWindow(WindowBase window) { if (window == null) return; windows.Remove(window); Destroy(window.gameObject); if (windows.Count <= 0) _sortingOrder = 0; } } }

AwesomeCompany/Common/UI/WindowBase.cs - базовый скрипт окна, от него будут наследоваться все остальные окна.

using UnityEngine; namespace AwesomeCompany.Common.UI { [RequireComponent(typeof(Canvas))] public abstract class WindowBase : MonoBehaviour { public Canvas canvas { get; private set; } private WindowManager _windowManager; public WindowBase Bind(WindowManager windowManager) { _windowManager = windowManager; return this; } public virtual void Close() { _windowManager.Close(this); } protected virtual void Awake() { canvas = GetComponent<Canvas>(); } } }

_/Scripts/App.cs - Основной скрипт нашего приложения, в нем будут все сервисы, менеджеры и различные данные. Наследуется от синглтона - значит будет существовать в единственном экземпляре и к нему будет доступ из любого скрипта.

using AwesomeCompany.Common; using AwesomeCompany.Common.UI; using AwesomeCompany.RollerSplat.UI.Windows; using UnityEngine; namespace AwesomeCompany.RollerSplat { public class App : Singleton<App> { public WindowManager windowManager { get; private set; } protected override void Awake() { base.Awake(); windowManager = new GameObject("WindowManager").AddComponent<WindowManager>(); windowManager.transform.SetParent(transform); } private void Start() { // test window manager windowManager.Show<MainMenuWindow>(); } } }

_/Scripts/UI/Windows/MainMenuWindow.cs - скрипт окна главного меню. Пока что ничего не делает.

using AwesomeCompany.Common.UI; using UnityEngine; namespace AwesomeCompany.RollerSplat.UI.Windows { public class MainMenuWindow : WindowBase { private void Start() { Debug.Log("MainMenuWindow Start"); } } }

Настроим сцену

Создадим пустой объект, правой кнопкой в окне Hierarchy -> Create Empty, назовем его App и добавим на него скрипт App.cs. Этот объект будет существовать на протяжении всего времени пока запущена игра, даже если загрузить другую сцену.

Затем, создадим UI -> Canvas. Переименуем объект в MainMenuWindow и повесим на него скрипт MainMenuWindow. Выставим параметры скейла в компоненте Canvas Scaler в Full HD (1080x1920) портретная ориентация. Создадим префаб данного окна, перетащив его в _/Resources/UI/Windows и удалим его со сцены.

В методе Start скрипта App мы добавили тестовый показ окна MainMenuWindow. А в методе Start окна мы выводим в консоль сообщение для проверки работоспособности.

Запускаем игру, видим что в окне иерархии внутри объекта WindowManager создался объект MainMenuWindow и в консоли сообщение "MainMenuWindow Start".

На сегодня, пожалуй, это все.

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

Материал опубликован пользователем.
Нажмите кнопку «Написать», чтобы поделиться мнением или рассказать о своём проекте.

Написать
{ "author_name": "Сергей Чемоданов", "author_type": "self", "tags": ["unity3d","unity","tutorial"], "comments": 59, "likes": 135, "favorites": 459, "is_advertisement": false, "subsite_label": "gamedev", "id": 68334, "is_wide": false, "is_ugc": true, "date": "Sat, 07 Sep 2019 00:05:12 +0300", "is_special": false }
0
{ "id": 68334, "author_id": 29446, "diff_limit": 1000, "urls": {"diff":"\/comments\/68334\/get","add":"\/comments\/68334\/add","edit":"\/comments\/edit","remove":"\/admin\/comments\/remove","pin":"\/admin\/comments\/pin","get4edit":"\/comments\/get4edit","complain":"\/comments\/complain","load_more":"\/comments\/loading\/68334"}, "attach_limit": 2, "max_comment_text_length": 5000, "subsite_id": 64954, "last_count_and_date": null }
59 комментариев
Популярные
По порядку
Написать комментарий...
22

большинство проектов и уроков дальше "#1 Начало" не заходят

Ответить
6

Планирую все же закончить начатое :)

Ответить
16

опыт: сначала пишешь всю серию чего бы там ни было по какой-либо теме, а потом публикуешь по графику. Запала и вдохновения без организации хватает как раз на "#0 Ставим Юнити и разбираемся в Ассет сторе" и "#1 Начало"

Ответить
1

Время покажет

Ответить
25

я приду потом сказать "я же говорил" просто из вредности

Ответить
6

Заскринил( надеюсь так простимулировать автора).

Ответить
3

Если сразу записать весь курс может оказаться что людям не интересно или не понятно (и надо менять подход).

Ответить
4

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

Ответить
1

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

Ответить
0

обо всем можно рассуждать с позиции "если машина не поедет" когда нет ни машины ни денег на нее, ни продавца. Там один продавец телефонов собирался стать программистом, потому что мобилки востребованы и там много плотють "посоветуйте что нибудь по с++ и java, мне компьютеры интересны". Не с того конца начало

Ответить
1

ты пишешь узкоспециализированную штуку для 3,5 человек, которым это нужно

получаешь 20 плюсов

расстраиваешься что ЛЮДЯМ не зашло

Ответить
0

Чисто мои ощущения от поисков уроков по Visual Studio и SQL =\

Ответить
0

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

Ответить
0

Хотя их уже наверное в электронке выкладывают.

Их в электронке выкладывали 10 лет назад уже.

Ответить
2

чуть больше десяти как закончил, учился по "книжечке" :)

Ответить
0

Ну я 9 лет назад поступил, и уже были прошлогодние методички на сайте ))

Причем методички слово в слово лекции )

Ответить
0

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

Ответить
16

Бро, это урок для какого уровня? Если для начинающих, то фейл. Ты выкладываешь сотню строк кода без малейших комментариев. Нуб не знает, чем отличается public WindowBase Bind(WindowManager windowManager) от windowManager = new GameObject("WindowManager").AddComponent<WindowManager>();
и что это значит. Если пишешь для про, то переходи сразу к объяснению реализации геймплея, а не создания менеджера окон, так как про это и так знает.

Ответить
2

Длч ньюбов в геймдеве. Например, меня.
В загашнике большой опыт с c# и поверхостные знания Unity(). Так что для меня эти уроки самое то.

Ответить
0

В данной части решил common скрипты не комментировать пока, использовать "AS IS" :)

Ответить
0

Я вот прям подумал, что попробую, начну, а потом полотно кода без комментов. Ок, понятно, у автора свое понимание начинающих. В любом случае - успехов

Ответить
0

Так начните :) Я эти скрипты не зря вынес отдельно от проекта. Пока будем считать что это один из сторонних sdk. На данном этапе надо понять только как вызывать методы этих классов.

Ответить
3

Астрологи объявили урок по Юнити.
Количество игр выпускаемых в будущем может резко возрасти.
Качество упадёт.

Ответить
1

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

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

из плюсов:
- т.к. все ленивые, то всегда проще написать Resrouces.Load чем сделать скриптабл обжект в котором будут храниться ссылки на ассеты (хотя в него можно также эти ссылки кодом забить)
- ассеты вне папки Resources загружаются в память на старте сцены, если у вас много ассетов и не все они понадобятся на сцене, а вам очень надо экономить потребялемую память, то лучше подгружать из папки Resources, хотя лучше для этого использовать другую магическую папку StreamingAssets..

поэтому вопрос автору: почему Resources?

Ответить
1

Потому что кроме "Don't use it" стоило бы также прочитать и остальное. Речь идет о менеджменте ресурсов в условиях по-платформенной доставки, а не кострищах и сортах дров на которых рассово-верно сжигать всех, кто использует Resources System. Забить на менеджмент ресурсов, превратив любой реестр в свалку, можно весьма успешно и в более гламурных местах чем Unity.

Пока человек понимает что за ресурсами нужен учет и учет тот занимает O(n*log(n)) на количество неделимых ресурсов, ничего плохого не происходит.

Ответить
0

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

Ответить
0

Так, а теперь прочитайте свое сообщение ещё раз. Туториал для новичков поэтому... нужно задвигать о менеджменте ресурсов в условиях продакшна? Хорошо, давайте тогда ещё про defensive programming, про батчи, про MVP, про ioc/di, про networking-first? А то вдруг война, а солдат ещё не заточил свою звезду смерти?

Ответить
0

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

Ответить
1

Жду расово-верный туториал. Разговор не о том, чтобы правильно донести эту информацию, а о том что новичкам в принципе не интересно откуда загружаются ресурсы: из сцены ли, из ресурсов ли, у них в принципе нет столько ресурсов чтобы это могло стать проблемой и не будет ещё долго.

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

Элементарно YAGNI и все.

Ответить
0

с YAGNI не поспоришь, согласен

Ответить
0

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

Ответить
1

Туториалы на DTF по юнити тема хорошая, но лучше не учить людей плохому..

Ответить
2

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

Ответить
2

Там кто-то какое-то время назад анонсировал цикл статей по разработке на юнити. Гиде?

Ответить
2

столько писанины только для того что бы "гарантировать что класс в единственном экземпляре"?

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

Ответить
0

Ничего себе с нуля, с нуля это надо юнити сначала изучать и переменные инт там флоат, а то сразу синглтон.

Ответить
4

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

Ответить
0

Прежде чем начать изучать движок, ясень-пень что нужно уже основы C# знать.

Ответить
0

Неплохо! А расскажите немного о себе: вы работаете Unity-девелопером казуальных игр или хобби? Как давно в теме?

Ответить
4

Работаю, больше трех лет.

Ответить
1

Сергееей!!! У меня большие надежды на твой цикл

Ответить
1

Спасибо за статью. Очень любопытно, так как сам изучаю Юнити и учусь делать гиперказуалки. Я, наверное, вхожу в целевую аудиторию этой статьи.
Я согласен с комментирующими выше по поводу необходимости объяснения использования синглтона, Resources и того кода, который вы здесь описали.
И отдельный вопрос - почему мы создаём канвас главного меню из скрипта, а не просто оставляем его в структуре сцены?

Ответить
1

Друг это так не работает.
Я вот ровно сейчас новичок в Юнити, но не новичок в IT и программировании в частности. Хороший вводный ликбез по Юнити - на вес золота.

Все обучалки где предлагается взять готовые куски кода или еще хуже - готовые префабы, призывая воспринимать их временно как "магию" - хрень полная. Т.к. банально непонятно где кончается эта самая магия, а где просто сам затупил и надо вдумчиво осмыслять.

Как пример самой лучшей коротенькой обучалки. Никакой "магии", каждый следующей шаг, базируется на предыдущем.
http://websketches.ru/blog/2d-igra-na-unity-podrobnoye-rukovodstvo-p2

Ответить
0

А почему именно синглтон?

Ответить
1

Для данного проекта, думаю, вполне нормально сделать так.

Ответить
0

Тоже не понял зачем он тут нужен вообще ¯\_(ツ)_/¯
Остается только надеяться, что это не шаблон ради шаблона для бесконечного количества таких нужных абстракций, без которых прям никак))

Ответить
1

Да я знаю почему, на стаковерфлоу по юнити синглтон - это решение всех проблем. )

Лучше бы про поведенческие паттерны рассказал, это намного сложнее и полезнее, для игр однозначно нужен или mediator, или observer.

Ответить
0

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

Ответить
1

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

Ответить
0

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

Ответить
0

Еее, больше статей по Юнити. Ширяев, пора?

Ответить
0

Автор, спасибо!!! Поставлю сегодня себ чудную прогу, попробую!!!

Ответить
0

Тут пробовать то пока нечего, особенно если не кодили никогда - начните лучше с учебных проектов самой Unity Technologies. Без понимания зачем и почему копипаст чужого кода особой радости или навыков не добавит.
Ну и вообще "пробовать" лучше, на мой взгляд, сначала непосредственно геймплей, а не ресурс менеджмент.

Ответить
0

Сделал все так же. При попытке запуска выдает нулл. И ничего не создаёт.( Пойду дальше учить код. Так себе "урок"

Ответить
0

Думаю окно лежит не там откуда его пытается достать менеджер окон

Ответить

Прямой эфир

[ { "id": 1, "label": "100%×150_Branding_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox_method": "createAdaptive", "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfl" } } }, { "id": 2, "label": "1200х400", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfn" } } }, { "id": 3, "label": "240х200 _ТГБ_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fizc" } } }, { "id": 4, "label": "Article Branding", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "p1": "cfovz", "p2": "glug" } } }, { "id": 5, "label": "300x500_desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "ezfk" } } }, { "id": 6, "label": "1180х250_Interpool_баннер над комментариями_Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "clmf", "p2": "ffyh" } } }, { "id": 7, "label": "Article Footer 100%_desktop_mobile", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjxb" } } }, { "id": 8, "label": "Fullscreen Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjoh" } } }, { "id": 9, "label": "Fullscreen Mobile", "provider": "adfox", "adaptive": [ "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fjog" } } }, { "id": 10, "disable": true, "label": "Native Partner Desktop", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyb" } } }, { "id": 11, "disable": true, "label": "Native Partner Mobile", "provider": "adfox", "adaptive": [ "phone" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fmyc" } } }, { "id": 12, "label": "Кнопка в шапке", "provider": "adfox", "adaptive": [ "desktop", "tablet" ], "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fdhx" } } }, { "id": 13, "label": "DM InPage Video PartnerCode", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox_method": "createAdaptive", "adfox": { "ownerId": 228129, "params": { "pp": "h", "ps": "clmf", "p2": "flvn" } } }, { "id": 14, "label": "Yandex context video banner", "provider": "yandex", "yandex": { "block_id": "VI-250597-0", "render_to": "inpage_VI-250597-0-1134314964", "adfox_url": "//ads.adfox.ru/228129/getCode?pp=h&ps=clmf&p2=fpjw&puid1=&puid2=&puid3=&puid4=&puid8=&puid9=&puid10=&puid21=&puid22=&puid31=&puid32=&puid33=&fmt=1&dl={REFERER}&pr=" } }, { "id": 15, "label": "Баннер в ленте на главной", "provider": "adfox", "adaptive": [ "desktop", "tablet", "phone" ], "adfox": { "ownerId": 228129, "params": { "p1": "byudo", "p2": "ftjf" } } }, { "id": 17, "label": "Stratum Desktop", "provider": "adfox", "adaptive": [ "desktop" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fzvb" } } }, { "id": 18, "label": "Stratum Mobile", "provider": "adfox", "adaptive": [ "tablet", "phone" ], "auto_reload": true, "adfox": { "ownerId": 228129, "params": { "pp": "g", "ps": "clmf", "p2": "fzvc" } } } ]