The Witness на Unity. Code Jorney

The Witness — це прекрасна гра, яка зачарувала мене з першого погляду і знайшла місце серед списку бажаного в Steam. Але чекати на роздачу чи розпродаж буде дуже довго... Тож саме час згадати, чому я став розробником ігор та повторити головну механіку головоломок власними лапками!

*Дизклеймер: рішення та код можуть бути не найкращими, тож для когось це буде гайд, а для когось план «як робити не треба». Також все описане нижче розробляється для 2-ої гри. Всім бажаю приємного читання і стартуємо в мій code jorney)*

Перш за все я вирішив загуглити, чи нема вже подібних проєктів. Механіка цікава, тож 100% хтось повинен був її повторити. Ну... З цінної інформації я знайшов лише пост на редіті та посилання на блог, у якому автор розповів про свій варіант реалізації. До речі оть посилання.

Проте така реалізація виявилася занадто складною для мене, а пояснень малувато. Отже треба придумувати власне! Через деякий час прийшла ідея, а згодом і відполірована версія. Отже, я вирішив трішки відійти від оригіналу, та дати власне відчуття від головоломок, що сформувало наступний список:

  1. Ручка рухається за допомогою інтерфейсів IDragHandler, IEndDragHandler
  2. Не можна рухатись назад
  3. Будь-яка помилка скидує стан та автоматично закриває поле
  4. Головоломка існує в world space, але її не можна побачити до відповідної взаємодії

Перша проблема не варта розгляду: кидаємо на камеру Raycaster, створюємо Event System, на ручку Rigidbody, вкидуємо запит до чату GPT та отримаємо робочий скрипт, який треба трішки підредачити під наші потреби: замінити назви та додати івент:

    public class DragObject : MonoBehaviour, IDragHandler, IEndDragHandler
    {
        public event Action OnRealse;
        
        [SerializeField] private Rigidbody2D _rb;
        
        private Vector3 _mouseOffset;
        private Vector3 _startPosition;

        private Camera _camera;
        ...
        public void OnDrag(PointerEventData eventData)
        {
            Vector3 mouseWorldPos = _camera.ScreenToWorldPoint(Input.mousePosition);
            mouseWorldPos.z = 0f;
            
            if (eventData.delta.magnitude == 0)
            {
                _mouseOffset = transform.position - mouseWorldPos;
            }
            
            _rb.MovePosition(mouseWorldPos + _mouseOffset);
        }

        public void OnEndDrag(PointerEventData eventData)
        {
            _rb.velocity = Vector2.zero;
            OnRealse?.Invoke();
        }
    }

Другий пункт вирішився доволі легко: у нас просто є поле, яке створене з квадратів та проміжків між ними. Останні заповнені тригерами, і якщо об’єкт виходить з нього, то тригер перетворюється на колайдер. Все, тепер ми спокійно рухаємо ручку і ніколи не маємо можливості повернутися назад!

До речі, на цьому етапі мені підказали маленький лайфгак: щоб швидко розставити об’єкти на сцені, можна використовувати Numeric field expressions. Тобто замість перетаскування, чи вписування у поля значень, можна вибрати всі сутності на сцені, написати в необхідну координату щось типу L(0, 10) і побачити, що вони стоятимуть на рівній відстані одна від одної у проміжку від 0 до 10. Вау! Стаття, у якій це пояснюється.

Тепер проблема полягає в скиданні станів. Трішки поторгувавшись, я примирився з думкою, що прийдеться створити менеджери, які зберігатимуть всі об’єкти та скидатимуть їх стан: точки, які необхідно пройти; філлери проміжків; позицію ручки та її хвіст. (Trail renderer врятував від ще більшої кількості костилів). Тепер просто через івент на ручці викликаємо у менеджерах функції на скидання і закриття головоломки. Шик🔥

Тепер в хід пішов DOTween та трішки архітектурних рішень. У нас є окрема сутність, яка хендлить у собі головоломку. При взаємодії з контролером викликається метод, який рухає пазл з позиції світу у центр екрану. Але у світі є свої колайдери, от в чому біда... Чи ні, якщо ми просто вимкнемо колізію певних леєрів у налаштуваннях білда.

Ну і щоб перевірити на те, завершили ми головоломку, чи ні, у менеджері точок ми можемо тримати лічильник, а в контролері кожної точки мати івент. Тепер при тригері ми будемо збільшувати змінну і за допомогою того ж івенту, що є на ручці, інвокати перевірку, чи всі точки пройдені.

На цьому механіка готова!

public class PathPointManager : MonoBehaviour
    {
        public bool IsAllPointsPassed => _currentPassPoints == _pointController.Length;
        
        [SerializeField] private PathPointController[] _pointController;

        private int _currentPassPoints;

        private void Start()
        {
            foreach (PathPointController point in _pointController)
            {
                point.OnPointPassed += IncreasePassedPoints;
            }
        }

        private void IncreasePassedPoints()
        {
            _currentPassPoints++;
        }
        
    }

У цій статті я розповів лише базові ідеї, проблеми, з якими я зіткнувся, та їх вирішення. Код ви можете знайти у моєму гітхабі, посилання. Також я не розглядав додаткові механіки, типу зв’язок кінця головоломки та двері, рух та взаємодія гравця зі світом. Сподіваюся вам сподобалося, пишіть коменти, та чекаю на ваші власні рішення. До зустрічі)

Підписуйтеся на Telegram-канал @gamedev_dou, щоб не пропустити найважливіші статті і новини про геймдев

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному1
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Підписатись на коментарі