Дьявол против четверых: как я реализую асимметричный хоррор на Unity и Netcode for GameObjects
Привет! Я работаю над своим новым проектом — асимметричным мультиплеерным хоррором. Суть простая, но дьявольски сложная в реализации: 4 игрока-человека пытаются выжить, а 5-й берет на себя роль Дьявола. Полная темнота, взаимодействие с предметами через физику, ритуалы, и постоянный страх.
Когда я начинал этот проект, я понимал: сетевой код — это та часть, на которой сыпятся большинство инди-разработчиков. Я выбрал Netcode for GameObjects (NGO) от самой Unity. Сегодня хочу рассказать, с какими сложностями я столкнулся, как я настраиваю взаимодействие игроков с миром и почему сетевая архитектура — это не про код, а про дисциплину.
Почему я выбрал NGO?
Выбор был между Mirror, Photon и официальным NGO. Я решил остановиться на официальном решении, так как оно развивается вместе с движком. Да, у него есть свои «детские болезни», но документация становится лучше, а интеграция с пакетами Unity — бесшовнее.
Основная боль любого мультиплеера — авторитетность сервера. Я сразу поставил себе правило: клиент никогда не должен верить себе. Все важные решения (подобрал предмет, нанес урон, активировал ритуал) проходят через сервер.
Главная механика: «Живые» руки и физическое взаимодействие
В моем хорроре взаимодействие — это не просто OnTriggerEnter + нажатие кнопки «E». Я хотел, чтобы персонажи физически брали предметы: рука «прилипает» к объекту.
Здесь я столкнулся с первой проблемой синхронизации. Когда игрок берет предмет, он должен стать его «владельцем» в сетевом смысле, иначе физика на клиенте и сервере разойдется, и предмет будет висеть в воздухе или дергаться.
Как я это реализовал (на примере подбора предмета)
В Netcode for GameObjects есть понятие NetworkObject. Чтобы игрок мог взаимодействовать с предметом, он должен запросить владение этим объектом.
Вот упрощенный пример того, как это выглядит у меня в коде:
Почему это важно: Если просто двигать объект на клиенте, другие игроки будут видеть телепортацию. ChangeOwnership — это база. Без понимания, у кого сейчас "права" на объект, вы получите ад с синхронизацией позиций.
Проблема «темноты» и освещения
Игра построена на том, что игроки почти ничего не видят. Источник света (фонарь, магический камень) — это критически важный объект.
Я решил не синхронизировать каждый кадр источника света через NetworkTransform, это убьет трафик. Вместо этого:
- Сервер знает, в каком состоянии свет (вкл/выкл) и у кого он в руках.
- Клиент только получает состояние (через NetworkVariable) и локально включает/выключает свой компонент Light.
Это спасает от лагов. Сетевой код должен передавать только состояние, а не каждый чих анимации.
Уроки, которые я извлек (и на которых набил шишки)
1. Не все нужно синхронизировать
Поначалу я пытался запихнуть в NetworkTransform абсолютно всё: от качания травы до поворота головы персонажа. Итог — игра начала «лагать» уже при двух игроках.
Мой подход сейчас:
- Синхронизирую позиции только тогда, когда это критично.
- Если объект декоративный — пусть он живет своей жизнью на клиенте.
- Если объект игровой (нож, книга) — только тогда он становится сетевым объектом.
2. RPC vs NetworkVariables
Я долго путался, что использовать.
- NetworkVariables — использую для состояний, которые меняются редко (уровень здоровья, статус игрока: жив/мертв, открыта ли дверь). Это надежнее, так как новые игроки, подключившиеся к сессии, сразу получают актуальное состояние.
- RPC (Remote Procedure Calls) — использую для событий, которые случаются один раз (выстрел, скример, поднятие предмета, проигрывание звука).
3. Режим «Дьявола»
Это самая сложная часть архитектуры. Дьявол обладает другими правами доступа, чем игроки. Он видит в темноте, он может расставлять капканы. Пришлось создать отдельный PlayerController для Дьявола и обычный для выживших. Они оба наследуются от базового класса, но логика взаимодействия с NetworkObject у них разная. Не пытайтесь написать один скрипт "на все случаи жизни" — это прямой путь к "спагетти-коду", в котором вы сами запутаетесь через неделю.
Что дальше?
Сейчас проект на этапе сборки базы. Я закончил с «прилипанием» рук и базовым освещением. Впереди самая вкусная часть — фаза подготовки, когда Дьявол видит всю карту, а игроки — лишь узкий конус света от фонарей.
Сетевой код в хоррорах — это, прежде всего, управление ожиданиями игрока. Если игрок поднял нож, он должен видеть это мгновенно (клиентская предсказательная логика), а сервер должен проверить, имел ли он на это право, чуть позже. Это баланс между «отзывчивостью» и «честностью».