Разработка игр на C# с нуля: от основ до первого проекта

Этот курс предлагает сбалансированное изучение теории C# и практики создания игр для абсолютных новичков, опираясь на популярные инструменты вроде Unity [habr.com](https://habr.com/ru/articles/828302/). Вы пройдете путь от базовых концепций программирования до продвинутых механик, работы с графикой, UI и оптимизации производительности [dotnet.microsoft.com](https://dotnet.microsoft.com/ru-ru/apps/games/unity). В результате вы спроектируете, разработаете и соберете свою первую полноценную игру.

1. Введение в программирование и язык C#

Введение в программирование и язык C#

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

Все эти правила, условия и законы физики виртуального мира не появляются сами по себе. Их создает человек с помощью программирования.

Что такое программирование на самом деле

В своей основе программирование — это процесс объяснения невероятно быстрому, но совершенно лишенному фантазии исполнителю (компьютеру), что именно он должен сделать. Компьютер не понимает человеческую речь, эмоции или контекст. Ему нужны предельно точные, однозначные инструкции.

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

Если вы когда-нибудь собирали мебель по инструкции или готовили блюдо по рецепту, вы уже сталкивались с алгоритмами. В разработке игр алгоритмы управляют всем: от искусственного интеллекта врагов до расчета урона от взрыва гранаты. Чтобы передать эти алгоритмы компьютеру, разработчики используют языки программирования — специальные системы символов и правил, которые служат мостом между человеческой логикой и машинным кодом.

> Программы должны писаться для людей, которые будут их читать, и лишь попутно — для машин, которые будут их выполнять. > > Харольд Абельсон, профессор MIT

Почему для создания игр выбирают C#

Язык C# (си-шарп) был разработан компанией Microsoft в начале 2000-х годов. Изначально он создавался для корпоративных программ и веб-приложений, но со временем стал одним из главных стандартов в мировой игровой индустрии.

Главная причина популярности C# в геймдеве — это игровой движок Unity. Движок — это набор готовых инструментов, который берет на себя самую сложную рутину (отрисовку графики, расчет столкновений объектов, воспроизведение звука), позволяя вам сосредоточиться на создании самой игры. Unity использует C# как основной язык для написания игровой логики.

Чтобы понять место C# в индустрии, полезно сравнить его с другими популярными языками:

| Язык программирования | Сложность изучения | Производительность | Основное применение в играх | | :--- | :--- | :--- | :--- | | C# | Средняя | Высокая | Игры на Unity, логика, мобильные и инди-игры | | C++ | Очень высокая | Максимальная | AAA-игры (крупнобюджетные), движок Unreal Engine | | Python | Низкая | Низкая | Написание вспомогательных скриптов, серверная часть | | JavaScript | Низкая | Средняя | Простые браузерные игры |

C# предлагает идеальный баланс. Он работает достаточно быстро, чтобы обрабатывать сложную игровую физику в реальном времени (выдавая стабильные 60 кадров в секунду), но при этом берет на себя управление памятью. В C# встроен сборщик мусора (Garbage Collector) — специальный механизм, который автоматически очищает оперативную память от данных, которые игре больше не нужны (например, от текстур уничтоженных врагов). В языках вроде C++ разработчику приходится делать это вручную, что часто приводит к ошибкам и вылетам игры.

От текста к работающей игре: как компьютер понимает код

Когда вы пишете код на C#, вы создаете исходный код. Это текст, состоящий из английских слов, математических знаков и скобок. Вы можете прочитать его и понять логику, но процессор вашего компьютера понимает только электрические сигналы, которые в программировании представляются как нули и единицы (машинный код).

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

