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.

2323
22 комментария

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

2
Ответить

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

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

6
Ответить

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

3
Ответить

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

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

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

3
Ответить

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

1
Ответить

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

1
Ответить

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

Ответить