Одна из моих целей на 2025 год — создание завершённой игры. Завершённой, то есть её можно будет купить в Steam или App Store за $2,99 или около того. Я уже делал маленькие игры, но завершение и выпуск игры, вероятно, будет самым крупным моим проектом (если не считать блога).
В зимние каникулы я какое-то время писал прототипы игр на LÖVE — фреймворке для создания 2D-игр на Lua. Таким образом я хотел изучить инструменты разработки игр, подходящие к моему набору навыков, и определить свои сильные стороны, чтобы в 2025 году распоряжаться временем эффективно.
До работы над этими прототипами я написал примерно двести строк кода на Lua, но у меня не возникло никаких проблем в освоении нужного мне синтаксиса.
Оказалось, что API LÖVE простой и мощный. Одно из преимуществ использования фреймворка вместо игрового движка в том, что я могу показать вам полный пример всего в десяти строках кода (в отличие от игрового движка, где пришлось бы определять объекты сцены, прикреплять скрипты и так далее).
Показанный ниже пример позволяет игроку перемещать квадратик по экрану.
x = 100 -- обновляем состояние игры в каждом кадре ---@param dt - время с последнего обновления в секундах function love.update(dt) if love.keyboard.isDown('space') then x = x + 200 * dt end end -- отрисовываем экран в каждом кадре function love.draw() love.graphics.setColor(1, 1, 1) love.graphics.rectangle('fill', x, 100, 50, 50) end
Хотя мои прототипы гораздо более сложные, этот пример демонстрирует саму суть LÖVE.
Шахматный UI
Каждую зиму я возвращаюсь к шахматам. Играю, пытаюсь совершенствоваться, а также браться за связанные с шахматами проекты (примерно в то же время четыре года назад я создал шахматный движок).
UI популярных игроков в шахматы (chess.com, lichess.org) невероятно хорошо продуман. Реализация шахматного UI может показаться простой задачей, но когда я начал разбирать переходы между состояниями, то осознал, насколько изящно всё согласуется. Особенно хорош UI анализа после партии на lichess.org.
Я хотел попрактиковаться на шахматных головоломках, но для начала мне нужно было организовать работу базового шахматного UI. Это и стало моей первой программой на LÖVE, занявшей примерно два часа.
Для перехвата ввода с мыши я использовал сочетание функций обратного вызова LÖVE (love.mousereleased
для завершения перетаскивания, love.mousepressed
для перемещения фигуры двумя щелчками).
Для рендеринга фигур в процессе перетаскивания я использовал love.mouse.getPosition()
.
local pieceImage = love.graphics.newImage("assets/chess_" .. piece.name .. ".png") -- .. -- отрисовываем перетаскиваемую фигуру в позиции курсора if piece.dragging then local mouseX, mouseY = love.mouse.getPosition() -- центрируем фигуру на курсоре local floatingX = mouseX - (pieceImage:getWidth() * scale) / 2 local floatingY = mouseY - (pieceImage:getHeight() * scale) / 2 -- отрисовываем поднятую фигуру нужным цветом if piece.color == "white" then love.graphics.setColor(1, 1, 1) else love.graphics.setColor(0.2, 0.2, 0.2) end love.graphics.draw(pieceImage, floatingX, floatingY, 0, scale, scale) end
За многие годы я создавал UI при помощи множества разных библиотек. Самым близким к работе с LÖVE опытом был браузерный Canvas API. Я считаю, что LÖVE — лучшее решение для прототипирования произвольного UI при помощи кода. Я говорю произвольного, потому что если бы мне потребовалось что-то с полями ввода и кнопками, то не думаю, что LÖVE стал бы подходящим выбором.
Одна из причин того, что LÖVE оказывается таким мощным решением, заключается в простоте, с которой LLM генерируют и анализируют код, необходимый для написания прототипов на LÖVE. API понятен (или его можно изучить по очень коротким docstrings), а для остального кода требуется лишь простая математика UI.
Это совершенно не похоже на ситуацию с GDScript движка Godot, с которым LLM испытывают трудности. Думаю, это можно исправить при помощи fine-tuning, RAG (Retrieval-Augmented Generation) или few-shot prompting, но в изучение этого вопроса я не углублялся.
Ранее я не пользовался LLM в визуальных проектах, поэтому был удивлён, насколько близко claude-3.5-sonnet
и gpt-4o
оказались способны реализовывать мои промты (через Cursor).
Хотя программы на LÖVE открываются очень быстро, мне всё равно не хватает горячей перезагрузки, которая есть при работе над браузерными UI. В более крупном проекте я бы, вероятно, потратил какое-то время на создание отладочного режима и/или горячей перезагрузки конфигурации UI.
У меня возникли небольшие проблемы с отделением логики UI от логики приложения. Мне не кажется, что разделение вышло особо чистым, но с ним было удобно работать. В примере ниже видно, как я использую свой «API фигуры».
-- вызывается при нажатии кнопки мыши ---@param x - координата x мыши ---@param y - координата y мыши function love.mousepressed(x, y, button) local result = xyToGame(x, y) -- проверяем, нажали ли мы на допустимую клетку if result.square then for _, piece in ipairs(pieces) do -- если мы нажали на фигуру и это допустимая клетка, то перемещаем её if piece.clicked and piece:validSquare(result.square) then piece:move(result.square) return end end end -- проверяем, нажали ли мы на фигуру if result.piece then result.piece:click(x, y) result.piece:drag() return end -- в противном случае выполняем unclick всех фигур for _, piece in ipairs(pieces) do piece:unclick() end end
UI карточной игры
Ещё один UI, о котором я недавно думал — это интерфейс игры Hearthstone, в которую я играл примерно год после её релиза. Это был мой первый опыт в состязательной карточной игре, и она доставила мне море удовольствия.
Похоже, сложность реализации карточных игр идеально сбалансирована. Основная часть работы заключается в планировании и гейм-дизайне. Сравните это, например, с 3D-играми, при разработке которых существенную долю времени нужно тратить на создание графики и игрового мира. По моим ощущениям, я бы смог написать MVP предварительно спланированной карточной игры примерно за месяц.
На прототип у меня ушло три часа.
По сравнению с шахматным UI, этот прототип карточной игры потребовал удвоения количества строк кода. Также я столкнулся с первыми трудностями при рендеринге плавных анимаций взаимодействия с картами.
Обычно я стараюсь не добавлять анимации в прототипы, но они необходимы для хорошо выглядящей карточной игры, поэтому я реализовал их ещё на этапе прототипа.
Как и в случае с шахматным UI, LLM помогли мне с подготовкой фундамента, например, с отрисовкой полей и текста, а также с объединением разбросанных частей состояния в две группы конфигурации (конфигурации игры и состояния игры).
При работе с чем-то простым наподобие полосок здоровья и маны LÖVE проявляет себя во всей красе.
local function drawResourceBar(x, y, currentValue, maxValue, color) -- фон love.graphics.setColor(0.2, 0.2, 0.2, 0.8) love.graphics.rectangle("fill", x, y, Config.resources.barWidth, Config.resources.barHeight) -- заливка local fillWidth = (currentValue / maxValue) * Config.resources.barWidth love.graphics.setColor(color[1], color[2], color[3], 0.8) love.graphics.rectangle("fill", x, y, fillWidth, Config.resources.barHeight) -- граница love.graphics.setColor(0.3, 0.3, 0.3, 1) love.graphics.setLineWidth(Config.resources.border) love.graphics.rectangle("line", x, y, Config.resources.barWidth, Config.resources.barHeight) -- текст значения love.graphics.setColor(1, 1, 1) local font = love.graphics.newFont(12) love.graphics.setFont(font) local text = string.format("%d/%d", currentValue, maxValue) local textWidth = font:getWidth(text) local textHeight = font:getHeight() love.graphics.print(text, x + Config.resources.barWidth/2 - textWidth/2, y + Config.resources.barHeight/2 - textHeight/2 ) end local function drawResourceBars(resources, isOpponent) local margin = 20 local y = isOpponent and margin or love.graphics.getHeight() - margin - Config.resources.barHeight * 2 - Config.resources.spacing drawResourceBar(margin, y, resources.health, Config.resources.maxHealth, {0.8, 0.2, 0.2}) drawResourceBar(margin, y + Config.resources.barHeight + Config.resources.spacing, resources.mana, resources.maxMana, {0.2, 0.2, 0.8}) end
После того, как я определился со своими требованиями, анимации карт (поднятие/увеличение при наведении курсора, возврат в руку при отпускании) реализовать было не очень сложно.
-- обновляем состояние игры в каждом кадре ---@param dt - время с последнего обновления в секундах function love.update(dt) -- .. -- обновляем анимации карт for i = 1, #State.cards do local card = State.cards[i] if i == State.hoveredCard and not State.draggedCard then updateCardAnimation(card, Config.cards.hoverRise, Config.cards.hoverScale, dt) else updateCardAnimation(card, 0, 1, dt) end updateCardDrag(card, dt) end end -- выполняем lerp карты в сторону нужных значений подъёма и масштаба local function updateCardAnimation(card, targetRise, targetScale, dt) local speed = 10 card.currentRise = card.currentRise + (targetRise - card.currentRise) * dt * speed card.currentScale = card.currentScale + (targetScale - card.currentScale) * dt * speed end -- выполняем lerp вытащенных карт local function updateCardDrag(card, dt) if not State.draggedCard then local speed = 10 card.dragOffset.x = card.dragOffset.x + (0 - card.dragOffset.x) * dt * speed card.dragOffset.y = card.dragOffset.y + (0 - card.dragOffset.y) * dt * speed end end
Приведённый выше код анимирует карты, выполняя плавные переходы значений их подъёма/масштаба между целевыми значениями. Классический пример линейной интерполяции (lerping), при котором текущие значения постепенно движутся в сторону целевых значений в зависимости от прошедшего времени и множителя скорости.
Куда я буду двигаться дальше
После создания этих прототипов (а также нескольких других, о которых я здесь не рассказал) я вполне осознал, какие проекты будет продуктивно разрабатывать на LÖVE.
Также я поэкспериментировал с движком Godot, но пока не составил никаких заметок. Можно подвести краткий итог: если мне нужны возможности игрового движка (очень насыщенный мир, сложные взаимодействия между сущностями, углублённая физика), то я выберу Godot.
Мой нечёткий план проекта на 2025 год выглядит примерно так:
-
Создать дизайн игры в блокноте
-
Изготовить бумажную игру и поиграть в прототип с женой
-
Собрать базовый MVP (без графики)
-
Провести плейтестинг с друзьями
-
Выполнять итерации/дополнительный плейтестинг
-
Создать графику
-
???
-
Выпустить игру
Не думаю, что код моего прототипа будет особо полезен, но я всё равно выложил его в open source!
ссылка на оригинал статьи https://habr.com/ru/articles/872616/
Добавить комментарий