Популярное
Свежее
Моя лента
Сообщения
Рейтинг
Пополнить Steam
Низкая комиссия
Темы
Игры
Офтоп
Гайды
Ночной музпостинг
Вопросы
Hollow Knight
Музыка
Творчество
Кино и сериалы
Арт
Показать все
DTF
О проекте
Правила
Реклама
Приложения
Аккаунт удален
Gamedev
11.08.2020

Статья удалена

В этой статье я расскажу, как задать цвет частиц приземления и следа за персонажем в зависимости от объекта, по которому движется персонаж.

Демонстрация работы системы частиц

Привет. Сейчас я работаю над игрой Pirates: Mystery of Skeleton Island. Я хочу рассказать о том, как мне удалось достичь эффекта окрашивания частиц после приземления и во врмя движения в зависимости от типа поверхности, которой касается пират. Работа ведётся в движке Unity (версии 2019.4), но этот подод применим в любой среде.

1. Добавим кастомные материалы

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

public enum MaterialType { Wood, Sand, Rock, SeaBottom, Roof, Water, None }; public class MaterialProperty : MonoBehaviour { public MaterialType materialType; }

Дальше прикрепляем скрипты к GameObject-ам, выбераем тип из выпадающего списка и сохраняем в префабы.

Вот так.
Вот так.

2. Создаём сами частицы

Чтобы не получился класс-монстр с кодом-спагетти, прибегая к принципу SRP, для менеджмента частицами игрока я создал класс PirateParticles.
Сначала добавлю поля для указания систем частиц в редакторе Unity. Для лучшей читаемости и удобства стоит добавить префикс 'Prefab'.

[SerializeField] private ParticleSystem landGround; [SerializeField] private ParticleSystem trailEffect;

У меня будет 2 системы частиц- landGround будет проигрываться при приземлении после прыжка, trailEffect- во время бега.
Дальше перечислю градиенты, из которых будут браться цвета частиц. Каждому градиенту соответствует свой MaterialType.

[SerializeField] private Gradient sandColorGradient; [SerializeField] private Gradient woodColorGradient; [SerializeField] private Gradient rockColorGradient; [SerializeField] private Gradient seaBottomColorGradient; [SerializeField] private Gradient waterColorGradient; [SerializeField] private Gradient roofColorGradient;

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

private ParticleSystem landInScene; private ParticleSystem trailsInScene;

Теперь повесим скрипт на персонажа, создадим системы частиц и определим значения в редакторе:

В подробности вдаватьсья не буду, можете продублировать одну систему.
В подробности вдаватьсья не буду, можете продублировать одну систему.
Статья удалена

Далее создаю функции, которые будут вызываться для спавна этих двух систем частиц, как очевидно из названия:

