Love2D. Камера! Мотор! Поехали!
У Вас было такое состояние, когда нахлынувшее чувство полета/вдохновения не давало покоя, чесались руки от нетерпения начать творить, а в голове "всплывали" яркие эпизоды ранее сыгранных игр, из которых, словно пазл выстраивались сцены, которые бы хотелось воплотить в жизнь? Но все это так и оставалось где-то там, в потайных уголках мозга, со временем тускнея и исчезая. Что мешало? Долгое и муторное изучение технологий OpenGL, OpenAL, HLSL и не желание иметь дело с IDE? Все это казалось таким далеким и недосягаемым, что уже через несколько дней чтения документации весь пыл улетучивался и от одного желания что-то создавать оставался лишь клочок бумаги с эскизами и набросками.
Конечно, вот так просто, взять и сдаться, это первое, что хочется сделать, но это легкий путь и нам он не подходит - наш путь тернист и долог . Терпеливо и упорно я продолжал искать и надеяться, что все же найду легкий инструмент, который поможет мне в реализации, пусть не всех моих задумок, но большей их части уж точно. Цель была достигнута и об этом читайте ниже. Под катом много иллюстраций и букв.
Большая любовь
Не все так просто
Естественно, я не обещаю, что после прочтения данной статьи Вы начнете одну за другой штамповать игры, но надеюсь она придаст Вам уверенности и сподвигнет к игроделу. Запасайтесь терпением и чипсами.Из названия статьи, думаю, что многие догадались - речь пойдет о фреймворке Love2D, который выбран мной не случайно. Это действительно легкий и достаточно мощный инструмент, API которого хорошо документировано. Love2D имеет все для создания простых и сложных 2D (2.5D) игр. В его арсенале имеется следующее: SDL2, OpenGL, OpenAL, Lua / LuaJIT / LLVM-lua, DevIL с MNG и TIFF, FreeType, PhysicsFS, ModPlug (трекерные форматы!), mpg123, Vorbisfile. Нет необходимости специально устанавливать IDE - начать писать код можно прямо в блокноте(!), но все же предпочтительно если это будет Sublime Text или Notepad++ (подробности настройки читайте ниже).
Love2D является платформой для создания 2D игр на языке программирования Lua , синтаксис которого, на мой взгляд, несколько схож с Pascal и С++. Lua более гибок и универсален в плане использования переменных, метатаблиц и определения функций (возможно список следует расширить, но это первое, что бросилось мне в глаза), нежели языки указанные ранее. Поэтому, для меня, человека более или менее знающего Pascal такой выбор оказался правильным решением.
Меня очень прельщает тот факт, что Love2D использует Lua, т.к. этот язык программирования используется во многих играх, в качестве описания различных сценариев и логики поведения объектов, а также в реализации искусственного интелекта (AI). Достаточно распространенный и известный язык, чтобы просто обойти его стороной. Хочу отметить, что Love2D абсолютно бесплатен, и может использоваться как в бесплатных открытых проектах, так и в закрытых коммерческих.
Отмечу, что от программ-конструкторов я отказался сразу - данный подход к "игроделу" категорически не устраивал. Мне все же интересны способы генерации уровней, возможность контролировать объекты и отслеживать их поведение с помощью программного кода написанного собственноручно.
Подготовительные работы
Love2D доступен для трех основных платформ: Windows, Mac OS X и Linux (ссылки для скачивания см. ниже). Вышедший недавно порт движка Love2D, под названием Hove - значительно расширил этот список: Android, iOS, Flash и HTML5. Настройка рабочего инструмента более чем тривиальна. Существует несколько способов отладки / запуска игры. Первый, Вы создаете каталог с проектом, в котором располагаете все основные ресурсы используемые в игре. Чтобы запустить проект достаточно перенести папку с проектом на исполняемый файл Love.exe (играет роль интерпритатора). Второй способ наиболее удобен. На примере Notepad++, настройка данного редактора кода выглядит так: при помощи горячей клавиши F5 вызываем окно запуска, где в качестве "Пути для запуска" указываем следующую строку: "C:\Program Files\LOVE\love.exe" "$(CURRENT_DIRECTORY)". Дополнительно можно установить назначение горячих клавиш для новой команды.Для того чтобы приступить к кодингу, нужно усвоить несколько простых правил. В основе любого проекта Love2D должен быть создан файл Main.lua. В Love2D функции обратного вызова используются для выполнения различных задач, и все они необязательны, но для создания полнофункционального игрового процесса используются все. love.load - эта функция вызывается только один раз в начале игры и используется для загрузки ресурсов, инициализации переменных и определения конкретных настроек; draw() - с помощью нее происходит весь процесс "отрисовки" (непрерывная функция); love.quit() - вызывается перед закрытием, игра может сохранить свое состояние; love.update(dt) - вызывается непрерывно, где dt - является количеством секунд, прошедших с последнего раза. Безусловно, определяемые Вами функции могут быть прописаны и в других файлах, но чтобы их задействовать, в Main.lua нужно прописать подключение файла - require('camera'), и все его функции станут доступны.
Наконец, чтобы начать медленное погружение в дебри API, пожалуй, следует перейти к этапу, который Вы уже однозначно заждались - начнем писать код. В качестве примера рассмотрим загрузку игровых ресурсов (тайлов), генерации игрового поля, управление условной "камерой" и рассмотрим тонкости использования математических функций в Lua.
Программный код мне только снится
В первую очередь объявим необходимые переменные. Изначально я не стану присваивать им значения, это мы сделаем позже, но по-умолчанию они уже будут иметь пустое значение (nil), т.к. не будет первого явного присвоения. Сейчас я просто покажу как можно инициализировать переменные. Хочу заметить, что локальные переменная объявлены не в области своей видимости , поэтому у нас это внешние локальные переменные.local map -- хранилище тайлов local mapWidth, mapHeight -- ширина и высота тайлов local mapX, mapY -- координаты левого верхнего угла "камеры" local tilesDisplayWidth, tilesDisplayHeight -- размер окна карты local zoomX, zoomY local tilesetImage -- первоначальный контейнер для основного тайла local tileSize -- размер тайла в пикселях local tileQuads = {} -- массив содержащий области, которые будут вырезаны из файла с тайлами local tilesetSprite
Следующей функций подготовим карту, заполнив таблицу значениями от 0 до 3. Каждая цифра будет соответствовать определенному тайлу загруженному в память. Например, нулю будет соответствовать изображение стены или пола, единице - дверь и т.п. Размеры карты задаются количеством тайлов по вертикали и горизонтали, следовательно полный размер будет равен произведению размера тайла на соответствующее значение ширины / длины. В нашем случае размер тайла равен 32px. Надеюсь, что вопросов по заполнению таблицы не будет, ведь таблицы в Lua представляют собой единственный механизм структурирования данных - они могут использоваться как простые массивы, таблицы сиволов, множества, поля записей и деревья, т.е. нам предложена очень гибкая структура, которой и воспользуемся. Таблицу заполним следующим образом: в рамках первого цикла указываем, что элементы таблицы "map[x]" тоже являются таблицами, а вторым циклом произведем заполнение элементов "х" и "у" случайными цифрами, благодаря функции Lua - math.random(0,3).
function setupMap() mapWidth = 60 --1920px mapHeight = 40 --1280px map = {} for x = 1, mapWidth do map[x] = {} for y = 1, mapHeight do map[x][y] = math.random(0,3) end end end
Мы подошли к тому, что сейчас нужно провести процедуру присваивания, некоторым важным переменным стартовыми значениями, комментарии тут излишни. Единственное, хочу отметить, что "zoomX" и "zoomY" сработают как показатель кратности увеличения карты.
function setupMapView() mapX = 1 mapY = 1 tilesDisplayWidth = 26 tilesDisplayHeight = 20 zoomX = 1 zoomY = 1 end
Использование тайловой (плиточной) графики является самым распространенным методом создания больших изображений. Такой метод отлично подходит для карт, уровней и т.п. Иразцы или фрагменты одного размера составлены в одно изображение, поэтому чтобы использовать определенный фрагмент нужно как-бы "вырезать" необходимый участок. Небольшой экскурс по функциям. Функция love.graphics.newImage("tiles.png") загрузит в память файл с тайлами. Применим фильтр. Заметьте, что форма записи метода setFilter("nearest", "linear") осуществляется через двоеточие, и предлагает три параметра: масштабирование изображения снизу, масштабирование изображения сверху и число анизотропии (по-умолчанию равно 1). SetFilter необходимо использовать если будет проходить масштабирование изображения. Я Вам советую поиграть с параметрами при этом задействуйте масштабирование (zoomX, zoomY).
Следующая функция, love.graphics.newQuad(), создает текстурный прямоугольник. Здесь пожалуй комментарии излишни, единственное для облегчения нахождения координат верхнего левого угла использованы условные коэфициенты, которые облегчают процесс нахождения тайла в "матрице тайлов" загруженного изображения. Далее необходимо создать группу спрайтов, чтобы далее иметь возможность их вывода на экран.
function setupTileset() tilesetImage = love.graphics.newImage("tiles.png") -- это "линейный фильтр" удаляющий некоторые артефакты (сглаживание) tilesetImage:setFilter("nearest", "linear") tileSize = 32 tileSize = 32 -- тайл 3.0 tileQuads[0] = love.graphics.newQuad(3 * tileSize, 0 * tileSize, tileSize, tileSize, tilesetImage:getWidth(), tilesetImage:getHeight()) -- тайл 0.0 tileQuads[1] = love.graphics.newQuad(0 * tileSize, 0 * tileSize, tileSize, tileSize, tilesetImage:getWidth(), tilesetImage:getHeight()) -- тайл 4.0 tileQuads[2] = love.graphics.newQuad(4 * tileSize, 0 * tileSize, tileSize, tileSize, tilesetImage:getWidth(), tilesetImage:getHeight()) -- тайл 3.1 tileQuads[3] = love.graphics.newQuad(3 * tileSize, 1 * tileSize, tileSize, tileSize, tilesetImage:getWidth(), tilesetImage:getHeight()) tilesetBatch = love.graphics.newSpriteBatch(tilesetImage, tilesDisplayWidth * tilesDisplayHeight) updateTilesetBatch() end
Мы немного забыли про функцию "updateTilesetBatch()" в предыдущем коде. Ее название совершенно точно отражает свое содержание и назначение, дело в том, что функция выводит тайлы, которые находятся в области окна карты. Выполнение осуществляется следующим образом: очищается хранилище тайлов - tilesetBatch() и происходит его заполнение в соответствии с положением массива карты относительно области окна, в котором выводятся тайлы на экран. Если говорить еще проще, то происходит обновление спрайтов в соответствии с размерами окна, в котором выводятся тайлы. Замечу, что массив, содержащий значения тайлов, мы не меняем, поэтому карта у нас постоянна.
function updateTilesetBatch() tilesetBatch:clear() for x = 0, tilesDisplayWidth-1 do for y = 0, tilesDisplayHeight-1 do tilesetBatch:add(tileQuads[map[x + math.floor(mapX)][y + math.floor(mapY)]], x*tileSize, y*tileSize) end end end
Подготовим следующую функцию, которая поможет нам выполнить следующие действия: отследить перемещение в границах карты и произвести то самое движение камеры. Здесь мы сравниваем два значения: положение карты (mapX) и старое положение карты (oldMapY). К тому же функция принимает два параметра: dx, dy. Советую внимательно посмотреть, на то, как высчитываются переменные mapX и mapY. Например, разность mapWidth и tilesDisplayWidth устанавливает границы дальше которых игрок не выйдет, т.е. этим мы ограничиваем перемещение камеры.
-- центральная функция для перемещения карты function moveMap(dx, dy) oldMapX = mapX oldMapY = mapY mapX = math.max(math.min(mapX + dx, mapWidth - tilesDisplayWidth), 1) mapY = math.max(math.min(mapY + dy, mapHeight - tilesDisplayHeight), 1) -- обновляем только тогда, когда произошло перемещение if math.floor(mapX) ~= math.floor(oldMapX) or math.floor(mapY) ~= math.floor(oldMapY) then updateTilesetBatch() end end
Перемещение карты зависит от обработки нажатия клавиш. Здесь нам пригодится love.keyboard.isDown(key) . При нажатии клавиш каждый раз вызывается функция moveMap(), которой мы передаем параметры для движения, что позволит передвинуть камеру. Здесь, мы не используем function love.keypressed(key), в виду того, что результат будет совершенно другой. В нашем случае при нажатии клавиши будет проходить постоянное движение, а в случае применения "keypressed" требуется нажимать клавишу постоянно, чтобы двигаться.
function love.update(dt) if love.keyboard.isDown("up") then moveMap(0, -0.2 * tileSize * dt) end if love.keyboard.isDown("down") then moveMap(0, 0.2 * tileSize * dt) end if love.keyboard.isDown("left") then moveMap(-0.2 * tileSize * dt, 0) end if love.keyboard.isDown("right") then moveMap(0.2 * tileSize * dt, 0) end end
Что же, мы с Вами на финишной прямой. Теперь просто выведем наши заготовленные спрайты на экран, с помощью функции love.graphics.draw() . Хочу отметить, что здесь можно настроить передвижение, но в целом менять ничего не нужно. Хочу обратить Ваше внимание, что действие mapX%1 - позволит получить остаток от деления, а math.floor() - возвращает наибольшее целое число, меньшее или равное x (округление "вниз").
function love.draw() love.graphics.draw(tilesetBatch, math.floor(-zoomX * (mapX%1) * tileSize), math.floor(-zoomY * (mapY%1) * tileSize), 0, zoomX, zoomY) end
На этом, пожалуй и закончим. В следующих статьях мы, возможно, затронем тему генерации уровней, обязательно уделим внимание разнообразным методам создания карт с помощью таблиц, движению персонажа по игровому полю, его столкновению со стенами и т.д. Как всегда приветствуются Ваши отзывы, предложения и вопросы.
UPD: Забыл отметить, что я не являюсь автором сего кода. Этот программный код был взять на просторах сети, в каком-то файловом хранилище, автор которого мне неизвестен.
- jimmyjonezz's блог
- Добавить комментарий
- 4326 просмотров
Комментарии
2 комментария(ев)Дата: ВС, 23/02/2014 - 21:37
Давно видел этот движок. Хотел посмотреть поближе. Рад, что это произошло
Дата: ВС, 23/02/2014 - 23:39
жаль развивается медленно