Кружок Программирования

Змейки


Иллюстрация с сайта lottiefiles

Постановка задачи

Хотим сделать графическую змейку в окне браузера. Змейка должна иметь цвет, длину, возможность плавного поворота влево или вправо. Змейка должна быть инкапсулирована, чтобы было удобно создавать их сразу несколько на одном поле. Потребуется также  инкапсуляция прототипа для Поля; хочется чтобы оно могло содержать не только массив змеек, но также и кроликов. Змейка, при поедании кролика, увеличивает длину.

Реализация

В качестве графического контейнера веб-страницы, можно выбирать из двух вариантов - это либо HTML Canvas (растровая, то есть пиксельная графика), либо SVG (векторная графика). Векторная выглядит интереснее, потому что хранит графику как набор фигур, не превращая их в массив точек. Очевидно что змейку можно представить в виде набора сегментов, где каждый имеет свои координаты и угол поворота. Чтобы змейка ползла, достаточно оперировать первым и последним сегментом - пошагово укорачивать хвост и удлинять голову.

Промт к LLM (chatGPT 4.0)

P.S.Сейчас, глядя на этот промт, конечно понятно, насколько он был наивен. Детали отрисовки оказались намного примитивнее, да и то, пришли в боле-менее приличный вид только после серьёзной ручной доработки. Кроме того, запрос обычно удобнее строить на родном языке, так что особого смысла писать его на английском не было.

I'd like to create an online game called snakes.
To start, please generate a js code for the snake prototype.
Snake properties must include the collection (or the resizable array) of chained elements, where each element must hold both coordinates x and y and the angle between x axis and the direction of snake element. Also, snake must hold variables for it's current size, the number of consumed foods total, and the number of current grow energy. Also, it holds the percentage of completion of the current move (this variable must be used for smoother graphics, must start from 0 and stay less than 100; zero means that the 'head' element is fully in 'old' position and the 100 would mean that new element is fully appeared already. The collection of elements must be circular and resizable, so both the head and the tail of the snake could be moved forward smoothly while the 'percentage of completion of the current move' is smoothly increasing). So, initially, the snake must have 3 elements: the tail, the current head and the element for the future head. All elements has fixed distance between each other, which equals to the size of elements, and each element holds orientation angle so they all form single snake without a breakouts. Elements can be rendered on web using dynamic SVG using the skin of the snake. It should be several skins including "chain"(like a chan made from metal), "cobra", "green snake". There should be methods for rendering old head, new head, body and old tail and new tail elements. These metods must take into account the percentage of completion of the current move, so while it is growing, the old tail must be shorten smoothly (I think if the parameter greater than 50%, than it must fully disappear and the new tail must start to be shown as the tail with some advanced length, which is decreasing, so this advancement is gone when the parameter come close to 100) and the head should advance accordingly. 
So, for the new head the direction vector initially will be the same as for the old one, however it must be calculated as a sum of previous angle and the currentRotation (variable of the snake), which can be positive or negative, depending of the snake wants to turn left o right.

Next, please generate a js code for field prototype. The field is a rectagonal area containing snakes and rabbits. Initially only one snake can play. If the left arrow is pressed then the currentRotation is slightly increasing, but can not be greater than Pi/6, in radians. For the right arrow - the opposite logic accordingly.
"percentage of completion of the current move" is always increasing, forcing the snake to move. Please add constants for snake speed (as to create how many new head elements per each 10 seconds), snake element size in pixels and all other you think is necessary. If snake comes to close to rabbit (less then the size of element) then, it 'consume' it. it's grow energy is increased by 1. When so, then, with the next move, the snake will increase it's size by one (and decrease grow energy by one) and the tail is freezed temporarily. rabbits must be drawn using SVG, and appear randomly on the field some times. The max number of rabbits must be limited with a constant.

Начальная (нулевая) версия

Первое, что бросается в глаза после получения ответа от LLM - оказывается, что инкапсуляция в современном языке javascript уже давно отошла от использования экзотического ключевого слова prototype, а перешла на синтаксический сахар класса. То есть, реализовать ООП стало можно примерно также, как и в большинстве остальных классических языков.

Таким образом, мы получаем два класса, Snake (для змейки) и Field  (игровое поле).

Далее, для того, чтобы сделать возможным обмен игровыми событиями, для многопользовательского режима игры, мы будем использовать Вебсокет. Причём для работы с последним, удобно использовать библиотеку, которая позволяет вызывать функции клиента с сервера, и наоборот. Выбор библиотеки зависит от серверной платформы, в нашем случае это платформа .net и библиотека signalR

Демо

Посмотреть, что получилось, можно на этой странице: chemrise.ru/games/snakes.

Код

Чтобы посмотреть код, находясь на Демо странице, нажимаем комбинацию клавиш CTRL-U и находим в нём файл /Scripts/snake.js.

Несмотря на внушительное количество строчек (около 500), код имеет довольно простую структуру, из трёх частей, первые две - это классы змейки и игрового поля, а далее идёт код запускающий игру в многопользовательском режиме. Управление кроликами включено в класс игрового поля.

Синхронизация пользователей

Если у какой-то из сущностей (змейка, кролик) вызывается метод, изменяющий её состояние, то этот метод должен вызвать также и одноимённый метод объекта window.chat.server и передать параметрами всю нужную информацию об изменении:

  • window.chat.server.moveSnake
  • window.chat.server.addRabbit
  • window.chat.server.removeRabbit

Для каждого из таких вызовов, сервер перенаправит вызов на каждый из подключенных клиентов, которые в джаваскрипте вызываются через объект chat.client:

  • chat.client.castMoveSnake
  • chat.client.addRabbit
  • chat.client.removeRabbit

И плюс есть ещё один метод, receiveContext. Этот вызывается в самом начале, при подключении нового клиента.

  • chat.client.receiveContext

Анимация

Отрисовка текущего фрейма организована джаваскрипт функцией animate которая принимает параметром текущее время, исходя из которого расчитывается текущие положения головы и хвоста. Последним своим действием эта функция делает заказ отрисовки следующегно фрейма, путём вызова метода requestAnimationFrame, в который функция перерисовки передаёт ссылку на саму себя, что позволяет змейкам перерисовыться каждый кадр (обычно это 60 раз в секунду).

animate(currentTime) { const elapsedTime = currentTime - lastUpdateTime; percentageCompletion += movesPerSecond * elapsedTime; //Тут делаем перерисовку //... //делаем заказ следующего фрейма requestAnimationFrame(animate); }

При перерисовке сначала вычисляется значение percentageCompletion, которое показывает процент завершённости текущего хода, то есть проползания змейки от одного сегмента к следующему, который имеет определённую длину.

Что примечательно, SVG также имеет SMIL, то есть свои собственные механизмы для анимации. Однако, chatGPT не советует переключать джаваскриптовую логику змеек, основанную на requestAnimationFrame и percentageCompletion, на этот механизм, даже для переходов от одного сегмента змейки к другому, утверждая что хотя некоторая оптимизация производительности и ожидается при таком подходе, но она здесь не настолько критична и важна, по сравнению с тем, как усложнится код.

М.О.