Не застрять в текстурах после ремастеринга

Hyper Casual игра на Unity с нуля. #3 Геймплей

Создадим класс, отслеживающий ввод игрока (на мобильных устройствах - свайпы, в редакторе - стрелки на клавиатуре).

AwesomeCompany/Common/Input/Direction.cs:

namespace AwesomeCompany.Common.Input { public enum Direction { Up, Right, Down, Left } }

AwesomeCompany/Common/Input/SwipeDetector.cs:

using System; using UnityEngine; namespace AwesomeCompany.Common.Input { public class SwipeDetector : MonoBehaviour { public event Action<Direction> onSwipe; private bool _isStarted; private Vector2 _startPosition; private void Update() { #if UNITY_EDITOR CheckKeyboard(); #else CheckTouches(); #endif } private void CheckKeyboard() { if (UnityEngine.Input.GetKeyDown(KeyCode.UpArrow)) { onSwipe?.Invoke(Direction.Up); } else if (UnityEngine.Input.GetKeyDown(KeyCode.RightArrow)) { onSwipe?.Invoke(Direction.Right); } else if (UnityEngine.Input.GetKeyDown(KeyCode.DownArrow)) { onSwipe?.Invoke(Direction.Down); } else if (UnityEngine.Input.GetKeyDown(KeyCode.LeftArrow)) { onSwipe?.Invoke(Direction.Left); } } private void CheckTouches() { if (UnityEngine.Input.touchCount <= 0) return; var touch = UnityEngine.Input.GetTouch(0); switch (touch.phase) { case TouchPhase.Began: _isStarted = true; _startPosition = touch.position; break; case TouchPhase.Moved: if (!_isStarted) return; var direction = (touch.position - _startPosition).normalized; if (Mathf.Abs(direction.y) > Mathf.Abs(direction.x)) { if (direction.y > 0) { onSwipe?.Invoke(Direction.Up); _isStarted = false; } else if (direction.y < 0) { onSwipe?.Invoke(Direction.Down); _isStarted = false; } } else { if (direction.x > 0) { onSwipe?.Invoke(Direction.Right); _isStarted = false; } else if (direction.x < 0) { onSwipe?.Invoke(Direction.Left); _isStarted = false; } } break; } } } }

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

AwesomeCompany/Common/Input/Extensions.cs:

using System; using UnityEngine; namespace AwesomeCompany.Common.Input { public static class Extensions { public static Vector2Int Add(this Vector2Int index, Direction direction) { switch (direction) { case Direction.Up: return index + Vector2Int.up; case Direction.Right: return index + Vector2Int.right; case Direction.Down: return index + Vector2Int.down; case Direction.Left: return index + Vector2Int.left; default: throw new ArgumentOutOfRangeException(nameof(direction), direction, null); } } } }

Изменим класс Cell. Добавим метод Complete для закрашивания. Закрашивать будем просто подменяя материал.

using AwesomeCompany.RollerSplat.Dynamic; using UnityEngine; namespace AwesomeCompany.RollerSplat.Gameplay { public class Cell : MonoBehaviour { public Vector2Int index { get; protected set; } public CellType type { get; private set; } public bool isCompleted { get; private set; } [SerializeField] private Material _completedMaterial; [SerializeField] private MeshRenderer _meshRenderer; public void Initialize(Vector2Int index, CellType type) { this.index = index; this.type = type; transform.position = new Vector3(index.x, 0f, index.y); } public void Complete() { if (isCompleted) return; isCompleted = true; if (_meshRenderer == null || _completedMaterial == null) return; _meshRenderer.material = _completedMaterial; } } }

Создадим материал (Assets-> Create -> Material) и поменяем цвет.

Зайдем в префаб Floor, перетащим в Completed Material только что созданный материал, а в Mesh Renderer дочерний объект Plane.

Далее создадим класс для шара. Он подписывается на событие свайпа у SwipeDetector. При свайпе отправляет в GameController свое текущее положение и направление, получает в ответ список ячеек, которые нужно проехать до препятствия. Затем закрашивает их и телепортируется в последнюю (пока что не делаем плавное перемещение).

_/Scripts/Gameplay/Ball.cs:

using System; using System.Linq; using AwesomeCompany.Common.Input; using UnityEngine; namespace AwesomeCompany.RollerSplat.Gameplay { public class Ball : MonoBehaviour { public event Action onMoved; private SwipeDetector _swipeDetector; private Vector2Int _currentPosition; public void Teleport(Vector2Int currentPosition) { _currentPosition = currentPosition; transform.position = new Vector3(_currentPosition.x, 0f, _currentPosition.y); } public void Activate() { _swipeDetector.onSwipe += OnSwipe; } public void Deactivate() { _swipeDetector.onSwipe -= OnSwipe; } private void Awake() { _swipeDetector = gameObject.AddComponent<SwipeDetector>(); } private void OnSwipe(Direction direction) { var path = App.instance.gameController.GetCellsPath(_currentPosition, direction); if (path.Count <= 0) return; foreach (var cell in path) cell.Complete(); Teleport(path.Last().index); onMoved?.Invoke(); } } }

Откроем префаб шара и добавим на него скрипт.

Изменим GameController, добавив методы получения ячеек и проверки завершения уровня. При старте уровня - активируем шар, чтобы можно было его двигать, при завершении - деактивируем и показываем окно LevelPassedWindow. Также добавляем метод Clear для удаления ячеек и шара со сцены.

using System.Collections.Generic; using AwesomeCompany.Common.Input; using AwesomeCompany.RollerSplat.Dynamic; using AwesomeCompany.RollerSplat.UI.Windows; using UnityEngine; namespace AwesomeCompany.RollerSplat.Gameplay { public class GameController : MonoBehaviour { private Cell[,] _cells; private Ball _ball; public void Play(Level level) { Clear(); var xLength = level.cells.GetLength(0); var yLength = level.cells.GetLength(1); _cells = new Cell[xLength, yLength]; for (var x = 0; x < xLength; x++) { for (var y = 0; y < yLength; y++) { var cellType = level.cells[x, y]; var index = new Vector2Int(x, y); var cell = LevelFactory.CreateCell(cellType, index); _cells[x, y] = cell; } } _ball = LevelFactory.CreateBall(level.spawnPoint); _ball.onMoved += OnBallMoved; _ball.Activate(); var startCell = GetCellAt(level.spawnPoint); if (startCell != null) startCell.Complete(); } public List<Cell> GetCellsPath(Vector2Int startIndex, Direction direction) { var path = new List<Cell>(); var startCell = GetCellAt(startIndex); if (startCell == null) return path; while (true) { var nextCellIndex = startIndex.Add(direction); var nextCell = GetCellAt(nextCellIndex); if (nextCell == null || nextCell.type != CellType.Floor) break; path.Add(nextCell); startIndex = nextCellIndex; } return path; } public void Clear() { if (_cells != null) { for (var x = 0; x < _cells.GetLength(0); x++) { for (var y = 0; y < _cells.GetLength(1); y++) { var cell = _cells[x, y]; Destroy(cell.gameObject); } } _cells = null; } if (_ball != null) { Destroy(_ball.gameObject); _ball = null; } } private Cell GetCellAt(Vector2Int index) { if (index.x < 0 || index.y < 0 || index.x >= _cells.GetLength(0) || index.y >= _cells.GetLength(1)) return null; return _cells[index.x, index.y]; } private void OnBallMoved() { for (var x = 0; x < _cells.GetLength(0); x++) { for (var y = 0; y < _cells.GetLength(1); y++) { var cell = _cells[x, y]; if (cell.type != CellType.Floor) continue; if (!cell.isCompleted) return; } } _ball.Deactivate(); App.instance.windowManager.Show<LevelPassedWindow>(); } } }

Создадим класс окна "Уровень пройден". Сейчас добавим только кнопку выхода в главное меню.

_/Scripts/UI/Windows/LevelPassedWindow.cs:

using AwesomeCompany.Common.UI; using UnityEngine; using UnityEngine.UI; namespace AwesomeCompany.RollerSplat.UI.Windows { public class LevelPassedWindow : WindowBase { [SerializeField] private Button _mainMenuButton; protected override void Awake() { base.Awake(); _mainMenuButton.onClick.AddListener(OnMainMenuButtonClicked); } private void OnMainMenuButtonClicked() { App.instance.gameController.Clear(); App.instance.windowManager.Show<MainMenuWindow>(); Close(); } } }

Префаб окна создаем также, как создавали MainMenuWindow. Я добавил в него дополнительно текст и полупрозрачное изображение на фон.

Также изменим метод CreateBall в классе LevelFactory, чтобы он возвращал Ball вместо GameObject.

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 Ball _ballPrefab; private static readonly Cell _floorPrefab; private static readonly Cell _wallPrefab; static LevelFactory() { _container = new GameObject("LevelContainer").transform; _ballPrefab = Resources.Load<Ball>("Gameplay/Ball"); _floorPrefab = Resources.Load<Cell>("Gameplay/Floor"); _wallPrefab = Resources.Load<Cell>("Gameplay/Wall"); } public static Ball CreateBall(Vector2Int spawnPoint) { var ball = Object.Instantiate(_ballPrefab, _container); ball.Teleport(spawnPoint); 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, cellType); return cell; } } }

