Некорректно настроенные коллайдеры — одна из самых частых и малозаметных проблем в Unity-проектах. Мы, как инженеры с 10+ летним опытом, видим это постоянно: персонаж проваливается сквозь пол на высокой скорости, пули исчезают, не попав в цель, физические объекты дрожат на статичной поверхности. Причины почти всегда в основах: неправильный тип коллайдера, отсутствие Continuous collision detection, неверная настройка слоёв. Наша команда гарантирует устранение этих проблем под ключ — пишите, оценим ваш проект.
Почему коллайдеры настроены неправильно?
Unity предлагает шесть примитивных коллайдеров: BoxCollider, SphereCollider, CapsuleCollider, MeshCollider, WheelCollider, TerrainCollider. Плюс 2D-аналоги. MeshCollider — главный источник проблем в руках неопытных разработчиков. Convex MeshCollider работает с Rigidbody корректно, но ограничен 255 полигонами. Non-convex не может применяться к динамическим объектам вообще — Unity выдаст предупреждение, но не заблокирует, и поведение будет неопределённым. Для персонажей и снарядов всегда используем примитивы; MeshCollider — только для статичной геометрии уровня.
CapsuleCollider оптимален для персонажей (вертикально) и пуль (горизонтально). Два параметра: radius и height. Типичная ошибка — капсула персонажа настроена по VisualMesh, а не по геймплейным нуждам: слишком широкая капсула делает персонажа «жирнее», чем он выглядит, и он не пролезает в узкие проходы. SphereCollider — самый дешёвый по вычислениям. Для снарядов, гранат и небольших предметов предпочтительнее CapsuleCollider, если форма позволяет.
Как настроить Layer Matrix?
Physics Layer Collision Matrix (Edit → Project Settings → Physics → Layer Collision Matrix) определяет, какие слои взаимодействуют. Некорректно выстроенная матрица приводит к: снарядам, попадающим в своих; trigger-зонам, реагирующим на окружение, а не только на игрока; врагам, толкающим друг друга при скоплении. Правильная структура слоёв для типичной игры: Default, Player, Enemy, Projectile, Environment, Trigger, Debris. Projectile взаимодействует с Player, Enemy, Environment — но не с Trigger, Debris, другими Projectile. Trigger не взаимодействует ни с чем физически — только OnTriggerEnter. При использовании Physics.Raycast или Physics.OverlapSphere всегда передавайте LayerMask явно — без маски каст проверяет все слои, включая невидимые UI-коллайдеры и зоны-триггеры, возвращая неожиданные хиты.
Collision Detection Mode: как победить тоннельный эффект
По умолчанию Rigidbody использует Discrete collision detection: позиция объекта проверяется в начале и конце каждого FixedUpdate. Если объект движется быстрее, чем размер_объекта / fixedDeltaTime единиц в секунду, он может «перепрыгнуть» сквозь тонкую геометрию — это тоннельный эффект. Пуля диаметром 0.1 units при скорости 50 m/s пролетает 50 * 0.02 = 1 unit за один FixedUpdate. Стена толщиной 0.5 units будет пропущена. Решения:
- Rigidbody.collisionDetectionMode = CollisionDetectionMode.Continuous — для динамических объектов, которые могут тоннелировать сквозь статику
- CollisionDetectionMode.ContinuousDynamic — для объектов, которые могут тоннелировать сквозь другие динамические объекты
- Для пуль с очень высокой скоростью — Physics.Raycast или Physics.SphereCast вместо физического Rigidbody вообще: это надёжнее и производительнее
Continuous режим дороже по CPU. Применяем только к быстрым снарядам и персонажам — не ко всем объектам подряд.
PhysicsMaterial и настройка трения
PhysicMaterial задаёт dynamicFriction, staticFriction и bounciness. Комбинирование двух материалов контактирующих объектов происходит по правилу frictionCombine и bounceCombine (Average, Minimum, Multiply, Maximum). Для персонажа на Rigidbody: PhysicMaterial с dynamicFriction = 0, staticFriction = 0, frictionCombine = Minimum. Без этого персонаж скользит по стенам, застревает на рёбрах, неожиданно тормозит на наклонных поверхностях. Для отскакивающих объектов: bounciness = 0.6, bounceCombine = Maximum. При bounceCombine = Average мяч, брошенный на поверхность с bounciness = 0, не отпрыгнет вообще — даже если у самого мяча высокое значение.
Compound Colliders и оптимизация
Сложные формы объектов описываем несколькими примитивными коллайдерами на дочерних объектах — compound collider. Один Rigidbody на родительском объекте управляет всей физической единицей. Это дешевле MeshCollider и точнее одного BoxCollider. Для транспортных средств: отдельные BoxCollider для корпуса, бамперов, колёсных арок. WheelCollider — специализированный компонент для реалистичного поведения подвески; не участвует в стандартных OnCollisionEnter событиях.
Что входит в настройку физики под ключ
| Этап |
Детали |
| Аудит текущей конфигурации |
Анализ коллайдеров, Layer Matrix, PhysicMaterial и collision detection |
| Проектирование и реализация |
Настройка типов коллайдеров, compound colliders, физических материалов |
| Оптимизация производительности |
Настройка Layer Matrix, уменьшение draw calls за счёт batching, настройка solver iteration |
| Тестирование |
Проверка на тоннельный эффект, стабильность физики на разных FPS |
| Документация и обучение |
Описание архитектуры, рекомендации для дальнейшей разработки |
Сроки: от 2 дней для базовой настройки персонажа до 8 недель для кастомного физического решателя. Стоимость рассчитывается индивидуально — свяжитесь с нами для оценки.
| Задача |
Срок |
| Базовая настройка коллайдеров персонажа + Layer Matrix |
1–2 дня |
| Физика транспорта (WheelCollider, suspension) |
3–7 дней |
| Система разрушаемых объектов (fractured meshes + Rigidbody) |
1–2 недели |
| Кастомный физический решатель (без PhysX) |
4–8 недель |
Типичные ошибки и как их избежать
Коллайдер перекрывает Renderer меш — персонаж визуально проходит сквозь стену, хотя физически упирается в невидимый барьер. Следите за соответствием габаритов коллайдера и меша. OnCollisionEnter не вызывается — один из объектов kinematic Rigidbody или StaticCollider без Rigidbody. OnCollisionEnter требует Rigidbody хотя бы на одном из объектов. Для статичной геометрии уровня достаточно OnTriggerEnter на trigger-зонах. Физические объекты дрожат на месте — solver iteration count слишком низкий или объект находится под действием конкурирующих сил. Увеличьте Default Solver Iterations с 6 до 10–12 для сложных сцен.
Наши инженеры с многолетним опытом помогут избежать этих ловушек. Закажите настройку физики — получите консультацию и готовое решение.
Программирование игр: геймплей и системная логика
К нам приходит проект с хаотичной архитектурой. Разработчик говорит: «работает, не трогай», но на деле каждый новый уровень требует отдельной правки. Типичная картина: контроллер персонажа на 2000 строк, где физика, анимация, UI и звук перемешаны в одном MonoBehaviour. Сохранение через PlayerPrefs с ключами типа "player_hp_current_value_int". Это не гипотетика — это результат отсутствия архитектурного решения в начале. Мы занимаемся геймплейным программированием более 7 лет и реализовали свыше 15 проектов — от мобильных гиперказуалок до PC-шутеров. Гарантируем, что после нашего вмешательства проект перестаёт быть «чёрным ящиком». Закажите бесплатный аудит кода — оценим состояние за 2 дня.
Мы берём такой код, проводим аудит и реорганизуем его в модульную систему. Геймплейное программирование — сердце игры. Здесь закладывается ощущение от управления, интеллект противников, честная физика и надёжная система прогресса. Сделано плохо — никакой арт не спасёт.
В материале разберём, как мы строим контроллеры, физику, ИИ и систему сохранений, чтобы игра работала предсказуемо и без багов. Если ваш проект уже страдает от хаотичной архитектуры — получите консультацию инженера до старта работ.
Контроллер персонажа
Почему контроллер персонажа задаёт тон всей игре? Первое, с чем сталкивается игрок — управление. Задержки, скользкое движение, «залипание» на препятствиях — всё это считывается мгновенно и портит впечатление раньше, чем игрок успевает увидеть геймплей. Базовый выбор сводится к двум вариантам: CharacterController или Rigidbody.
CharacterController — встроенный компонент Unity, специализированный для персонажей. Игнорирует физический движок для движения, но корректно обрабатывает ступени, наклонные поверхности и препятствия. Рекомендован для экшн-игр, платформеров, шутеров от первого лица — там, где нужен точный предсказуемый отклик.
Rigidbody — физический объект. Необходим, когда персонаж должен взаимодействовать с физическими объектами: толкать ящики, реагировать на взрывы, быть подброшенным. Требует аккуратной работы через FixedUpdate и осторожного отключения гравитации или трения, чтобы управление не казалось «плавающим».
Для большинства 3D-проектов мы используем CharacterController с кастомным обработчиком гравитации — это даёт контроль без артефактов физического движка. Для 2D — Rigidbody2D с constraints на вращение и тщательно настроенным Collision Detection Mode: Continuous. Каждое решение принимаем под жанр и целевую платформу.
Физика и коллизии
Rigidbody и коллайдеры — источник регулярных проблем, если их не настроить правильно с самого начала. Несколько правил, которые экономят время:
-
Collision Detection: Continuous для быстрых объектов (пули, снаряды) — иначе они «пролетают» сквозь тонкую геометрию
- Сложные меш-коллайдеры заменяем составными примитивами (Box + Capsule + Sphere) — это дешевле для физики на 70–80%
- Слои (
Physics Layers) и матрица коллизий в Physics Settings настраиваются в начале проекта — потом добавить их без рефакторинга очень болезненно
- Все физические вычисления — в
FixedUpdate, не в Update. Иначе поведение зависит от FPS
Соблюдение этих правил снижает количество багов с коллизиями на 90% уже на этапе прототипа.
Чек-лист типичных ошибок при настройке физики
- Частицы или UI-объекты с коллайдерами — невидимые препятствия для пуль
- Триггеры, висящие на объектах без
Rigidbody — события не срабатывают
- Скорость пули > 100 м/с без
Continuous Dynamic — сквозные пролёты
- Один коллайдер на сложную сетку вместо композитных — падение FPS на 40–50%
Как архитектура ИИ влияет на игровой опыт?
Плохой AI виден сразу: противники застревают в углах, атакуют через стены, предсказуемо патрулируют по одному маршруту. Разница между «работает» и «работает хорошо» здесь наиболее ощутима. Рассмотрим три уровня детализации.
Конечные автоматы (State Machines)
Самый распространённый подход — иерархический конечный автомат (HSM). Каждое состояние: Idle, Patrol, Chase, Attack, Dead — это класс или метод с входом, обновлением и выходом. Wikipedia: конечный автомат
public enum EnemyState { Idle, Patrol, Chase, Attack, Dead }
private void UpdateStateMachine() {
switch (_currentState) {
case EnemyState.Patrol:
UpdatePatrol();
if (CanSeePlayer()) TransitionTo(EnemyState.Chase);
break;
case EnemyState.Chase:
_navMeshAgent.SetDestination(_player.position);
if (InAttackRange()) TransitionTo(EnemyState.Attack);
if (!CanSeePlayer() && _lostSightTimer > 5f) TransitionTo(EnemyState.Patrol);
break;
// ...
}
}
State Machine хорошо работает для противников с небольшим числом состояний (5–8). При росте сложности — взрывной рост переходов между состояниями, код становится трудно читать и тестировать.
Behaviour Trees
Behaviour Tree — следующий уровень. Дерево поведения описывает логику агента через иерархию задач: Sequence, Selector, Decorator, Leaf. Wikipedia: дерево поведения
Преимущество перед State Machine: каждый узел атомарен и переиспользуем. Узел CheckLineOfSight написан один раз и используется в десяти деревьях. Добавить новое поведение — значит добавить ветку в дерево, не рефакторить существующую логику.
В Unity BT реализуется через ассеты (NodeCanvas, Behaviour Designer) или кастомную реализацию. Для крупных проектов с несколькими типами врагов это окупается уже на этапе второго типа противника. Пример структуры дерева для патрульного противника:
Root
└── Selector
├── Sequence (Combat)
│ ├── IsPlayerVisible
│ ├── IsPlayerInRange
│ └── AttackPlayer
├── Sequence (Alert)
│ ├── HeardSound
│ └── InvestigatePosition
└── Sequence (Patrol)
├── HasPatrolRoute
└── FollowPatrolRoute
GOAP — когда BT недостаточно
Goal-Oriented Action Planning — подход для действительно сложного AI, где агент должен планировать последовательность действий для достижения цели с учётом текущего состояния мира. Классический пример — противник, которому нужно «убить игрока». Если у него нет оружия, он ищет оружие. Если нет боеприпасов, он ищет патроны. Если игрок укрылся, он ищет обходной маршрут. GOAP позволяет задать эти действия и их предусловия/постусловия, а планировщик строит цепочку автоматически.
GOAP значительно сложнее в реализации, чем BT, и оправдан не всегда. Для платформеров и казуальных игр это избыточно. Для тактических игр, симуляторов выживания, стелс-экшн — может быть правильным выбором.
NavMeshAgent и навигация
NavMeshAgent — стандартный инструмент для навигации в Unity. Работает корректно при правильной настройке NavMesh и агентов:
-
Agent Radius и Agent Height должны точно соответствовать коллайдеру персонажа
-
Stopping Distance нужно настраивать под дальность атаки каждого типа врага
-
NavMesh Obstacle с Carve: true для динамических препятствий (падающие ящики, закрывающиеся двери) — иначе агенты будут пытаться пройти сквозь них
- Для больших открытых миров —
NavMesh Links для соединения отдельных сегментов и Off-Mesh Links для прыжков и спусков
| Критерий |
State Machine |
Behaviour Tree |
| Переиспользование логики |
Низкое (состояния привязаны к контексту) |
Высокое (узлы независимы) |
| Масштабирование |
Взрыв переходов при 10+ состояний |
Линейный рост дерева |
| Отладка |
Сложная (требуется полный стейт-трекер) |
Простая (видно текущий узел) |
| Сложность внедрения |
Низкая (старт за 1 день) |
Средняя (3–5 дней на настройку) |
| Рекомендуемый объём |
До 6 типов врагов |
От 6 типов врагов |
Почему система сохранений требует версионирования?
Вторая область, где архитектурные решения в начале критически влияют на всё последующее. Сохранения, добавленные в конце разработки «за неделю», почти всегда ломаются при изменении структуры данных. Рассмотрим инструменты.
PlayerPrefs — когда подходит и когда нет
PlayerPrefs — это хранилище простых ключ-значение (string, int, float). Подходит строго для настроек (громкость, управление, язык). Использовать его для хранения состояния игрового мира — ошибка: нет типизации, нет версионирования, нет удобного дебага.
JSON-сериализация
Рабочий подход для большинства проектов — сериализация данных в JSON через JsonUtility (встроенный, быстрый, но ограниченный) или Newtonsoft.Json (полноценный, поддерживает словари, наследование, nullable типы). Структура системы сохранений:
[Serializable]
public class SaveData {
public int version = 1; // версионирование
public PlayerSaveData player;
public WorldSaveData world;
public SettingsSaveData settings;
}
public class SaveSystem : MonoBehaviour {
private const string SAVE_FILE = "/save.json";
public void Save(SaveData data) {
string json = JsonConvert.SerializeObject(data, Formatting.Indented);
File.WriteAllText(Application.persistentDataPath + SAVE_FILE, json);
}
public SaveData Load() {
string path = Application.persistentDataPath + SAVE_FILE;
if (!File.Exists(path)) return new SaveData();
string json = File.ReadAllText(path);
return JsonConvert.DeserializeObject<SaveData>(json);
}
}
ScriptableObject как контейнер данных
ScriptableObject — недооценённый инструмент для хранения игровых данных. Конфигурации предметов, характеристики противников, параметры уровней — всё это удобнее держать в ScriptableObject, чем в JSON или константах в коде. Для сохранений ScriptableObject используется в паттерне Runtime Set и Variable: значения хранятся в ScriptableObject, сохранение записывает только дельту относительно дефолтных значений.
Версионирование сохранений
Поле version в корне SaveData — не бюрократия, а необходимость. Когда после релиза добавляется новая механика с новыми полями, нужно корректно мигрировать старые сохранения. Миграционный метод:
private SaveData MigrateSaveData(SaveData data) {
if (data.version < 2) {
data.player.newField = defaultValue;
data.version = 2;
}
if (data.version < 3) {
// следующая миграция
data.version = 3;
}
return data;
}
Без версионирования приходится выбирать между сломанными сохранениями у игроков или отказом от изменения структуры данных.
ScriptableObject-архитектура
Для средних и крупных проектов мы используем подход, популяризированный Ryan Hipple на GDC: ScriptableObject как шина событий и хранилище разделяемого состояния.
// Переменная-событие
[CreateAssetMenu]
public class GameEvent : ScriptableObject {
private List<GameEventListener> _listeners = new();
public void Raise() {
for (int i = _listeners.Count - 1; i >= 0; i--)
_listeners[i].OnEventRaised();
}
}
Это позволяет системам в игре взаимодействовать без прямых ссылок друг на друга. PlayerHealth не знает о UI, UI не знает о GameManager — все они знают только о ScriptableObject-событиях. Проект становится значительно легче тестировать и расширять.
Как мы работаем: этапы и результат
Каждый проект проходит через пять этапов. Ниже — ориентировочные сроки и ключевые артефакты. Стоимость каждого этапа рассчитывается индивидуально, ориентир: один модуль (например, система сохранений) — от 20 000 до 50 000 рублей в зависимости от сложности.
| Этап |
Длительность (рабочие дни) |
Результат |
| Аудит текущей архитектуры |
1–3 |
Документ с проблемами и рекомендациями |
| Проектирование систем |
2–5 |
Архитектурная схема, описание модулей |
| Реализация (итерации) |
от 10 |
Рабочий код, протестированный в связке с артом |
| Код-ревью и рефакторинг |
2–4 |
Чистая кодовая база, комментарии к сложным участкам |
| Документация и сдача |
1–2 |
Гайд для команды, описание настроек (Physics Layers, NavMesh и т.д.) |
Сроки варьируются в зависимости от объёма legacy-кода и сложности механик. Получите консультацию инженера до старта работ — убедимся, что подход подходит именно вашему проекту.
Что входит в работу
После завершения вы получаете:
- Архитектурную документацию игровых систем (контроллер, AI, сохранения)
- Исходный код с комментариями, готовый к дальнейшей разработке
- Настройки инструментов: Physics Layers, NavMesh, проект ScriptableObject‑событий
- Код-ревью существующих модулей (если проект не с нуля)
- Поддержку на этапе интеграции (2 недели после сдачи)
Свяжитесь с нами, чтобы обсудить детали. Закажите аудит архитектуры — это бесплатно и займёт не более 3 рабочих дней.