public void SpawnLandGroundParticles() { if (landInScene == null) { Vector3 spawnPosition = new Vector3(transform.position.x, transform.position.y, 0.0f); landInScene = Instantiate(landGround, spawnPosition, Quaternion.Euler(0, 0, 0), transform); } CheckGroundType(landInScene); landInScene.Play(); } public void SpawnTrailParticles() { // Здесь можно задать условие, при котором частицы спавниться не будут- у меня это прыжок пирата // И вернуться из функции с помощью return. if (trailsInScene == null) { Vector3 spawnPosition = new Vector3(transform.position.x, transform.position.y, 0.0f); trailsInScene = Instantiate(trailEffect, spawnPosition, Quaternion.Euler(0, 0, 0), transform); } CheckGroundType(trailsInScene); trailsInScene.Play(); }

3. Спавн частиц

Вызываю SpawnTrailParticles() из скрипта ввода и обработки состояний персонажа.
timeBtwTrails- таймер, инициализируйте со значением желаемого промежутка времени между спавном частиц бега.

public void Run() { // Движение if (timeBtwTrails <= 0 && Pirate.instance.rigidbody2d.velocity.x > 0.0f) { timeBtwTrails=0.3f; // У вас должна быть ссылка на скрипт частиц pirateParticles. // У меня он в синглтоне Pirate. Pirate.instance.pirateParticles.SpawnTrailParticles(); } else { timeBtwTrails -= Time.deltaTime; } }

SpawnLandGroundParticles() вызывается из всё того же скрипта.

if (isJustLanded) { if (Pirate.instance.State == PirateState.jump) { Pirate.instance.pirateParticles.SpawnLandGroundParticles(); // ... } }

4. Проверка материала поверхности.

Дальше наконец-то определим функцию CheckGroundType() в скрипте чатиц:

private void CheckGroundType(ParticleSystem currentParticleSystem) { OnceRaycastAll(currentParticleSystem); //MultipleRaycast(currentParticleSystem); }

Про MultipleRaycast я напишу ближе к концу статьи. Вы можете написать тело OnceRaycastAll() прямо в CheckGroundType(). Я оставил это для выяснения более надёжного способа проверки земли. В статье будут рассмотрены оба.
Определю OnceRaycastAll(), но сначала инициализирую высоту спрайта персонаж, и длину луча. В вашем случае знание может отличаться.

private float spriteHeight; private const float rayDistance = 0.25f; private void Start() { spriteHeight = GetComponentInChildren<SpriteRenderer>().bounds.size.y; }

Дальше я покажу функцию OnceRaycastAll().
Определяется она с тем же аргументом, что и CheckGroundType().

private void OnceRaycastAll(ParticleSystem currentParticleSystem) { }

Определю координаты спавна луча, который будет принимать в rays[] данные обо всех коллайдерах

Vector3 deltaPostiton = new Vector3(0.0f, -spriteHeight * 0.3f, 0.0f); RaycastHit2D[] rays = Physics2D.RaycastAll(transform.position + deltaPostiton, Vector2.down, rayDistance);

Дальше проверяем все коллайдеры в rays. Изначально был цикл freach(), но из его итерации нельзя выйти, используя break, поэтому я использую цик for():

for (int i = 0; i < rays.Length; i++) { }

Теперь напишем тело цикла. Сначала начертим сам наш луч, который будет рендериться только в редакторе:

if (rays[i].collider != null) { Debug.DrawRay(rays[i].point, rays[i].normal, Color.red, 20, true); }

Четвёртый аргумент- длительность отображения луча в секундах.
Дальше выйду из цикла, если коллайдер- триггер (например, хитбокс врага) или у его GameObject’а нет компонента MaterialProperty:

if (rays[i].collider.isTrigger || rays[i].collider.TryGetComponent<MaterialProperty>(out MaterialProperty materialProperty) == false) { Debug.LogError("Denied " + rays[i].collider.name); break; }

В противном случае возьму пробу материала, и окрашу систему частиц.

print("Applied materlal of " + rays[i].collider.name); ParticleSystem.MainModule ParticlesModule = currentParticleSystem.main; ParticlesModule.startColor = GetColorByMaterial(materialProperty.materialType); return;

Определение функции GetColorByMaterial():

private Color errorColor = Color.red; private Color GetColorByMaterial(MaterialType materialType) { switch (materialType) { case MaterialType.Wood: return woodColorGradient.Evaluate(Random.Range(0f, 1f)); case MaterialType.Sand: return sandColorGradient.Evaluate(Random.Range(0f, 1f)); case MaterialType.Rock: return rockColorGradient.Evaluate(Random.Range(0f, 1f)); case MaterialType.SeaBottom: return seaBottomColorGradient.Evaluate(Random.Range(0f, 1f)); case MaterialType.Roof: return roofColorGradient.Evaluate(Random.Range(0f, 1f)); case MaterialType.Water: return waterColorGradient.Evaluate(Random.Range(0f, 1f)); } return errorColor; }

Готово!

Проблема

RaycastAll возвращает облако (с триггером) 2 раза, а огромный триггер ворона- несчетное количество раз. Но крышу не распознаёт- хотя луч проходит через неё.
RaycastAll возвращает облако (с триггером) 2 раза, а огромный триггер ворона- несчетное количество раз. Но крышу не распознаёт- хотя луч проходит через неё.

У функции Physics2D.RaycastAll() есть проблема- она может возвращать один объект несколько раз, а другие игнорировать. Я спросил на форуме Unity о то,, как можно решить эту проблему, но, что ожидаемо, никто не ответил.
Как попытка исправить это недоразумение, я написал метод MultipleRaycast()- его отличие в том, что он создаёт несколько лучей, каждый из которых сохраняет информацию об одном- первом коллайдере в разных местах.

Спойлер: это не помогло.

private void MultipleRaycast(ParticleSystem currentParticleSystem) { MaterialType groundMaterialType = MaterialType.None; int iteratorRay = 0; do { groundMaterialType = GetGroundMaterial(iteratorRay); iteratorRay++; if (iteratorRay > 6) { //Не допустить ухода в бесконечный цикл Debug.LogError("Stopped: 5 iterations"); return; } } while (groundMaterialType == MaterialType.None); ParticleSystem.MainModule ParticlesModule = currentParticleSystem.main; ParticlesModule.startColor = GetColorByMaterial(groundMaterialType); }

GetGroundMaterial() создаёт один луч- всего не более 5 лучей, со сдвигом по x на deltaX * 0.3f, по y на - 0.1f * deltaX на каждой итерации.

private MaterialType GetGroundMaterial(int deltaX) { //Многократный вызовGetComponent()- плохая практика, но код написан на скорую руку. float xSpriteSize = (GetComponentInChildren<SpriteRenderer>().bounds.size.x); Vector3 deltaPostiton = new Vector3(-xSpriteSize * 0.5f + deltaX * 0.3f, -0.3f - 0.1f * deltaX, 0.0f); RaycastHit2D ray = Physics2D.Raycast(transform.position + deltaPostiton, Vector2.down, rayDistance); if (ray.collider != null) { Debug.DrawRay(ray.point, ray.normal, Color.red, 20, true); } if (ray.collider == null || ray.collider.isTrigger || ray.collider.TryGetComponent<MaterialProperty>(out MaterialProperty materialProperty) == false) { Debug.LogError("Denied " + ray.collider.name); return MaterialType.None; } print("Applied " + ray.collider.name); return materialProperty.materialType; }

Результат работы функции MultipleRaycast(), увы, всё тот же: несмотря на увеличившееся число рейкастов, при попадание в триггер ворона лучи не возвращают крышу.

Статья удалена

Костыль

Альтернативное решение- изменить цвет систем цастиц по умолчанию на наиболее часто используемый в игре- когда рейкаст не получает MaterialProperty- частицы рендерятся с последним использованным цветом. Для отладки и полной видимости у себя я установил цвета по умолчанию систем частиц trails- белый, land- черный.
Надеюсь, статья вам была полезна. Если нашли решение, которое полностью устраняет проблемы- прошу написать в комментарии. Если интересно читать больше тонкостей разработки- я веду микроблог и ютуб-канал, где рассказываю о разработке игр.

twitter.com
https://twitter.com/LGS_ru

#Unity