[Это мои заметки/черновик/стенограмма выступления]

Всем привет! Я Давид да Силва. Я очень рад поделиться своими знаниями здесь, в HackUPC, чтобы увидеть, сколько из них остается с вами, и какие прекрасные вещи вы делаете с ними. Так что спасибо, что пришли!

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

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

Если вы хотите отметить меня на каких-либо фотографиях или что-то еще, вот мое имя пользователя во всех социальных сетях (@dasilvacontin). В основном я активен в Twitter и Instagram.

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

  • Я сделал свою первую многопользовательскую онлайн-игру еще до поступления в университет. Конечно, после того, как я много раз ударился головой о стену. Именно благодаря курсу, прочитанному здесь, в UPC, летом, мне, наконец, удалось это сделать.
  • На данный момент я провел 6 курсов по разработке многопользовательских онлайн-игр, примерно по одному в год, и в настоящее время я работаю над онлайн-версией курса.
  • Еще до поступления в университет я опубликовал игру для iOS, которую скачали более 80 000 раз. И сделал много других игр, в основном незаконченных или для соревнований, в которые никто не играл в сравнении. Вот несколько секунд моих любимых.
  • Я стажировался в Improbable, британской компании, разработавшей SpatialOS, платформу для запуска многопользовательских онлайн-игр поверх распределенной системы, что означает балансировку нагрузки и распределение нагрузки на сервер в зависимости от «географической» плотности размещения игры. вселенная. Это позволяет вам иметь множество живых ИИ и неигровых персонажей, делающих что-то, даже если поблизости нет игровых персонажей, и другие интересные вещи. Здесь я провел исследование и написал руководства о том, как реализовать движение игроков, конечные автоматы и органически стабильные экосистемы, которые работают поверх и используют преимущества распределенной системы, такой как SpatialOS.
  • В настоящее время я удаленно работаю дизайнером продуктов в консалтинговой компании Sensa, расположенной в Севилье.

Сооо, приступим.

Показать сначала пошаговую, потом в реальном времени?

Я хочу:

  • покажи как сделать простую игру

Повествование: значит, это искусство. Но почему это не игра? Поскольку нет взаимодействия, это не реактивно. А может потому, что цели нет.

Иногда игра находится в вашем уме.

Объясните, как работает игра

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

Это холст, un lienzo на испанском языке. Это элемент HTML, на котором мы можем рисовать фигуры, линии и графические элементы. Использование JavaScript. В веб-браузере.

Давай сделаем это.

Для начала вот разметка для добавления Canvas в ваш HTML-файл. Вы можете установить его размер, используя свойства `width` и `height`, или вы можете сделать это позже с помощью JavaScript.

Когда у нас есть Canvas, нам нужно иметь возможность взаимодействовать с ним — нам нужна ссылка на него, какой-то способ вызова его методов. Есть много способов сделать это — вот один из них:

Используя `document.getElementsByTagName()[0]`, мы можем получить ссылку на первый элемент на нашем веб-сайте, который соответствует имени тега, который мы предоставляем, например, `h1`, `p`, `a`, или, в этом случае, `холст`. Мы сохраняем эту ссылку в переменной с именем `canvas`, и все готово.

`const canvas = document.getElementByTagName(‘canvas’)[0]`

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

`const ctx = canvas.getRenderingContext('2d')`.

Прежде чем я двинусь дальше, было бы неплохо уточнить некоторые английские слова, которые будут использоваться. Когда вы рисуете фигуру, она состоит из двух частей: заливка и обводка. Заливка — это «краска» внутри контура, которая заполняет его — как в книжке-раскраске. Штрих — это линия, которая следует за путем. Вы можете заливать и обводить любым выбранным вами цветом, вы можете обводить разными уровнями толщины, и вам не нужно и заполнять, и обводить, вы можете просто сделать один из них, если хотите.

