У цій статті ми розглянемо створення динамічної 2D води з найпростішої фізикою. Ми будемо використовувати рендер ліній, мешів. тригери і частки. Кінцевий результат з хвилями і бризками ви зможете додати в якусь свою гру. Я виклав те, що вийшло у мене на Unity (Unity 3D), але, використовуючи принципи з даної статті, ви зможете зробити те ж саме на будь-якому движку.
Отже, приступимо
Ми будемо створювати верхній контур води, використовуючи рендер ліній і багато вузлів, щоб це виглядало, як хвилі.
Нам потрібно буде зберігати позиції, швидкості і прискорення для кожного вузла, так що ми будемо використовувати масиви:
За допомогою LineRenderer ми створимо кордон нашої води, але нам все ще потрібна сама вода. Для цього ми будемо використовувати меши. Для їх зберігання нам також потрібен буде масив GameObject:
Ще нам потрібно буде відстежувати зіткнення з поверхнею води:
Тепер кілька констант:
Тут springconstant - «коефіцієнт жорсткості» наших хвиль (вони будуть рухатися подібно пружинному маятнику); damping - коефіцієнт гасіння (інакше хвиля, одного разу почавши гойдатися, не зупиниться ніколи); spread - коефіцієнт, який відповідає за швидкість поширення хвиль. Ви можете грати з цими значеннями, намагаючись досягти властивостей, найбільш нагадують справжню воду. Значення z (координату води по осі Z) ви можете змінювати в залежності від того, що повинно стояти ближче до переднього плану в вашому додатку.
Далі, нам потрібно зберігати розташування нашої води:
... І її зовнішній вигляд:
Об'єкт, в якому всі ці дані будуть зберігатися - щось на зразок менеджера, він же буде створювати воду. Створимо для цього функцію - SpawnWater (). У неї ми будемо передавати координату лівого краю води, ширину водного простору, а також його верхню і нижню межі:
створюємо вузли
Порахуємо кількість вузлів, яке нам потрібно:
Ми будемо використовувати п'ять вузлів на одиницю ширини, але ви можете змінювати це значення в пошуках свого балансу між продуктивністю і гладкістю води.
Тепер саме час налаштувати наш LineRenderer:
Після инициализируем змінні, які ми створювали вище:
... І заповнимо їх реальними значеннями:
В кінці цієї ділянки коду, як ви бачите, ми поміщаємо кожен вузол з LineRenderer на його місце.
Створюємо саму воду
Тут починається найцікавіше. Зараз у нас є межа води, але самої води немає. Тому зараз ми створимо для неї меши:
Тепер в меш потрібно передати кілька значень. Для початку - координати його кутів.
На діаграмі відзначені потрібні нам кути першого заважав. У загальному вигляді це буде виглядати так:
Як ви могли помітити, в нульовий осередку зберігається верхня ліва точка, в першій - верхня права, в другій - нижня ліва, а в третій - нижня права. Тепер нам потрібно вибрати, яку частину текстури ми хочемо використовувати для цього заважав. Ми хочемо використовувати всю, а значить нам потрібно просто написати:
Меши повинні складатися з трикутників. На щастя, будь-який чотирикутник можна розділити на два трикутника.
Як ви можете бачити, трикутник A складається з вершин 0. 1. 3. а трикутник B - з вершин 3. 2. 0. Ці шість чисел нам потрібно записати в окремий масив, в цьому ж порядку:
Тепер три створених нами масиву потрібно передати в меш:
Тепер у нас є меши, але немає GameObject -ів, щоб їх рендерить. Виправимо це:
Всі ці меши тепер є нащадками менеджера.
Налаштування детекторів зіткнень
Тут ми створюємо кілька BoxCollider -ів, призначаємо їм імена і робимо їх нащадками менеджера. Також ми призначаємо їм позиції - посередині між вузлами, визначаємо їх розмір і додаємо їм WaterDetector.
Ну і наступна функція буде оновлювати позиції наших вузлів:
Додамо трохи фізики
Щоб знаходити прискорення, швидкості і нові позиції вузлів, нам нам знадобляться закон Гука і метод Ейлера.
Отже, закон Гука говорить, що F = -k * x. де F - сила, з якою пружина прагне повернутися в початковий стан (не забувайте, що наші хвилі будуть складатися з безлічі маленьких пружин); k - коефіцієнт жорсткості (пам'ятаєте, ми записували його в константах?); x - розтягнення. За цією формулою ми будемо вираховувати прискорення для вузлів, значення розтягування буде вираховуватися як різниця між поточною позицією води і її базовим рівнем. Ще до сили ми будемо додавати швидкість, помножену на коефіцієнт гасіння - щоб хвилі не були нескінченними.
Як бачите, метод Ейлера дуже простий - ми просто кожен раз додаємо до позиції вузла його швидкість, а до швидкості додаємо прискорення. Зрозуміло, для точних обчислень більше б підійшло інтегрування Верье. але через використання гасіння це насилу представляється можливим. До того ж метод Ейлера на порядок простіше.
Тепер додамо трохи поширення хвиль. Створимо два масиви, в яких ми будемо зберігати різницю між висотами сусідніх вузлів, помножену на коефіцієнт поширення:
... І заповнимо їх, попутно змінюючи швидкості і висоти:
Як ви помітили, вище ми здійснюємо дії 8 разів. Це зроблено для більшої плавності.
Додамо зіткнення
Тепер у нас є вода, яка може не просто відображатися, але і переливатися. Тепер нам потрібна можливість її сколихнути!
Для цього створимо метод Splash (). який буде перевіряти місце і швидкість удару по воді:
Для початку перевіримо коректність переданої координати по Іксу:
Тепер зробимо так, щоб зберігати не абсолютна координата, а відстань від першого вузла:
Тепер потрібно обчислити, на який вузол доводиться удар:
Тут ми робимо буквально наступне:
- Ми беремо координату щодо першого вузла - xpos.
- Ділимо її на різницю між позицією останнього і першого вузла.
- Таким чином ми отримуємо дробове число, яке говорить нам, куди саме припав удар.
- Множимо отримане число на кількість кутів і округляємо до цілого.
Тепер прирівнюємо швидкість знайденого вузла до швидкості удару:
Ви можете вчинити інакше, наприклад, додати швидкість удару до поточної швидкості вузла або дейсвовать відповідно до закону збереження імпульсу.
Тепер додамо бризки:
Ви можете здивуватися тому, що я призначив стартову швидкість двічі. Причина у вбудованій системі частинок. яка призначає початкову швидкість як випадкове число між двома константами. На жаль, у нас немає особливого доступу до неї через скрипти, тому доводиться діяти так.
Далі я додав одну лінію, але ви можете її пропустити:
Бризки НЕ знищаться при зіткненні з іншими об'єктами, тому вам варто або призначити їм Z координату фону (у мене - 5), або зробити так, щоб частинки завжди потрапляли назад на воду. Я вибрав щось середнє:
Начебто все, так?
виявлення зіткнень
Ні, не так! Нам потрібно відстежувати зіткнення об'єктів з водою, інакше все це було написано дарма. Пам'ятайте клас WaterDetector. який ми згадували вище? Зараз ми займемося саме їм. Все, що нам потрібно від нього - лише один метод:
Тепер вам залишилося лише викликати SpawnWater () де-небудь у вашому коді і насолоджуватися прекрасним хвилями і бризками!