Архитектура и код Among Us: от анализа до тестирования

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

1. Введение в архитектуру Unity и инструменты декомпиляции кода

Введение в архитектуру Unity и инструменты декомпиляции кода

Добро пожаловать в курс «Архитектура и код Among Us: от анализа до тестирования». Мы начинаем погружение в мир реверс-инжиниринга и анализа игрового кода на примере одной из самых популярных инди-игр последних лет. Чтобы понять, как работает Among Us, как устроена её сетевая логика и как писать для неё моды или тесты, нам необходимо сначала разобраться с фундаментом, на котором она построена — движком Unity.

В этой статье мы разберем, чем отличается архитектура Among Us от простых Unity-проектов, что такое IL2CPP, почему вы не найдете привычного кода C# в папке с игрой и какие инструменты нам понадобятся, чтобы превратить набор байтов обратно в читаемую структуру.

Архитектура Unity: Mono против IL2CPP

Когда разработчик нажимает кнопку «Build» в Unity, движок должен превратить написанный на C# код в то, что сможет запустить компьютер игрока. Существует два основных способа, которыми Unity делает это: Mono и IL2CPP.

Mono (JIT-компиляция)

В старых или простых проектах Unity часто используется среда выполнения Mono. При этом код C# компилируется в промежуточный язык CIL (Common Intermediate Language) и упаковывается в файлы .dll (обычно Assembly-CSharp.dll).

Когда игра запускается, Mono использует JIT (Just-In-Time) компиляцию, чтобы переводить CIL в машинный код прямо во время работы программы. Для исследователя это «легкий режим»: такие игры декомпилируются почти до исходного кода с сохранением имен переменных и логики методов.

IL2CPP (AOT-компиляция)

Among Us, как и большинство современных мобильных и кроссплатформенных игр, использует бэкенд IL2CPP (Intermediate Language to C++). Это технология, которая меняет правила игры.