Хорошо, это облегчит понимание свойств и методов контекста рендеринга, который мы увидим. Контекст рендеринга, или `ctx`, как я буду называть его в дальнейшем, имеет множество методов, которые мы можем использовать: рисовать прямоугольники, круги, линии, текст и т. д. Лично мне это помогает понять все это. лучше, если я представлю, что к каждому холсту прилагается волшебная кисть, что эту кисть можно трансформировать и заставить делать что-то, используя свойства и методы, которые есть у ctx.

Чтобы нам было на чем примерить эффекты трансформации кисти, позвольте мне показать вам, как нарисовать прямоугольник:

`ctx.fillRect(50, 50, 200, 100)`

Давайте посмотрим, что означает каждое из этих чисел. Аргументы по порядку: «x», «y», «width» и «height». Или, более гуманно, исходное положение прямоугольника, которое будет его верхним левым углом, и размер прямоугольника.

Система координат на холсте имеет начало в верхнем левом углу и имеет положительное значение X слева и положительное значение Y внизу. Зная это, мы должны ожидать, что прямоугольник начнется здесь и будет выглядеть так, если мы выполним `fillRect(50, 50, 200, 100)`.

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

Если мы хотим изменить цвет заливки, мы назначаем шестнадцатеричную строку can для `ctx.fillStyle`.

Если мы хотим изменить цвет обводки, то же самое, но на `ctx.strokeStyle`.

И наконец, если мы хотим увеличить ширину обводки, мы можем присвоить новое значение `ctx.lineWidth`, которое по умолчанию равно 1. (`ctx.lineWidth = 5`) [Показать до и после].

Мы также можем установить цвета, используя строку с их названием, например, «оранжевый», или используя способ RGB, например, «rgb(255, 165, 0)» или «rgba(255, 165, 0, 0.5)"`, если вы хотите альфа. Это то же самое, что и в CSS, если вы видели это.

