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

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

#1 Начало
#2 Первый уровень

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

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) и поменяем цвет.

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

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

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

Далее создадим класс для шара. Он подписывается на событие свайпа у 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(); } } }

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

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

Изменим 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.

3737 показов
2.1K2.1K открытий
22 комментария

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

Ответить

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

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

Ответить

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

Ответить

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

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

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

Ответить

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

Ответить

Комментарий недоступен

Ответить

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

Ответить