Процесс выглядит так:

  • Вы пишете правила игры на C# (например, логику получения урона).
  • Вы нажимаете кнопку «Запуск».
  • Компилятор мгновенно анализирует ваш текст на наличие синтаксических ошибок (не пропущена ли запятая, правильно ли написано слово).
  • Если ошибок нет, компилятор переводит C# в промежуточный код, а затем в машинные инструкции.
  • Процессор выполняет эти инструкции, и на экране уменьшается полоска здоровья персонажа.
  • Если вы допустите хотя бы одну опечатку, компилятор откажется собирать игру и выдаст ошибку. C# — это язык со строгими правилами, и он не терпит приблизительности.

    Анатомия первой программы

    Традиционно изучение любого языка начинается с программы, которая просто выводит текст на экран. В контексте игр представим, что мы выводим системное сообщение в консоль разработчика при загрузке уровня.

    Вот как выглядит минимальная рабочая программа на C#:

    Разберем этот код по частям, так как каждая строчка имеет смысл:

    * using System; — это подключение базовой библиотеки. Мы говорим компьютеру: «Подготовь стандартные инструменты системы, они нам понадобятся». * class GameLevel — объявление класса. В C# весь код должен находиться внутри классов. Класс можно представить как чертеж или контейнер. В данном случае это контейнер для логики нашего игрового уровня. * static void Main() — это точка входа. Когда игра запускается, компьютер ищет метод с именем Main и начинает выполнять команды ровно с этого места. Фигурные скобки { и } обозначают начало и конец блока команд. * Console.WriteLine("..."); — это сама команда (инструкция). Она приказывает вывести текст в консоль. Обратите внимание на точку с запятой ; в конце строки. В C# это аналог точки в конце предложения в русском языке. Без нее компилятор не поймет, где заканчивается одна команда и начинается следующая.

    Базовые строительные блоки: переменные и типы данных

    Игры состоят из данных. Координаты игрока в пространстве, количество патронов в магазине, имя гильдии, статус отравления ядом — всё это данные, которые постоянно меняются.

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

    В C# вы не можете просто положить что угодно в любую коробку. Язык требует заранее указать тип данных — форму и размер коробки. Это называется строгой типизацией. Она защищает игру от нелепых ошибок (например, попытки умножить имя игрока на количество патронов).

    Рассмотрим четыре основных типа данных, которые используются в 99% игровых механик:

    1. Целые числа (int)

    Тип int (от слова integer) хранит целые числа без дробной части. Идеально подходит для счетчиков, которые не могут быть дробными. Примеры в играх: количество жизней, уровень персонажа, количество золотых монет.

    2. Числа с плавающей точкой (float)

    Тип float хранит дробные числа. В геймдеве это самый популярный числовой тип, так как физика и перемещение в пространстве требуют высокой точности. Примеры в играх: координаты X/Y/Z в 3D-мире, скорость передвижения, время перезарядки в секундах. При записи таких чисел в C# обязательно ставится буква f на конце.

    3. Строки текста (string)

    Тип string используется для хранения любого текста. Значение всегда заключается в двойные кавычки. Примеры в играх: имя персонажа, диалоги NPC, названия предметов в инвентаре.

    4. Логический тип (bool)

    Тип bool (от слова boolean) — это самый простой тип, который может хранить только два значения: true (истина) или false (ложь). Это идеальный инструмент для создания переключателей состояний. Примеры в играх: жив ли игрок, открыта ли дверь, находится ли персонаж в воздухе.

    Математика и физика в коде

    Создав переменные, мы можем заставить их взаимодействовать. Вся игровая динамика строится на математических операциях. В C# доступны стандартные арифметические действия: сложение +, вычитание -, умножение * и деление /.

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

    Базовая формула расчета остатка здоровья выглядит так:

    Где: * — здоровье врага после попадания. * — текущее здоровье врага до попадания. * — базовый урон оружия. * — коэффициент брони (например, 0.5 означает, что броня поглощает половину урона).

    Если перенести эту логику в код C#, мы получим следующее:

    В этом примере при базовом уроне 40 и коэффициенте брони 0.5, итоговый урон составит 20. Здоровье врага уменьшится со 100 до 80. Именно так, строка за строкой, математика превращается в игровой процесс.

    Итоги

    * Программирование — это создание точных пошаговых инструкций (алгоритмов) для компьютера. * C# является стандартом в индустрии разработки игр благодаря балансу между высокой производительностью и удобством использования, а также тесной интеграции с движком Unity. * Компилятор выступает в роли переводчика, превращая понятный человеку код на C# в машинные нули и единицы. * Переменные — это контейнеры для хранения данных игры. Для разных данных нужны разные типы: int для целых чисел, float для дробных, string для текста и bool для состояний (да/нет). * Каждая команда в C# должна завершаться точкой с запятой ;, а блоки кода группируются с помощью фигурных скобок { }.

    10. Проектирование игровой логики и базовых механик

    Проектирование игровой логики и базовых механик

    Представьте, что вы стоите перед закрытой виртуальной дверью. Вы нажимаете кнопку взаимодействия. Дверь мгновенно исчезает, а в инвентаре появляется надпись «Ключ удален». Технически задача выполнена: преграда устранена, условие соблюдено. Но почувствует ли игрок удовольствие от такого процесса? Скорее всего, нет.

    В прошлых уроках мы научились перемещать объекты в пространстве с помощью игрового цикла и управлять временем через Time.deltaTime. Теперь нам предстоит превратить сухое перемещение математических координат в увлекательный игровой процесс. Для этого необходимо научиться проектировать игровые механики — фундаментальные правила, по которым живет ваш виртуальный мир.

    Анатомия игровой механики

    Любая игра, от простейшего тетриса до масштабной ролевой игры, состоит из набора механик. Игровая механика (Game Mechanic) — это способ взаимодействия игрока с игровым миром в рамках установленных ограничений.

    Профессиональные геймдизайнеры разбивают любую механику на три обязательных этапа:

  • Действие (Action): Игрок или система принимает решение и подает команду (нажатие кнопки, срабатывание таймера).
  • Изменение (Change): Внутреннее состояние игры пересчитывается (уменьшается здоровье, изменяются координаты).
  • Обратная связь (Feedback): Игра сообщает игроку о результате изменений через графику, звук или интерфейс.
  • > Механики — это тайные пружины и шестеренки, скрытые под капотом игры. Игрок видит лишь движение стрелок на циферблате, но именно механики заставляют их двигаться. > > Джесси Шелл, автор книги «Геймдизайн»

    Рассмотрим механику стрельбы из лука. Если игрок нажимает левую кнопку мыши (Действие), скрипт отнимает одну стрелу из переменной ammo и создает объект стрелы в координатах лука (Изменение). Сразу после этого проигрывается анимация натяжения тетивы, звучит свист, а на экране обновляется счетчик боеприпасов (Обратная связь).

    Если вы уберете хотя бы один из этих элементов, механика сломается. Без обратной связи игрок не поймет, выстрелил ли он. Без изменения состояния стрельба станет бесконечной и бессмысленной.

    Управление состояниями: Конечный автомат

    Когда новички начинают писать логику персонажа, они часто используют десятки проверок if. Например: «Если игрок нажал пробел, и он не в воздухе, и он не атакует, и он не оглушен — тогда прыгать». С добавлением новых механик такой код быстро превращается в запутанный клубок, где одно действие ломает другое.

    Для элегантного решения этой проблемы в программировании используется паттерн Конечный автомат (Finite State Machine, FSM). Его суть в том, что объект в любой момент времени может находиться только в одном конкретном состоянии из заранее заданного списка.

    В языке C# для создания списка состояний идеально подходит перечисление enum.

    Такой подход дает жесткий контроль над логикой. Если персонаж находится в состоянии Attacking, метод CheckForMovement() просто не будет вызываться. Игрок физически не сможет начать бежать, пока анимация удара не завершится и состояние не вернется в Idle.

    | Состояние | Доступные переходы | Визуальная обратная связь (Анимация) | | :--- | :--- | :--- | | Idle | В Running, в Jumping, в Attacking | Персонаж спокойно дышит | | Running | В Idle, в Jumping, в Attacking | Цикл бега, пыль из-под ног | | Jumping | Только в Idle (при приземлении) | Поза полета, тень на земле | | Attacking | Только в Idle (после удара) | Взмах мечом, вспышка света |

    Взаимодействие объектов: Триггеры и Коллайдеры

    Логика игры строится на взаимодействии сущностей. Персонаж должен подбирать монеты, получать урон от шипов и открывать двери. В игровых движках за это отвечает физическая система.

    Каждый объект, который должен взаимодействовать с миром, имеет невидимую оболочку — Коллайдер (Collider). Коллайдеры бывают двух типов, и выбор правильного типа критически важен для механики:

  • Твердые тела (Collision): Объекты сталкиваются и отталкивают друг друга. Используется для стен, пола, ящиков. Движок рассчитывает силу удара и отскок.
  • Триггеры (Trigger): Объекты проходят друг сквозь друга, но движок фиксирует сам факт пересечения. Идеально для зон обнаружения, сбора предметов и финишных линий.
  • Рассмотрим механику сбора золотой монеты. Монета должна быть триггером, чтобы игрок не спотыкался об нее, как о кирпич.

    В этом примере при пересечении невидимых границ объектов срабатывает событие. Логика монеты проверяет тег вошедшего объекта. Если это игрок, вызывается метод пополнения инвентаря, после чего монета удаляется из памяти.

    Связь логики с пользовательским интерфейсом (UI)

    Собрав монету, игрок должен узнать об этом. Здесь вступает в работу Пользовательский интерфейс (User Interface, UI).

    Главное правило проектирования UI в играх: интерфейс не должен сам искать данные, данные должны сообщать интерфейсу об изменениях. Начинающие разработчики часто совершают ошибку, заставляя UI каждый кадр проверять количество монет.

    Если у игрока 150 монет, этот код 60 раз в секунду будет стирать текст «Монеты: 150» и писать его заново. Это пустая трата ресурсов процессора. Правильный подход — обновлять текст только в момент изменения значения.

    При таком подходе, если игрок гуляет по уровню 10 минут и не собирает монеты, интерфейс не потребляет вычислительные ресурсы вообще. Это базовая оптимизация, которая спасет вашу игру от зависаний на слабых мобильных устройствах.

    Оптимизация математики в игровых механиках

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

    Для расчета дистанции между двумя точками в 2D или 3D пространстве используется теорема Пифагора:

    Где: * — итоговая дистанция между объектами. * — координаты турели. * — координаты врага.

    Проблема этой формулы кроется в операции извлечения квадратного корня (). Для процессора это одна из самых тяжелых математических задач. Если на уровне 50 турелей и 100 врагов, и они проверяют дистанцию каждый кадр, игра начнет тормозить.

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

    Оптимизированная формула выглядит так:

    Допустим, турель находится в координатах (0, 0), а враг в координатах (6, 8). Радиус атаки турели равен 15 метрам.

  • Считаем разницу координат: по оси X это 6, по оси Y это 8.
  • Возводим в квадрат и складываем: .
  • Вместо того чтобы извлекать корень из 100 (получая 10) и сравнивать с 15, мы возводим радиус атаки в квадрат: .
  • Сравниваем квадраты: . Враг в зоне поражения!
  • В коде на C# (в движке Unity) это реализуется через встроенное свойство sqrMagnitude:

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

    Итоги

    * Игровая механика всегда состоит из трех этапов: действия игрока, изменения математического состояния игры и понятной визуальной или звуковой обратной связи. * Использование паттерна Конечный автомат (через enum и switch) позволяет избежать запутанного кода и четко разделить состояния персонажа (бег, прыжок, атака). * Для взаимодействия объектов используются Коллайдеры (для физических столкновений) и Триггеры (для фиксации пересечения зон без физического сопротивления). * Пользовательский интерфейс (UI) следует обновлять только в момент фактического изменения данных (по событию), а не каждый кадр в методе Update(). * Для оптимизации проверок расстояния следует использовать квадрат дистанции (sqrMagnitude), чтобы избежать ресурсоемкой операции извлечения квадратного корня.

    11. Обработка пользовательского ввода

    Обработка пользовательского ввода

    Представьте, что вы играете в напряженный хоррор на выживание. За вами гонится монстр, вы добегаете до спасительной двери и в панике нажимаете кнопку на клавиатуре, чтобы ее открыть. Если игра отреагирует мгновенно — вы спасены и чувствуете прилив адреналина. Если игра «не заметит» нажатия или отреагирует с задержкой в полсекунды — ваш персонаж погибнет, а вы испытаете лишь раздражение от сломанной механики.

    В предыдущих статьях мы создали фундамент виртуального мира: научились писать классы, управлять временем через игровой цикл и проектировать базовые механики. Но пока наш мир напоминает аквариум с рыбками — он живет сам по себе, а мы можем лишь наблюдать за ним через стекло экрана. Чтобы превратить симуляцию в настоящую игру, нам нужно разрушить эту стену и дать игроку власть над происходящим.

    Процесс передачи команд от человека к компьютеру называется обработкой пользовательского ввода (Input Handling). Это мост между физическим миром (клавиатурами, мышками, геймпадами) и математической логикой C#.

    Консольный ввод: истоки интерактивности

    Прежде чем переходить к сложным игровым движкам, важно понять, как язык C# работает с вводом на самом базовом уровне. В классических текстовых программах компьютер буквально останавливает свою работу и ждет, пока пользователь напечатает текст и нажмет клавишу Enter.

    Для этого используется встроенный метод Console.ReadLine(). Он считывает все введенные символы и возвращает их в виде строки (string).

    Проблема заключается в том, что все данные, приходящие с клавиатуры, изначально являются текстом. Если вы попросите игрока ввести его возраст или количество золота, вы не сможете сразу использовать эти данные в математических формулах. Строку "25" нельзя умножить на 2, так же как нельзя умножить слово "Яблоко".

    Данные необходимо конвертировать. Для безопасного преобразования текста в числа профессионалы используют метод TryParse.

    Такой подход отлично работает для пошаговых текстовых квестов. Но в динамичных играх (шутерах, платформерах) мы не можем просить игрока печатать слово «Прыжок» и нажимать Enter каждый раз, когда перед ним появляется пропасть. Нам нужно считывать состояние клавиш десятки раз в секунду, не останавливая игровой цикл.

    Классическая система ввода в Unity: опрос состояний

    В игровых движках, таких как Unity, обработка ввода происходит внутри метода Update(), который, как мы помним, вызывается каждый кадр. Движок предоставляет специальный класс Input, который позволяет мгновенно узнать, что происходит с любой кнопкой на клавиатуре или мыши в данную миллисекунду.

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

    | Метод в C# | Когда срабатывает (возвращает true) | Идеальное применение в играх | | :--- | :--- | :--- | | Input.GetKeyDown | Ровно в тот кадр, когда кнопка была нажата вниз. | Одиночные действия: прыжок, выстрел из пистолета, открытие инвентаря. | | Input.GetKey | Каждый кадр, пока кнопка удерживается нажатой. | Непрерывные действия: бег вперед, стрельба из пулемета, зарядка лука. | | Input.GetKeyUp | Ровно в тот кадр, когда кнопка была отпущена. | Завершение действия: выпуск стрелы из лука, остановка скольжения. |

    Рассмотрим механику стрельбы. Если вы используете GetKey для снайперской винтовки, то при малейшем зажатии кнопки мыши на долю секунды (например, на 10 кадров), игра выстрелит 10 раз подряд. Игрок мгновенно потратит все патроны. Для одиночных выстрелов нужен строгий контроль через GetKeyDown.

    Виртуальные оси: секрет плавного управления

    Чтение конкретных кнопок (например, KeyCode.W для движения вперед) — это простой, но устаревший подход. Что если игрок захочет управлять персонажем с помощью стрелочек? А если он подключит геймпад от Xbox, где вместо кнопок используются стики?

    Чтобы не писать десятки проверок if для каждого возможного устройства, разработчики используют виртуальные оси (Virtual Axes).

    Ось можно представить как руль автомобиля. Когда руль в нейтральном положении, его значение равно 0. Если вы поворачиваете его до упора вправо, значение плавно возрастает до 1. Если влево — опускается до -1.

    В Unity есть встроенная ось Horizontal (Горизонтальная). Движок сам знает, что к этой оси привязаны клавиши A и D, стрелки Влево и Вправо, а также левый стик геймпада. Вам больше не нужно думать о физических кнопках, вы просто запрашиваете математическое значение оси.

    Математика перемещения с учетом ввода

    В прошлой статье мы вывели формулу независимого от частоты кадров перемещения. Теперь мы добавим в нее пользовательский ввод, чтобы получить финальную математическую модель движения в любой современной игре.

    Где: * — новые координаты персонажа в пространстве. * — текущие координаты персонажа до начала расчетов. * — направление и сила ввода (значение оси от -1 до 1). * — базовая скорость персонажа (например, 5 метров в секунду). * — время, прошедшее с предыдущего кадра (Time.deltaTime).

    Допустим, игрок слегка отклонил стик геймпада вправо. Значение оси составило 0.5. Базовая скорость равна 5 м/с. Время кадра равно 0.02 секунды. Смещение составит: метра. Персонаж плавно пойдет шагом. Если игрок отклонит стик до упора (), смещение составит метра за кадр, и персонаж побежит.

    Мышь и координаты: взаимодействие с миром

    Клавиатура и геймпад отлично подходят для передачи абстрактных команд (прыжок, бег). Но когда дело доходит до точного указания объектов на экране (выбор юнита в стратегии, прицеливание в шутере), королевой ввода становится мышь.

    Работа с мышью таит в себе концептуальную сложность: разницу между экранными и мировыми координатами.

    Экран вашего монитора — это плоская 2D-сетка, измеряемая в пикселях. Нижний левый угол имеет координаты (0, 0), а верхний правый — например, (1920, 1080). Когда вы запрашиваете позицию мыши через Input.mousePosition, C# возвращает именно эти пиксельные координаты.

    Но ваш игровой мир — это бесконечное 3D-пространство (или 2D-плоскость), измеряемое в метрах. Если вы попытаетесь переместить орка на координаты мыши (1500, 800), он улетит за пределы карты на полтора километра.

    Чтобы кликнуть по объекту в игре, необходимо перевести пиксели экрана в метры виртуального мира. Этот процесс называется Рейкастинг (Raycasting — бросание луча).

    Представьте, что из объектива виртуальной камеры (которая показывает вам игру) через курсор мыши выстреливает невидимый лазерный луч. Он летит вглубь 3D-мира, пока не врежется в первый попавшийся объект с коллайдером (физической оболочкой).

    Именно так работают механики стрельбы в играх вроде Counter-Strike (хитскан-оружие) или выделение войск рамкой в StarCraft.

    > Хорошее управление — это то, которое игрок перестает замечать через пять минут после начала игры. Плохое управление заставляет бороться с интерфейсом, а не с врагами. > > Сигэру Миямото, создатель Mario и The Legend of Zelda

    Эволюция архитектуры: Новая система ввода

    Классический класс Input, который мы рассмотрели выше, прост в освоении, но имеет критический недостаток — жесткую привязку к железу (хардкодинг). Если вы написали Input.GetKeyDown(KeyCode.Space), вы навсегда прибили действие прыжка к клавише Пробел. Чтобы дать игроку возможность переназначить клавиши в настройках, вам придется писать сотни строк сложного и запутанного кода.

    Чтобы решить эту проблему, в современной разработке применяется паттерн Action-based Input (Ввод на основе действий). В Unity он реализован в виде пакета New Input System.

    Суть паттерна заключается во внедрении дополнительного слоя абстракции между физической кнопкой и кодом на C#.

  • Устройство (Device): Клавиатура, мышь, геймпад PS5, сенсорный экран смартфона.
  • Действие (Action): Логическая концепция. Например, «Прыжок», «Атака», «Тормоз».
  • Логика (Code): Ваш скрипт на C#, который реагирует на Действие.
  • Вместо того чтобы спрашивать в коде: «Нажат ли Пробел?», ваш скрипт просто подписывается на событие: «Сообщи мне, когда произойдет действие Прыжок».

    В специальном визуальном редакторе движка вы связываете действие «Прыжок» с кнопкой Пробел на клавиатуре, кнопкой 'A' на геймпаде Xbox и свайпом вверх на экране смартфона. Если игрок захочет изменить управление, игра просто поменяет связь в редакторе, а ваш код на C# останется абсолютно нетронутым. Это золотой стандарт индустрии для кроссплатформенных игр.

    Золотое правило: Update против FixedUpdate

    При работе с пользовательским вводом начинающие программисты часто совершают ошибку, которая приводит к «проглатыванию» нажатий. Игрок жмет кнопку, слышит щелчок пластика, но персонаж не прыгает.

    Эта проблема возникает из-за непонимания разницы между графическим циклом Update() и физическим циклом FixedUpdate() (о которых мы говорили в прошлой статье).

    Напомним: * Update() вызывается каждый раз, когда рисуется новый кадр на мониторе (например, 144 раза в секунду на мощном ПК). * FixedUpdate() вызывается строго по таймеру для расчета физики (по умолчанию 50 раз в секунду).

    Состояние кнопок клавиатуры и мыши обновляется операционной системой синхронно с отрисовкой кадров, то есть в цикле Update(). Если вы попытаетесь проверить Input.GetKeyDown внутри FixedUpdate(), вы рискуете пропустить нажатие.

    Представьте, что игра работает при 150 FPS. Метод Update вызовется 3 раза за то время, пока FixedUpdate ждет своей очереди. Если игрок нажмет и отпустит кнопку за эти 3 графических кадра, к моменту срабатывания физического цикла кнопка уже будет считаться отпущенной. Нажатие потеряно навсегда.

    Правильный архитектурный подход: Считывайте ввод только в Update() и сохраняйте результат в логическую переменную (bool). А уже в FixedUpdate() проверяйте эту переменную и применяйте физическую силу.

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

    Итоги

    * Преобразование типов: Данные, вводимые пользователем через консоль, всегда являются текстом. Для безопасного использования их в математике необходимо применять методы конвертации, такие как int.TryParse(). * Состояния кнопок: Для одиночных действий (выстрел) используется проверка нажатия в конкретный кадр (GetKeyDown), а для непрерывных (бег) — проверка удержания (GetKey). * Виртуальные оси: Использование осей (Input.GetAxis) вместо жестко заданных кнопок позволяет сделать управление плавным и автоматически поддерживает разные устройства (клавиатуры и геймпады). * Рейкастинг: Для взаимодействия с 3D-миром с помощью мыши необходимо переводить плоские пиксельные координаты экрана в пространственные координаты с помощью выпускания невидимого луча. * Разделение логики: Считывать пользовательский ввод необходимо исключительно в методе Update(), чтобы не пропустить быстрые нажатия, а применять к объектам физические силы — в FixedUpdate().

    12. Взаимодействие объектов и базовая физика

    Взаимодействие объектов и базовая физика

    Представьте, что вы играете в гоночный симулятор. Вы разгоняете свой спорткар до 200 километров в час, не справляетесь с управлением и на полном ходу влетаете в бетонную стену. Но вместо оглушительного удара, скрежета металла и остановки, ваша машина просто проезжает сквозь стену, как бестелесный призрак. Иллюзия реального мира мгновенно рушится, и игра превращается в набор пикселей.

    В предыдущих статьях мы научились считывать команды игрока и перемещать объекты, изменяя их математические координаты. Но простая математика координат ничего не знает о плотности, массе или гравитации. Чтобы виртуальный мир ощущался настоящим, объектам нужны физические свойства. В этой статье мы разберем, как игровые движки симулируют законы физики и как с помощью языка C# заставить объекты реалистично взаимодействовать друг с другом.

    Невидимые границы: Коллайдеры

    Как компьютер понимает, где заканчивается моделька персонажа и начинается стена? 3D-модели состоят из тысяч полигонов (треугольников), которые отрисовывает видеокарта. Если бы процессор каждый кадр проверял пересечение каждого треугольника персонажа с каждым треугольником стены, даже самый мощный компьютер завис бы через секунду.

    Для решения этой проблемы в геймдеве используются Коллайдеры (Colliders) — невидимые математические оболочки, которые натягиваются поверх визуальной модели объекта.

    Физический движок игнорирует сложную графику и работает только с этими простыми формами. В Unity существует несколько базовых типов коллайдеров:

    | Тип коллайдера | Описание и математическая суть | Идеальное применение в играх | | :--- | :--- | :--- | | Box Collider | Прямоугольный параллелепипед. Требует для расчетов только центр и три размера (ширина, высота, глубина). Очень быстрый. | Ящики, стены, двери, платформы, автомобили. | | Sphere Collider | Идеальная сфера. Самый быстрый коллайдер, так как для проверки столкновения нужно лишь сравнить дистанцию между центрами с радиусом. | Мячи, снаряды, гранаты, зоны взрыва. | | Capsule Collider | Цилиндр с полусферами на концах. Быстро вычисляется и не застревает на ступеньках благодаря круглым краям. | Главные герои, враги, NPC (гуманоиды). | | Mesh Collider | Точно повторяет форму 3D-модели. Требует огромных вычислительных мощностей. | Сложный ландшафт (горы, кратеры), уникальные архитектурные формы. |

    > Физика в играх — это искусство обмана. Наша цель не в том, чтобы симулировать реальность с атомной точностью, а в том, чтобы создать правдоподобную иллюзию, потратив на это минимум ресурсов процессора. > > Эрин Катто, создатель физического движка Box2D

    Составные коллайдеры (Compound Colliders)

    Что делать, если у вас есть сложный объект, например, стол на четырех ножках? Использовать Mesh Collider слишком дорого для производительности. Профессиональный подход заключается в создании составного коллайдера.

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

    Масса и гравитация: компонент Rigidbody

    Наличие коллайдера делает объект твердым, но не заставляет его падать или отскакивать. Коллайдер — это просто форма. Чтобы вдохнуть в объект жизнь и передать управление над ним физическому движку (в Unity это NVIDIA PhysX), необходим компонент Rigidbody (Твердое тело).

    Как только вы добавляете Rigidbody к объекту, он начинает подчиняться законам ньютоновской механики. У него появляются новые свойства:

    * Mass (Масса): Измеряется в килограммах. Определяет, насколько тяжело сдвинуть объект. Если грузовик с массой 2000 кг врежется в деревянный ящик с массой 10 кг, ящик отлетит на десятки метров, а грузовик почти не потеряет скорость. * Drag (Сопротивление воздуха): Замедляет линейное движение объекта с течением времени. Значение 0 означает вакуум (объект будет лететь вечно), а высокое значение (например, 10) заставит объект падать медленно, как перышко. * Use Gravity (Использовать гравитацию): Логический переключатель (bool). Если включен, на объект постоянно действует сила притяжения (по умолчанию -9.81 м/с² по оси Y). * Is Kinematic (Кинематика): Важнейшая настройка. Если включить этот параметр, объект перестанет реагировать на толчки и гравитацию, но сохранит свою твердость.

    Кинематические тела идеально подходят для движущихся платформ. Платформа не должна падать вниз под весом прыгнувшего на нее игрока, но она должна иметь возможность физически толкать игрока вверх, когда поднимается.

    Физика в коде: Применение сил

    В прошлой статье мы перемещали персонажа, напрямую меняя его координаты через Transform. Когда на объекте висит Rigidbody, такой подход строго запрещен. Прямое изменение координат телепортирует объект, игнорируя физику, что приводит к застреванию в стенах и поломке коллайдеров.

    Чтобы сдвинуть физическое тело, мы должны приложить к нему силу с помощью метода AddForce().

    В основе этого метода лежит Второй закон Ньютона:

    Где: * — сила, приложенная к объекту (в ньютонах). * — масса объекта (в килограммах). * — ускорение, которое получит объект (в метрах на секунду в квадрате).

    Если мы хотим толкнуть ящик массой 50 кг так, чтобы он получил ускорение 10 м/с², нам нужно приложить силу в 500 ньютонов.

    Рассмотрим скрипт прыжка персонажа на C#:

    Обратите внимание на два критически важных момента:

  • FixedUpdate: Любые взаимодействия с Rigidbody должны происходить внутри метода FixedUpdate(). В отличие от Update(), который зависит от частоты кадров (FPS), FixedUpdate() вызывается со строгой периодичностью (обычно 50 раз в секунду). Это гарантирует, что прыжок будет одинаковой высоты и на мощном ПК, и на слабом смартфоне.
  • ForceMode.Impulse: Движок позволяет применять силу по-разному. Режим Impulse выдает всю энергию за одну долю секунды (идеально для прыжка, взрыва или удара битой). Режим Force (по умолчанию) применяет силу постепенно, пока кнопка нажата (идеально для педали газа в автомобиле или реактивного ранца).
  • Обработка столкновений: Collision

    Когда два объекта с коллайдерами и хотя бы одним Rigidbody врезаются друг в друга, физический движок не только отталкивает их, но и генерирует событие в коде. Мы можем «поймать» это событие с помощью встроенного метода OnCollisionEnter.

    Представим механику: если снежок попадает в стену, он должен разбиться (уничтожиться).

    Объект типа Collision, который передается в метод, содержит массу полезной информации. Например, collisionInfo.relativeVelocity покажет силу (скорость) удара. Вы можете написать условие: если скорость удара больше 10 м/с, машина получает урон, а если меньше — просто слегка отскакивает без повреждений.

    Триггеры: зоны без физического сопротивления

    Далеко не все объекты в игре должны быть твердыми. Представьте золотую монетку, висящую в воздухе, или зону радиации. Игрок должен проходить сквозь них, но игра обязана зафиксировать момент пересечения.

    Для этого в настройках любого коллайдера есть галочка Is Trigger (Является триггером). Как только вы ее включаете, объект теряет свою плотность. Он больше не отталкивает другие тела, но начинает генерировать другие события в коде — OnTriggerEnter, OnTriggerStay и OnTriggerExit.

    Рассмотрим классическую механику ядовитого облака. Пока игрок находится внутри облака, он должен получать урон каждую секунду.

    Разница между параметрами методов очевидна: OnCollisionEnter принимает объект Collision (содержащий данные о силе удара и точках контакта), а OnTriggerStay принимает просто Collider (так как физического удара не было, есть только факт пересечения границ).

    Оптимизация: Физические слои и матрица столкновений

    В масштабных играх на сцене могут находиться тысячи объектов: пули, осколки, враги, частицы пыли. Если физический движок будет проверять столкновения каждого объекта с каждым, производительность упадет до нуля.

    Чтобы этого избежать, разработчики используют Матрицу столкновений (Collision Matrix). Все объекты в игре распределяются по слоям (Layers): «Игрок», «Враги», «Снаряды», «Окружение».

    В настройках движка вы можете жестко задать правила. Например: * Слой «Снаряды» игнорирует слой «Снаряды» (пули пролетают друг сквозь друга). * Слой «Враги» игнорирует слой «Враги» (монстры могут проходить сквозь друг друга, чтобы не толкаться в узких коридорах). * Слой «Снаряды» взаимодействует со слоями «Враги» и «Окружение».

    Отключение ненужных проверок на уровне матрицы столкновений — это один из самых мощных инструментов оптимизации, который позволяет экономить до 30% ресурсов процессора в нагруженных сценах.

    Итоги

    * Коллайдеры — это невидимые математические формы (коробки, сферы, капсулы), которые определяют границы объекта. Простые формы вычисляются в разы быстрее, чем сложные меши. * Компонент Rigidbody передает объект под управление физического движка, наделяя его массой, гравитацией и инерцией. * Для перемещения физических объектов используется метод AddForce(), который должен вызываться строго внутри цикла FixedUpdate(), чтобы избежать зависимости от частоты кадров. * Твердые столкновения обрабатываются методом OnCollisionEnter, а пересечения прозрачных зон (триггеров) — методами группы OnTrigger. * Для оптимизации производительности следует использовать составные коллайдеры вместо Mesh Collider и настраивать матрицу столкновений, отключая расчеты между ненужными слоями.

    13. Работа с 2D и 3D графикой в играх

    Работа с 2D и 3D графикой в играх

    Вы нажимаете кнопку прыжка, физический движок применяет импульс к твердому телу, и математические координаты объекта изменяются по оси Y. С точки зрения процессора прыжок состоялся. Но на экране монитора ничего не произошло. Игрок видит лишь пустую пустоту или серый фон по умолчанию. В прошлой статье мы создали невидимый скелет игры — физику и коллайдеры. Теперь пришло время нарастить на этот скелет «мясо» — визуальную оболочку, которая превратит сухие цифры в захватывающий виртуальный мир.

    Графика в играх — это не просто красивые картинки. Это основной канал передачи образов, атмосферы и обратной связи. Компьютер должен десятки раз в секунду рассчитывать, как свет падает на поверхность меча, как искажается тень от бегущего монстра и какие пиксели нужно закрасить красным цветом при взрыве бочки.

    Двухмерное пространство: магия плоских миров

    Как классический Марио выглядит как водопроводчик, а не как набор цифр? В основе любой 2D-игры лежит спрайт (Sprite). Это плоское растровое изображение, состоящее из пикселей, которое отрисовывается на экране.

    В игровом движке Unity спрайт — это не просто картинка, открытая в галерее. Это полноценный компонент SpriteRenderer, который прикрепляется к пустому игровому объекту. Этот компонент берет исходный файл (например, в формате PNG) и сообщает видеокарте, в каком месте экрана и с каким масштабом его нужно нарисовать.

    Для отображения плоских миров используется ортографическая камера (Orthographic Camera). В отличие от человеческого глаза, такая камера лишена перспективы. Объекты не становятся меньше при отдалении от объектива. Это позволяет разработчикам точно контролировать размеры платформ и персонажей, создавая идеальные условия для платформеров и головоломок.

    Математика памяти: сколько весит 2D-графика

    Начинающие разработчики часто скачивают из интернета огромные картинки в разрешении 4K и вставляют их в мобильную игру в качестве маленьких иконок. Это приводит к мгновенному переполнению оперативной памяти смартфона и вылету игры.

    Размер несжатой текстуры в оперативной памяти видеокарты вычисляется по формуле:

    Где: * — итоговый объем памяти в байтах. * — ширина изображения в пикселях. * — высота изображения в пикселях. * — количество байт на один пиксель (обычно 4 байта для формата RGBA: красный, зеленый, синий и канал прозрачности).

    Если вы загружаете спрайт фона размером 2048 на 2048 пикселей, расчет будет следующим: байт. Разделив это число на 1024 дважды, мы получим ровно 16 мегабайт. Если на уровне используется 50 таких уникальных фонов, они займут 800 мегабайт оперативной памяти только под графику, не считая звуков и физики.

    Управление графикой через C#

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

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

    Переход в 3D: глубина, полигоны и материалы

    Трехмерная графика работает по совершенно иным законам. У нас больше нет готовых плоских картинок. Вместо этого мы создаем математические модели в виртуальном пространстве.

    Основой любого 3D-объекта является меш (Mesh — полигональная сетка). Меш состоит из точек в пространстве (вершин), соединенных линиями. Три соединенные линии образуют треугольник — полигон. Из тысяч таких крошечных треугольников складывается форма автомобиля, дерева или дракона.

    Сам по себе меш невидим. Это просто каркас. Чтобы объект обрел внешний вид, на него накладывается материал (Material). Материал — это набор инструкций для видеокарты, описывающий, как поверхность должна реагировать на свет. Будет ли она блестеть как мокрый асфальт, или поглощать свет как бархат?

    Внутрь материала помещается текстура (Texture) — плоская картинка, которая «натягивается» на трехмерный каркас подобно подарочной упаковке. Процесс указания того, какая часть картинки должна лечь на какую часть 3D-модели, называется UV-разверткой.

    > Графика — это первое, что видит игрок, но последнее, что заставит его остаться в игре. Однако без хорошей графики он может даже не попытаться начать. > > Джон Кармак, создатель Doom и Quake

    Полигональный бюджет и производительность

    Мощность видеокарты не безгранична. Чем больше треугольников на экране, тем сложнее процессору их обрабатывать.

    Представим, что вы скачали высокодетализированную модель орка, состоящую из 50 000 полигонов. Для главного босса это приемлемое число. Но если вы создадите армию из 100 таких орков, видеокарте придется отрисовывать 5 000 000 полигонов каждый кадр. При 60 кадрах в секунду это 300 миллионов вычислений в секунду только на геометрию. Слабый компьютер не справится с такой нагрузкой, и игра начнет тормозить.

    Профессиональные 3D-художники используют технику ретопологии: они создают низкополигональную модель (например, на 3 000 полигонов), а мелкие детали (морщины, царапины на броне) имитируют с помощью специальных текстур — карт нормалей (Normal Maps), которые обманывают освещение, создавая иллюзию объема на плоском треугольнике.

    Сравнение подходов: 2D против 3D

    Выбор между плоской и объемной графикой фундаментально меняет архитектуру всего проекта.

    | Характеристика | 2D Графика | 3D Графика | | :--- | :--- | :--- | | Основа визуала | Спрайты (плоские картинки) | Меши (полигональные сетки) и Материалы | | Тип камеры | Ортографическая (без искажений перспективы) | Перспективная (объекты вдалеке кажутся меньше) | | Создание контента | Рисование в Photoshop, Aseprite | Моделирование в Blender, Maya, ZBrush | | Физика | Расчеты по осям X и Y (быстро) | Расчеты по осям X, Y и Z (требует больше ресурсов) | | Освещение | Часто нарисовано прямо на спрайте | Рассчитывается движком в реальном времени |

    Освещение: свет и тени в виртуальном мире

    В 3D-играх атмосфера на 80% зависит от работы со светом. Игровой движок предлагает несколько типов источников освещения:

  • Направленный свет (Directional Light): Имитирует солнце. Находится бесконечно далеко, его лучи параллельны друг другу и освещают всю сцену равномерно.
  • Точечный свет (Point Light): Имитирует лампочку или костер. Светит во все стороны из одной точки, сила света затухает с расстоянием.
  • Прожектор (Spot Light): Имитирует фонарик или фары автомобиля. Светит конусом в заданном направлении.
  • Расчет теней от каждого источника света в реальном времени — невероятно тяжелая математическая задача. Если в комнате висит 10 лампочек, и каждая отбрасывает динамические тени от 20 объектов, игра гарантированно зависнет.

    Для оптимизации используется запекание света (Light Baking). Движок заранее (еще на этапе разработки) рассчитывает, как свет от неподвижных ламп падает на неподвижные стены, и просто рисует эти тени поверх текстур. В самой игре эти лампы отключаются, экономя 100% ресурсов процессора, а игрок видит красивую, фотореалистичную, но статичную картинку.

    Оживление графики: основы анимации

    Статичный спрайт или застывшая 3D-модель выглядят неестественно. Чтобы персонаж побежал, нам нужна анимация.

    В 2D-играх чаще всего используется покадровая анимация. Художник рисует 8 разных картинок бегущего персонажа. Движок просто меняет эти картинки со скоростью 12 кадров в секунду, создавая иллюзию движения, подобно классическим мультфильмам Disney.

    В 3D-играх используется скелетная анимация (Skeletal Animation). Внутрь полигональной сетки персонажа помещается виртуальный скелет, состоящий из «костей». Каждая вершина меша привязывается к определенной кости. Аниматор не двигает саму модель — он вращает кости (например, сгибает кость локтя), а полигоны руки автоматически тянутся за ней.

    Управление анимацией через конечный автомат

    В статье про игровую логику мы изучали паттерн «Конечный автомат» для управления состояниями персонажа. В Unity этот паттерн встроен прямо в систему анимации и называется Animator.

    Вы создаете визуальную схему: состояние Idle (стоит) соединяется стрелочкой с состоянием Run (бежит). На стрелочку вешается условие — параметр Speed. Если скорость больше нуля, включается анимация бега.

    Из скрипта на C# мы можем передавать математические значения в этот визуальный автомат:

    Если игрок нажал кнопку вправо, физический движок толкает Rigidbody, скорость возрастает до 5 м/с. Скрипт считывает эту цифру и передает в Animator. Автомат видит, что , и плавно переключает анимацию стояния на анимацию бега. Логика и графика работают в идеальной синхронии.

    Главный враг оптимизации: Вызовы отрисовки

    Даже если у вас мало полигонов и простые текстуры, игра может тормозить из-за проблемы, известной как вызовы отрисовки (Draw Calls).

    Каждый раз, когда видеокарте нужно нарисовать объект с новым материалом, центральный процессор (CPU) должен подготовить пакет данных и отправить команду видеокарте: «Нарисуй этот меш вот с этим материалом». Эта команда и есть Draw Call.

    Процессор работает медленнее видеокарты. Если команд слишком много, видеокарта простаивает в ожидании, а игра начинает тормозить.

    Представим сцену с лесом. У вас есть 1000 деревьев. Каждое дерево использует свой уникальный материал листьев. Процессору придется отправить 1000 отдельных команд. Это катастрофа для мобильного телефона.

    Решение — Батчинг (Batching, пакетирование). Разработчики объединяют текстуры всех деревьев в одну большую картинку (Атлас текстур) и создают один общий материал. Теперь процессор говорит видеокарте: «Возьми этот один материал и нарисуй им сразу 1000 деревьев за один раз». Количество вызовов отрисовки падает с 1000 до 1. Производительность игры взлетает в десятки раз.

    Итоги

    * 2D-графика строится на спрайтах (плоских изображениях) и ортографической камере, которая убирает искажения перспективы. Размер текстур напрямую влияет на потребление оперативной памяти. * 3D-графика состоит из мешей (полигональных сеток), на которые накладываются материалы и текстуры. Чем больше полигонов, тем выше нагрузка на видеокарту. * Освещение — самый ресурсоемкий процесс в 3D. Для оптимизации статичные тени «запекаются» в текстуры заранее, отключая расчеты в реальном времени. * Анимация управляется через компонент Animator, который работает по принципу конечного автомата, получая команды и математические переменные напрямую из C# скриптов. * Вызовы отрисовки (Draw Calls) — главная причина тормозов процессора. Для оптимизации необходимо объединять материалы и использовать атласы текстур, чтобы рисовать множество объектов за одну команду.

    14. Основы создания и управления анимацией

    Основы создания и управления анимацией

    Представьте, что вы создали идеальную 3D-модель рыцаря. Вы настроили коллайдеры, написали скрипт перемещения и добавили гравитацию. Вы нажимаете кнопку «Вперед», и рыцарь действительно скользит по экрану со скоростью 5 метров в секунду. Но он делает это, застыв в Т-образной позе, словно пластиковый манекен на конвейерной ленте. Иллюзия живого мира мгновенно разрушается. В прошлой статье мы научились отрисовывать графику и работать с материалами, но именно анимация вдыхает жизнь в набор пикселей и полигонов.

    Анимация в играх выполняет две критически важные функции. Во-первых, она обеспечивает визуальную обратную связь: игрок должен видеть, что его команда (например, удар мечом) выполняется. Во-вторых, она передает вес, характер и эмоции персонажа. Тяжелый орк замахивается дубиной медленно, а ловкий ассасин наносит удары молниеносно. Управление этим процессом через код на C# — один из важнейших навыков разработчика игр.

    Анатомия игровой анимации

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

    В игровой индустрии существует два фундаментально разных подхода к созданию анимаций, зависящих от размерности вашей игры:

  • Покадровая анимация (2D Спрайты): Художник рисует каждое движение вручную. Если цикл бега состоит из 8 кадров, движок просто по очереди подставляет эти картинки в компонент SpriteRenderer. При частоте 12 кадров в секунду один полный цикл бега займет секунды.
  • Скелетная анимация (3D и современное 2D): Внутрь модели помещается виртуальный скелет. Аниматор не перерисовывает модель, он лишь задает ключевые кадры (Keyframes) для вращения костей. Например, на 1-м кадре рука опущена, а на 10-м — поднята. Движок сам рассчитывает промежуточные положения руки для всех кадров между 1-м и 10-м. Этот процесс называется интерполяцией.
  • > Анимация — это не то, как движется рисунок. Это то, что движет рисунком. Это иллюзия жизни. > > Уолт Дисней, основатель Walt Disney Productions

    Независимо от того, используете вы 2D-спрайты или 3D-скелеты, в движке Unity готовая последовательность движений сохраняется в специальный файл — Animation Clip (Анимационный клип). Клип содержит математические данные о том, как свойства объекта (координаты, поворот, цвет) изменяются во времени.

    Конечный автомат: мозг анимации

    У персонажа может быть множество клипов: Idle (стоит на месте), Run (бежит), Jump (прыгает), Attack (атакует). Как движок понимает, какой клип проигрывать в данный момент?

    В статье про игровую логику мы изучали паттерн «Конечный автомат» (Finite State Machine). В Unity этот паттерн реализован визуально в виде компонента Animator и ассета Animator Controller.

    Контроллер аниматора представляет собой граф (схему), где каждый прямоугольник — это состояние (анимационный клип), а стрелочки между ними — переходы (Transitions).

    Настройка переходов и время смешивания

    Переход от бега к прыжку не должен происходить мгновенно, иначе модель дернется, что будет выглядеть неестественно. В 3D-играх Unity использует смешивание (Blending).

    Когда срабатывает переход, движок берет последние кадры анимации бега и первые кадры анимации прыжка, математически усредняя положения костей. Длительность этого смешивания задается в секундах (параметр Transition Duration).

    Например, если длительность перехода равна 0.25 секунды, то на середине этого времени (0.125 сек) положение руки персонажа вычисляется по формуле:

    Где: * — итоговый угол поворота кости, который увидит игрок. * — угол поворота кости в анимации бега. * — угол поворота кости в анимации прыжка.

    Для 2D-спрайтов смешивание костей невозможно (так как это просто плоские картинки), поэтому параметр Transition Duration для 2D-игр всегда жестко устанавливается в , чтобы картинки переключались мгновенно.

    Параметры Animator: язык общения с C#

    Чтобы стрелочки переходов в Контроллере срабатывали не хаотично, а по правилам вашей игры, используются Параметры. Это специальные переменные, которые живут внутри Animator и служат мостом между вашим C# скриптом и визуальным графом.

    Существует четыре типа параметров:

    | Тип параметра | Описание | Пример использования в логике перехода | | :--- | :--- | :--- | | Float | Дробное число. Идеально для аналоговых значений. | Переход из Idle в Run, если Speed . | | Int | Целое число. Используется для дискретных состояний. | Переход в анимацию смерти, если Health . | | Bool | Логическое значение (Да/Нет). Для длительных состояний. | Проигрывать анимацию падения, пока IsGrounded равно false. | | Trigger | Одноразовый импульс. Срабатывает и тут же гаснет. | Резкий переход в анимацию Attack при клике мыши. |

    Важное архитектурное правило: скрипт на C# ничего не знает о том, какие анимации существуют. Он просто передает математические факты (скорость равна 5.2, кнопка удара нажата). А уже Animator Controller решает, какую анимацию включить при таких данных. Это сохраняет код чистым и независимым от графики.

    Управление анимацией через код

    Давайте свяжем физику перемещения, которую мы разбирали ранее, с системой анимации. У нас есть персонаж с компонентом Rigidbody2D для физики и Animator для графики.

    Наша задача: заставить персонажа переключаться между анимациями Idle и Run в зависимости от его реальной физической скорости.

    В этом примере мы используем математическую функцию Mathf.Abs(), которая возвращает модуль числа: . Это необходимо, потому что анимации бега неважно, бежит персонаж вправо (скорость ) или влево (скорость ). Ей важен сам факт движения. Внутри Animator мы настраиваем стрелочку перехода: если параметр Speed , включить клип Run.

    Одноразовые действия: использование Trigger

    Для механики атаки параметр Float или Bool подходит плохо. Удар — это импульсивное действие. Если использовать Bool, вам придется вручную писать код для его выключения после удара, иначе персонаж застрянет в бесконечном цикле взмахов мечом.

    Для таких задач создан Trigger.

    Деревья смешивания (Blend Trees): продвинутая плавность

    В современных 3D-играх персонаж редко просто стоит или бежит с максимальной скоростью. При использовании геймпада игрок может отклонить стик наполовину, и персонаж должен пойти медленным шагом. Создавать отдельные состояния для «Шаг», «Легкий бег», «Быстрый бег» и настраивать десятки стрелочек между ними — это кошмар для разработчика.

    Для решения этой проблемы используются Деревья смешивания (Blend Trees).

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

    Представьте, что у вас есть параметр Velocity (от до ) и три клипа:

  • Idle (привязан к значению )
  • Walk (привязан к значению )
  • Run (привязан к значению )
  • Если игрок отклонит стик так, что Velocity станет равно , движок автоматически возьмет 50% от анимации Walk и 50% от анимации Run. Персонаж будет двигаться идеальной трусцой. Математика смешивания работает под капотом, а в коде на C# вы по-прежнему меняете всего одну переменную через anim.SetFloat("Velocity", input).

    События анимации: идеальная синхронизация логики и визуала

    Одна из самых частых проблем новичков — рассинхронизация урона и анимации. Вы нажимаете кнопку атаки, скрипт мгновенно отнимает здоровье у врага, а анимация взмаха мечом только начинается. Враг умирает еще до того, как лезвие коснется его тела.

    Чтобы логика игры идеально совпадала с визуальным рядом, используются События анимации (Animation Events).

    Это специальные метки, которые разработчик ставит на конкретный кадр прямо внутри анимационного клипа. Когда ползунок воспроизведения доходит до этой метки, Animator автоматически вызывает указанный вами метод в C# скрипте.

    Рассмотрим правильную архитектуру ближнего боя:

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

    Оптимизация анимаций: невидимый труд процессора

    Анимация — ресурсоемкий процесс. Расчет положения сотен костей для десятков персонажей каждый кадр может сильно нагрузить центральный процессор (CPU).

    Главное правило оптимизации анимаций: не вычисляйте то, чего игрок не видит.

    В компоненте Animator есть настройка Culling Mode (Режим отсечения). Она определяет, что должен делать движок, если персонаж находится за спиной игрока или за пределами экрана.

    * Always Animate: Анимация просчитывается всегда. Используется только для главного героя или критически важных боссов. * Cull Update Transforms: Визуальная анимация костей останавливается, но логика конечного автомата продолжает работать. Идеально для большинства врагов. * Cull Completely: Аниматор полностью отключается. Используется для фоновых объектов (например, вращающихся ветряных мельниц вдалеке).

    Если на уровне находится 100 зомби, но в камеру попадают только 5, включение режима Cull Update Transforms сэкономит до 90% ресурсов процессора, выделенных на анимацию, сохранив при этом стабильные 60 кадров в секунду.

    Итоги

    * Анимация в играх строится на смене кадров (2D) или интерполяции костей (3D) и управляется через паттерн «Конечный автомат» с помощью компонента Animator. * Для связи C# скриптов и визуального графа используются Параметры (Float, Int, Bool, Trigger). Скрипт передает данные, а Animator принимает решение о смене клипа. * Деревья смешивания (Blend Trees) позволяют плавно комбинировать несколько анимаций (например, шаг и бег) на основе одной математической переменной, избавляя от сложной паутины переходов. * События анимации (Animation Events) критически важны для синхронизации логики и графики. Они позволяют вызывать методы C# (нанесение урона, звук шагов) строго в определенный кадр анимации. * Для оптимизации производительности необходимо настраивать Culling Mode, чтобы процессор не тратил ресурсы на расчет анимаций объектов, находящихся за пределами экрана.

    15. Проектирование пользовательского интерфейса (UI)

    Проектирование пользовательского интерфейса (UI)

    Представьте ситуацию: вы сражаетесь с финальным боссом. Ваш рыцарь, анимации которого мы кропотливо настраивали в прошлой статье, наносит сокрушительный удар. Босс падает, физический движок фиксирует столкновение, а скрипт логики начисляет вам 1000 очков опыта. Но на экране ничего не меняется. Вы не знаете, сколько у вас осталось здоровья, не видите добавленного опыта и не можете нажать кнопку выхода в главное меню. Без визуальной панели управления даже самая гениальная математическая симуляция остается скрытой от игрока.

    Взаимодействие человека с виртуальным миром происходит через пользовательский интерфейс (User Interface, UI). Это мост между сухими цифрами в оперативной памяти компьютера и восприятием игрока. Полоски здоровья, счетчики патронов, инвентарь, мини-карта и диалоговые окна — все это элементы UI, которые делают игру понятной и управляемой.

    UX против UI: два столпа удобства

    В игровой индустрии аббревиатуру UI почти всегда ставят рядом с UX. Начинающие разработчики часто путают эти понятия, хотя они отвечают за совершенно разные этапы создания игры.

    UX (User Experience — пользовательский опыт) — это логика, структура и эмоции. UX-дизайнер решает, сколько кликов нужно сделать игроку, чтобы скрафтить меч. Он продумывает, что кнопка «Атака» должна быть большой и находиться под большим пальцем правой руки, а кнопка «Настройки» может быть маленькой и прятаться в углу.

    UI (User Interface — пользовательский интерфейс) — это визуальное воплощение логики UX. UI-художник решает, какого цвета будет кнопка «Атака», какой шрифт использовать для счетчика золота и как будет выглядеть рамка вокруг иконки меча.

    > Интерфейс — это как шутка. Если вам приходится ее объяснять, значит, она не удалась. > > Мартин Леблан, эксперт по пользовательским интерфейсам

    Если игрок открывает инвентарь и не может понять, как надеть броню на персонажа — это провал UX. Если он понял как это сделать, но текст описания брони сливается с фоном и не читается — это провал UI.

    Холст для творчества: Canvas и его режимы

    В игровом движке Unity все элементы интерфейса живут на специальном невидимом слое, который называется Canvas (Холст). Вы не можете просто бросить кнопку в 3D-мир рядом с деревом; кнопка обязана находиться внутри Холста.

    Canvas определяет, как именно интерфейс будет накладываться на игру. Существует три фундаментальных режима работы Холста:

  • Screen Space - Overlay (Наложение на экран). Интерфейс рисуется поверх всей 3D или 2D графики, словно наклейка на стекле вашего монитора. Камера игры никак не влияет на этот холст. Идеально подходит для главного меню, счетчика очков и полоски здоровья самого игрока.
  • Screen Space - Camera (Привязка к камере). Холст находится на фиксированном расстоянии перед объективом виртуальной камеры. Если между камерой и холстом пролетит 3D-объект (например, частицы взрыва), он перекроет элементы интерфейса. Используется для создания эффектов погружения (например, капли дождя на визоре шлема).
  • World Space (Мировое пространство). Холст превращается в обычный 3D-объект. Он имеет координаты X, Y, Z и подчиняется законам перспективы. Если вы отойдете от него, он станет меньше. Этот режим используется для создания полосок здоровья над головами врагов или голографических дисплеев на панелях космического корабля.
  • Базовые строительные блоки UI

    Внутри Холста мы размещаем конкретные компоненты. Рассмотрим основные из них:

    | Компонент | Назначение | Пример использования в играх | Тип данных в C# | | :--- | :--- | :--- | :--- | | Text | Вывод любой текстовой информации | Имя персонажа, диалоги, количество патронов | string | | Image | Отображение 2D-спрайтов и иконок | Иконка зелья, рамка мини-карты, фон меню | Sprite | | Button | Интерактивная зона для клика | Кнопка «Играть», покупка предмета в магазине | bool (нажата/нет) | | Slider | Ползунок для выбора значения из диапазона | Настройка громкости звука, полоска здоровья | float |

    Адаптивность: проблема разных экранов

    Одна из самых страшных ошибок новичка — собрать красивый интерфейс на своем мониторе, скомпилировать игру и обнаружить, что на смартфоне друга половина кнопок уехала за пределы экрана.

    Это происходит из-за разницы в разрешениях. Ваш монитор может иметь разрешение 1920 на 1080 пикселей. Если вы поставите иконку мини-карты в правый верхний угол, ее координаты могут быть равны X: 1700, Y: 900.

    Если запустить эту игру на старом планшете с разрешением 1280 на 720 пикселей, точка с координатами (1700, 900) окажется далеко за пределами физического экрана. Мини-карта просто исчезнет.

    Для решения этой проблемы используются Якоря (Anchors).

    Якорь привязывает элемент UI не к абсолютным пиксельным координатам, а к процентам от размера экрана. Если вы привяжете мини-карту к правому верхнему углу с помощью якоря, движок будет рассчитывать ее позицию относительно этого угла. На любом устройстве, будь то огромный телевизор или маленький телефон, мини-карта всегда будет находиться в 50 пикселях от правого верхнего края.

    Кроме того, компонент Canvas Scaler позволяет интерфейсу автоматически увеличиваться или уменьшаться. Математика масштабирования работает по следующей формуле:

    Где: * — итоговый коэффициент масштабирования интерфейса. * — разрешение устройства, на котором запущена игра. * — эталонное разрешение, под которое вы рисовали UI (например, 1920x1080).

    Если эталонная ширина 1920, а игра запущена на экране шириной 960 пикселей, коэффициент составит 0.5. Движок автоматически уменьшит все кнопки и шрифты ровно в два раза, сохраняя идеальные пропорции.

    Оживление интерфейса: связь с кодом на C#

    Красивая кнопка бесполезна, если она ничего не делает. Чтобы интерфейс реагировал на действия игрока и отображал актуальные данные, его необходимо связать с нашими скриптами.

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

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

    В редакторе Unity мы берем текстовый объект с Холста и перетаскиваем его в пустое поле coinTextUI нашего скрипта. Теперь скрипт точно знает, какой именно текст на экране нужно менять.

    Обработка нажатий кнопок

    С кнопками процесс работает в обратную сторону. Не скрипт управляет кнопкой, а кнопка вызывает метод в скрипте.

    Допустим, мы пишем логику для кнопки лечения:

    В настройках компонента Button в Unity есть специальный блок OnClick(). Мы добавляем туда наш скрипт HealthPotion и из выпадающего списка выбираем метод DrinkPotion(). Теперь каждый раз, когда игрок кликает по кнопке мышью или нажимает пальцем на сенсорный экран, движок будет запускать этот кусок кода.

    Событийно-ориентированная архитектура: оптимизация UI

    В статье про игровой цикл мы подробно разбирали метод Update(), который вызывается каждый кадр (например, 60 раз в секунду). Начинающие программисты часто совершают критическую ошибку, связывая интерфейс с логикой через этот метод.

    Посмотрите на этот пример плохого кода:

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

    Если игра работает при 60 кадрах в секунду, за одну минуту этот код обновит текст 3600 раз. При этом игрок мог стоять в безопасной зоне, и его здоровье за эту минуту не изменилось ни разу! Мы заставили процессор выполнить 3600 тяжелых операций впустую. На мобильном телефоне это приведет к быстрому разряду батареи и перегреву устройства.

    Профессионалы используют событийно-ориентированный подход (Event-driven architecture). Суть проста: интерфейс должен обновляться только в тот момент, когда данные реально изменились.

    Рассмотрим правильную реализацию:

    При таком подходе, если игрок гуляет по уровню 10 минут и не получает урона, процессор тратит ровно 0 ресурсов на обновление полоски здоровья. Это фундаментальное правило оптимизации, которое отличает любительские поделки от коммерческих проектов.

    Визуальная обратная связь: микроанимации в UI

    Статичный интерфейс кажется мертвым. Когда игрок наносит критический урон, цифра урона не должна просто появиться и исчезнуть. Она должна выскочить, увеличиться в размере, изменить цвет на красный и плавно раствориться в воздухе.

    Такие эффекты называются микроанимациями. Они не влияют на математику игры, но колоссально усиливают чувство «сочности» (Juiciness) геймплея.

    Для создания микроанимаций в C# часто используются сопрограммы (Coroutines) или сторонние библиотеки для плавного изменения чисел (например, DOTween или LeanTween).

    Представим механику получения урона. Вместо того чтобы полоска здоровья мгновенно прыгнула со 100% до 50%, мы можем заставить ползунок Slider плавно уменьшаться в течение 0.5 секунды. Глаз игрока улавливает это движение, и мозг воспринимает потерю здоровья как физический, ощутимый процесс, а не просто как смену цифр в таблице.

    Итоги

    * UX и UI — это разные дисциплины. UX отвечает за логику, удобство и количество кликов, а UI — за визуальное оформление, цвета и шрифты. * Все элементы интерфейса в Unity располагаются на Canvas (Холсте), который может накладываться поверх экрана или существовать как физический объект в 3D-мире. * Для корректного отображения интерфейса на экранах разных размеров необходимо использовать Якоря (Anchors), привязывая элементы к краям экрана, а не к абсолютным пикселям. * Связь интерфейса с кодом на C# осуществляется через публичные переменные (например, public Text), а кнопки запускают методы скриптов через событие OnClick. * Для оптимизации производительности интерфейс должен обновляться только в момент изменения данных (по событию), а не каждый кадр в методе Update().

    16. Продвинутые техники C# для геймдева

    Продвинутые техники C# для геймдева

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

    В предыдущих уроках мы заложили прочный фундамент: изучили физику, настроили анимации и связали логику с пользовательским интерфейсом. Однако знание того, как заставить объект двигаться, — это лишь половина дела. Профессиональный разработчик должен знать, как сделать это максимально эффективно. По мере роста масштабов проекта базовых конструкций языка становится недостаточно. Код превращается в запутанный клубок, а производительность падает из-за неоптимального использования памяти.

    Для решения этих проблем в индустрии сформировались продвинутые архитектурные паттерны и специфические инструменты языка C#.

    Пул объектов: спасение от сборщика мусора

    В C# управление памятью автоматизировано. Когда вы создаете новую пулю с помощью команды Instantiate, операционная система выделяет под нее блок оперативной памяти. Когда пуля улетает за экран, вы вызываете Destroy. Объект помечается как мертвый, но память не освобождается мгновенно.

    Периодически в игру вступает Сборщик мусора (Garbage Collector, GC). Он сканирует память, находит уничтоженные объекты и очищает место. Проблема в том, что во время работы сборщика мусора выполнение основного кода игры приостанавливается.

    Для стабильных 60 кадров в секунду на отрисовку одного кадра у процессора есть всего миллисекунд. Если сборка мусора займет миллисекунд, игра пропустит три кадра. Игрок увидит резкий рывок — фриз.

    Чтобы избежать этого, используется паттерн Пул объектов (Object Pool). Его суть заключается в отказе от постоянного создания и уничтожения сущностей.

    Вместо этого мы создаем «бассейн» (список) объектов один раз при загрузке уровня:

    Когда игрок нажимает на курок, мы не выделяем новую память. Мы берем готовую пулю из пула, перемещаем к дулу пистолета и включаем ее (SetActive(true)). Когда пуля попадает в стену, скрипт пули не уничтожает себя, а просто выключается (SetActive(false)), возвращаясь в пул. Сборщику мусора нечего убирать, и игра работает идеально плавно.

    Управление временем: Корутины и Асинхронность

    Игровой цикл Update выполняется каждый кадр. Но что делать, если нам нужно выполнить действие с задержкой? Например, сделать так, чтобы яд отнимал по 10 единиц здоровья каждую секунду в течение 5 секунд.

    Если вы используете стандартную команду C# Thread.Sleep(1000), вся игра просто зависнет на секунду. Для решения задач, растянутых во времени, используются Корутины (Coroutines — сопрограммы).

    Корутина — это специальный метод, который может приостановить свое выполнение, вернуть управление игровому движку, а в следующем кадре (или через заданное время) продолжить работу с того же места.

    Ключевое слово yield return сообщает движку: «Я пока закончил, иди рисуй графику и считай физику, а ко мне вернись через секунду».

    Эволюция: переход на UniTask

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

    В современной разработке стандартом становится асинхронное программирование на базе библиотеки UniTask. Она использует ключевые слова C# async и await.

    | Характеристика | Корутины (IEnumerator) | Асинхронность (UniTask) | | :--- | :--- | :--- | | Синтаксис ожидания | yield return new WaitForSeconds(1f); | await UniTask.Delay(1000); | | Возврат значений | Невозможно напрямую | Может возвращать любые типы данных | | Нагрузка на память | Выделяет память (создает мусор) | Работает без выделения памяти (Zero Allocation) | | Остановка при удалении| Останавливается вместе с объектом | Требует ручной передачи токена отмены |

    Асинхронный код выглядит чище и работает быстрее, особенно когда вам нужно загрузить тяжелую 3D-модель из памяти телефона, не замораживая при этом экран загрузки.

    Событийно-ориентированная архитектура: Делегаты и События

    Представьте, что персонаж погибает. В этот момент игра должна: проиграть звук смерти, показать экран «Game Over», обнулить счетчик очков в UI и остановить появление новых врагов.

    Новичок напишет в скрипте здоровья игрока прямые ссылки на все эти системы:

    Этот код ужасен. Скрипт здоровья теперь зависит от четырех других систем. Если вы удалите из игры scoreManager, скрипт здоровья выдаст ошибку и сломает всю игру.

    Профессионалы используют паттерн Наблюдатель (Observer), который в C# реализуется через делегаты и события (Events).

    Событие — это радиовышка. Скрипт здоровья просто кричит в эфир: «Игрок умер!». Ему абсолютно неважно, кто его слушает. А другие системы (UI, звук) настраивают свои радиоприемники на эту частоту и реагируют.

    Для создания событий удобнее всего использовать встроенный тип Action:

    Теперь в скрипте интерфейса мы «подписываемся» на это событие:

    Такая архитектура называется слабосвязанной. Вы можете добавлять десятки новых реакций на смерть игрока, вообще не прикасаясь к скрипту PlayerHealth.

    Scriptable Objects: вынесение данных за пределы логики

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

    Если вы создадите класс Item и повесите его на 100 гоблинов, из которых выпадает «Ржавый меч», в оперативной памяти появится 100 копий строки «Ржавый меч» и 100 копий иконки.

    Математика расхода памяти проста. Если данные одного меча весят 50 байт, то 1000 таких мечей на уровне займут: байт. Это дублирование одних и тех же данных.

    Для решения этой проблемы в Unity существует класс ScriptableObject. Он позволяет сохранять данные не внутри объектов на сцене, а в виде отдельных файлов (ассетов) в папке проекта.

    Вы создаете один файл ItemData в проекте, заполняете его характеристики и передаете ссылку на этот файл всем 1000 гоблинам. Теперь в памяти хранится только один экземпляр данных (50 байт), а гоблины просто ссылаются на него (ссылка весит 4 байта). Итоговый расход памяти составит: байт. Мы сократили потребление памяти более чем в 10 раз!

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

    Статический анализ: автоматический поиск багов

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

    > Самым главным своим достижением в качестве программиста за последние годы я считаю знакомство с методикой статического анализа кода и её активное применение. Дело даже не столько в сотнях серьёзных багов, не допущенных в код благодаря ей, сколько в перемене, вызванной этим опытом в моём программистском мировоззрении. > > Джон Кармак, создатель Doom и Quake

    Статический анализ — это процесс проверки исходного кода специальными программами (анализаторами) без фактического запуска самой игры.

    Инструменты вроде PVS-Studio или встроенного в Visual Studio Roslyn Analyzers сканируют ваш C# код на лету. Они знают тысячи паттернов ошибок. Например, анализатор мгновенно подсветит красным цветом участок кода, где вы используете тяжелую математическую операцию извлечения квадратного корня внутри цикла Update, и предложит заменить ее на сравнение квадратов дистанций, о котором мы говорили в прошлых уроках.

    Внедрение статического анализа на ранних этапах разработки экономит сотни часов на этапе тестирования, так как баги исправляются в момент их написания.

    Итоги

    * Пул объектов предотвращает зависания игры, устраняя необходимость постоянного выделения памяти и снижая нагрузку на Сборщик мусора. * Корутины и асинхронность (UniTask) позволяют выполнять действия, растянутые во времени (таймеры, плавные анимации), не блокируя основной игровой цикл. * События и делегаты (Action) реализуют паттерн «Наблюдатель», позволяя системам общаться друг с другом без жесткой связности, что делает код гибким и масштабируемым. * Scriptable Objects выносят статические данные (характеристики предметов, настройки врагов) в отдельные файлы, радикально экономя оперативную память. * Статический анализ кода выступает в роли автоматического проверяющего, который находит скрытые ошибки, утечки памяти и неоптимальные вычисления еще до запуска игры.

    17. Оптимизация производительности и управление памятью

    Оптимизация производительности и управление памятью

    Представьте ситуацию: вы создали потрясающий уровень для своей игры. На экране одновременно находятся сотни врагов, повсюду взрываются бочки, а система частиц отрисовывает реалистичный огонь. На вашем мощном компьютере разработчика игра выдает стабильные 144 кадра в секунду. Вы с гордостью отправляете проект другу, он запускает его на обычном офисном ноутбуке, и игра превращается в слайд-шоу, выдавая 15 кадров в секунду. Каждое нажатие кнопки обрабатывается с задержкой, а звук заикается.

    Написать код, который просто работает — это лишь первый шаг. Написать код, который работает быстро и эффективно расходует ресурсы устройства — это признак профессионального разработчика. В предыдущих материалах мы изучили продвинутые техники C#, такие как пулы объектов и асинхронность. Теперь мы погрузимся на уровень архитектуры памяти и процессора, чтобы понять, почему игры тормозят и как этого избежать.

    Анатомия памяти: Стек и Куча

    Чтобы понять причины падения производительности, необходимо разобраться, как язык C# хранит данные вашей игры. Вся оперативная память, выделяемая приложению, условно делится на две области: Стек (Stack) и Кучу (Heap).

    Стек работает по принципу стопки тарелок. Это очень быстрая, но небольшая область памяти. Сюда попадают так называемые значимые типы данных (Value Types): целые числа int, дробные числа float, логические значения bool и структуры struct. Когда вызывается метод, переменные кладутся на вершину стека. Как только метод завершает работу, переменные мгновенно и автоматически удаляются. Процессору не нужно тратить время на поиск и очистку — он просто «снимает верхнюю тарелку».

    Куча напоминает огромный, слегка захламленный склад. Сюда попадают ссылочные типы данных (Reference Types): любые классы class, массивы, списки List и строки текста string. Когда вы пишете команду new Enemy(), компьютер ищет свободное место на складе, кладет туда объект врага, а в Стек записывает лишь маленькую бумажку с адресом (ссылку), где именно на складе лежит этот враг. Выделение памяти в Куче происходит медленнее, но главная проблема кроется в ее очистке.

    Сборщик мусора: невидимый уборщик

    В отличие от Стека, Куча не умеет очищаться мгновенно. Когда убитый враг больше не нужен игре, он остается лежать на складе мертвым грузом. Этот груз называется «мусором».

    Периодически в дело вступает Сборщик мусора (Garbage Collector, GC). Это специальный системный процесс, который сканирует всю Кучу, находит объекты, на которые больше нет ссылок, и удаляет их, освобождая оперативную память.

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

    Для обеспечения плавной картинки в 60 кадров в секунду, время на просчет одного кадра должно подчиняться правилу:

    Где: * — время, затрачиваемое процессором на логику и отрисовку одного кадра, в миллисекундах.

    Если Сборщик мусора накопил слишком много работы, его запуск может занять 40 миллисекунд. В этот момент игра физически не может отрисовать новые кадры. Игрок воспринимает это как резкое зависание картинки (фриз). Именно поэтому в геймдеве действует золотое правило: минимизировать создание новых объектов в Куче во время активного игрового процесса.

    > Преждевременная оптимизация — корень всех зол в программировании. > > Дональд Кнут, автор книги «Искусство программирования»

    Эта цитата напоминает нам: не нужно пытаться оптимизировать каждую строчку кода с самого начала. Оптимизировать нужно только те места, которые реально вызывают падение производительности.

    Ловушка метода Update: кэширование данных

    Самая частая причина тормозов в проектах новичков — это выполнение тяжелых операций поиска внутри игрового цикла. Метод Update() вызывается каждый кадр. Если игра работает при 60 кадрах в секунду, код внутри этого метода выполняется 60 раз за одну секунду.

    Рассмотрим классический пример плохого кода:

    Метод GetComponent заставляет процессор перебирать все компоненты на объекте, чтобы найти нужный. Метод GameObject.Find заставляет процессор перебирать вообще все объекты в виртуальном мире. Если на сцене 100 врагов, и каждый из них ищет игрока через Find в методе Update, процессор будет выполнять 6000 тяжелых поисковых операций каждую секунду. Игра гарантированно начнет тормозить.

    Решение этой проблемы называется кэшированием (сохранением в память для быстрого доступа).

    Аналогия проста: вместо того чтобы каждый раз перед выходом из дома искать ключи по всей квартире (Update), вы один раз кладете их в карман куртки (Start) и затем просто достаете их за долю секунды.

    Строки и память: скрытая угроза

    В языке C# тип данных string (строка текста) обладает уникальным свойством — он неизменяем (Immutable). Это означает, что после создания строки в оперативной памяти ее невозможно отредактировать. Любая операция изменения строки на самом деле создает в Куче абсолютно новый объект, а старый превращает в мусор.

    Представим, что вы выводите таймер обратного отсчета на экран:

    Каждый кадр процессор берет слово "Осталось времени: ", берет число timeLeft, конвертирует число в текст, склеивает их вместе и выделяет под этот новый текст место в Куче. За одну минуту игры этот безобидный код создаст 3600 мертвых строк, которые заставят Сборщик мусора агрессивно вмешаться в игру.

    Чтобы избежать этого, интерфейс должен обновляться только по событию (когда прошла ровно одна секунда), а не каждый кадр. Для сложных текстовых манипуляций профессионалы используют класс StringBuilder, который умеет изменять текст без выделения новой памяти.

    Диагностика проблем: Unity Profiler

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

    Для точной постановки диагноза используется встроенный инструмент — Profiler (Профайлер). Он записывает все, что происходит в игре каждую миллисекунду, и выводит данные в виде наглядных графиков.

    | Тип узкого места (Bottleneck) | Симптомы на графиках Профайлера | Основные причины | Способ решения | | :--- | :--- | :--- | :--- | | CPU (Процессор) | Высокие пики на графике логики (Scripts). | Тяжелые циклы, Find в Update, сложный ИИ врагов. | Кэширование ссылок, упрощение математики, перенос логики в корутины. | | GPU (Видеокарта) | Высокие пики на графике рендеринга (Rendering). | Слишком много полигонов, динамические тени от множества источников света. | Использование запеченного света, снижение детализации моделей (LOD). | | Memory (Память) | График памяти постоянно растет вверх, затем резко падает (скачок GC). | Создание строк в Update, частое использование Instantiate и Destroy. | Использование паттерна Пул объектов (Object Pool), отказ от конкатенации строк каждый кадр. |

    Если вы видите, что 80% времени кадра занимает физика, вам не нужно трогать графику. Вы можете зайти в настройки проекта и увеличить шаг фиксированного времени (Fixed Timestep) с 0.02 секунды до 0.03. Физика станет чуть менее точной, но нагрузка на процессор упадет на треть.

    Оптимизация математики: отказ от квадратного корня

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

    Классическая формула расстояния между двумя точками выглядит так:

    Где: * — итоговая дистанция. * — координаты первого объекта. * — координаты второго объекта.

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

    Секрет оптимизации заключается в сравнении квадратов дистанций. Если нам нужно узнать, меньше ли дистанция, чем 15 метров, мы можем просто возвести 15 в квадрат (получив 225) и сравнить с формулой без извлечения корня.

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

    Итоги

    * Стек и Куча: Быстрые значимые типы хранятся в Стеке и удаляются мгновенно. Сложные объекты хранятся в Куче и требуют работы Сборщика мусора, который вызывает зависания игры. * Кэширование: Никогда не используйте методы поиска (GetComponent, Find) внутри метода Update(). Находите ссылки один раз в Start() и сохраняйте их в переменные. * Строки неизменяемы: Склеивание текста каждый кадр создает огромное количество мусора в памяти. Обновляйте текстовый интерфейс только в момент реального изменения данных. * Профайлер — ваш лучший друг: Не пытайтесь угадать причину тормозов. Используйте Unity Profiler, чтобы точно определить, что именно перегружено: процессор, видеокарта или оперативная память. * Упрощение математики: Заменяйте тяжелые математические операции на более легкие аналоги. Например, используйте квадрат дистанции (sqrMagnitude) вместо извлечения квадратного корня.

    18. Архитектура и планирование простого игрового проекта

    Архитектура и планирование простого игрового проекта

    Представьте, что вы решили построить загородный дом. У вас есть кирпичи (переменные и методы), цемент (игровая логика), окна (пользовательский интерфейс) и даже система умного дома (продвинутые техники и оптимизация). Вы вываливаете все это на пустой участок и начинаете хаотично скреплять кирпичи цементом, надеясь, что в итоге получится уютное жилье. С вероятностью в сто процентов вы построите кривую, неустойчивую башню, которая рухнет при первом же порыве ветра.

    В предыдущих статьях нашего курса мы детально изучили все строительные блоки языка C# и движка Unity. Мы научились двигать объекты, обрабатывать столкновения, рисовать интерфейс и оптимизировать память. Но знание того, как работает молоток, не делает вас архитектором. Чтобы превратить набор разрозненных скриптов в полноценную, работающую игру, необходим четкий план и правильная архитектура.

    Дизайн-документ: чертеж вашей игры

    Профессиональная разработка начинается не с написания кода, а с текстового редактора. Любая игра должна иметь Дизайн-документ (Game Design Document, GDD). В крупных студиях это талмуды на сотни страниц, но для инди-разработчика или новичка достаточно одной-двух страниц четкого текста.

    Главная задача GDD — зафиксировать правила виртуального мира, чтобы в середине разработки вы не решили внезапно добавить в гоночный симулятор элементы фермы и механику стрельбы из лука (это явление называется Feature Creep — расползание масштаба).

    Рассмотрим структуру микро-GDD на примере классической аркады, которую мы назовем «Космический рубеж» (Space Defender):

    * Жанр: 2D Top-Down Shooter (Шутер с видом сверху). * Цель игры: Продержаться 60 секунд под натиском астероидов и набрать максимальное количество очков. * Управление: Клавиши W, A, S, D для перемещения корабля, левая кнопка мыши для стрельбы. * Сущности: Игрок:* Имеет 3 жизни. При столкновении с астероидом теряет 1 жизнь. Астероид:* Летит сверху вниз. Уничтожается одним попаданием лазера. Приносит 10 очков. Лазер:* Летит снизу вверх. Уничтожается при столкновении с астероидом или выходе за пределы экрана. * Условия победы/поражения: Победа — таймер достиг нуля. Поражение — жизни игрока упали до нуля.

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

    Формула расчета интенсивности появления врагов (спавна):

    Где: * — количество астероидов, появляющихся за одну секунду. * — общее количество астероидов, которое движок должен создать за игру (например, 120 штук). * — время выживания в секундах (60 секунд).

    Подставив значения, мы получим: . Значит, наш скрипт генерации должен создавать ровно 2 астероида каждую секунду. Это конкретная математическая задача, которую легко перенести в код C#.

    Декомпозиция: от идеи к скриптам

    Имея на руках правила игры, мы должны разбить их на независимые программные модули. Начинающие программисты часто создают один гигантский скрипт Game.cs на 2000 строк, который управляет и игроком, и врагами, и звуком, и интерфейсом. Это классический антипаттерн, называемый Божественный объект (God Object). Такой код невозможно читать, тестировать и изменять.

    Правильная архитектура строится на принципе единственной ответственности (Single Responsibility Principle). Каждый скрипт должен выполнять только одну задачу.

    Разобьем наш «Космический рубеж» на логические компоненты:

    | Название скрипта (Класс C#) | Зона ответственности | Взаимодействие с другими системами | | :--- | :--- | :--- | | PlayerController | Считывание ввода (Input), перемещение корабля по осям X и Y. | Вызывает метод стрельбы у Weapon. | | Weapon | Создание объектов лазера (желательно через Пул объектов). | Не зависит ни от чего. | | Health | Хранение количества жизней, обработка урона, смерть. | Сообщает GameManager о смерти игрока. | | Asteroid | Движение вниз, обработка столкновений с лазером или игроком. | Сообщает GameManager о начислении очков. | | Spawner | Генерация астероидов по таймеру за пределами экрана. | Читает состояние игры из GameManager. | | UIManager | Обновление текста очков и жизней на Canvas. | Слушает события от GameManager и Health. | | GameManager | Глобальный контроль: старт игры, пауза, победа, поражение. | Центральный узел связи. |

    Такое разделение позволяет вам, например, взять скрипт Health и без изменений перенести его на астероид (если вы решите, что большим астероидам нужно 3 попадания для уничтожения). Код становится универсальным.

    Паттерн Singleton: глобальный менеджер игры

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

    Если астероид уничтожен, он должен прибавить 10 очков к общему счету. Как скрипту астероида найти скрипт менеджера? Использовать GameObject.Find() каждый раз — это убийство производительности, о чем мы говорили в прошлой статье.

    Для решения этой задачи в простых играх используется архитектурный паттерн Одиночка (Singleton). Он гарантирует, что у класса есть только один экземпляр (объект) на всю игру, и предоставляет к нему глобальную точку доступа.

    Реализация паттерна Singleton на C# в Unity выглядит так:

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

    > Паттерны проектирования — это не готовый код, а описание решения типичной проблемы, которое можно адаптировать к вашей ситуации. > > Роберт Мартин (Дядя Боб), автор книги «Чистая архитектура»

    Важное предупреждение: Singleton — это мощный, но опасный инструмент. Если вы начнете делать синглтонами игрока, врагов и оружие, ваш код превратится в жестко связанный монолит. Используйте этот паттерн только для глобальных систем (Менеджер игры, Менеджер аудио, Менеджер сохранений).

    Управление состояниями игры (Game States)

    Игра не статична. Она проходит через различные фазы. Когда игрок находится в главном меню, астероиды не должны появляться, а корабль не должен стрелять. Когда игрок ставит игру на паузу, время должно остановиться.

    Для управления этими фазами внутри GameManager используется паттерн Конечный автомат, который мы ранее применяли для анимаций. В коде это реализуется через перечисление enum.

    Теперь скрипт Spawner, который создает астероиды, перед созданием нового объекта может просто спросить: if (GameManager.Instance.CurrentState == GameState.Playing). Если игра на паузе или окончена, спавнер ничего не сделает. Это избавляет вас от необходимости писать сложную логику остановки в каждом отдельном скрипте.

    Этапы разработки: от серых кубов к релизу

    Архитектура кода — это половина успеха. Вторая половина — это правильный процесс сборки самого проекта в движке. Начинающие разработчики часто совершают фатальную ошибку: они скачивают красивые 3D-модели, настраивают реалистичное освещение, добавляют музыку, а потом пытаются заставить персонажа прыгать. В итоге красивая модель проваливается сквозь красивый пол, а проект отправляется в корзину.

    Профессиональная разработка всегда идет по строгим этапам (пайплайну).

    Этап 1: Грейбоксинг (Greyboxing) и прототипирование

    На этом этапе в игре нет ни одной текстуры или звука. Вы используете стандартные примитивы движка: серые кубы, сферы и капсулы.

    Ваша задача — написать скрипты PlayerController, Asteroid и Weapon и заставить серый куб (игрока) стрелять маленькими серыми сферами (лазерами) по падающим красным кубам (астероидам). Если игра из серых кубов играется скучно, никакая современная графика ее не спасет. Грейбоксинг позволяет быстро тестировать механики, не тратя время на настройку анимаций.

    Этап 2: Основной цикл (Core Loop)

    Когда кубы двигаются правильно, вы внедряете GameManager и UIManager. Вы замыкаете игру в кольцо:

  • Нажатие кнопки «Старт» в меню.
  • Игровой процесс (набор очков).
  • Смерть (появление экрана Game Over).
  • Нажатие кнопки «Рестарт» (возврат к пункту 2).
  • Пока этот цикл не работает безупречно, переходить к следующему этапу нельзя.

    Этап 3: Интеграция контента (Art & Audio)

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

    Этап 4: Полировка и «Сочность» (Juice)

    Игра работает и выглядит хорошо, но ощущается «деревянной». На этом этапе вы добавляете микро-взаимодействия: * Тряска экрана (Screen Shake) при получении урона. * Системы частиц (Particle Systems) — искры при попадании лазера в астероид. * Плавное изменение цифр в интерфейсе (микроанимации UI). * След от двигателей корабля.

    Именно эти мелочи отличают любительскую поделку от коммерческого продукта.

    Оценка времени: правило Питера

    Планирование архитектуры включает в себя и планирование времени. Новички всегда недооценивают сложность задач. Вам может казаться, что написать скрипт движения корабля — это 10 минут. Но настройка плавного торможения, ограничение выхода за пределы экрана и обработка столкновений займут 3 часа.

    В IT-индустрии существует эмпирическая формула для оценки времени разработки небольших проектов:

    Где: * — реальное время, которое вы потратите на проект. * — ваша оптимистичная оценка времени на написание скриптов. * — время на поиск или создание графики. * — время на подбор звуков. * — коэффициент запаса на отладку (поиск багов), полировку и непредвиденные проблемы.

    Если вы посчитали, что соберете «Космический рубеж» за 10 часов, смело умножайте это время на 1.5. Планируйте потратить 15 часов, и вы не разочаруетесь в процессе.

    Итоги

    * Дизайн-документ (GDD) — обязательный первый шаг. Он фиксирует правила игры, условия победы и базовую математику баланса, защищая проект от бесконечного разрастания идей. * Принцип единственной ответственности требует разбивать логику на множество мелких скриптов (Игрок, Оружие, Здоровье), избегая создания гигантских нечитаемых классов. * Паттерн Singleton идеально подходит для создания GameManager — единой точки доступа к глобальным данным (счет, состояние игры), избавляя от необходимости искать объекты на сцене. * Грейбоксинг — важнейший этап разработки. Механики и физика всегда тестируются на простых серых кубах до того, как в проект добавляется сложная графика и анимация. * Управление состояниями через enum (Меню, Игра, Пауза) позволяет централизованно контролировать поведение всех систем игры, останавливая время или блокируя ввод игрока в нужный момент.

    19. Практическая реализация первой игры шаг за шагом

    Практическая реализация первой игры шаг за шагом

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

    Создание игры — это процесс перевода человеческих идей на строгий язык математики и логики C#. В качестве нашего первого проекта мы реализуем классическую 2D-аркаду «Космический рубеж» (Space Defender).

    Архитектура проекта: подготовка чертежей

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

    | Игровой объект (GameObject) | Необходимые компоненты Unity | Зона ответственности скрипта на C# | | :--- | :--- | :--- | | Player (Игрок) | SpriteRenderer, BoxCollider2D, Rigidbody2D | Считывание нажатий клавиш, перемещение корабля, запуск лазеров. | | Asteroid (Враг) | SpriteRenderer, CircleCollider2D, Rigidbody2D | Падение вниз, уничтожение при попадании лазера, нанесение урона игроку. | | Laser (Снаряд) | SpriteRenderer, CapsuleCollider2D, Rigidbody2D | Полет вверх, фиксация столкновения с астероидом. | | GameManager (Менеджер) | Canvas, Text (для UI) | Подсчет очков, обновление интерфейса, контроль состояния игры. |

    > Завершить игру — это 90% работы. Оставшиеся 10% — это полировка, которая занимает еще 90% времени. > > Тим Суини, основатель Epic Games

    Шаг 1: Оживление игрока и математика движения

    Начнем с главного героя. Корабль должен двигаться влево и вправо в пределах экрана. Для этого мы создадим скрипт PlayerController.

    В основе плавного перемещения лежит формула, отвязанная от частоты кадров (FPS):

    Где: * — новая позиция корабля. * — текущая позиция корабля. * — направление движения (от -1 до 1, получаемое с клавиатуры). * — базовая скорость корабля в метрах в секунду. * — время между кадрами (Time.deltaTime).

    Перенесем эту математику в код:

    Если скорость , а игрок зажал стрелку вправо (), при частоте 60 кадров в секунду () за один кадр корабль сдвинется на метра. За секунду он пролетит ровно 8 метров. Условия if создают невидимые математические стены, не позволяя координате X превысить 9 или упасть ниже -9.

    Шаг 2: Создание угрозы (Генерация астероидов)

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

    Для создания объектов в Unity используется метод Instantiate. Чтобы астероиды не появлялись в одной точке, мы будем использовать генератор случайных чисел Random.Range.

    Сам скрипт астероида Asteroid будет предельно простым: в методе Update он должен двигаться вниз (по вектору Vector3.down), умножая скорость на Time.deltaTime. Если его координата Y становится меньше -7 (он улетел за нижний край экрана), он должен вызвать метод Destroy(gameObject), чтобы освободить оперативную память.

    Шаг 3: Механика стрельбы и физика столкновений

    Чтобы игрок мог защищаться, добавим стрельбу. В скрипт PlayerController добавим проверку нажатия клавиши Пробел.

    Теперь самое интересное — обработка попадания. Лазер и астероид — это физические объекты. В настройках их коллайдеров (Collider2D) мы обязаны поставить галочку Is Trigger (Является триггером). Лазер не должен физически толкать астероид, как бильярдный шар, он должен пронзить его и вызвать программное событие.

    Напишем скрипт Laser, который будет висеть на префабе нашего снаряда:

    Шаг 4: Связь логики и UI через GameManager

    Игра работает, но игрок не получает обратной связи. Нам нужен счетчик очков. Создадим на сцене Canvas, добавим текстовый элемент и напишем глобальный класс GameManager, используя паттерн Singleton (Одиночка), чтобы лазер мог легко передать ему информацию о сбитом астероиде.

    Теперь мы возвращаемся в наш скрипт Laser и перед строчкой Destroy(other.gameObject) добавляем вызов менеджера:

    Каждый сбитый астероид будет приносить 10 очков. Обратите внимание на архитектуру: интерфейс не проверяет счет каждый кадр в методе Update. Он ждет, пока GameManager сам вызовет метод UpdateUI(). Это экономит ресурсы процессора.

    Шаг 5: Оптимизация производительности (Пул объектов)

    Наша игра работает, но в ней скрыта бомба замедленного действия. Если игрок будет очень быстро нажимать Пробел, методы Instantiate (создание) и Destroy (уничтожение) будут вызываться десятки раз в секунду.

    Каждый вызов Destroy оставляет в оперативной памяти «мусор». Когда мусора станет слишком много, Сборщик мусора (Garbage Collector) остановит игру на долю секунды, чтобы очистить память. Игрок увидит неприятный рывок (фриз).

    Чтобы сделать игру профессиональной, мы применим паттерн Пул объектов (Object Pool). Вместо того чтобы создавать и уничтожать лазеры, мы создадим 20 лазеров при запуске игры, выключим их и будем просто перемещать и включать по мере необходимости.

    Математика экономии колоссальна. Если один объект лазера занимает в памяти 256 байт, то 1000 выстрелов за уровень с использованием Instantiate заставят процессор выделить и очистить байт памяти. При использовании пула из 20 объектов мы выделяем всего байт один раз при загрузке, и эта цифра больше не растет.

    Реализация базового пула выглядит так:

    Теперь в скрипте игрока вместо Instantiate мы пишем:

    А в скрипте лазера при попадании в астероид вместо Destroy(gameObject) мы пишем gameObject.SetActive(false). Снаряд мгновенно исчезает с экрана и возвращается в «обойму», готовый к следующему выстрелу. Никакого мусора, никаких зависаний.

    Итоги

    * Архитектура решает всё: Разделение игры на независимые скрипты (PlayerController, Asteroid, GameManager) позволяет легко находить ошибки и масштабировать проект. * Математика времени: Использование Time.deltaTime при перемещении объектов гарантирует, что скорость игры будет одинаковой на любых мониторах и устройствах. * Физика без сопротивления: Для фиксации попаданий снарядов используются коллайдеры в режиме триггера (Is Trigger) и метод OnTriggerEnter2D. * Событийный интерфейс: Текстовые элементы UI должны обновляться только в момент фактического изменения данных (например, при начислении очков), а не каждый кадр. * Оптимизация памяти: Использование паттерна Пул объектов (Object Pool) избавляет игру от микрозависаний, предотвращая постоянную работу Сборщика мусора.

    2. Переменные, типы данных и базовые операции

    Переменные, типы данных и базовые операции

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

    В прошлом уроке мы научились отдавать компьютеру базовые команды и выводить текст на экран. Но статичный текст — это еще не игра. Любая игра, от простейшего тетриса до масштабной стратегии, представляет собой непрерывный процесс изменения данных. Координаты пули меняются каждый кадр, здоровье босса уменьшается от ударов, а таймер отсчитывает время до взрыва. Чтобы управлять этим хаосом, программистам нужен способ надежно хранить информацию и обращаться к ней.

    Для этого используются переменные.

    Что такое переменная в контексте памяти

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

    Самая простая аналогия — это коробка на складе. Чтобы не потерять нужную вещь среди тысяч других коробок, вы наклеиваете на нее этикетку с понятным названием (например, «Здоровье игрока»). Внутрь вы кладете само значение (например, число 100). Когда игре нужно узнать, жив ли персонаж, она «смотрит» на этикетку, открывает коробку и читает текущее число.

    В языке C# создание такой коробки называется объявлением переменной. Процесс состоит из двух обязательных шагов: указания типа данных и придумывания имени.

    В этом примере мы создали пустую коробку. Компьютер выделил память, но внутри пока ничего нет. Чтобы положить туда значение, используется операция инициализации (присваивания) с помощью знака равенства =.

    Чаще всего разработчики объединяют эти два шага в одну строку, чтобы сделать код более компактным и читаемым:

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

    Строгая типизация: почему C# требует порядка

    Вы могли заметить слово int перед именем переменной. Это тип данных.

    Язык C# является статически типизированным. Это означает, что при создании переменной вы обязаны жестко задать форму и размер вашей «коробки». Если вы сказали компьютеру, что в коробке будут лежать только целые числа, вы больше никогда не сможете положить туда текст или дробное значение. Компилятор просто выдаст ошибку и откажется запускать игру.

    > Плохие программисты думают о коде. Хорошие программисты думают о структурах данных и их взаимосвязях. > > Линус Торвальдс, создатель Linux

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

    Основные типы данных в арсенале разработчика

    Платформа .NET (на которой базируется C#) предлагает десятки различных типов данных, но для создания 90% игровых механик вам понадобятся всего четыре основных.

    | Тип данных | Описание | Пример использования в играх | Пример в коде | | :--- | :--- | :--- | :--- | | int | Целые числа (от -2 млрд до 2 млрд) | Количество патронов, уровень персонажа, счетчик очков | int ammo = 30; | | float | Дробные числа (с плавающей точкой) | Координаты в пространстве, скорость, время перезарядки | float speed = 5.5f; | | string | Текст любой длины | Имя игрока, диалоги, названия предметов | string hero = "Артур"; | | bool | Логическое значение (истина/ложь) | Состояние (жив/мертв), наличие ключа от двери | bool hasKey = true; |

    Обратите внимание на тип float. При присвоении ему значения в C# обязательно нужно ставить букву f на конце (например, 5.5f). Если этого не сделать, компилятор посчитает число типом double (числом двойной точности), который занимает в два раза больше памяти. В игровой индустрии, особенно в движке Unity, стандартом является именно float, так как он обеспечивает идеальный баланс между точностью физических расчетов и экономией оперативной памяти.

    Неявная типизация (var)

    Иногда названия типов становятся слишком длинными, и код теряет читаемость. Начиная с третьей версии, в C# появилось ключевое слово var. Оно позволяет не писать тип данных явно — компилятор сам догадается, какой тип нужен, посмотрев на значение справа от знака равенства.

    Важно понимать: var не отменяет строгую типизацию. Переменная enemyLevel навсегда останется целым числом int. Вы не сможете позже записать в нее текст. Использовать var можно только в том случае, если вы сразу присваиваете переменной значение (инициализируете ее).

    Базовые операции: как оживить игру

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

    В C# доступны все классические математические действия: * Сложение: + * Вычитание: - Умножение: * Деление: / * Остаток от деления: %

    Рассмотрим механику получения урона. У нас есть здоровье игрока и урон, который наносит ловушка.

    После выполнения этого кода в переменной playerHealth будет лежать число 85.

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

    Вместо playerHealth = playerHealth - trapDamage; профессиональные разработчики пишут:

    Это работает для всех операторов: += (прибавить к текущему), *= (умножить текущее) и так далее. Если же вам нужно просто увеличить счетчик ровно на единицу (например, игрок подобрал одну монетку), используется оператор инкремента ++:

    Оператор остатка от деления (%)

    Если сложение и умножение интуитивно понятны, то оператор % часто вызывает вопросы у новичков. Он возвращает остаток, который получается при делении одного целого числа на другое.

    Например, . Почему? Потому что число 3 помещается в 10 ровно три раза (это 9), и до 10 остается ровно 1.

    В геймдеве этот оператор невероятно полезен. Представьте, что вы делаете пошаговую стратегию, где ходы передаются по кругу между 4 игроками (игроки под номерами 0, 1, 2, 3). Как определить, чей сейчас ход, если номер текущего хода равен 17?

    Номер игрока = Номер хода % Количество игроков. При 17 ходе и 4 игроках получится 1. Значит, сейчас ходит игрок под номером 1.

    Область видимости: где живут переменные

    В C# переменные не существуют вечно и не видны отовсюду. У каждой переменной есть своя область видимости (scope) — территория в коде, где к ней можно обратиться.

    Границы этой территории определяются фигурными скобками { }. Переменная рождается в тот момент, когда код доходит до ее объявления, и навсегда уничтожается (стирается из памяти), когда программа выходит за пределы закрывающей фигурной скобки того блока, где переменная была создана.

    Рассмотрим пример структуры игрового уровня:

    Понимание области видимости критически важно для оптимизации игр. Если вы создаете переменную внутри метода, который отвечает за выстрел, эта переменная займет память на долю секунды, выполнит свою задачу (например, рассчитает траекторию) и тут же освободит память. Если бы все переменные хранились в памяти вечно, игра бы быстро «съела» всю оперативную память компьютера и зависла.

    Итоги

    * Переменные — это именованные ячейки памяти для хранения данных игры. Перед использованием переменную нужно объявить (задать тип и имя) и инициализировать (присвоить значение). C#* — язык со строгой типизацией. Тип данных переменной нельзя изменить после ее создания. Для чисел используются int и float, для текста — string, для логических состояний — bool. * Ключевое слово var позволяет компилятору самому определить тип данных на основе присвоенного значения, но не отменяет правил строгой типизации. Для изменения данных используются арифметические операторы (+, -, , /, %). Для краткости применяются комбинированные операторы вроде += или ++. * Жизненный цикл переменной ограничен ее областью видимости — блоком кода между фигурными скобками { }, в котором она была объявлена.

    20. Тестирование, отладка и финальная сборка игры

    Тестирование, отладка и финальная сборка игры

    Представьте ситуацию: вы потратили три месяца на создание своей первой ролевой игры. Вы написали сотни строк кода на C#, настроили анимации, сбалансировали урон оружия и добавили эпичного финального босса. На вашем компьютере всё работает безупречно. Вы с гордостью отправляете игру другу, он запускает её, нажимает кнопку «Новая игра»... и приложение мгновенно закрывается, выбрасывая на рабочий стол окно с непонятным текстом ошибки.

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

    Искусство поиска ошибок: Отладка (Debugging)

    Даже самые опытные программисты допускают ошибки. В геймдеве ошибка может проявляться по-разному: персонаж проваливается сквозь пол, здоровье уходит в минус вместо нуля, или игра просто зависает намертво. Процесс поиска и устранения таких ошибок называется отладкой (debugging).

    Самый простой и популярный среди новичков инструмент отладки — это вывод сообщений в консоль. В движке Unity для этого используется класс Debug.

    Каждый раз, когда враг бьет игрока, в специальном окне редактора появляется текстовое сообщение. Это позволяет вам убедиться, что метод действительно вызывается, а математика работает верно. Если базовое здоровье равно 100, а урон равен 20, вы ожидаете увидеть в консоли число 80. Если вы видите 120, значит, вы случайно перепутали знак минуса с плюсом в формуле.

    Однако у Debug.Log есть темная сторона. Вывод текста в консоль — это невероятно тяжелая операция для процессора. Если вы оставите команду Debug.Log внутри метода Update(), который вызывается 60 раз в секунду, игра начнет катастрофически тормозить. Перед финальным релизом все отладочные сообщения необходимо удалять или отключать.

    Точки останова: заморозка времени

    Вывод текста в консоль помогает при простых ошибках, но бессилен перед сложной логикой. Что делать, если алгоритм поиска пути врага работает неверно, и вы хотите посмотреть, как меняются переменные шаг за шагом?

    Для этого профессионалы используют точки останова (breakpoints). В редакторах кода (таких как Visual Studio или Rider) вы можете кликнуть на поля слева от номера строки кода, и там появится красный круг.

    Когда вы запустите игру и выполнение кода дойдет до этой строки с красным кругом, игра буквально «заморозится». Время остановится. В этот момент вы можете навести курсор мыши на любую переменную в вашем коде и увидеть, какое именно число или текст хранится в ней прямо сейчас. Вы можете нажимать специальную кнопку в редакторе, чтобы выполнять код по одной строчке за раз, наблюдая, как программа принимает решения внутри конструкций if и switch. Это самый мощный инструмент для понимания того, как именно работает ваш код.

    Анатомия краша: Исключения (Exceptions)

    Иногда ошибка в коде настолько критична, что компьютер не знает, как продолжать работу. В таких случаях программа генерирует исключение (exception) — аварийный сигнал о том, что произошло нечто недопустимое.

    Самый страшный кошмар любого разработчика на C# — это NullReferenceException (исключение нулевой ссылки).

    Оно возникает, когда вы пытаетесь обратиться к объекту, которого не существует в оперативной памяти. Представьте, что вы дали команду: «Открой дверь». Но вы забыли указать, какую именно дверь, или эта дверь уже была уничтожена взрывом. Компьютер впадает в ступор и выбрасывает исключение.

    Чтобы игра не вылетала на рабочий стол при возникновении предсказуемых ошибок, используется конструкция безопасного выполнения try...catch (попробуй...поймай).

    В этом примере мы понимаем, что игрок мог случайно удалить файл save.txt с жесткого диска. Если бы мы не использовали блок try...catch, попытка прочитать удаленный файл привела бы к мгновенному крашу игры. Теперь же мы элегантно «ловим» ошибку и запускаем новую игру.

    Виды тестирования в геймдеве

    Отладка помогает исправить ошибку, когда вы уже знаете о ее существовании. Но как найти ошибки до того, как их найдут игроки? Для этого применяется системное тестирование.

    | Вид тестирования | Что проверяет | Кто проводит | Пример из игры | | :--- | :--- | :--- | :--- | | Модульное (Unit) | Отдельные изолированные методы и классы. | Программист | Проверка формулы расчета критического урона. | | Интеграционное | Взаимодействие нескольких систем вместе. | Программист / QA-инженер | Проверка того, что при получении урона обновляется UI и проигрывается звук. | | Плейтест (Playtest) | Общий игровой опыт, баланс, удобство управления. | Геймдизайнер / Игроки | Проверка того, не слишком ли сложен босс на 5 уровне. |

    Модульное тестирование: автоматизация проверок

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

    Чтобы избежать таких регрессий (когда новое ломает старое), разработчики пишут модульные тесты (unit tests). Это специальные скрипты, которые автоматически проверяют другие скрипты.

    В Unity для этого встроен инструмент Test Runner. Вот как выглядит простой тест:

    Этот тест создает виртуальный инвентарь на 5 слотов, кладет туда один меч и проверяет, стал ли счетчик предметов равен единице. Вы можете написать сотни таких тестов. Перед тем как выпустить обновление игры, вы нажимаете кнопку «Запустить тесты». Компьютер за долю секунды прогоняет все проверки. Если хотя бы один тест загорается красным — значит, вы что-то сломали, и выпускать игру нельзя.

    > Тестирование не может доказать отсутствие ошибок, оно может лишь показать их наличие. > > Эдсгер Дейкстра, пионер компьютерных наук

    Математика случайности: тестирование вероятностей

    Одной из самых сложных механик для тестирования является случайность (рандом). Допустим, вы создали редкого дракона, из которого с вероятностью 5% должен выпадать легендарный щит. Игроки на форумах начинают жаловаться: «Я убил 50 драконов, а щит так и не выпал! Ваш рандом сломан!».

    Как разработчику доказать, что код работает правильно? Обратимся к теории вероятностей.

    Формула вероятности того, что событие не произойдет ни разу за серию попыток, выглядит так:

    Где: * — итоговая вероятность того, что предмет ни разу не выпадет. * — шанс выпадения предмета в виде десятичной дроби (5% = 0.05). * — количество попыток (убитых драконов).

    Подставим данные нашего возмущенного игрока в формулу:

    Умножив результат на 100, мы получаем 7.6%. Это означает, что почти 8 игроков из 100, убив 50 драконов, абсолютно законно и математически обоснованно не получат свой щит. Код работает идеально, просто человеческий мозг плохо воспринимает чистую теорию вероятностей.

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

    От редактора к игроку: Финальная сборка (Build)

    Когда все баги исправлены, а тесты горят зеленым, наступает этап упаковки проекта. Игроки не будут устанавливать себе редактор Unity и скачивать ваши исходные коды на C#. Им нужен готовый исполняемый файл (например, .exe для Windows или .apk для Android).

    Процесс превращения исходного кода и сырых картинок в готовую игру называется компиляцией и сборкой (build).

    Этот процесс можно сравнить с автомобильным конвейером. В редакторе Unity у вас лежат чертежи (скрипты), листы металла (3D-модели) и банки с краской (текстуры). Во время сборки движок:

  • Переводит ваш понятный человеку код на C# в машинный код, состоящий из нулей и единиц, который понимает процессор.
  • Сжимает текстуры и аудиофайлы под конкретную платформу (например, для мобильных телефонов текстуры сжимаются сильнее, чем для ПК).
  • Упаковывает все сцены, модели и код в единый зашифрованный архив.
  • Настройки сборки (Build Settings)

    Перед тем как нажать заветную кнопку «Build», необходимо настроить проект. В Unity это делается в окне Build Settings и Player Settings.

    Здесь вы указываете критически важные параметры: * Scenes in Build: Какие именно уровни войдут в игру. Если вы забыли добавить сцену «Уровень 2» в этот список, игра просто вылетит при попытке перехода на него. * Resolution and Presentation: Будет ли игра запускаться в полноэкранном режиме или в окне? Можно ли менять размер окна? * Icon: Установка красивой иконки для ярлыка игры на рабочем столе. * Scripting Backend: Выбор технологии компиляции (Mono или IL2CPP). IL2CPP переводит ваш C# код в C++, что делает игру значительно быстрее и защищает код от взлома и пиратства.

    Оптимизация размера сборки

    Начинающие разработчики часто удивляются: «Моя игра состоит из одного уровня и пары кубиков, почему готовый .exe файл весит 2 гигабайта?».

    Ответ кроется в ресурсах. Движок упаковывает в сборку всё, что лежит в папке Resources, и всё, на что есть ссылки в сценах. Если вы скачали из интернета 4K-текстуру асфальта весом 50 мегабайт и применили её к полу, она попадет в сборку в несжатом виде.

    Для оптимизации размера необходимо:

  • Установить правильные параметры сжатия (Compression) для каждой картинки в инспекторе.
  • Снизить максимальное разрешение текстур (Max Size). Для мобильной игры текстура 1024x1024 пикселей визуально не отличается от 4096x4096, но весит в 16 раз меньше.
  • Удалить из проекта неиспользуемые ассеты и плагины.
  • Применив эти простые шаги, вы можете уменьшить размер финальной сборки с 2 ГБ до 150 МБ, что критически важно для удержания игроков (никто не хочет скачивать тяжелую игру ради пяти минут геймплея).

    Итоги

    Отладка (Debugging) — это процесс поиска ошибок. Использование точек останова (breakpoints*) позволяет заморозить время в игре и построчно проверить логику, что гораздо эффективнее, чем вывод текста через Debug.Log. * Исключения (Exceptions) — это критические ошибки, приводящие к вылету игры (например, NullReferenceException). Для их безопасной обработки и предотвращения крашей используется конструкция try...catch. * Модульное тестирование (Unit Testing) позволяет написать скрипты, которые автоматически проверяют работоспособность других скриптов, защищая игру от поломок при добавлении новых функций. * Сборка (Build) — это процесс компиляции кода и сжатия ресурсов в готовый исполняемый файл (.exe, .apk). * Перед релизом необходимо тщательно настроить параметры сборки и оптимизировать размер текстур, чтобы игра не занимала лишнее место на диске игрока.

    3. Управление потоком: условные конструкции и циклы

    Управление потоком: условные конструкции и циклы

    Представьте, что вы играете в классическую ролевую игру. Ваш израненный персонаж подходит к тяжелой дубовой двери. Вы нажимаете кнопку взаимодействия, и игра мгновенно принимает решение: если в вашем инвентаре есть ржавый ключ, дверь со скрипом открывается; если ключа нет, на экране появляется надпись «Дверь заперта».

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

    Искусство выбора: конструкция if...else

    Как компьютер понимает, когда нужно показать экран «Game Over»? Он постоянно задает вопросы и проверяет условия. Главный инструмент для создания таких проверок в языке C# — это условная конструкция if (с английского — «если»).

    Она работает по простейшему принципу: программа оценивает логическое выражение (условие). Если это условие оказывается истинным (true), выполняется определенный блок кода. Если ложным (false) — программа просто игнорирует этот блок и идет дальше.

    Рассмотрим базовую механику получения урона:

    В этом примере после удара врага здоровье игрока становится равным -10. Компьютер доходит до строки с if и проверяет математическое условие: . Это утверждение истинно. Следовательно, код внутри фигурных скобок выполняется, и игрок видит сообщение о смерти.

    Но что, если мы хотим предусмотреть альтернативный вариант развития событий? Для этого существует ключевое слово else («иначе»).

    Здесь у игрока 150 золотых монет, а меч стоит 200. Условие является ложным. Программа пропускает блок if и автоматически выполняет блок else. Игрок не получает меч и сохраняет свои деньги.

    Цепочки условий: else if

    Игровые ситуации редко бывают черно-белыми. Часто нам нужно проверить сразу несколько вариантов. Представьте искусственный интеллект врага, который реагирует на дистанцию до игрока. Для создания сложных цепочек проверок используется конструкция else if.

    При дистанции 5.5 метров программа сначала проверит первое условие (). Оно ложно. Затем она перейдет ко второму условию (). Оно истинно! Враг начнет стрелять из лука. Важно понимать: как только программа находит первое истинное условие в цепочке, она выполняет его блок кода и полностью игнорирует все остальные проверки ниже.

    Логические операторы: когда одного условия мало

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

    Для объединения нескольких условий в одно C# предлагает логические операторы.

    | Оператор | Название | Как работает | Пример из игр | | :--- | :--- | :--- | :--- | | && | Логическое И (AND) | Возвращает true, только если оба условия истинны. | Игрок стоит на земле && нажал кнопку прыжка Прыжок. | | \|\| | Логическое ИЛИ (OR) | Возвращает true, если хотя бы одно условие истинно. | Здоровье равно 0 \|\| вышло время таймера Проигрыш. | | ! | Логическое НЕ (NOT) | Инвертирует значение. Превращает true в false и наоборот. | !Игрок отравлен Начать восстановление здоровья. |

    Посмотрим, как оператор && применяется на практике при проверке возможности использовать мощное заклинание:

    Обратите внимание на запись !isSilenced. Переменная хранит значение false (игрок может говорить). Оператор ! переворачивает его в true. Таким образом, оба условия по бокам от && становятся истинными, и заклинание срабатывает.

    > Логика — это анатомия мышления. > > Джон Локк, английский философ

    Множественный выбор: конструкция switch

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

    Чаще всего switch используется для управления состояниями игры (Главное меню, Игра, Пауза, Настройки) или для обработки инвентаря. Представим систему выбора оружия по нажатию цифровых клавиш:

    Компьютер берет значение переменной weaponSlot (в нашем случае это число 2) и ищет совпадение среди блоков case. Найдя case 2, он выполняет код внутри него.

    Ключевое слово break обязательно: оно приказывает программе немедленно выйти из конструкции switch после выполнения нужного блока. Блок default срабатывает в том случае, если ни одно из значений не подошло (аналог else).

    Циклы: автоматизация рутины

    Представьте, что вам нужно создать на уровне 100 золотых монет. Вы можете написать команду создания монеты 100 раз подряд. Это займет много времени, сделает код огромным, а если вы решите изменить монеты на кристаллы, вам придется переписывать все 100 строк.

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

    Цикл while (Пока)

    Цикл while — это самый простой вид цикла. Он работает точно так же, как if, но с одним отличием: после выполнения блока кода программа возвращается к началу и снова проверяет условие. Это продолжается до тех пор, пока условие не станет ложным.

    Рассмотрим механику постепенного восстановления здоровья (регенерации), когда игрок выпивает зелье:

    В этом примере цикл выполнит ровно три итерации. Здоровье увеличится до 80, затем до 90, затем до 100. На четвертой проверке условие окажется ложным, цикл остановится, и программа пойдет дальше.

    Цикл for (Для)

    Цикл while отлично подходит, когда мы не знаем заранее, сколько раз нужно выполнить действие. Но в геймдеве чаще встречаются задачи с известным количеством повторений: проверить 5 слотов инвентаря, нанести урон 3 врагам в радиусе взрыва, создать 10 платформ.

    Для таких задач используется цикл for. Его синтаксис выглядит сложнее, но он дает максимальный контроль над процессом. Объявление цикла состоит из трех частей, разделенных точкой с запятой:

  • Инициализация счетчика: создание переменной, которая будет считать итерации (обычно ее называют i).
  • Условие: правило, при котором цикл продолжает работу.
  • Изменение счетчика: действие, которое происходит в конце каждой итерации (обычно увеличение на 1).
  • Напишем код, который выдает игроку 5 стрел и подсчитывает их общее количество:

    Переменная i существует только внутри этого цикла. На первой итерации она равна 1, на второй — 2, и так далее. Это невероятно удобно, если вам нужно расставить объекты в ряд. Например, если умножить i на 2 метра, вы сможете создать 5 платформ, каждая из которых будет находиться на 2 метра дальше предыдущей.

    Управление внутри цикла: break и continue

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

    Ключевое слово break полностью прерывает работу цикла. Представьте, что вы ищете ключ в массиве из 100 сундуков. Если вы нашли ключ в третьем сундуке, нет смысла проверять оставшиеся 97. Вы используете break, чтобы сэкономить ресурсы компьютера.

    Ключевое слово continue прерывает только текущую итерацию и заставляет цикл сразу перейти к следующей. Например, вы наносите урон всем врагам в комнате, но хотите проигнорировать тех, кто спрятался за укрытием. Вы проверяете статус врага, и если он в укрытии, вызываете continue — урон не наносится, и цикл переходит к следующему врагу.

    Оптимизация производительности: цена бесконечности

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

    Посмотрите на этот код:

    Поскольку выносливость всегда остается равной 10, условие всегда истинно. Компьютер будет печатать текст миллионы раз в секунду, пытаясь выполнить задачу. Вся оперативная память и ресурсы процессора будут мгновенно поглощены этой задачей. В контексте игры это означает полное и безвозвратное зависание приложения (фриз). Игроку придется принудительно закрывать игру через диспетчер задач.

    В игровых движках, таких как Unity, код выполняется десятки раз в секунду (каждый кадр). Поэтому золотое правило оптимизации гласит: никогда не помещайте тяжелые вычисления или длинные циклы в методы, которые вызываются каждый кадр, если в этом нет абсолютной необходимости.

    Итоги

    * Управление потоком позволяет программе принимать решения и нелинейно реагировать на действия игрока. * Конструкции if, else if и else используются для ветвления логики на основе истинности или ложности условий. * Логические операторы (&&, ||, !) позволяют комбинировать простые условия в сложные игровые правила. * Конструкция switch идеально подходит для проверки одной переменной на множество конкретных значений (например, состояний или типов предметов). Циклы while и for автоматизируют повторяющиеся действия. While удобен при неизвестном количестве повторений, а for* — когда количество итераций известно заранее. * Ошибки в логике циклов могут привести к созданию бесконечного цикла, что вызывает полное зависание игры.

    4. Массивы, списки и работа с коллекциями

    Массивы, списки и работа с коллекциями

    Представьте, что вы создаете классический космический шутер. Игрок управляет кораблем и стреляет по надвигающимся астероидам. В первых уроках мы научились создавать переменные для хранения здоровья игрока и использовать условные конструкции для проверки столкновений. Но как быть с самими астероидами?

    Если на экране одновременно находится пять астероидов, вы можете создать пять переменных: asteroid1, asteroid2, asteroid3 и так далее. Но что, если игра генерирует метеоритный дождь из сотни объектов? А если каждую секунду старые астероиды уничтожаются, а новые появляются? Создавать тысячи переменных вручную невозможно.

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

    Массивы: строгая геометрия данных

    Самая базовая и быстрая структура данных в языке C# — это массив (array). Массив можно представить как длинный стеллаж с пронумерованными ячейками одинакового размера. В каждую ячейку можно положить только один предмет, и все предметы должны быть одного типа.

    Главная особенность массива — его фиксированный размер. В момент создания стеллажа вы обязаны сказать компьютеру, сколько в нем будет ячеек. Достроить новые ячейки позже не получится.

    Создание и заполнение массива

    Представим, что мы создаем ролевую игру, и у персонажа есть пояс на 4 слота для зелий лечения. Каждое зелье восстанавливает определенное количество здоровья (целое число).

    Объявление массива выглядит так:

    Квадратные скобки [] после типа данных int говорят компилятору, что это не одно число, а целая коллекция. Ключевое слово new выделяет место в оперативной памяти компьютера ровно для четырех целых чисел.

    Чтобы положить зелье в слот или узнать, что там лежит, нам нужно обратиться к конкретной ячейке по ее индексу (порядковому номеру).

    В программировании счет всегда начинается с нуля. Поэтому в массиве из 4 элементов индексы будут распределены от 0 до 3.

    Если вы попытаетесь обратиться к potionSlots[4], игра мгновенно вылетит с ошибкой IndexOutOfRangeException (выход за пределы массива), потому что пятой ячейки не существует. Индекс последнего элемента всегда вычисляется по формуле , где — общее количество элементов в массиве.

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

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

    Списки: динамичный инвентарь

    В современных играх данные редко бывают статичными. Игрок может собрать 10 монет, а может 1000. Враги появляются и умирают. Для таких ситуаций массивы слишком неповоротливы. На помощь приходит список — класс List.

    Список — это умная обертка над массивом. Он ведет себя как бездонный рюкзак: вы можете добавлять в него новые предметы и удалять старые в любой момент времени, а список сам позаботится о том, чтобы изменить свой размер.

    Чтобы использовать списки, в самом начале файла с кодом должна быть подключена библиотека коллекций (в Unity она подключается по умолчанию):

    Управление списком

    Создадим динамический список квестов, которые игрок берет у неигровых персонажей (NPC):

    Списки обладают множеством встроенных инструментов. Например, чтобы узнать, сколько квестов сейчас активно, достаточно обратиться к свойству Count. Если activeQuests.Count равен нулю, значит, список пуст.

    > Покажите мне ваш код, скрыв структуры данных, и я буду продолжать недоумевать. Покажите мне ваши структуры данных, и код больше не понадобится — он станет очевиден. > > Фредерик Брукс, инженер-программист и ученый

    Цикл foreach: элегантный перебор

    В прошлом уроке мы изучили цикл for. Он отлично работает с массивами и списками, если нам нужен точный контроль над индексами. Но чаще всего в играх нам нужно просто применить действие ко всем объектам в коллекции. Например, нанести урон всем врагам, попавшим в зону взрыва гранаты.

    Для этого в C# существует специализированный цикл foreach (с английского — «для каждого»).

    Цикл foreach берет на себя всю рутину: он сам узнает размер коллекции, сам переходит от первого элемента к последнему и сам останавливается. Вам не нужно создавать счетчик i и следить за выходом за границы массива.

    Важное правило: внутри цикла foreach нельзя изменять саму коллекцию (например, добавлять или удалять врагов из enemiesHealth). Если вы попытаетесь это сделать, цикл собьется и выдаст ошибку. Для удаления элементов во время перебора следует использовать классический цикл for, идущий с конца в начало.

    Словари: мгновенный поиск по ключу

    Представьте систему крафта (создания предметов). У вас есть сундук с ресурсами. Игрок хочет создать железный меч, для которого нужно 5 слитков железа. Как проверить, есть ли в сундуке железо?

    Если использовать список, компьютеру придется перебирать каждый предмет в сундуке один за другим, пока он не найдет железо. Если в сундуке 10 000 предметов, это займет время.

    Для таких задач используется словарь — класс Dictionary. Он хранит данные в виде пар «Ключ-Значение». Вы задаете уникальный ключ (например, название ресурса), и словарь мгновенно выдает связанное с ним значение (количество), не перебирая остальные элементы.

    Словари невероятно эффективны. Поиск по ключу в словаре происходит практически мгновенно, независимо от того, лежит в нем 10 элементов или 1 000 000.

    Очереди и Стеки: специализированные инструменты

    Помимо списков и словарей, в арсенале C# есть коллекции со строгими правилами добавления и извлечения элементов.

    Очередь (Queue)

    Работает по принципу «Первым пришел — первым ушел» (FIFO: First In, First Out). Это обычная очередь в магазине. Тот, кто встал первым, будет обслужен первым. В играх Queue используется для пошаговых стратегий (очередь ходов персонажей), систем строительства (очередь постройки зданий в RTS) или обработки нажатий клавиш.

    Стек (Stack)

    Работает по принципу «Последним пришел — первым ушел» (LIFO: Last In, First Out). Представьте стопку тарелок: вы кладете новую тарелку на самый верх, и когда вам нужно взять тарелку, вы берете ту, которую положили последней. В играх Stack идеален для управления пользовательским интерфейсом (UI). Когда игрок открывает «Инвентарь», затем «Настройки», а затем нажимает кнопку «Назад» (Escape), игра берет из стека последнее открытое окно («Настройки») и закрывает его, возвращая игрока в «Инвентарь».

    Сводная таблица коллекций

    | Структура данных | Изменение размера | Как получаем доступ к данным | Идеальное применение в играх | | :--- | :--- | :--- | :--- | | Массив (Array) | Нельзя изменить | По числовому индексу | Фиксированные данные (слоты экипировки, точки спавна) | | Список (List) | Динамический | По числовому индексу | Изменяющиеся группы (активные враги, снаряды в полете) | | Словарь (Dictionary)| Динамический | По уникальному ключу | Инвентарь ресурсов, база данных характеристик предметов | | Очередь (Queue) | Динамический | Только первый элемент | Очередь команд, пошаговые бои | | Стек (Stack) | Динамический | Только последний элемент | Навигация по меню, отмена последних действий (Undo) |

    Оптимизация: цена динамичности

    В разработке игр производительность стоит на первом месте. Если игра начнет «тормозить», игрок быстро потеряет к ней интерес. Списки (List) очень удобны, но за их гибкость приходится платить ресурсами компьютера.

    Как список работает внутри? На самом деле, любой список в C# — это замаскированный массив. Когда вы создаете пустой список, система создает под капотом массив на 4 элемента. Вы добавляете 4 предмета. Когда вы пытаетесь добавить пятый предмет, список понимает, что места нет.

    Он делает следующее:

  • Создает новый массив, размер которого в два раза больше предыдущего (на 8 элементов).
  • Копирует все старые данные в новый массив.
  • Добавляет ваш пятый элемент.
  • Выбрасывает старый массив.
  • Выброшенный массив становится «мусором» в оперативной памяти. Специальная программа, Сборщик мусора (Garbage Collector), периодически сканирует память и удаляет этот мусор. Если вы каждый кадр создаете и удаляете огромные списки, Сборщик мусора будет работать без остановки, что вызовет микрозависания игры (фризы).

    Как этого избежать? Если вы примерно знаете, сколько элементов будет в списке, укажите его начальную вместимость (Capacity) при создании:

    В этом случае список сразу создаст внутренний массив на 100 ячеек. При добавлении первых ста зомби не произойдет ни одного расширения массива, ни одного копирования и не появится никакого мусора. Это правило — один из главных секретов оптимизации профессиональных разработчиков на Unity.

    Итоги

    * Массивы — это коллекции фиксированного размера. Они работают очень быстро, но их размер нельзя изменить после создания. Индексация всегда начинается с нуля. * Списки (List) — это динамические коллекции, которые могут автоматически увеличиваться и уменьшаться. Они идеально подходят для объектов, количество которых постоянно меняется во время игры. * Цикл foreach — самый удобный и безопасный способ перебрать все элементы коллекции от начала до конца, не рискуя выйти за ее пределы. * Словари (Dictionary) хранят данные парами «Ключ-Значение» и позволяют мгновенно находить нужную информацию без перебора всей коллекции. * Для оптимизации игр следует заранее указывать начальную вместимость списков, чтобы снизить нагрузку на Сборщик мусора и избежать зависаний.

    5. Основы объектно-ориентированного программирования

    Основы объектно-ориентированного программирования

    Представьте, что вы создаете масштабную ролевую игру. В вашем виртуальном мире есть главный герой, десятки неигровых персонажей (NPC), сотни монстров, сундуки с сокровищами и ловушки. Если использовать только базовые переменные и списки, изученные нами ранее, код быстро превратится в хаос. Вам придется создавать отдельные переменные для здоровья первого гоблина, второго гоблина, третьего гоблина, а затем писать бесконечные проверки для каждого из них.

    Чтобы справиться с такой сложностью, программисты придумали объектно-ориентированное программирование (ООП). Это не просто набор новых команд, а совершенно иной способ мышления. В ООП мы перестаем смотреть на программу как на простой список математических инструкций. Вместо этого мы представляем программу как виртуальный мир, населенный самостоятельными сущностями — объектами, которые взаимодействуют друг с другом.

    Классы и объекты: чертежи и детали

    В основе ООП лежат два неразрывно связанных понятия: класс и объект.

    Класс — это абстрактный чертеж, шаблон или инструкция. Он описывает, какими характеристиками должна обладать сущность и что она умеет делать. Сам по себе класс не существует в игровом мире и не занимает оперативную память (так же, как чертеж автомобиля не может ездить по дороге).

    Объект (или экземпляр класса) — это конкретная сущность, созданная по этому чертежу. Объекты существуют в памяти компьютера, имеют свои уникальные данные и могут выполнять действия.

    Посмотрим на процесс создания класса в языке C#:

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

    Теперь, когда у нас есть чертеж, мы можем «наштамповать» по нему сколько угодно реальных противников в главном коде игры:

    Ключевое слово new приказывает компьютеру выделить память и собрать новый объект по чертежу Enemy. Теперь goblin и troll — это два абсолютно независимых объекта. Если гоблин получит урон и его здоровье упадет до нуля, здоровье тролля останется равным 150.

    > Объектно-ориентированное программирование — это исключительно удачная идея, которая позволяет управлять сложностью программного обеспечения. > > Грэди Буч, один из создателей языка UML

    Конструкторы: рождение объекта

    Заполнять характеристики объекта по одной строчке (как мы делали выше) неудобно. В играх объекты часто появляются каждую секунду (например, пули при стрельбе из автомата). Нам нужен способ задавать начальные данные объекта в момент его появления на свет.

    Для этого используется конструктор — специальный метод внутри класса, который вызывается автоматически при использовании слова new. Имя конструктора всегда строго совпадает с именем класса.

    Теперь создание объекта сокращается до одной элегантной строки:

    Три кита ООП

    Вся мощь объектно-ориентированного подхода держится на трех фундаментальных принципах: инкапсуляции, наследовании и полиморфизме. Понимание этих концепций отличает новичка от профессионального разработчика игр.

    1. Инкапсуляция: защита данных

    Инкапсуляция — это механизм сокрытия внутренних данных объекта от вмешательства извне.

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

    В коде инкапсуляция реализуется с помощью модификаторов доступа.

    | Модификатор | Описание | Аналогия из жизни | | :--- | :--- | :--- | | public | Доступ открыт для всех других классов. | Кнопки на панели банкомата. | | private | Доступ разрешен только внутри самого класса. | Внутренний сейф с деньгами. | | protected | Доступ разрешен классу и его наследникам. | Служебное меню для инкассаторов. |

    Если мы сделаем здоровье игрока public, любой другой скрипт (например, скрипт падающего камня) сможет напрямую изменить его: player.health = -5000;. Это приведет к багам. Правильный подход — сделать здоровье приватным, а для его изменения написать публичный метод.

    Теперь скрипт ловушки не может сломать переменную health. Он обязан вежливо «попросить» игрока получить урон, вызвав метод TakeDamage(20). А сам игрок внутри метода может решить, как обработать этот урон (например, учесть броню или уклонение).

    2. Наследование: эволюция кода

    В играх часто встречаются объекты с похожим поведением. Например, у вас есть обычный зомби, быстрый зомби и зомби-босс. У всех них есть здоровье, скорость и метод движения. Писать три разных класса с нуля — значит дублировать один и тот же код.

    Наследование позволяет создать новый класс на основе уже существующего. Существующий класс называется базовым (или родителем), а новый — производным (или наследником). Наследник автоматически получает все поля и методы родителя, но может добавлять свои собственные.

    Если мы создадим объект класса Bird, он сможет использовать как метод Fly() (свой собственный), так и метод Move() (полученный по наследству от Animal). Это колоссально экономит время при разработке сложных игровых систем.

    3. Полиморфизм: многоликость объектов

    Слово полиморфизм переводится с греческого как «множество форм». В программировании это означает способность разных объектов по-разному реагировать на одну и ту же команду.

    Представьте, что игрок нажимает кнопку «Использовать» перед объектом. Если это дверь — она откроется. Если это зелье — оно выпьется. Если это рычаг — он опустится. Команда одна, а результаты разные.

    В C# полиморфизм часто реализуется через переопределение методов. Базовый класс задает общее правило с помощью ключевого слова virtual, а наследники изменяют его под себя с помощью слова override.

    Теперь игра может собрать всех персонажей в один список и в цикле приказать каждому Speak(). Компьютер сам разберется, кто именно должен произнести какую фразу, основываясь на их реальном классе.

    Взаимодействие объектов: математика в ООП

    Объекты в играх не существуют в вакууме. Они постоянно взаимодействуют: сталкиваются, измеряют дистанцию, обмениваются уроном.

    Рассмотрим классическую задачу: враг должен решить, атаковать ли игрока. Для этого врагу нужно узнать расстояние до игрока. В 2D-играх расстояние между двумя точками рассчитывается с помощью теоремы Пифагора.

    Формула расчета дистанции выглядит так:

    Где: * — итоговое расстояние между объектами. * и — координаты первого объекта (например, игрока по осям X и Y). * и — координаты второго объекта (врага).

    Как это выглядит в парадигме ООП? Мы не пишем эту формулу просто в пустоте. Мы создаем метод внутри класса врага, который принимает объект игрока в качестве параметра, извлекает его координаты и проводит вычисления.

    Если игрок находится на координатах (0, 0), а враг на координатах (3, 4), то разница по X составит 3, а по Y составит 4. Возводим в квадрат: 9 и 16. Складываем: 25. Корень из 25 равен 5. Дистанция между объектами — 5 метров. Если радиус атаки врага равен 2 метрам, он поймет, что игрок слишком далеко, и начнет к нему приближаться.

    Итоги

    * Класс — это абстрактный чертеж, описывающий свойства и поведение сущности. Объект — это конкретный экземпляр, созданный по этому чертежу в памяти компьютера. * Конструктор — специальный метод, который автоматически настраивает начальные параметры объекта в момент его создания (при вызове new). * Инкапсуляция защищает внутренние данные объекта от случайных изменений извне с помощью модификаторов доступа (private, public). * Наследование позволяет создавать новые классы на базе существующих, перенимая их функционал и избегая дублирования кода. * Полиморфизм дает возможность объектам разных классов по-разному реагировать на вызов одного и того же метода.

    6. Классы, объекты, методы и свойства

    Классы, объекты, методы и свойства

    Представьте, что вы создаете виртуальный город для своей игры. По улицам ходят сотни прохожих, по дорогам ездят автомобили, а в небе летают птицы. В прошлой статье мы выяснили, что управлять таким хаосом помогает объектно-ориентированное программирование (ООП). Мы научились создавать базовые чертежи — классы, и штамповать по ним конкретные сущности — объекты.

    Но пустой чертеж бесполезен. Чтобы автомобиль поехал, у него должен быть двигатель, запас топлива и механизм управления. В языке C# внутреннее устройство любого объекта состоит из четко определенных строительных блоков. Сегодня мы разберем анатомию классов и научимся наделять наши игровые объекты данными и поведением.

    Поля: хранилище данных объекта

    Любая игра — это непрерывный поток данных. Координаты пули, количество золотых монет в кошельке, имя гильдии — все это нужно где-то хранить. Когда переменная объявляется внутри класса, она называется полем (field).

    Поля представляют собой состояние объекта в конкретный момент времени.

    Рассмотрим класс игрового персонажа:

    В этом примере playerName и currentHealth — это публичные поля. Любой другой скрипт в игре может обратиться к ним и изменить их. Поле maxHealth является приватным: оно доступно только внутри самого класса Player.

    Оставлять поля публичными — это как оставить кошелек открытым на столе в оживленной таверне. Любой проходящий мимо скрипт (например, скрипт ловушки или врага) может случайно записать в здоровье игрока отрицательное значение или текст вместо числа, что приведет к поломке игры.

    Чтобы защитить данные, профессиональные разработчики используют свойства.

    Свойства: умные стражи ваших данных

    Свойство (property) в C# — это специальный механизм, который выглядит для внешнего мира как обычное поле, но внутри себя содержит логику (методы) для чтения и записи данных. Эти скрытые методы называются аксессорами (get и set).

    > Свойства позволяют объектам скрывать реализацию, сохраняя при этом удобный интерфейс доступа к данным. > > Андерс Хейлсберг, создатель языка C#

    Полные свойства с проверкой логики

    Представьте механику получения урона. Здоровье персонажа не может опуститься ниже нуля. Если использовать обычное поле, нам придется писать эту проверку каждый раз, когда персонаж получает урон. Свойство позволяет встроить эту проверку прямо в момент изменения данных.

    Теперь, если скрипт ловушки попытается выполнить команду player.Health = -50;, сработает блок set. Переменная value будет равна -50. Условие не пропустит отрицательное число, и здоровье персонажа станет равным 0. Игра спасена от критической ошибки.

    Автоматические свойства

    Если вам не нужна сложная логика проверок, но вы хотите сохранить правильную архитектуру (например, разрешить чтение данных всем, а изменение — только самому классу), в C# есть автосвойства.

    Компилятор сам создаст невидимые приватные поля под капотом. Это делает код невероятно чистым и компактным.

    Сравнение полей и свойств

    | Характеристика | Поле (Field) | Свойство (Property) | | :--- | :--- | :--- | | Назначение | Простое хранение данных в памяти | Контроль доступа и валидация данных | | Синтаксис | public int speed; | public int Speed { get; set; } | | Наличие логики | Невозможно добавить код при изменении | Можно добавить проверки в блоки get и set | | Использование в играх | Скрытые внутренние счетчики класса | Публичные характеристики (Здоровье, Мана, Урон) |

    Методы: поведение и действия

    Если поля и свойства — это существительные (имя, здоровье, скорость), то методы — это глаголы. Метод представляет собой именованный блок кода, который выполняет определенное действие.

    Меч, который просто лежит в инвентаре, бесполезен. Он должен уметь атаковать. Зелье должно уметь восстанавливать здоровье.

    Анатомия метода

    Каждый метод состоит из нескольких обязательных частей:

  • Модификатор доступа (например, public или private).
  • Тип возвращаемого значения (что метод отдает после своей работы).
  • Имя метода (в C# принято писать с большой буквы).
  • Параметры (входные данные в круглых скобках).
  • Тело метода (код в фигурных скобках).
  • Ключевое слово void означает «пустота». Оно используется, когда метод просто выполняет действие (например, проигрывает звук или выводит текст) и не должен возвращать никакого математического результата.

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

    Математика внутри методов

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

    Формула расчета критического урона:

    Где: * — итоговый урон, который наносится врагу. * — базовый урон вашего оружия. * — множитель критического удара (например, 1.5 означает увеличение урона на 50%).

    Реализуем эту формулу в виде метода на C#:

    Если базовый урон равен 40, а множитель равен 2.0, метод вернет число 80. Этот результат можно сразу вычесть из здоровья босса.

    Перегрузка методов: гибкость действий

    Иногда одно и то же действие в игре может происходить при разных обстоятельствах. Например, игрок может получить урон от падения (нужно передать только число урона), а может получить урон от отравленного кинжала (нужно передать число урона и тип яда).

    Вместо того чтобы придумывать разные имена вроде TakeFallDamage и TakePoisonDamage, язык C# позволяет использовать перегрузку методов (method overloading).

    Перегрузка — это создание нескольких методов с абсолютно одинаковым именем, но разным набором параметров.

    Когда вы вызовете player.TakeDamage(20), компилятор сам поймет, что нужно использовать первый вариант. А при вызове player.TakeDamage(15, "Огонь") будет использован второй. Это делает код интуитивно понятным и удобным для использования.

    Статические члены: общие правила мира

    До сих пор мы говорили о полях и методах, которые принадлежат конкретному объекту. Здоровье одного орка никак не связано со здоровьем другого. Но что, если нам нужны данные, которые общие для всей игры?

    Например, гравитация. Она не принадлежит конкретному камню или персонажу. Она едина для всего виртуального мира. Для таких случаев используется ключевое слово static.

    Статические поля и методы принадлежат самому классу, а не его объектам. Вам не нужно создавать объект через new, чтобы обратиться к статическому члену.

    В любом другом скрипте вашей игры вы можете написать GameSettings.AddScore(50);. Вам не нужно создавать экземпляр настроек.

    Статические классы и методы идеально подходят для создания менеджеров игры (GameManager), хранения глобальных настроек, подсчета общего количества убитых врагов или создания математических библиотек. Например, встроенный в C# класс Math является статическим: вы просто пишете Math.Sqrt(25) для извлечения корня, не создавая объект математики.

    Взаимодействие объектов через методы

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

    Для расчета процента здоровья воина после лечения используем формулу:

    Где: * — текущий процент здоровья. * — здоровье после лечения. * — максимальный запас здоровья.

    В этом примере класс Priest имеет метод CastHeal, который требует передать ему любой объект типа Unit. Когда метод вызывается, жрец обращается к публичному методу ReceiveHealing внутри воина, передавая туда силу своего заклинания. Объекты взаимодействуют, инкапсуляция сохранена, а логика игры работает безупречно.

    Итоги

    * Поля — это переменные внутри класса, хранящие состояние объекта. Их принято делать приватными (private), чтобы защитить данные от случайного изменения извне. * Свойства — это безопасная оболочка для полей. С помощью блоков get и set они позволяют контролировать, кто и как может читать или изменять данные объекта. * Методы — это функции внутри класса, определяющие поведение объекта. Они могут принимать входные параметры и возвращать результат с помощью ключевого слова return. * Перегрузка методов позволяет создавать несколько методов с одинаковым именем, но разными параметрами, что делает код более гибким. * Статические члены (static) принадлежат самому классу, а не конкретным объектам. Они используются для глобальных данных и функций, общих для всей игры.

    7. Продвинутое ООП: наследование, полиморфизм и интерфейсы

    Продвинутое ООП: наследование, полиморфизм и интерфейсы

    Представьте, что вы разрабатываете фэнтезийную ролевую игру. В вашем мире есть десятки видов противников: медлительные зомби, быстрые гоблины, бронированные орки и летающие драконы. Опираясь на знания из прошлых уроков, вы можете создать отдельный класс для каждого из них. Но вскоре вы заметите проблему: у всех этих существ есть здоровье, скорость передвижения и метод получения урона. Если вы напишете один и тот же код для каждого из пятидесяти монстров, ваш проект превратится в неподдерживаемый хаос. Изменение формулы расчета брони потребует ручной правки в пятидесяти разных файлах.

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

    Наследование: эволюция игровых сущностей

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

    Наследование позволяет создать новый класс на основе уже существующего. Существующий класс выступает в роли фундамента и называется базовым классом (или родителем). Новый класс, который перенимает свойства и методы родителя, называется производным классом (или наследником).

    Рассмотрим базовый класс любого существа в нашей игре:

    Теперь нам нужен гоблин. Вместо того чтобы заново писать свойства для имени и здоровья, мы указываем компилятору C#, что гоблин является наследником класса Enemy. Для этого используется символ двоеточия :.

    Создав объект Goblin, мы автоматически получаем доступ к методу Move() и свойству Health, хотя мы не писали их внутри класса гоблина. При этом у гоблина появилось уникальное свойство StealChance (шанс кражи), которого нет у обычных врагов.

    > Повторное использование кода — это святой Грааль программной инженерии. > > Дуглас Крокфорд, архитектор программного обеспечения

    Защищенные данные: модификатор protected

    В прошлой статье мы изучили модификаторы public (доступно всем) и private (доступно только внутри текущего класса). Но как быть, если мы хотим скрыть переменную от внешнего мира, но разрешить доступ к ней классам-наследникам?

    Для этого существует модификатор protected. Если базовый урон хранится в переменной protected int baseDamage, то скрипт игрока не сможет изменить это значение напрямую, но класс Goblin (как прямой наследник) сможет использовать эту переменную для своих расчетов.

    Полиморфизм: многоликость поведения

    Вы нажимаете кнопку атаки. Если в руках у персонажа меч, он делает взмах. Если лук — выпускает стрелу. Если магический посох — кастует огненный шар. Команда (метод) одна и та же — «Атаковать», но физическое воплощение этого действия кардинально отличается в зависимости от типа оружия.

    Способность объектов разных классов по-разному реагировать на вызов одного и того же метода называется полиморфизмом (от греческого poly — много, morph — форма).

    Чтобы реализовать полиморфизм в C#, базовый класс должен разрешить наследникам изменять свое поведение. Для этого перед методом ставится ключевое слово virtual (виртуальный). А класс-наследник, желающий изменить это поведение, использует ключевое слово override (переопределить).

    Истинная магия полиморфизма раскрывается при работе с коллекциями. Вы можете создать список типа Weapon и поместить в него и мечи, и луки. Затем, перебирая список в цикле foreach, вы просто вызываете метод Attack() для каждого элемента. Компьютер сам определит реальный тип объекта в момент выполнения программы и вызовет правильную анимацию и логику.

    Физика и полиморфизм: расчет энергии

    В играх с реалистичной физикой урон от снаряда часто зависит не от выдуманных чисел, а от законов реального мира. Разные классы снарядов (ядро, стрела, пуля) могут иметь разную массу и скорость, но подчиняться единому физическому закону.

    Кинетическая энергия летящего объекта вычисляется по классической формуле:

    Где: * — кинетическая энергия (в игре она конвертируется в базовый урон). * — масса снаряда в килограммах. * — скорость полета снаряда в метрах в секунду.

    Если мы создадим виртуальный метод CalculateDamage(), то тяжелое пушечное ядро массой 10 кг, летящее со скоростью 50 м/с, нанесет 12 500 единиц урона. А легкая стрела массой 0.1 кг при скорости 80 м/с нанесет всего 320 единиц урона. Полиморфизм позволяет каждому классу снаряда подставлять свои уникальные параметры в общую математическую модель.

    Абстрактные классы: чертежи без воплощения

    Иногда базовый класс получается настолько общим, что создание его прямого экземпляра теряет всякий смысл. Вы можете создать в игре конкретного орка или конкретного скелета. Но можете ли вы создать просто «Врага»? Как он выглядит? Какая у него моделька?

    Чтобы запретить создание объектов базового класса, его делают абстрактным с помощью ключевого слова abstract.

    Абстрактный класс — это строгий шаблон. Если метод помечен как abstract, он не содержит фигурных скобок с кодом. Это жесткое требование к наследникам: любой класс, наследующий Animal, обязан предоставить свою реализацию метода MakeSound(), иначе компилятор выдаст ошибку и игра не запустится.

    Интерфейсы: контракты игрового мира

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

    Логично было бы создать метод TakeDamage(). Но как объединить их? Герой наследуется от класса Character, ящик — от класса Prop, а колонна — от Environment. В языке C# запрещено множественное наследование классов (один класс не может иметь двух родителей одновременно). Это сделано для того, чтобы избежать конфликтов, если у обоих родителей есть методы с одинаковыми именами.

    Здесь на сцену выходят интерфейсы.

    Интерфейс — это чистый контракт. Он не содержит полей для хранения данных и не содержит логики выполнения. Он лишь описывает, что должен уметь делать объект, но не говорит, как он должен это делать.

    По негласному стандарту программистов на C#, имена интерфейсов всегда начинаются с заглавной буквы I.

    Теперь мы можем заставить любые, совершенно не связанные между собой классы, подписать этот контракт (реализовать интерфейс):

    Обратите внимание на синтаксис: class Player : Character, IDamageable. Игрок наследует базовый класс Character и одновременно реализует интерфейс IDamageable. В отличие от классов, один объект может реализовывать бесконечное множество интерфейсов (например, IDamageable, IMovable, IInteractable).

    Как интерфейсы меняют архитектуру

    Когда происходит взрыв гранаты, игре не нужно знать, кто именно находится в радиусе поражения — игрок, враг или ящик. Игра просто ищет все объекты вокруг и проверяет, подписали ли они контракт IDamageable.

    Такой подход называется слабой связностью (loose coupling). Система взрывов ничего не знает о системе инвентаря, ящиках или игроке. Она знает только об интерфейсе. Это делает код невероятно устойчивым к багам и легким для расширения.

    Сравнение абстрактных классов и интерфейсов

    Чтобы не путаться в этих двух похожих концепциях, используйте простую шпаргалку:

    | Характеристика | Абстрактный класс | Интерфейс | | :--- | :--- | :--- | | Суть | Кем объект является (Is-a). Собака является животным. | Что объект умеет делать (Can-do). Собака умеет бегать (IRunnable). | | Наследование | Можно наследоваться только от одного класса. | Можно реализовать множество интерфейсов одновременно. | | Хранение данных | Может содержать поля и переменные с данными. | Не может содержать поля (только методы и свойства). | | Реализация методов | Может содержать готовый код внутри методов. | Содержит только названия методов без кода (до C# 8.0). |

    Итоги

    * Наследование позволяет создавать новые классы на базе существующих, перенимая их свойства и методы, что радикально сокращает дублирование кода. * Полиморфизм дает возможность объектам разных классов по-разному реагировать на вызов одного и того же метода (используя связку virtual и override). * Абстрактные классы служат строгими шаблонами. Создать объект абстрактного класса невозможно, он существует только для того, чтобы от него наследовались другие. * Интерфейсы — это контракты, гарантирующие, что объект умеет выполнять определенные действия. Они решают проблему отсутствия множественного наследования классов в C# и делают архитектуру игры гибкой.

    8. Введение в разработку игр и игровые движки

    Введение в разработку игр и игровые движки

    Представьте, что вы написали идеальный код для своей ролевой игры. Вы создали абстрактный класс оружия, реализовали интерфейсы для получения урона и настроили инкапсуляцию так, что ни одна переменная не может быть изменена случайно. Ваша логика безупречна. Но когда вы запускаете программу, вы видите лишь черный экран консоли и строчки текста: «Гоблин получил 15 урона».

    Как превратить эти сухие математические расчеты в живой мир? Как заставить модельку гоблина отшатнуться от удара, проиграть звук лязга металла и отрисовать вспышку искр? Писать систему рендеринга графики и расчета физики столкновений с нуля на C++ — это задача, на которую у команды профессионалов уходят годы. Чтобы не изобретать велосипед заново, разработчики используют игровые движки.

    Что такое игровой движок

    Игровой движок (Game Engine) — это комплексная программная среда, которая берет на себя самую тяжелую и рутинную работу по взаимодействию с «железом» компьютера (видеокартой, процессором, оперативной памятью и устройствами ввода).

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

    Современный движок состоит из нескольких ключевых подсистем: Рендерер (Renderer*): отвечает за отрисовку 2D и 3D графики, расчет освещения, теней и отражений. Физический движок (Physics Engine*): рассчитывает гравитацию, массу объектов, их ускорение и обрабатывает столкновения (коллизии). * Аудиосистема: управляет воспроизведением звуков, их позиционированием в пространстве (3D-звук) и эффектами эха. Система ввода (Input System*): считывает нажатия клавиш клавиатуры, движения мыши или стиков геймпада.

    > Игровой движок — это то, что остается от игры, когда из нее убирают весь художественный контент, модели и уровни. > > Джейсон Грегори, ведущий программист Naughty Dog

    Битва титанов: выбор инструмента

    Сегодня на рынке существует множество движков, но индустрия стандартизировалась вокруг нескольких гигантов. Выбор движка определяет язык программирования, на котором вы будете писать логику, и общую архитектуру проекта.

    | Игровой движок | Основной язык | Сложность освоения | Идеальное применение | | :--- | :--- | :--- | :--- | | Unity | C# | Средняя | Мобильные игры, инди-проекты, 2D/3D игры, AR/VR приложения. | | Unreal Engine | C++ / Blueprints | Высокая | AAA-игры с фотореалистичной графикой, крупные студийные проекты. | | Godot | GDScript / C# | Низкая | Небольшие 2D-игры, прототипирование, проекты с открытым исходным кодом. |

    В нашем курсе мы фокусируемся на языке C#, поэтому естественным выбором становится Unity. Это самый популярный движок в мире: более 50% всех мобильных игр и огромное количество хитов для ПК (таких как Hollow Knight, Subnautica, Escape from Tarkov) созданы именно на нем. Unity предоставляет идеальный баланс между мощностью и дружелюбностью к новичкам.

    Архитектура Unity: композиция вместо наследования

    В предыдущих статьях мы глубоко изучали объектно-ориентированное программирование (ООП) и строили длинные цепочки наследования: Орк наследовался от Врага, а Враг от Персонажа.

    Когда вы переходите в игровой движок, парадигма немного меняется. Unity использует архитектурный паттерн Entity-Component-System (ECS) или его упрощенную версию — компонентный подход. Главное правило здесь: композиция важнее наследования.

    В Unity абсолютно всё, что вы видите на сцене (камера, источник света, главный герой, камень на дороге), является пустым контейнером. Этот контейнер называется GameObject (Игровой объект).

    Сам по себе GameObject не умеет ничего, кроме как существовать в пространстве. Чтобы наделить его свойствами, мы прикрепляем к нему Компоненты (Components).

    Представьте, что мы создаем космический корабль игрока. Мы берем пустой GameObject и добавляем к нему:

  • Компонент Transform (есть у всех по умолчанию) — задает координаты .
  • Компонент SpriteRenderer — рисует картинку корабля на экране.
  • Компонент BoxCollider2D — создает невидимую физическую рамку вокруг корабля, чтобы он мог врезаться в астероиды.
  • Компонент PlayerController — это наш собственный скрипт на C#, который мы написали. Он считывает нажатия стрелочек и двигает корабль.
  • Если мы захотим создать вражеский корабль, нам не нужно писать новый класс, наследующийся от корабля игрока. Мы просто создаем новый GameObject, вешаем на него ту же картинку, тот же коллайдер, но вместо PlayerController прикрепляем скрипт EnemyAI, который управляется компьютером. Это делает разработку невероятно гибкой.

    Игровой цикл и концепция времени

    Обычная программа на C# (например, калькулятор) работает по принципу ожидания: она ждет, пока вы введете числа, затем мгновенно выдает результат и снова замирает.

    Игры работают иначе. Игра — это бесконечный цикл, который безостановочно крутится с момента запуска до момента выхода в меню. Этот цикл называется Game Loop.

    В Unity каждый ваш скрипт (компонент) может подключаться к этому циклу с помощью специальных встроенных методов. Самые важные из них — Start() и Update().

    * Метод Start() вызывается ровно один раз в момент появления объекта в игре. Здесь мы задаем начальные значения (например, выдаем игроку 100 здоровья при возрождении). * Метод Update() вызывается каждый кадр. Если ваша игра работает при 60 кадрах в секунду (FPS), то код внутри Update() выполнится 60 раз за одну секунду.

    Математика движения и Time.deltaTime

    Поскольку метод Update() вызывается каждый кадр, именно в нем мы пишем логику перемещения. Но здесь кроется главная ловушка для новичков.

    Представьте, что вы написали код: «Каждый кадр сдвигать персонажа на 1 метр вперед». У игрока с мощным компьютером игра выдает 100 FPS. Его персонаж пробежит 100 метров за секунду. У игрока со слабым ноутбуком игра выдает 30 FPS. Его персонаж пробежит всего 30 метров за секунду. Это катастрофа для баланса: скорость игры стала зависеть от мощности компьютера.

    Чтобы отвязать логику от частоты кадров, физика в играх опирается на реальное время. Для этого используется переменная Delta Time () — время, прошедшее с момента отрисовки предыдущего кадра.

    Базовая формула перемещения в пространстве выглядит так:

    Где: * — итоговая дистанция, на которую нужно сдвинуть объект в текущем кадре. * — желаемая скорость объекта (например, в метрах в секунду). * — время между кадрами (в Unity это Time.deltaTime).

    Рассмотрим пример с конкретными числами. Допустим, скорость нашего персонажа метров в секунду.

    Сценарий 1: Мощный ПК (60 FPS) При 60 кадрах в секунду один кадр отрисовывается примерно за 0.016 секунды (). За один кадр персонаж сдвинется на: метра. За 60 кадров (1 секунду) он пройдет: метра (с учетом округления — ровно 5 метров).

    Сценарий 2: Слабый ПК (20 FPS) При 20 кадрах в секунду один кадр отрисовывается за 0.05 секунды (). За один кадр персонаж сдвинется на: метра. За 20 кадров (1 секунду) он пройдет: метров.

    Умножая любое непрерывное действие (перемещение, вращение, заполнение шкалы здоровья) на Time.deltaTime, вы гарантируете, что игра будет работать с одинаковой скоростью на любых устройствах.

    Графика, анимация и пользовательский интерфейс (UI)

    Движок берет на себя сложную математику отрисовки, предоставляя вам удобные инструменты.

    В 3D-играх визуальная часть объекта состоит из Меша (Mesh) — полигональной сетки, задающей форму (например, форму автомобиля), и Материала (Material), который определяет, как эта форма реагирует на свет (будет ли она матовой как резина или блестящей как хром).

    В 2D-играх используются Спрайты (Sprites) — плоские изображения. Анимация создается путем быстрой смены спрайтов (как в классической мультипликации) или путем перемещения «костей» внутри 2D-модели.

    Отдельного внимания заслуживает Пользовательский интерфейс (UI). Полоски здоровья, счетчик патронов, мини-карта и главное меню — всё это элементы UI. В Unity они располагаются на специальном невидимом холсте — Canvas.

    Интерфейс тесно связан с вашим кодом на C# через систему событий. Вы создаете кнопку «Играть» на Canvas, а затем в инспекторе движка указываете, что при нажатии на эту кнопку должен вызваться публичный метод StartGame() из вашего скрипта GameManager. Так визуальная часть соединяется с логикой.

    Оптимизация производительности: Пул объектов

    В статье про коллекции мы упоминали Сборщик мусора (Garbage Collector). В разработке игр он является главным врагом стабильной частоты кадров.

    Когда вы создаете новый объект в памяти (например, пулю при выстреле) с помощью команды Instantiate в Unity, движок выделяет под нее оперативную память. Когда пуля попадает в стену, вы уничтожаете ее командой Destroy. Память помечается как «мусор».

    Если вы стреляете из пулемета со скоростью 20 пуль в секунду, за минуту вы создадите и уничтожите 1200 объектов. Рано или поздно Сборщик мусора остановит игру на долю секунды, чтобы очистить память от этих 1200 мертвых пуль. Игрок почувствует это как резкое зависание (фриз).

    Чтобы избежать этого, профессионалы используют паттерн Пул объектов (Object Pooling).

    Вместо того чтобы создавать и уничтожать объекты на лету, вы делаете следующее:

  • Во время загрузки уровня (когда игрок готов подождать) вы создаете список из 50 пуль.
  • Вы отключаете их (делаете невидимыми и неактивными). Это ваш «пул» (бассейн) объектов.
  • Когда игрок нажимает на курок, вы не создаете новую пулю. Вы берете первую неактивную пулю из пула, перемещаете ее к дулу автомата и включаете.
  • Когда пуля попадает в стену, вы не уничтожаете ее. Вы просто снова ее отключаете и возвращаете в пул.
  • При таком подходе Сборщику мусора нечего убирать. Память выделяется один раз при загрузке. Этот метод критически важен для мобильных игр, где ресурсы процессора сильно ограничены.

    Итоги

    * Игровой движок (например, Unity) — это программная среда, предоставляющая готовые системы рендеринга, физики и звука, что позволяет разработчику сосредоточиться на логике игры. * Unity использует компонентный подход. Игровые сущности строятся из пустых контейнеров (GameObject), к которым прикрепляются различные модули (Components), включая ваши скрипты на C#. Основа жизни игры — это игровой цикл (Game Loop*). Метод Update() выполняется каждый кадр, непрерывно считывая ввод игрока и обновляя состояния объектов. * Для независимости скорости игры от мощности компьютера (FPS) все изменения во времени должны умножаться на Time.deltaTime. Для оптимизации производительности и предотвращения зависаний от работы Сборщика мусора следует избегать частого создания и уничтожения объектов, используя паттерн Пул объектов (Object Pooling*).

    9. Игровой цикл и управление временем

    Игровой цикл и управление временем

    Представьте, что вы запустили текстовый редактор. Вы можете отойти налить себе кофе, вернуться через полчаса, и на экране ничего не изменится. Программа терпеливо ждет, пока вы нажмете клавишу на клавиатуре или кликнете мышкой. Как только вы это делаете, она мгновенно реагирует, отрисовывает букву и снова засыпает.

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

    В прошлой статье мы познакомились с игровыми движками и узнали, что объекты в Unity управляются компонентами. Но что именно заставляет эти компоненты работать непрерывно? Ответ кроется в фундаментальной архитектуре любого интерактивного развлечения — игровом цикле.

    Сердце виртуального мира: Game Loop

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

    Современные офисные приложения работают на основе событийной модели (Event-driven). Они спят, пока операционная система не пришлет им событие (например, «пользователь сдвинул мышь»).

    Игры используют совершенно иной подход — игровой цикл (Game Loop). Это бесконечный цикл while, который запускается в момент старта игры и безостановочно крутится десятки или сотни раз в секунду, пока игрок не нажмет кнопку выхода.

    > Игровой цикл — это квинтэссенция шаблонов в игровом программировании. Он есть практически в каждой игре, и двух одинаковых практически нет. И при этом вне игр он встречается крайне редко. > > Роберт Найстром, автор книги Game Programming Patterns

    Каждый оборот этого цикла называется кадром (Frame). За одну долю секунды, пока длится кадр, движок успевает сделать три главных действия:

  • Сбор ввода (Input): Опрашивает клавиатуру, мышь или геймпад. Нажал ли игрок кнопку прыжка? Куда повернут стик?
  • Обновление логики (Update): Перемещает персонажей, рассчитывает траекторию пуль, проверяет, не закончилось ли здоровье у босса.
  • Отрисовка (Render): Берет все новые координаты объектов и рисует красивую картинку на мониторе.
  • В движке Unity шаг обновления логики доступен нам через метод Update(). Все, что вы напишете внутри этого метода, будет выполняться каждый кадр.

    Ловушка частоты кадров

    Количество кадров, которое компьютер успевает просчитать и отрисовать за одну секунду, называется FPS (Frames Per Second). И здесь кроется главная проблема начинающих разработчиков.

    Представим, что мы пишем скрипт для перемещения космического корабля. Мы хотим, чтобы корабль летел вперед. Мы открываем метод Update() и пишем команду: «Сдвинуть корабль на 1 метр вперед».

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

    У Алисы мощный игровой компьютер. Ее видеокарта легко выдает 100 FPS. Это значит, что метод Update() вызовется 100 раз за одну секунду. Корабль Алисы пролетит 100 метров за секунду.

    У Бориса старый офисный ноутбук. Он с трудом тянет игру и выдает всего 20 FPS. Метод Update() вызовется 20 раз. Корабль Бориса пролетит всего 20 метров за ту же самую секунду реального времени.

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

    Delta Time: великий уравнитель

    Чтобы отвязать игровую логику от частоты кадров, программисты используют концепцию реального времени. Нам нужно знать, сколько именно времени прошло между предыдущим и текущим кадром. Эта величина называется Delta Time (дельта времени, или разница времени).

    В Unity это значение хранится в статической переменной Time.deltaTime. Оно измеряется в секундах. Если игра работает при 60 FPS, то Time.deltaTime будет равно примерно 0.016 секунды.

    Чтобы сделать движение независимым от кадров, мы должны умножить желаемую скорость на дельту времени. В основе этого лежит классическая формула из физики:

    Где: * — дистанция, на которую объект должен переместиться в текущем кадре. * — скорость объекта в метрах в секунду. * — время, прошедшее с прошлого кадра (Time.deltaTime).

    Исправим наш код, задав скорость корабля равной 5 метрам в секунду:

    Проверим математику на наших игроках.

    Компьютер Алисы (100 FPS): Один кадр длится 0.01 секунды (). За один кадр корабль сдвинется на: метра. За 100 кадров (ровно 1 секунда) корабль пролетит: метров.

    Ноутбук Бориса (20 FPS): Один кадр длится 0.05 секунды (). За один кадр корабль сдвинется на: метра. За 20 кадров (ровно 1 секунда) корабль пролетит: метров.

    Магия сработала! Независимо от того, выдает компьютер 20, 60 или 144 кадра в секунду, за одну секунду реального времени корабль пролетит ровно 5 метров. Умножение на Time.deltaTime — это золотое правило геймдева. Его нужно применять к любому процессу, который длится во времени: перемещению, вращению, заполнению шкалы здоровья или перезарядке оружия.

    Физика против графики: FixedUpdate

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

    Физика ненавидит нестабильность. Если частота кадров резко упадет (например, во время взрыва на экране), Time.deltaTime станет очень большим. Если объект двигался быстро, за один такой длинный кадр он может пролететь сквозь тонкую стену, потому что в прошлом кадре он был перед стеной, а в следующем — уже за ней. Физический движок просто не успеет зафиксировать момент столкновения.

    Чтобы физика работала предсказуемо, в Unity существует второй, параллельный цикл обновления — FixedUpdate (фиксированное обновление).

    | Характеристика | Update() | FixedUpdate() | | :--- | :--- | :--- | | Частота вызовов | Зависит от мощности ПК (FPS). Может быть 30, 60, 120 раз в секунду. | Строго фиксирована. По умолчанию 50 раз в секунду (каждые 0.02 сек). | | Назначение | Считывание нажатий кнопок, таймеры, перемещение объектов без физики. | Применение физических сил, толчков, работа с массой и гравитацией. | | Использование Delta Time | Обязательно умножать на Time.deltaTime. | Не нужно умножать на время, так как шаг всегда одинаковый. |

    Если вы хотите толкнуть ящик с помощью физической силы, вы пишете этот код внутри FixedUpdate(). Движок гарантирует, что этот метод будет вызываться ровно 50 раз в секунду, даже если игра тормозит и выдает всего 10 кадров в секунду для графики. В таком случае движок просто вызовет FixedUpdate() пять раз подряд перед тем, как отрисовать один графический кадр, чтобы физика не отставала от реального времени.

    Властелин времени: паузы и замедления

    Поскольку все процессы в правильно написанной игре умножаются на Time.deltaTime, у нас появляется невероятно мощный инструмент для управления самим течением времени.

    В Unity есть свойство Time.timeScale (масштаб времени). По умолчанию оно равно 1.0. Это означает, что время в игре течет с нормальной скоростью (1 секунда в игре равна 1 секунде в реальности).

    Но что произойдет, если мы изменим это значение?

    Если timeScale равен 0.5, то движок автоматически уменьшает значение deltaTime в два раза. Все объекты, которые используют deltaTime для движения, начнут двигаться в два раза медленнее. Вы получите кинематографичный эффект замедления времени, как в фильме «Матрица», написав всего одну строчку кода! При этом метод Update() продолжит вызываться с той же частотой, игра не начнет тормозить, просто математические шаги станут меньше.

    А как сделать паузу в игре, когда игрок открывает меню инвентаря?

    При значении 0.0 переменная deltaTime становится равна нулю. Любое число, умноженное на ноль, дает ноль. Все враги, пули и таймеры мгновенно замирают на месте.

    Важный нюанс: даже при timeScale = 0 метод Update() продолжает работать. Это критически важно, потому что иначе игра не смогла бы отследить нажатие кнопки «Снять с паузы». Вы можете продолжать считывать ввод игрока и перемещаться по меню, пока весь остальной виртуальный мир поставлен на паузу.

    Итоги

    * Игровой цикл (Game Loop) — это бесконечный процесс, который каждый кадр считывает ввод игрока, обновляет логику мира и отрисовывает графику на экране. * Метод Update() вызывается каждый кадр. Частота его вызовов (FPS) нестабильна и зависит от мощности компьютера. * Чтобы скорость игры не зависела от мощности железа, любые изменения во времени (скорость, таймеры) необходимо умножать на Time.deltaTime — время, прошедшее с предыдущего кадра. * Для работы с реалистичной физикой и столкновениями используется метод FixedUpdate(), который вызывается со строго фиксированной частотой (обычно 50 раз в секунду). * Свойство Time.timeScale позволяет глобально управлять скоростью игры: создавать эффекты замедления времени (значения меньше 1) или ставить игру на паузу (значение 0).