!Схема преобразования C# кода в нативный машинный код через промежуточную генерацию C++.

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

  • Код C# компилируется в IL.
  • Unity конвертирует IL-код в код на языке C++.
  • Код C++ компилируется нативным компилятором (например, MSVC для Windows или Xcode для iOS) в машинный код.
  • В результате мы получаем не аккуратную библиотеку .dll с байт-кодом, а исполняемый файл, содержащий чистый машинный код. В папке с игрой Among Us вы увидите файл GameAssembly.dll. Это огромная библиотека, где слиты воедино все скрипты игры, но в виде ассемблерных инструкций, а не C# классов.

    Роль Metadata в IL2CPP

    Если весь код превратился в машинные инструкции, как Unity понимает, где какой класс и метод? Здесь на сцену выходит файл global-metadata.dat.

    Этот файл находится в папке Among Us_Data/il2cpp_data/Metadata/. Он содержит карту всей игры:

  • Имена всех классов, методов и полей.
  • Связи между ними (какой метод принадлежит какому классу).
  • Строковые литералы (текст, используемый в коде).
  • Однако сам код методов (тело функций) находится в GameAssembly.dll. Метаданные лишь говорят: «Метод PlayerControl.FixedUpdate находится по смещению 0x123456».

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

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

    Без global-metadata.dat файл GameAssembly.dll был бы просто набором безымянных функций, и анализ занял бы годы.

    Инструменты исследователя

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

    1. Il2CppDumper

    Это самый важный инструмент в нашем арсенале. Его задача — взять GameAssembly.dll и global-metadata.dat и восстановить структуру проекта.

    Что делает Il2CppDumper:

  • Считывает метаданные.
  • Сопоставляет их с бинарным файлом.
  • Генерирует Dummy DLLs (фиктивные библиотеки).
  • Dummy DLL — это файлы .dll, которые содержат все классы, методы и поля игры, но тела методов в них пусты. Они выглядят примерно так:

    Кроме того, Il2CppDumper создает файл script.json (для использования в IDA/Ghidra) и dump.cs (текстовое представление всех классов).

    2. dnSpy

    dnSpy — это мощный отладчик и редактор сборок .NET. Мы будем использовать его для просмотра тех самых Dummy DLL, которые сгенерировал Il2CppDumper.

    С помощью dnSpy мы сможем:

  • Изучать иерархию классов Among Us (например, найти класс GameData, PlayerControl, ShipStatus).
  • Видеть типы переменных (понять, что isImpostor — это boolean).
  • Узнавать смещения (offsets) методов для дальнейшего анализа или создания модов.
  • !Интерфейс dnSpy, отображающий структуру классов игры.

    3. Ghidra или IDA Pro (для продвинутых)

    Если нам нужно узнать, как именно работает метод (например, по какой формуле рассчитывается дистанция убийства), нам придется смотреть машинный код внутри GameAssembly.dll. Для этого используются дизассемблеры вроде Ghidra (бесплатный) или IDA Pro.

    Мы загружаем в них GameAssembly.dll и применяем скрипт, сгенерированный Il2CppDumper. Скрипт переименовывает безымянные функции sub_182A40 в понятные PlayerControl$$FixedUpdate.

    Практический подход к анализу Among Us

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

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

  • Извлечение: Находим файлы игры.
  • Дампинг: Прогоняем их через Il2CppDumper.
  • Разведка: Открываем Assembly-CSharp.dll (из папки DummyDll) в dnSpy.
  • Поиск: Ищем ключевые классы (например, отвечающие за роль предателя).
  • Анализ: Изучаем поля и методы, чтобы понять, какие данные хранит объект.
  • Почему это законно?

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

    Структура данных Among Us

    Игра построена на классической компонентной системе Unity, но имеет свои особенности. Весь геймплейный код сосредоточен в сборке Assembly-CSharp.

    Ключевые элементы, которые мы будем искать:

  • InnerNet: Сетевой слой игры. Among Us использует кастомную сетевую систему, и понимание того, как передаются пакеты (RPC — Remote Procedure Calls), критически важно.
  • GameData: Класс, хранящий информацию о всех игроках в текущей сессии (имена, цвета, роли).
  • PlayerControl: Основной скрипт, управляющий персонажем игрока.
  • > «Понимание структуры данных — это 80% успеха в реверс-инжиниринге. Если вы знаете, где лежат данные, вы знаете, как ими управлять».

    Заключение

    Мы выяснили, что Among Us использует технологию IL2CPP, что делает её код недоступным для простого чтения, но не защищает от анализа полностью. Используя связку GameAssembly.dll + global-metadata.dat и инструмент Il2CppDumper, мы можем восстановить карту классов игры.

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

    Готовы к погружению в код? Тогда переходим к проверке знаний.

    2. Сетевое взаимодействие: библиотека Hazel и синхронизация данных между клиентами

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

    В предыдущей статье мы разобрали, как Among Us скрывает свой код за стеной IL2CPP и как с помощью инструментов вроде Il2CppDumper и dnSpy мы можем пробить эту стену. Теперь, когда у нас есть доступ к структуре классов, пришло время разобраться в самой динамичной части игры — сетевом взаимодействии.

    Among Us — это мультиплеерная игра, где синхронизация действий критически важна. Если вы убиваете члена экипажа, сервер должен мгновенно узнать об этом и сообщить всем остальным клиентам, что этот игрок теперь мертв. Как это происходит? Почему иногда персонажи «телепортируются»? И где в коде искать функции, отвечающие за предательство?

    В этой статье мы изучим библиотеку Hazel, разберем понятие RPC (Remote Procedure Calls) и узнаем, как игра синхронизирует состояние мира между десятью игроками.

    Hazel: Фундамент связи

    В отличие от многих Unity-игр, использующих стандартные решения вроде Photon (PUN) или устаревший UNET, разработчики Among Us (студия Innersloth) пошли своим путем. Они написали собственную сетевую библиотеку под названием Hazel Networking.

    Почему Hazel?

    Hazel — это низкоуровневая библиотека, работающая поверх протокола UDP (User Datagram Protocol). Чтобы понять выбор разработчиков, нужно вспомнить разницу между TCP и UDP.

  • TCP (Transmission Control Protocol): Гарантирует доставку данных в правильном порядке. Это надежно, но медленно. Если один пакет потеряется, вся очередь ждет его повторной отправки. Для динамичных игр это смерть: вы нажали кнопку движения, а персонаж сдвинулся через секунду.
  • UDP (User Datagram Protocol): Отправляет пакеты «как есть», не заботясь о том, дошли ли они. Это очень быстро, но данные могут потеряться или прийти в неправильном порядке.
  • Hazel берет лучшее от обоих миров. Она использует UDP для скорости, но реализует собственный слой надежности (Reliability Layer) там, где это необходимо. Это называется RUDP (Reliable UDP).

    !Многоуровневая архитектура сетевого стека Among Us.

    В коде игры (в dnSpy) вы часто будете встречать пространство имен Hazel. Оно отвечает за упаковку байтов в пакеты и их отправку.

    Типы сообщений: Надежность против Скорости

    В Among Us используется два основных типа отправки данных, и понимание разницы между ними критично для анализа трафика:

    * Unreliable (Ненадежный): Используется для данных, которые быстро устаревают. Пример: позиция игрока. Если пакет с координатой потерялся, нет смысла отправлять его заново, потому что через 100 мс игрок уже будет в точке . Мы просто ждем следующий пакет. * Reliable (Надежный): Используется для критически важных событий. Пример: начало голосования, убийство, выполнение задания. Если сервер не получит сигнал об убийстве, игра сломается. Hazel будет отправлять этот пакет повторно, пока не получит подтверждение (ACK).

    InnerNet: Мост в Unity

    Если Hazel — это «почтальон», который носит письма (пакеты), то InnerNet — это «сортировочный центр» внутри самой игры. Класс InnerNetClient (наследуется от MonoBehaviour) управляет подключением к серверу и распределением сообщений по объектам.

    Ключевая концепция здесь — NetId (Network ID).

    Каждый объект в игре, который должен синхронизироваться (игрок, консоль заданий, лобби), имеет уникальный идентификатор NetId. Когда сервер присылает пакет, он говорит: «Эй, это сообщение для объекта с NetId 5». InnerNetClient находит этот объект и передает ему данные.

    Структура пакета

    Упрощенно сетевой пакет Among Us выглядит так:

    | Поле | Описание | | :--- | :--- | | Length | Длина пакета | | Tag | Тип сообщения (например, RPC, Data, Spawn) | | NetId | ID объекта, которому адресовано сообщение | | Payload | Полезная нагрузка (параметры метода, координаты и т.д.) |

    RPC: Удаленный вызов процедур

    Самый интересный механизм для исследователя — это RPC (Remote Procedure Call). Это способ заставить метод выполниться на чужом компьютере.

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

  • Ваш клиент вызывает метод RpcKillPlayer.
  • Этот метод не убивает игрока сразу. Он формирует пакет данных, в котором сказано: «Вызвать команду Kill на жертве с таким-то ID».
  • Пакет улетает на сервер.
  • Сервер рассылает этот пакет всем остальным игрокам.
  • Клиенты других игроков получают пакет, видят тег RPC и вызывают у себя метод HandleRpc.
  • В коде (через dnSpy) это выглядит как огромный переключатель switch внутри метода HandleRpc в классе PlayerControl:

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

    Синхронизация движения и Интерполяция

    Одной из самых сложных задач в сетевых играх является синхронизация движения. Вы не можете отправлять позицию игрока каждый кадр (60 раз в секунду) — это «забьет» канал связи. Among Us отправляет координаты с определенной частотой (например, 10-20 раз в секунду).

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

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

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

    Когда клиент получает пакет «Игрок X теперь в точке B», он не перемещает его мгновенно. Он плавно двигает модельку от текущей точки A к точке B, используя эту формулу. Именно поэтому, если у вас высокий пинг, вы можете видеть, как игроки проходят сквозь стены — их клиент «предсказывает» движение, а сервер потом поправляет его.

    GameData: Источник правды

    Помимо PlayerControl, который отвечает за физического персонажа на карте, существует статический класс GameData. Это база данных текущей сессии.

    В GameData хранится информация, которая не должна исчезать, даже если персонаж умрет или выйдет из поля зрения: * Имя игрока. * Цвет и шляпа. * Роль (Impostor / Crewmate). * Список выполненных заданий.

    При анализе кода важно различать PlayerControl (объект на сцене) и GameData.PlayerInfo (запись в таблице данных). Логика игры часто обращается именно к GameData, чтобы проверить, жив ли игрок, прежде чем разрешить ему голосовать.

    Практическое задание для анализа

    Теперь, когда вы понимаете теорию, попробуйте открыть Assembly-CSharp.dll (из папки DummyDll, полученной в прошлом уроке) в dnSpy и найти следующее:

  • Откройте класс PlayerControl.
  • Найдите метод FixedUpdate. Посмотрите, как он проверяет amOwner (являюсь ли я владельцем этого персонажа), прежде чем обрабатывать ввод с клавиатуры. Это базовый принцип защиты: мы управляем только своим персонажем.
  • Найдите метод HandleRpc. Изучите, какие действия могут быть вызваны по сети.
  • Заключение

    Сетевая архитектура Among Us построена на эффективности и простоте. Библиотека Hazel обеспечивает быструю передачу данных по UDP, а система RPC позволяет легко синхронизировать сложные игровые события. Понимание того, как пакеты превращаются в вызовы методов, — это ключ к созданию модов, ботов и поиску уязвимостей.

    В следующей статье мы углубимся в игровую логику и разберем, как именно реализована система ролей, как игра определяет Предателя и как работают алгоритмы видимости (Line of Sight).

    3. Ключевые классы: PlayerControl, GameData и управление жизненным циклом объектов

    Ключевые классы: PlayerControl, GameData и управление жизненным циклом объектов

    Мы продолжаем наш курс по архитектуре Among Us. В прошлых статьях мы научились вскрывать «черный ящик» IL2CPP и разобрались, как пакеты данных летают по сети через библиотеку Hazel. Теперь у нас есть инструменты и понимание транспорта, но мы всё ещё не знаем, как устроена сама логика игры.

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

    В этой статье мы разберем «Святую Троицу» кода Among Us: классы GameData, PlayerControl и PlayerPhysics. Понимание их взаимодействия — это ключ к созданию любых модов, от простых читов на скорость до сложных ролевых режимов.

    Разделение ответственности: Данные против Представления

    В разработке игр существует паттерн, называемый «Model-View-Controller» (MVC) или его вариации. Суть проста: данные (числа, строки) должны жить отдельно от их визуального отображения (спрайтов, моделек).

    Among Us строго следует этому принципу, хотя и реализует его специфично для Unity. В игре есть два основных места, где «живет» игрок:

  • GameData: Это база данных. Здесь записано, кто есть кто.
  • PlayerControl: Это марионетка. Это объект Unity, который бегает по экрану.
  • !Диаграмма, показывающая различие между хранением данных в GameData и физическими объектами PlayerControl на сцене.

    GameData: Источник правды

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

    Внутри GameData находится важнейший вложенный класс — PlayerInfo. Именно он хранит всё самое интересное:

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

    PlayerControl: Управление марионеткой

    Класс PlayerControl — это то, что вы видите на экране. Он наследуется от InnerNetObject (который наследуется от MonoBehaviour), что позволяет ему иметь NetId и принимать RPC-вызовы.

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

    Внутри PlayerControl есть ссылка на PlayerInfo. Когда игре нужно узнать, предатель ли этот персонаж, код делает примерно следующее:

    Жизненный цикл: Spawn и Despawn

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

    Процесс появления (Spawning)

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

  • Получает пакет Spawn.
  • Смотрит на PrefabId в пакете (какой тип объекта создать: игрока, голосование, труп).
  • Инстанцирует (создает) объект Unity из префаба.
  • Присваивает ему NetId.
  • Если это игрок, регистрирует его в GameData.
  • Процесс уничтожения (Despawning)

    Когда игрок выходит из игры, происходит обратный процесс. Но здесь есть нюанс. Объект PlayerControl уничтожается (Destroy), но запись в GameData не удаляется, а лишь помечается флагом Disconnected = true.

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

    Взаимодействие с миром и математика дистанции

    Одной из главных функций PlayerControl является взаимодействие с объектами: выполнение заданий, убийство, использование вентиляции. Как игра понимает, что вы достаточно близко, чтобы нажать кнопку «Use»?

    Для этого используется проверка евклидова расстояния. В коде это часто выглядит как метод CanUse.

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

    Где — искомое расстояние, — координаты игрока, а — координаты целевого объекта (например, задания).

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

    Условие «расстояние меньше радиуса действия» () можно записать как:

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

    В коде Among Us вы часто встретите проверки вроде Vector2.SqrMagnitude, которая как раз возвращает квадрат длины вектора.

    PlayerPhysics: Движение и коллизии

    Третий важный компонент — PlayerPhysics. Он отвечает за то, как персонаж перемещается в пространстве. Among Us — 2D игра, но она имитирует глубину (персонажи могут заходить друг за друга).

    PlayerPhysics обрабатывает: * Коллизии со стенами: Используется Raycast (лучи), чтобы проверить, не уперся ли игрок в стену карты. * Сортировку слоев: Чтобы персонаж, стоящий «ниже» по экрану, перекрывал того, кто стоит «выше».

    Интересный факт: скорость игрока определяется не в PlayerPhysics, а берется из настроек лобби (GameOptionsData), которые синхронизируются через GameData.

    Практический пример: Как работает кнопка Kill

    Давайте соберем всё вместе и посмотрим, как работает механика убийства с точки зрения классов.

  • Проверка условий: В методе FixedUpdate класса PlayerControl проверяется:
  • * Data.IsImpostor (из GameData): Являюсь ли я предателем? * KillTimer: Прошло ли время перезарядки? * Дистанция: Находится ли ближайшая жертва в радиусе действия (математика выше).

  • Действие: Если игрок нажимает кнопку Kill:
  • * Вызывается RpcMurderPlayer(targetNetId). * Пакет уходит на сервер через Hazel.

  • Исполнение: Все клиенты получают пакет и вызывают метод MurderPlayer.
  • * Жертва проигрывает анимацию смерти. * В GameData у жертвы ставится IsDead = true. * Создается объект трупа (DeadBody). * Оригинальный PlayerControl жертвы выключается (спрайт становится невидимым), но не удаляется полностью, чтобы призрак мог летать.

    Заключение

    Мы разобрали фундамент игровой логики Among Us. Разделение на GameData (данные) и PlayerControl (представление) позволяет игре стабильно работать в условиях нестабильной сети и смены состояний.

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

    Готовы проверить, насколько хорошо вы усвоили структуру классов? Переходите к заданиям.

    4. Реализация игровой логики: система заданий, механика предателя и голосование

    Реализация игровой логики: система заданий, механика предателя и голосование

    Мы продолжаем наш курс по архитектуре Among Us. В предыдущих статьях мы разобрали фундамент: как работает IL2CPP, как данные передаются через Hazel Networking и где хранятся параметры игроков в GameData. Теперь у нас есть «скелет» (классы) и «нервная система» (сеть), но чтобы игра ожила, ей нужен «мозг» — игровая логика.

    В этой статье мы разберем три кита геймплея Among Us:

  • Систему заданий: как игра понимает, что вы соединили провода или выбросили мусор.
  • Механику саботажей: как предатели взаимодействуют с системами корабля.
  • Голосование: как технически реализован процесс обсуждения и изгнания.
  • Мы увидим, что за простыми действиями скрывается сложная иерархия классов и неочевидные архитектурные решения.

    Система заданий: Больше, чем просто мини-игры

    Для игрока задание — это просто иконка на карте и мини-игра. Для разработчика — это сложная система учета прогресса, распределенная между клиентом и сервером.

    Иерархия классов заданий

    В коде Among Us (в сборке Assembly-CSharp) все задания наследуются от базового класса PlayerTask. Это абстрактный класс, определяющий общие свойства: имеет ли задание несколько этапов, где оно находится и завершено ли оно.

    Основные наследники PlayerTask: * NormalPlayerTask: Обычные задания (провода, сканирование, мусор). Они имеют счетчик шагов (taskStep) и максимальное количество шагов. * SabotageTask: Да, технически саботаж для игры — это тоже «задание», только для предателей и с таймером. * ImportantTextTask: Информационные задачи (например, «Почините свет»).

    !Иерархия наследования классов задач в коде игры

    Жизненный цикл задания

    Когда начинается игра, GameData назначает каждому игроку список заданий. Но само выполнение происходит через взаимодействие двух сущностей:

  • Консоль (Console): Объект на карте, к которому вы подходите (триггер).
  • Мини-игра (Minigame): Префаб UI, который открывается на весь экран.
  • Когда вы успешно соединяете провода, мини-игра вызывает метод Complete. Этот метод отправляет RPC-запрос на сервер, сообщая: «Игрок X выполнил шаг задания Y».

    Математика прогресс-бара

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

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

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

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

    ShipStatus и системы корабля

    Если PlayerControl управляет игроком, то кто управляет картой? Знакомьтесь с классом ShipStatus.

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

    Примеры систем: * SwitchSystem: Управляет электричеством и светом. * ReactorSystem: Отвечает за реактор и сейсмические стабилизаторы. * LifeSuppSystem: Контролирует кислород (O2). * DoorSystem: Управляет закрытием дверей.

    Как работает саботаж

    Когда предатель нажимает кнопку саботажа, происходит следующее:

  • Клиент предателя проверяет кулдаун.
  • Отправляется RPC-пакет с типом саботажа (например, SystemTypes.Electrical).
  • Все клиенты получают этот пакет.
  • ShipStatus находит нужную систему в своем словаре.
  • Система переходит в состояние «Active» (сломана).
  • Для ремонта используется обратная логика. Например, в SwitchSystem (свет) есть массив битов, представляющий 5 тумблеров. Свет включается только тогда, когда состояние системы удовлетворяет условию:

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

    Голосование: MeetingHud

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

    Этот объект не существует на сцене постоянно. Он создается (Instantiate) только в момент нажатия кнопки или обнаружения тела и уничтожается после выброса игрока.

    Состояния собрания

    MeetingHud работает как конечный автомат (State Machine) с тремя состояниями:

  • Discussion (Обсуждение): Игроки могут писать в чат, но не могут голосовать. Таймер идет вниз.
  • Voting (Голосование): Кнопки голосования разблокированы. Состояние длится, пока все не проголосуют или не истечет время.
  • Results (Результаты): Показ анимации выброса и раскрытие ролей (если включено в настройках).
  • Логика подсчета голосов

    Голоса хранятся в массиве PlayerVoteArea. Каждый раз, когда игрок голосует, клиентам рассылается RPC, но не с информацией за кого проголосовали (это скрыто до конца), а просто с фактом голосования (чтобы показать галочку «I Voted»).

    В конце фазы голосования хост (сервер) подсчитывает итоги. Алгоритм выбора изгоняемого:

  • Подсчитываются голоса за каждого игрока + голоса за «Skip».
  • Находится кандидат с максимумом голосов ().
  • Проверяется наличие ничьей (Tie).
  • Условие изгнания игрока можно записать так:

    Где — истина, если игрок изгоняется, — количество голосов за , — количество пропусков, — голоса за любого другого кандидата. Если условие не выполняется (например, или ), никто не изгоняется.

    ExileController: Анимация изгнания

    После принятия решения управление передается классу ExileController. Это скрипт, который отвечает за ту самую анимацию полета в космосе (или падения в лаву на Polus).

    Интересный факт: текст «X was The Impostor» формируется динамически на основе GameData. Если в настройках выключено Confirm Ejects, игра намеренно подменяет строку на нейтральную «X was ejected».

    Условия победы

    В каждом кадре (в методе FixedUpdate класса ShipStatus или GameData) игра проверяет условия окончания матча. Это критическая логика, которую часто пытаются взломать читеры, чтобы мгновенно выиграть.

    Победа экипажа наступает, если: * (все предатели изгнаны). * (все задания выполнены).

    Победа предателей наступает, если: * (предателей столько же или больше, чем мирных). * Критический саботаж (O2 или Реактор) дошел до 0.

    Заключение

    Игровая логика Among Us построена на четком разделении обязанностей: * PlayerTask управляет прогрессом заданий. * ShipStatus следит за здоровьем корабля и саботажами. * MeetingHud берет на себя управление фазой голосования.

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

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

    5. Создание итогового теста: проверка понимания механик и поиск уязвимостей

    Создание итогового теста: проверка понимания механик и поиск уязвимостей

    Поздравляем! Вы прошли долгий путь от декомпиляции GameAssembly.dll до разбора сетевых пакетов Hazel и логики голосования. Мы изучили, как Among Us устроена изнутри: как Unity превращает код в C++, как GameData хранит состояние мира и как PlayerControl управляет персонажем.

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

    Наша цель — создать «итоговый тест» не для себя, а для самой игры. Мы проверим её на прочность, используя три главных вектора атаки: доверие клиенту, утечку информации и манипуляцию RPC.

    Вектор 1: Проблема доверия (Client-Side Authority)

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

    Однако Among Us использует гибридную модель. Чтобы игра работала плавно даже на плохом интернете, клиент сам управляет своим перемещением и сообщает серверу: «Я теперь здесь».

    Тест на скорость (Speed Hack)

    Вспомним класс PlayerControl. В нём есть переменная, отвечающая за скорость передвижения (обычно множитель, приходящий из GameOptionsData).

    Сценарий теста:

  • Находим в памяти адрес переменной скорости.
  • Изменяем значение с на .
  • Пробуем двигаться.
  • Результат: Сервер Hazel получает пакеты с координатами, которые меняются слишком быстро. Если на сервере нет проверки (Anti-Cheat), он просто рассылает эти новые координаты другим игрокам. Вы начинаете бегать быстрее всех.

    Тест на дистанцию убийства

    В статье про PlayerControl мы рассматривали формулу проверки дистанции перед убийством:

    Где — текущая дистанция, — координаты жертвы, а — координаты убийцы.

    В коде клиента есть условие:

    Уязвимость: Эта проверка (if) выполняется на клиенте атакующего. Если мы напишем программу, которая отправляет пакет RpcMurderPlayer напрямую, минуя этот блок if, сервер может принять его, даже если жертва находится на другом конце карты.

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

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

    Вектор 2: Утечка информации (Information Disclosure)

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

    Раскрытие предателя

    Взглянем еще раз на структуру PlayerInfo:

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

    Тестовый сценарий:

  • Подключаемся к процессу игры через отладчик (например, dnSpy или Cheat Engine).
  • Находим в памяти массив GameData.AllPlayers.
  • Читаем поле IsImpostor для каждого игрока.
  • Результат: Мы знаем, кто предатель, с первой секунды матча. Это архитектурная проблема. В идеале сервер не должен сообщать клиенту роли других игроков до тех пор, пока это не станет необходимо (например, в конце игры). Однако в Among Us это сделано для упрощения логики (например, чтобы предатели видели друг друга).

    Вектор 3: Манипуляция RPC и состоянием

    Самые опасные и интересные уязвимости кроются в системе Remote Procedure Calls. Мы знаем, что действия в игре — это просто байты с определенным CallId.

    Принудительное голосование

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

    Но есть более тонкий момент — Race Conditions (состояние гонки).

    Представьте ситуацию:

  • Игрок А убит.
  • Игрок А (через модифицированный клиент) отправляет пакет «Я выполнил задание».
  • Должен ли сервер принять этот пакет? По логике — да, призраки могут делать задания. А если отправить пакет «Я починил саботаж»? Призраки не могут чинить саботаж.

    Тестовый сценарий (Fuzzing): Попробуйте отправлять различные RPC-вызовы (репорт трупа, использование люка, закрытие дверей) в состояниях, когда это невозможно: * Будучи мертвым. * Во время голосования. * Находясь в лобби.

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

    Создание автоматизированного теста

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

    Допустим, мы хотим проверить уязвимость «Cooldown Bypass» (обход перезарядки убийства).

    Алгоритм нашего теста:

  • Setup: Подключаемся к игре, получаем роль Предателя.
  • Action 1: Вызываем RpcMurderPlayer на ближайшей жертве.
  • Assert 1: Проверяем, что KillTimer стал равен, например, 30 секундам.
  • Action 2: Не дожидаясь истечения таймера, сразу отправляем второй RpcMurderPlayer на другую жертву.
  • Result:
  • * Если сервер отклонил пакет — тест пройден (уязвимости нет). * Если вторая жертва умерла — тест провален (найдена критическая уязвимость).

    Защита и противодействие

    Понимая эти векторы, разработчики внедряют проверки на стороне сервера (Server-Side Validation).

    Формула надежной защиты выглядит так:

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

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

    Заключение курса

    Мы завершаем наш курс «Архитектура и код Among Us». Мы прошли путь от:

  • Анализа: Поняли, как IL2CPP прячет код и как его восстановить.
  • Сети: Разобрали Hazel и структуру пакетов.
  • Данных: Изучили GameData и PlayerControl.
  • Логики: Углубились в задания и голосование.
  • Тестирования: Научились искать уязвимости в этой архитектуре.
  • Эти знания универсальны. Unity, IL2CPP, RPC и клиент-серверная синхронизация используются в тысячах игр. Теперь, запуская любую онлайн-игру, вы будете видеть не просто полигоны и текстуры, а потоки данных, классы и методы.

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

    Удачи в ваших исследованиях!