Hyper Casual игра на Unity с нуля. #2 Первый уровень

Hyper Casual игра на Unity с нуля. #2 Первый уровень

Первая часть: Hyper Casual игра на Unity с нуля. #1 Начало

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

_/Scripts/Dynamic/CellType.cs:

namespace AwesomeCompany.RollerSplat.Dynamic { public enum CellType { Floor, Wall } }

Затем создадим класс Level, в котором будет информация об ячейках и стартовой позиции шара.

_/Scripts/Dynamic/Level.cs:

using UnityEngine; namespace AwesomeCompany.RollerSplat.Dynamic { public class Level { public CellType[,] cells { get; } public Vector2Int spawnPoint { get; } public Level(CellType[,] cells, Vector2Int spawnPoint) { this.cells = cells; this.spawnPoint = spawnPoint; } } }

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

_/Scripts/Dynamic/DataProviders/IDynamicDataProvider.cs:

using System.Collections.Generic; namespace AwesomeCompany.RollerSplat.Dynamic { public interface IDynamicDataProvider { List<Level> levels { get; } } }

Создадим тестового провайдера. Добавим в него один уровень 5x5 со стартовой позицией шара снизу слева.

_/Scripts/Dynamic/DataProviders/DebugDynamicDataProvider.cs:

using System.Collections.Generic; using UnityEngine; namespace AwesomeCompany.RollerSplat.Dynamic { public class DebugDynamicDataProvider : IDynamicDataProvider { public List<Level> levels { get; } public DebugDynamicDataProvider() { var cells = new[,] { {CellType.Wall, CellType.Wall, CellType.Wall, CellType.Wall, CellType.Wall}, {CellType.Wall, CellType.Floor, CellType.Floor, CellType.Floor, CellType.Wall}, {CellType.Wall, CellType.Floor, CellType.Wall, CellType.Floor, CellType.Wall}, {CellType.Wall, CellType.Floor, CellType.Floor, CellType.Floor, CellType.Wall}, {CellType.Wall, CellType.Wall, CellType.Wall, CellType.Wall, CellType.Wall}, }; var spawnPoint = new Vector2Int(1, 1); var level = new Level(cells, spawnPoint); levels = new List<Level> {level}; } } }

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

_/Scripts/Gameplay/Cell.cs:

using UnityEngine; namespace AwesomeCompany.RollerSplat.Gameplay { public class Cell : MonoBehaviour { public Vector2Int index { get; protected set; } public virtual void Initialize(Vector2Int index) { this.index = index; transform.position = new Vector3(index.x, 0f, index.y); } } }

Теперь создадим префабы пола, стены и шара и поместим их в папку _/Resources/Gameplay.

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

Создаем класс LevelFactory, который будет отвечать за создание объектов. В нем два метода: создать шар и создать ячейку.

_/Scripts/Gameplay/LevelFactory.cs:

using System; using AwesomeCompany.RollerSplat.Dynamic; using UnityEngine; using Object = UnityEngine.Object; namespace AwesomeCompany.RollerSplat.Gameplay { public static class LevelFactory { private static readonly Transform _container; private static readonly GameObject _ballPrefab; private static readonly Cell _floorPrefab; private static readonly Cell _wallPrefab; static LevelFactory() { _container = new GameObject("LevelContainer").transform; _ballPrefab = Resources.Load<GameObject>("Gameplay/Ball"); _floorPrefab = Resources.Load<Cell>("Gameplay/Floor"); _wallPrefab = Resources.Load<Cell>("Gameplay/Wall"); } public static GameObject CreateBall(Vector2Int spawnPoint) { var ball = Object.Instantiate(_ballPrefab, _container); ball.transform.position = new Vector3(spawnPoint.x, 0f, spawnPoint.y); return ball; } public static Cell CreateCell(CellType cellType, Vector2Int index) { Cell cell; switch (cellType) { case CellType.Floor: cell = Object.Instantiate(_floorPrefab, _container); break; case CellType.Wall: cell = Object.Instantiate(_wallPrefab, _container); break; default: throw new ArgumentOutOfRangeException(nameof(cellType), cellType, null); } cell.Initialize(index); return cell; } } }

Далее создаем класс GameController, который будет управлять игрой. Пока что в нем один метод Play, в который передается необходимый уровень. Контроллер берет данные уровня и создает необходимое количество ячеек и шар с помощью LevelFactory.

_/Scripts/Gameplay/GameController.cs:

using AwesomeCompany.RollerSplat.Dynamic; using UnityEngine; namespace AwesomeCompany.RollerSplat.Gameplay { public class GameController : MonoBehaviour { public void Play(Level level) { var xLength = level.cells.GetLength(0); var zLength = level.cells.GetLength(1); for (var x = 0; x < xLength; x++) { for (var z = 0; z < zLength; z++) { var cellType = level.cells[x, z]; var index = new Vector2Int(x, z); LevelFactory.CreateCell(cellType, index); } } LevelFactory.CreateBall(level.spawnPoint); } } }

Добавляем ссылки на GameController и DynamicDataProvider в класс App. Теперь он имеет вид:

using AwesomeCompany.Common; using AwesomeCompany.Common.UI; using AwesomeCompany.RollerSplat.Dynamic; using AwesomeCompany.RollerSplat.Gameplay; using AwesomeCompany.RollerSplat.UI.Windows; using UnityEngine; namespace AwesomeCompany.RollerSplat { public class App : Singleton<App> { public IDynamicDataProvider dynamicDataProvider { get; private set; } public WindowManager windowManager { get; private set; } public GameController gameController { get; private set; } protected override void Awake() { base.Awake(); dynamicDataProvider = new DebugDynamicDataProvider(); windowManager = new GameObject("WindowManager").AddComponent<WindowManager>(); windowManager.transform.SetParent(transform); gameController = new GameObject("GameController").AddComponent<GameController>(); gameController.transform.SetParent(transform); } private void Start() { windowManager.Show<MainMenuWindow>(); } } }

Добавляем кнопку в класс MainMenuWindow, подписываемся на ее событие "Click". При нажатии берем первый уровень из списка уровней, передаем его GameController и закрываем окно.

using System.Linq; using AwesomeCompany.Common.UI; using UnityEngine; using UnityEngine.UI; namespace AwesomeCompany.RollerSplat.UI.Windows { public class MainMenuWindow : WindowBase { [SerializeField] private Button _playButton; protected override void Awake() { base.Awake(); _playButton.onClick.AddListener(OnPlayButtonClicked); } private void OnPlayButtonClicked() { var level = App.instance.dynamicDataProvider.levels.First(); App.instance.gameController.Play(level); Close(); } } }

Аттрибут [SerializeField] позволяет редактировать приватные переменные из редактора.

Далее нужно эту кнопку добавить в префаб окна. Я скачал SVG иконку кнопки play (256х256) с flaticon.com, закинул ее в figma.com, изменил цвет заливки на белый и сделал экспорт в PNG.

PNG картинку кладем в папку _/Sprites и меняем у нее в окне Inspector тип на Sprite (2D and UI), чтобы ее можно было добавить в интерфейс.

Hyper Casual игра на Unity с нуля. #2 Первый уровень

Открываем префаб MainMenuWindow, создаем кнопку, удаляем из нее дочерний объект Text, перетаскиваем иконку в компонент Image. Ставим галочку Preserve Aspect (для сохранения пропорций изображения) и кликаем Set Native Size чтобы установить размер кнопки равный размеру изображения. Перетаскиваем кнопку в поле PlayButton у объекта MainMenuWindow.

Перемещаем временно вручную камеру в нужное положение, примерно так:

Hyper Casual игра на Unity с нуля. #2 Первый уровень

Нажимаем Play и видим созданный уровень и шар. На сегодня это все. Еще раз ссылка на репозиторий, он будет обновляться вместе с выходом статей.

6565
24 комментария

Нихуя не понял, но в закладки добавил.

18

всё очень прикольно, но тебе с таким на хабр.
здесь не оч любят такой формат

3

Почему это? Лично я давно ждал нормальной ждвижухи на dtf.

14

А лучше на ютуб.

1

Все круто, жду продолжения!
Для иконок могу посоветовать https://game-icons.net/ там сразу можно и цвета и форму подложки настроить

3

Большое спасибо автору за материал.

Согласен с большинством. Код избыточен и перегружен для казуальной игры. И уж тем более для первоначального знакомства с Unity.

Код пишется или с большой закладкой на будущее. Или с попыткой показать больше возможности языка и Unity.

Но имея опыт работы с Unity и С# мне материал нравится и понятен. А некоторые идеи взял себе на заметку. Думаю автору стоит продолжать в том же духе. И не менять сложность и стиль изложения.

Хотел дополнить по окнам, я с ними работаю иначе. По модели MVP. Правда я Model и Presenter объединяю в один класс. И интерфейс делаю событийно-ориентированный. Я вообще часто использую event.

3

Вообще не всосал причин определенных решений.