Запускаем игру и тестируем.

P.S. в статье не указывал, но местами старые переменные могут быть переименованы.

Все изменения в репозитории: github.com/sergeychemodanov/roller-splat.

{ "author_name": "Сергей Чемоданов", "author_type": "self", "tags": ["unity","tutorial","if","endif","else","2","1"], "comments": 22, "likes": 19, "favorites": 73, "is_advertisement": false, "subsite_label": "gamedev", "id": 71398, "is_wide": false, "is_ugc": true, "date": "Tue, 24 Sep 2019 22:07:43 +0300", "is_special": false }
0
22 комментария
Популярные
По порядку
Написать комментарий...
2

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

Ответить
6

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

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

Ответить
1

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

Ответить
2

Предлоди свой код, что лучше изменить и почему :)  я не против посмотреть на разные ретки кода 

Ответить
0

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

Ответить
0

Если не ошибаюсь, то пробрасование префабов в поля MonoBehaviour или ScriptableObject по факту ничем не отличается от кешированием их в переменные. Здесь как понимаю основная проблема в том, что используется статические поля для кеширования, из-за чего объекты будут постоянно висеть в памяти.

Ответить
0

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

Ответить
0

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

Вот, что пишут в основном мануале "Unity supports Resource Folders in the project to allow content to be supplied in the main game file yet not be loaded until requested (https://docs.unity3d.com/Manual/LoadingResourcesatRuntime.html).

