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

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

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

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

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

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

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

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

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

В папке 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.

2020 показов
13K13K открытий
59 комментариев

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

Ответить

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

Ответить

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

Ответить

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

Ответить

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

Ответить

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

Ответить

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

Ответить