Сначала вы зачем-то впиливаете интерфейс с единственной реализацией, что есть плохо (я понимаю, что вы, видимо, в следующей статье сделаете еще реализацию, но сейчас её нет - и это плохо для тутора и путает).

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

Потом вы берете и делаете фабрику уровня тупо статиком, что увеличивает связность и заставляет вас использовать ресурсы просто чтобы получить префабы (Что делать не надо. Я потом уже посмотрел - вы вообще не гнушаетесь использовать ресурсы).

Потом вы добиваете это все тем, что делаете "корень композиции" синглтоном. Зачем? К нему никто не обращается, а если кто будет - надо бить его по рукам.

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

1
Раскрывать всегда
РКН о сбоях в Рунете: «Рекомендуем пользоваться мощностями российских хостинг-провайдеров»

Неполадки связаны с использованием иностранных серверов.

РКН о сбоях в Рунете: «Рекомендуем пользоваться мощностями российских хостинг-провайдеров»
285285
124124
99
99
55
22
11
11
11
11
11
контора каждого охотника, желающего знать, где сидит фаназн.
«Это только начало для Shadows»: команда Assassin's Creed обратилась к игрокам с благодарностью

Разработчики отметили, что прислушиваются к отзывам.

«Это только начало для Shadows»: команда Assassin's Creed обратилась к игрокам с благодарностью
211
263263
187187
1818
99
77
44
22
22
11
11
Механика Nemesis появилась в Shadow of Mordor из-за опасений WB по поводу вторичного рынка игр

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

Механика Nemesis появилась в Shadow of Mordor из-за опасений WB по поводу вторичного рынка игр
145145
2424
99
55
44
22
11
11
11
11
Какая то ядерная шиза
Alibaba представила нейросеть, которая копирует движения людей на исходных видео и анимирует персонажей

Её можно протестировать в бесплатной демоверсии.

149149
2121
1414
1010
44
22
22
11
Новый виток порномультиков ✅
10 новых игр о которых никто не говорит (но зря!) 2025

Вот честно, иногда кажется, что игровой индустрии нечем удивить. Очередной Assassin’s Creed, ещё один открытый мир и опять нейросети пишут квесты. Стоп! А как насчёт реально крутых и неожиданных игр, которые сейчас никто не обсуждает, но которые заслуживают внимания не меньше, а порой даже больше чем хвалёные ААА? Сегодня о них и поговорим.

810810
4242
3737
1212
44
22
22
22
11
11
Новые игры - это игры, которые вышли, а невышедшие игры - это невышедшие игры. Чего их обсуждать, если их нет?
Bloodborne исполнилось 10 лет — в честь этого фанаты возвращаются в Ярнам

На анонс ремастера или ПК-версии мало кто надеется.

Bloodborne исполнилось 10 лет — в честь этого фанаты возвращаются в Ярнам
5858
66
44
22
11
11
11
пора уже менять описание грангер
Корейский симулятор жизни inZOI возглавил топ самых желаемых игр в Steam, обойдя Silksong

Аналог The Sims выйдет уже 28 марта в раннем доступе.

Корейский симулятор жизни inZOI возглавил топ самых желаемых игр в Steam, обойдя Silksong
3737
1111
99
44
44
11
Ждем, девушка будет задротить больше моего 😂
Путешествия во времени и пространстве: трейлер второго сезона нового «Доктора Кто»

Герои попадут даже в анимационный мир.

4141
1717
44
11
11
11
Летит в феодальную Японию, размахивать своей огромной дубиной
А что, после шестой смены перестают строго следить?
А что, после шестой смены перестают строго следить?
615615
3030
88
33
22
11
Мем про "устроюсь работать за 50 тыщ в месяц и за 4 месяца накоплю 200 тыщ" вышел из-под контроля.
Bloodlines 2 получила возрастной рейтинг от ESRB

Очередной признак, что релиз всё ближе.

Bloodlines 2 получила возрастной рейтинг от ESRB
8383
1212
88
44
11
11
Когда выпнули Авеллона и создателей оригинала, проект в принципе можно было закрывать.
Творческий директор Helldivers 2 считает, что текущие проблемы игровой индустрии отчасти связаны с «однообразием игр»

Он просит коллег меньше копировать.

Творческий директор Helldivers 2 считает, что текущие проблемы игровой индустрии отчасти связаны с «однообразием игр»
112112
2525
1818
22
11
11
11
Легко так говорить, когда скопировал звёздный десант
Спустя долгое время зашел в м-видео глянуть цены

Слова излишни

Спустя долгое время зашел в м-видео глянуть цены
133133
1313
66
44
44
22
11
11
11
М.Видео это всегда х2 от нормальной цены.
[]