Ответить
0

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

https://learn.unity.com/tutorial/assets-resources-and-assetbundles?signup=true#5c7f8528edbc2a002053b5a7

2.1. Best Practices for the Resources System
Don't use it.
This strong recommendation is made for several reasons:
Use of the Resources folder makes fine-grained memory management more difficult
Improper use of Resources folders will increase application startup time and the length of builds
As the number of Resources folders increases, management of the Assets within those folders becomes very difficult
The Resources system degrades a project's ability to deliver custom content to specific platforms and eliminates the possibility of incremental content upgrades
AssetBundle Variants are Unity's primary tool for adjusting content on a per-device basis

Более того - 

Only assets that are in the Resources folder can be accessed through Resources.Load(). However many more assets might end up in the “resources.assets” file since they are dependencies. (For example a Material in the Resources folder might reference a Texture outside of the Resources folder)

Ответить
0

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

Ссылку эту видел и согласен, что лучше без необходимости ручную загрузку ресурсов не использовать, но это лучше оставить на усмотрение автора статьи, прямого произвола здесь нет, особенно если он собирается делать процедурную генерацию уровня. В остальных случаях я бы подумал. (Я так понял он это использует, только потому что нравится, т.к. это способ работы с ресурсами через код, без других веских причин.)

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

Ответить
0

Ну если время загрузки увеличивается, то куда это всё грузится? ) 

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

Ответить
0

Хорошо, я провёл свое маленькое и независимое исследование и выяснилось, что в Editor, да, ассеты всегда кешируются, а те, которые помещены в папку ресурсов по-моему даже вдвойне. Я переместил большой ассет в папку ресурсов и игра запустилась значительно дольше, очень заметно. Но это всё в Editor.

Когда я сделал билд игры и убрал все ссылки из сцены на большой ассет, то профайлер показал, что игра жрет 20 мб. памяти, а когда поместил ссылку на ассет обратно в сцену, то профайлер показал 300мб.. А вот когда я переместил этот ассет в папку ресурсов и убрал все ссылки на него из сцены, то профайлер опять показал 20 мб., что говорит о том, что по-умолчанию все файлы из ресурсов не грузятся.

Могу согласиться только в одном, что когда большой файл находился в папке ресурсов, то запускать игру в Editor было и правда мучительно, т.е. вести разработку так точно муторно. (А вот в билде с этим все нормально.)

Ответить
0

Мне кажется заявление, что лучше писать в одном месте и установление родителя и координаты здесь не уместно. Создание объекта и его использование могут быть различным, например, если эти объекты использовались в каком-нибудь пулле, то там это очевидно: создание в одном месте (установление родителя) и установление координат в другом. Здесь я вижу похожий подход как и к пуллу. Но да, сэкономить строчки кода можно было, но не бросаться же из-за этого заявлять, что всё плохо!

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

Ответить
3

Это точно цикл обучающих статей? 

Ответить
3

Всегда помогает задуматься о мотиве обучающего.

Что он продает? Явно не помощь начинающим.

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

Ответить
1

Обожаю полотна кода без комментариев.

Ответить
–6

Зачем использовать англицизм "Hyper Casual", когда есть отличное русское слово "плохая".

Ответить
0

ту же FlappyBird например, я не могу назвать плохой) казуальненько? да. плохо? -нет)

Ответить

Подозрительный Абдужаббор

sloa
0

дроч-игра :)

Ответить
0

Спасибо за статьи! Продолжайте, очень интересно!

Ответить
0

Статьи я перестал писать, но снял пару видео, может понравятся: https://youtube.com/gamedevzone

Ответить
0

статьи удобнее читать, но видео тоже интересно конечно

Ответить
Комментарии
null