Это напоминает мне, что вы также можете изменить альфу всего, что сделано кистью, назначив значение от 0 до 1 для `ctx.

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

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

  • прямоугольник, круг, текст, линии
  • рисоватьИзображение
  • анимация
  • как сделать его независимым от процессора
  • переводить, вращать
  • сохранить контекст, восстановить контекст
  • повторение, случайное, рекурсивность
  • быстро упомянуть sin и cos, применимость для анимации
  • упоминание курса от mattdeslauriers
  • давайте сначала создадим игру для первого игрока, чтобы представить структуру игры. какую игру делаем? игра в снежки, конечно. Я выбрал его, потому что
  1. движение игрока не ограничивается сеткой, что делает его более интересным
  2. есть объекты, которые двигаются сами по себе, например, снаряды, снежки, которые бросают
  3. могут быть препятствия, с которыми можно столкнуться
  4. может быть полоса здоровья и предметы, которые можно подобрать, например, повысить вашу статистику
  • хорошо, давайте начнем с создания нескольких функций, которые программно рисуют то, что мы хотим. Мы будем называть «Аватаром» персонажа, которым игрок управляет и владеет. Если мы назовем обе вещи «игроком», это будет очень запутанно.
  • Хорошо, теперь мы знаем, как рисовать. но откуда мы знаем, что нам нужно нарисовать? Простой способ структурировать игру — отслеживать состояние игры в структуре данных, вроде объекта, и тогда то, что мы рисуем, будет зависеть от этого объекта. Поскольку состояние игры, хранящееся в этом объекте, со временем меняется, то будет меняться и то, что рисуется на холсте, поскольку наш код рендеринга зависит от состояния игры.

[создайте функцию рендеринга, которая в зависимости от содержимого объекта gameState отображает игроков и снежки]

  • Хорошо, теперь у нас есть рендерер, отрисовывающий состояние игры. Но состояние игры на данный момент постоянное — оно не меняется. Нам нужен способ, чтобы вещи могли двигаться в каждом кадре. Поскольку то, что мы рисуем, зависит от состояния игры, нам нужен способ изменять состояние игры в каждом кадре — нам нужно иметь возможность 1) изменять состояние объектов, которые движутся автоматически, например, брошенных снежков, которые должны следовать свой путь и 2) чтобы игроки могли выполнять действия для своего аватара, например, двигаться, бросать мячи и т. д. Способ чтения ввода.
  • Итак, как нам сделать фрагмент кода, который будет запускаться в каждом кадре нашей игры? Есть многопользовательские способы. Мы собираемся повторно использовать `requestAnimationFrame`, который мы использовали в прошлом. Теперь в нашем игровом цикле есть две функции: logic(), которая изменяет состояние игры, и render(), которая рисует состояние игры.
  • Вот пример того, как мы заставим снежки двигаться в каждом кадре.
  • А теперь, чтобы игроки могли взаимодействовать с игрой, вот как вы читаете клавиатуру в javascript:
  • Но эти вызовы addEventListener запускаются только тогда, когда клавиша начинает нажиматься, отпускаться и т. д. В идеале мы хотим, чтобы игроки двигались только один раз при каждом выполнении логики. Так как же узнать состояние клавиатуры внутри функции logic()? Мы создаем представление состояния клавиатуры, используя данные, полученные от обратных вызовов.
  • То же самое для чтения состояния мыши внутри функции logic(). Мы настраиваем некоторые обратные вызовы, которые уведомляют нас об изменениях в состоянии мыши, и мы сохраняем представление ее состояния в объекте. Затем мы проверяем этот объект внутри logic().
  • Хорошо, и давайте быстро напишем функцию столкновения для нашей игры… мы будем использовать ее, чтобы избежать столкновения между игроками и определять, когда снежки попадают в игроков. Мы будем использовать круговые столкновения, которые легко кодировать. Концепция следующая: если все является кругом, круг A сталкивается с кругом B только в том случае, если расстояние между их центрами меньше суммы их радиусов. Таким образом, мы итерируем каждый объект, способный столкнуться с любым другим объектом, с которым он может столкнуться. Если они сталкиваются, мы что-то делаем. При столкновении снежка с игроком, если снежок был брошен не игроком (чтобы избежать случая, когда мы спауним снежок поверх игрока), мы убираем снежок, а у игрока вычитаем здоровье, или мы полностью устраняем игрока, что мы и сделаем здесь.
  • Мы можем использовать функцию столкновения при перемещении игрока, чтобы увидеть, переместился ли он в правильную позицию.
  • Круто, этого достаточно для игры!

… Итак, как нам сделать этот мультиплеер? Нам нужна возможность синхронизировать состояние игры между компьютерами, чтобы многопользовательские игроки могли играть в одну и ту же игру, используя свой собственный компьютер. Следовательно, нам нужен 1) способ связи между этими компьютерами и 2) стратегия, позволяющая решить, как поддерживать согласованность состояния игры на всех компьютерах.

К счастью, во-первых, обычно есть библиотеки, которые делают всю работу за нас. В этом случае мы будем использовать замечательный socket.io.

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

Существует два основных способа подключения устройств. Вы соединяете их всех между собой, что называется P2P, человек-к-человеку. Или у вас есть что-то посередине, называемое сервером, которое соединяет все устройства звездообразно. Наиболее популярным является наличие сервера, который соединяет устройства, называемые клиентами. Одним из больших преимуществ является то, что сервер действует как источник правды — все, что говорит сервер, является правдой, и вы просто верите в это. При создании игр с использованием P2P часто бывает так, что одно из устройств выступает источником правды, а остальные игроки верят всему, что говорят о состоянии игры. Но тогда как узнать, что игрок не читерит и меняет состояние игры с помощью хаков? Чтобы решить эту проблему, необходимо использовать умные способы синхронизации игр, которые не требуют доверия к другим игрокам, а в некоторых типах игр очень сложно, если не невозможно, избежать мошенничества. В основном вы увидите игры, в которых нет скрытых компонентов.

Хорошо, теперь, когда мы знаем, что мы собираемся использовать сервер, и что каждый клиент (игрок) будет поддерживать активный «разговор» или канал обмена сообщениями с сервером… как выглядит разговор с сервером, чтобы синхронизировать наши игровые состояния? Опять же, есть два основных способа:

авторитетный способ сервера и авторитетный способ клиентской стороны.

Подожди, подожди, что? Авто что? Власть или авторитетный — это модное слово, которое означает «тот, кто контролирует», точнее, источник правды, которому доверяют остальные участники. В серверном авторитетном способе источник истины находится на сервере, который находится под нашим контролем (хорошо). В авторитарном подходе на стороне клиента есть несколько источников правды, разбросанных по клиентам (плохой, потенциальный обман) — они обычно контролируют положение своего аватара, и на самом деле все, что вы заставляете их контролировать, например, их HP, содержимое инвентаря, боеприпасы и т. д.

Сначала давайте пойдем чисто авторитарным путем на стороне клиента. Именно так большинство людей делают свои первые многопользовательские онлайн-игры, так как это проще всего реализовать.

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

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

Как выглядит обновление состояния от игрока?

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

Давайте углубимся в то, как решить любые проблемы, которые могут возникнуть.

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

Самое интересное - это снежки. Особенно ту часть, когда они бьют другого игрока. Это событие, которое изменяет данные, принадлежащие разным игрокам: 1) игроку А, который бросил снежок, и 2) игроку Б, в которого попали. Когда снежок попадает в игрока, снежок должен исчезнуть, а у игрока, в которого попали, должно уменьшиться здоровье или он будет напрямую исключен из игры. Что делать? Самый простой способ — обрабатывать изменения отдельно. Когда игрок А обнаруживает, что его снежок попал в другой, игрок А удаляет свой снежок. И когда игрок Б обнаруживает, что в него попал снежок, он снижает свой HP или переводит свой аватар в состояние «устранено». Может случиться так, что игрок А видит, как его снежок попадает в игрока Б, и поэтому убирает снежок, но игрок Б локально не видит столкновения, поэтому на него это не оказывает никакого влияния. Но мы просто пожмем плечами на этот случай и назовем это особенностью ¯\_(ツ)_/¯ — просто случайно попал снежком в руку, и это не причиняет вреда.

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

Но видите ли вы проблему с авторизованным решением на стороне клиента? Доверяя клиентам состояние своего аватара и своего снежка, особенно положение в данном случае, игроки могут взломать игровой клиент и телепортировать своего игрока или свои снежки, и мы просто поверим всему, что они говорят об их состоянии. Они также могут выбрать, чтобы они никогда не подвергались воздействию снежного кома, воздействующего на их игрока, что фактически делает их непобедимыми.

Как мы можем обойти это? Доверие игрокам, клиентам, меньше.

Введите авторитетный способ сервера, способ, которым сервер является источником истины для всего игрового состояния. Сервер имитирует игру (запускает логический цикл с коллизиями и всем остальным), и каждый кадр отправляет обновление с состоянием игры всем игрокам, которые перезаписывают их локальное состояние игры тем, которое они только что получили. Хорошо, это легко понять. Но теперь все усложняется. Как игроки перемещают свой аватар? Они не являются источником правды для своего аватара, поэтому, если они локально изменят свои свойства, эти изменения никого не коснутся и будут перезаписаны всякий раз, когда они получают обновление состояния игры с сервера, то есть каждый кадр. Поэтому, если сервер является источником правды, и только данные, измененные на сервере, имеют значение, нам нужно сделать так, чтобы клиенты сообщали серверу о намерениях игроков, будь то перемещение или бросание снежка. .

Для метания снежка это легко. Если игрок хочет бросить снежок, мы отправляем сообщение на сервер, что именно это и означает. Мы можем назвать это `throwSnowball`. И это только то, что игрок сообщает серверу, что он собирается бросить снежок. Не сказано ни где рождается снежок, ни в каком направлении. Эта информация может быть получена сервером с помощью состояния игры — направление может быть определено по направлению, в котором смотрит игрок. Когда сервер получает сообщение `throwSnowball` от одного из игроков, он порождает снежный ком в позиции игрока и делает направление снежного кома направлением игрока. Может быть хорошей идеей, что сообщение `throwSnoball` приходит с указанием направления снежка, на всякий случай. Вы можете доверять любой информации, поступающей от игрока, если эта информация не может быть расценена как мошенничество. Например, игроки могут обманывать, если мы полагаемся на то, что они отправят свою позицию, поскольку они могут отправить нам фальшивую. Но обман из-за желания бросить снежок? Вы либо хотите бросить снежок, либо нет. И в этой игре вы можете бросить снежок когда угодно. Если бы мы хотели ограничить скорость броска снежков, мы могли бы добавить кулдаун на стороне сервера для каждого игрока, и когда они говорят нам, что хотят бросить снежок, мы сначала проверяем, действительно ли они могут это сделать. Если они не могут, потому что это действие перезаряжается, мы просто игнорируем его.

Для движения игрока все становится немного сложнее. Как мы позволяем игрокам сообщать нам о своем намерении сделать ход? Они могут посылать нам такие сообщения, как «moveUp», «moveDown», «moveLeft» и т. д. Это сработает. Но если они хотят двигаться в том же направлении, т.е. вправо, на пару секунд, допустим 4 секунды, и каждую секунду 60 логических шагов, это 240 сообщений `moveRight`. Нам также нужно будет проверить, не двигали ли мы игрока с момента последнего кадра, иначе он мог бы отправить кучу сообщений, чтобы двигаться быстрее, как способ обмана. Решение состоит в том, чтобы игрок отправлял нам обновления о состоянии своей клавиатуры (только те клавиши, которые нас интересуют), и мы создаем представление клавиатуры каждого игрока на сервере. А затем, внутри шага сервера logic(), мы проверяем клавиатуру каждого игрока, чтобы переместить его. Такого обмана нет. Вы хотите сказать, что нажимаете клавишу W? Конечно. Вы больше не нажимаете его? Хорошо. И нам нужно отправить только два обновления состояния клавиатуры, если мы хотим переместиться вправо на пару секунд: одно обновление, когда мы начинаем удерживать клавишу, и другое обновление, когда мы ее отпускаем.

Как мы видели, чистый авторитетный способ сервера избавляет от читерства. Но это создает проблему: игроки не сразу видят движение своего аватара, когда нажимают клавиши движения. Чтобы увидеть движение своего игрока, на сервер должно быть отправлено сообщение о состоянии клавиатуры, сервер учитывает это обновление на своем следующем шаге logic(), затем сервер отправляет обновление состояния игры всем игрокам, и новое игровое состояние отображается в следующем кадре клиента. Именно столько времени требуется, чтобы увидеть, как ваш игрок двигается.

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

Что мы могли бы сделать вместо этого? Он используется в больших играх, таких как PUBG. Вы доверяете клиентам, совсем немного. Вместо того, чтобы отправлять вам обновления состояния клавиатуры для перемещения, вы позволяете им отправлять обновления о своей позиции. Но чтобы избежать телепортации, вы игнорируете обновления, которые находятся слишком далеко от их местоположения. Если это слишком далеко, они обманывают, и вы игнорируете это. Также может случиться так, что соединение сбросило некоторые из их сообщений, но не повезло ¯\_(ツ)_/¯.

Наконец, как разместить свою игру в сети? Вам понадобится виртуальный частный сервер, на котором будет работать сервер вашей игры. В данном случае NodeJS. Heroku - отличный способ. Вы создаете «приложение Heroku», которое связывается с одним из ваших репозиториев GitHub, и всякий раз, когда вы отправляете в репозиторий, Heroku замечает это, загружает последнюю версию кода на свой сервер и запускает ее.