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
25 комментариев

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

18
Ответить

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

3
Ответить

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

14
Ответить

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

1
Ответить

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

3
Ответить

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

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

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

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

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

3
Ответить

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

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

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

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

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

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

1
Ответить