Мультиплеер
Всё, что ниже, работает только тогда, когда в проекте включён мультиплеер и плеер открыл комнату.
В остальное время эти вызовы тихо ничего не делают, и это намеренно: скрипт, написанный под мультиплеер, прекрасно работает и в одиночном предпросмотре. Двух версий не нужно.
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 каждый кадр, проигрывая буфер с небольшим отставанием, чтобы у него всегда было две известные позиции, между которыми двигаться.
Пакеты приходят гораздо реже кадров, поэтому объект стоит, а потом прыгает: удалённые игроки как будто шагают, а не идут. Плавный подход к последнему пакету тоже не спасает — между пакетами впереди текущего момента нет ничего, к чему стремиться, так что он тормозит, а затем дёргается.
Повторно скормить ту же позицию не стоит ничего, так что вызывать follow каждый кадр с тем, что
у вас сейчас есть, — это и есть задуманное использование.
Схема, которая работает
Если начинаете с нуля, такое разделение труда уведёт вас далеко:
- каждый игрок владеет своим персонажем — локальное тело, управляемое его вводом;
- его позиция уходит наружу через
net.stateили канал, десять-двадцать раз в секунду; - чужие персонажи рисуются через
follow; - объекты мира — это shared-тела, так что физика бесплатно держит их согласованными;
- счёт и состояние раунда живут в
net.state, их пишет тот, кто авторитетен для этого события.
Дальше: Решение проблем — когда что-то ведёт себя не так.