Pull to refresh

Comments 14

Даже не слышал о таком проекте, спасибо! Мы как раз в офисе в неё поигрываем ИРЛ. Затестим.

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

Не очень понятно, зачем вообще что-то бросать на стороне клиента. Клиент передал на сервер намерение - взять карту. Сервер бросил кубик и ответил этому клиенту, какая взята карта, всем остальным - факт, что карта взята.

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

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

Это Delta-update propagation по терминологии статьи. Ну да, конечно, мне кажется автор преувеличил проблему секьюрности в этом подходе.


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


У меня секьюрность достигается методами toClient / toOthers в моделях


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


// Это redux thunk action если что:
export const server$gameGiveCards = (gameId, userId, count) => (dispatch, getState) => {

  // Берем карты
  const cards = selectGame(getState, gameId).deck.take(count);

  // Диспатчим экшн раздачи на стор сервера, чтобы он раздал карты
  dispatch(gameGiveCards(gameId, userId, cards));

  // Отдельно диспатчим экшн юзеру чтобы он эти карты получил
  dispatch(toUser$Client(userId, gameGiveCards(gameId, userId, cards.map(card => card.toClient()))));

  // А вот всем остальным юзерам (uid !== userId) высылаем "отфильтрованные" карты. Поидее можно было бы число, не помню почему так сделал, вроде из-за анимации проще было.
  dispatch(to$({clientOnly: true, users: selectUsersInGame(getState, gameId).filter(uid => uid !== userId)}
    , gameGiveCards(gameId, userId, cards.map(card => card.toOthers().toClient()))));
};

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


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

код геймплея выполняется в клиенте

Но это ведь ненадёжно для сетевых игр. Как защищаться например от подделки памяти? Вот я себе нагенерирую миллион монет на клиенте. А на другом клиенте у меня 10 монет. Как они разрешают этот конфликт?

Так сервер все равно проверяет корректность действий. Хакнутый клиент отправит команду «купить шлем за миллион», но в ответ получит не шлем а ошибку.

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

Вместо того, чтобы просто выполнить код на сервере и выдать клиентам заведомо валидный результат?

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

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

Я так понял, что игра у них не очень-то пошаговая. В том смысле, что другие игроки на твоём ходу не просто смотрят на чёрный экран, а в реальном времени видят, что ты делаешь (ну, в рамках механик, конечно)

Так это "делаешь" все равно проходит верификацию на сервере. Ну, не весь ход, кусками можно передавать. На вопрос "почему это делается И на клиенте, И на сервере" я все равно ответа не вижу.

Автор предлагает 3 варианта:


update propagation — это "намерение — результаты". Здесь минусы в том, что на клиенте создается невалидное состояние.


delta-update propagation — это "намерение — результат". Минусы в лишнем коде на клиенте и в секретном состоянии. Например у вас есть видимый юнит А и скрытый юнит Б. Вы двигаете их, получаете дельту "юнит А передвинулся, юнит Б передвинулся", а дальше вам надо лезть в логику сервера и передавать одному игроку "юнит А передвинулся, юнит Б передвинулся", а другим игрокам "юнит А передвинулся".


action propagation — передавать просто намерение. "Передвинуть юнит А, передвинуть юнит Б". Дальше на сервере мы можем проверить, двигаются они или нет и выслать не результат, а только намерение.


Важно заметить, что намерение отфильтровать легче:


Пришло 2 намерения, одно передаем, второе пропускаем.


В случае с дельтой это будет собираем дельту 1, собираем дельту 2, одну туда, вторую сюда.


И вот сервер высылает клиентам намерение:
1) Игроку "Подтверждаю: (Передвинуть юнит А, передвинуть юнит Б)"
2) Другим: "Подтверждаю: (Передвинуть юнит А)".


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


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


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


Поэтому, Q: "почему это делается И на клиенте, И на сервере"
A: Так проще чем городить дельты, объединять их, разделять их и пр.

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

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

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

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

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

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

Sign up to leave a comment.

Articles