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

В закладки

Уровень будет состоять из ячеек - пустых (пол, который нужно закрасить) и стен. Первым делом создадим 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), чтобы ее можно было добавить в интерфейс.

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

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

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

{ "author_name": "Сергей Чемоданов", "author_type": "self", "tags": ["unity","tutorial","1"], "comments": 25, "likes": 64, "favorites": 187, "is_advertisement": false, "subsite_label": "gamedev", "id": 68568, "is_wide": false, "is_ugc": true, "date": "Sun, 08 Sep 2019 23:23:09 +0300", "is_special": false }
0
25 комментариев
Популярные
По порядку
Написать комментарий...
18

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

Ответить
1

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

Ответить
13

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

Ответить
1

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

Ответить
3

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

Ответить
3

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

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

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

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

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

Ответить
1

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

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

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

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

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

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

Ответить
0

Писал в прошлом посте вопрос, повторю: почему объекты создаются в Awake?
Например, вот:
windowManager = newGameObject("WindowManager").AddComponent<WindowManager>();
Вы так же поступили с канвасом в прошлом посте. Почему нельзя просто оставить их на сцене?

Другой вопрос: почему используется static?
private static readonly GameObject _ballPrefab;

Ответить
1

"Почему нельзя просто оставить их на сцене?" - WindowManager и GameController можно и так и так. Я просто делаю одним из методов.

А у окон есть метод Close, который обращается к WindowManager, и чтобы вручную не закидывать в редакторе в каждое окно ссылку на WindowManager я создаю их в коде.

LevelFactory является static классом, чтобы не создавать его экземпляр, соответственно ссылки на префабы тоже static.

Ответить
0

Всё понял, спасибо!

Ответить
0

Потому что кто-то пытался поиграть в проектирование но проиграл

Ответить
0

С вашими уроками на ютубе я бы такие комментарии не писал

Ответить
0

Присылайте ваш кодец, разберу по полочкам за тридцать минуток в чём вы не правы

P.S: Нашёл репозиторий, обязательно сделаю выпуск про наивное проектирование с показательным примером от вас

Ответить
0

Да я не против конструктивной критики, только, думаю, имеет смысл дождаться пока я закончу этот проект ;)

Ответить
0

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

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

Удачи!

Ответить
1

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

От себя хочу сказать, что нравится стиль повествования автора и сложность (относительно большинства уроков по Unity на Youtube). Про простейшие вещи видео очень много, а вот про то, как все скомпоновать, чтобы постоянно не переделывать и не костылить, к сожалению, мало. Спасибо!

Ответить
0

Могу предположить, потому что данный проект делается в одиночку человеком, являющимся в первую очередь программистом. В данном случае все создание объектов через "new GameObject().AddComponent<>()" происходит в классе App, который является чем-то вроде локатора служб приложения (ru.wikipedia.org/wiki/Локатор_служб)

Ответить
0

"Почему нельзя просто оставить их на сцене?" что бы можно было редактировать уровни с сервера

Ответить
0

Вы что то путаете

Ответить

Фирменный мангал

0

Комментарий удален по просьбе пользователя

Ответить
0

удалено

Ответить
0

По закладкам можно судить сколько "Мамкиных девелоперов" на DTF)

Ответить
0

Все :) статей больше нет :)

Ответить

Комментарии

{ "jsPath": "/static/build/dtf.ru/specials/DeliveryCheats/js/all.min.js?v=05.02.2020", "cssPath": "/static/build/dtf.ru/specials/DeliveryCheats/styles/all.min.css?v=05.02.2020", "fontsPath": "https://fonts.googleapis.com/css?family=Roboto+Mono:400,700,700i&subset=cyrillic" }
null