Перейти к основному содержимому

Мультиплеер

Всё, что ниже, работает только тогда, когда в проекте включён мультиплеер и плеер открыл комнату.

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

ctx.net.id;        // идентификатор этого участника
ctx.net.peers(); // все, кто сейчас в комнате
ctx.net.onJoin((peer) => {});
ctx.net.onLeave((peer) => {});

Что реплицируется само

Только позиции физических тел, помеченных как shared. Всё остальное вы отправляете сами.

Звучит как малость — и это правильная малость: ящик, дверь или мяч должны быть одним объектом, о котором все договорились, а персонаж игрока не должен, иначе шаг одного человека сдвинул бы аватары всех. → владение

Сообщения

Для событий: кто-то забил, начался раунд, открылась дверь.

ctx.net.send('score', { points: 10 });
ctx.net.on('score', ({ from, data }) => {
/* … */
});

Отправил и забыл, всем, кроме себя.

Общее состояние

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

ctx.net.state.set(ctx.net.id, { name, colour, ready: true });
ctx.net.state.get(somePeerId);
ctx.net.state.all();
ctx.net.state.on(ctx.net.id, (value) => {});

Ключ по ctx.net.id — стандартный приём для состояния каждого игрока: каждый пишет свою запись и читает чужие.

Порождение для всех

const id = ctx.net.spawn(props.bullet, { position, rotation });
ctx.net.despawn(id);

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

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

Как плавно рисовать других

Именно здесь чаще всего всё ломается, поэтому для этого есть отдельный вызов.

ctx.net.follow(otherPlayerObject, { position, rotation });
ctx.net.unfollow(otherPlayerObject);

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

Не пишите приходящие позиции прямо в transform

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

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

Схема, которая работает

Если начинаете с нуля, такое разделение труда уведёт вас далеко:

  • каждый игрок владеет своим персонажем — локальное тело, управляемое его вводом;
  • его позиция уходит наружу через net.state или канал, десять-двадцать раз в секунду;
  • чужие персонажи рисуются через follow;
  • объекты мира — это shared-тела, так что физика бесплатно держит их согласованными;
  • счёт и состояние раунда живут в net.state, их пишет тот, кто авторитетен для этого события.

Дальше: Решение проблем — когда что-то ведёт себя не так.