Система сетчатого инвентаря в игре на GameMaker

В этом руководстве я объясню, как создать гибкую систему инвентаря-сетки для вашей игры, которая будет похожа на те, что есть в Deus Ex, S.T.A.L.K.E.R., Pathologic и др. Вы сможете с нуля написать эту систему пошагово, либо, если у вас уже есть игра, внедрить ее в свой код. Я постарался написать это руководство максимально подробно, так что в нем будут затронуты некоторые принципы работы самого движка, но даже если вы используете другой движок, статья все равно может быть вам полезна.

итоговый результат

Перед началом

Для старта понадобится объект игрока (я назвал его oPlayer) со спрайтом.

Также создадим объект контейнера (я назвал его oContainer). oContainer будет любым объектом окружения, который вы захотите наделить инвентарем. Это может быть мусорное ведро, ящик, рюкзак и т.д. Оставляем этот объект без спрайта по умолчанию: для каждого экземпляра, размещенного в комнате, можно будет настроить свой спрайт в меню слева при нажатии на экземпляр объекта.

для теста конкретного экземпляра я нарисовал некое подобие сундука
для теста конкретного экземпляра я нарисовал некое подобие сундука

Подготовим несколько спрайтов на тест для визуализации предметов инвентаря. В рамках данного руководства размер одной ячейки в сетке равен 32 на 32 пикселя, так что спрайты предметов размером 1 на 1 клетку будут иметь размер 32x32, предметов 1x2 — 32x64 и т.д. spItemError нужен на случай ошибки загрузки предмета.

Система сетчатого инвентаря в игре на GameMaker

spApple — 32x32;

spItemError — 32x32;

spMysteriousPackage — 64x64;

spWaterBottle — 32x64.

Еще нужна комната, где мы будем тестировать систему инвентаря, я назову ее rTest.

Создайте в комнате два слоя объектов (Instance Layer), назовите один “Instances”, а другой “UI”. На слое Instances мы будем размещать экземпляры объекта игрока и объекта контейнера, а на слое UI будем отрисовывать инвентарь.

В комнате на слое Instances нужно расставить экземпляры объектов игрока и контейнера (контейнеру добавьте какой-нибудь спрайт, как показано на скриншоте выше).

Инициализация глобальных переменных для работы с инвентарем

Нам понадобятся некоторые глобальные переменные для работы системы инвентаря. Безопасной практикой объявления глобальных переменных считается их объявление сразу после запуска игры. Есть несколько способов объявить глобальные переменные:

  • В отдельной “стартовой” комнате, в коде создания комнаты (Room Creation Code);
  • В отдельном объекте-менеджере, размещенном в самой первой комнате и самым первым в списке очереди создания экземпляров объектов;
  • В отдельном скрипте, который в свою очередь может быть вызван как в Room Creation Code, так и в событии Create объекта-менеджера.

Я рекомендую объединить первые два пункта: создать отдельную комнату (rInit), которая будет самой первой при запуске, и разместить туда объект-менеджер (oGameManager), но только без использования Room Creation Code. oGameManager должен быть Persistent-объектом, то есть он не должен уничтожаться при смене комнат, а должен существовать с момента создания и до закрытия игры.

порядок смены комнат
порядок смены комнат
свойства oGameManager
свойства oGameManager

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

Система сетчатого инвентаря в игре на GameMaker

В событии Create напишем следующее:

global.inventoryDebugMode = true; #macro InteractionDistance 100 global.ItemDB = {}; room_goto(rTest);
  • global.inventoryDebugMode — это переменная, при истинном значении которой у нас будет активен дебаг-режим. С помощью него мы будем отслеживать работоспособность системы инвентаря;

  • #macro InteractionDistance 100 — этой строкой мы создаем константу, которая описывает максимально возможное расстояние от игрока до объекта, у которого есть инвентарь, для взаимодействия с ним;

  • global.ItemDB — это список всех предметов, существующих в игре, представленный в виде структуры (struct), внутри которой будут лежать отдельные структуры, описывающие конкретные предметы;

  • room_goto(index) перебрасывает нас в нужную комнату.

Факт смены комнаты не помешает созданию следующих в очереди экземпляров (тех, что вы, возможно, позже разместите ниже oGameManager в меню, которое выделено на скриншоте), потому что функция room_goto(index) не сразу меняет комнату, а только по завершению всех событий текущего кадра игры. Например, сразу после oGameManager у меня стоит Persistent-объект oInputHandler, который управляет обработкой нажатий клавиш, и он успешно создается перед сменой комнаты.

json-файл

Создадим json-файл, в котором будут описаны предметы, которые мы будем загружать в память при запуске игры. Для теста я использую такой json-файл, в котором описаны 3 разных предмета.

Каждый предмет здесь представлен в виде структуры из 7 полей:

Name — имя предмета (не то же самое, что и id)
Type — тип предмета (снаряжение, еда, оружие и т.д.)
Width, Height — размеры предмета в ячейкахMaxStack - максимальное количество предметов в одном стаке
Sprite — название спрайта (который вы создали в самом GameMaker - spApple, spWaterBottle и т.д.) предмета
Description — описание предмета

Вы можете убрать часть полей или добавить новые.

Этот файл нужно добавить в папку datafiles вашего проекта. Ее можно быстро открыть прямо из GameMaker’а.

Система сетчатого инвентаря в игре на GameMaker

Скрипт scItemDatabase

Создаем скрипт scItemDatabase, он будет предназначен для работы с самой базой данных предметов. В нем объявим функцию loadItemDefinitions, которая будет инициализировать global.ItemDB, считывая данные о предметах из json-файла. Она будет выглядеть так:

function loadItemDefinitions() { var _filename = "items.json"; //проверка на наличие файла if (!file_exists(_filename) && global.inventoryDebugMode) { show_debug_message("Файл " + _filename + " не найден!"); return; } //читаем файл через буфер var _buffer = buffer_load(_filename); var _jsonString = buffer_read(_buffer, buffer_string); buffer_delete(_buffer); //парсим json в промежуточную структуру var _parsedData = json_parse(_jsonString); //получаем массив всех ключей из json (структуры предметов) var _itemIDs = struct_get_names(_parsedData); //проходимся по каждому предмету, чтобы преобразовать строки в ассеты for (var i = 0; i < array_length(_itemIDs); ++i) { var _id = _itemIDs[i]; //сюда записываем строковой идентификатор предмета var _item = _parsedData[$ _id]; //сюда — структуру этого предмета //преобразуем строку Sprite в индекс спрайта в движке if (variable_struct_exists(_item, "Sprite")) { _item.Sprite = asset_get_index(_item.Sprite); //если спрайт с таким именем не найден if (_item.Sprite == -1) { _item.Sprite = spItemError; //присваиваем спрайт, сигнализирующий об ошибке if (global.inventoryDebugMode) show_debug_message("Спрайт не найден для предмета " + _id); } } //сохраняем обработанный предмет в глобальную базу global.ItemDB[$ _id] = _item; } if (global.inventoryDebugMode) show_debug_message("База предметов загружена. Количество: " + string(array_length(_itemIDs))); }

Выражение global.ItemDB[$ _id] означает, что мы обращаемся к элементу этого списка с ключом _id (предмету с идентификатором _id). Выражение global.ItemDB[_id] в данном случае привело бы к ошибке при компиляции, потому что компилятор думал бы, что вы пытаетесь обратиться не к структуре, а к обычному массиву.

Иными словами, конструкция [$ _id] — это акцессор (accessor), который нужен для быстрого доступа к определенному элементу, но для разных структур данных есть свои акцессоры. Например, для ds_grid, которую мы чуть позже будем использовать, акцессор выглядит так: [# i, j].

Добавим в этот же скрипт следующую функцию:

function getItemData(_itemID) { //проверяем, существует ли предмет с таким текстовым ключом в структуре if (variable_struct_exists(global.ItemDB, _itemID)) return global.ItemDB[$ _itemID]; //возвращаем данные предмета if (global.inventoryDebugMode) show_debug_message("Предмета не существует: " + string(_itemID)); return undefined; }

Эта функция будет принимать идентификатор предмета и возвращать его структуру. Если мы передаем несуществующий идентификатор, функция вернет undefined.

В событии Create объекта oGameManager перед строкой room_goto(rTest); добавим следующую строчку:

loadItemDefinitions();

Она запустит функцию, которая проинициализирует global.ItemDB.

На этом работа с парсингом json-файла и инициализации global.ItemDB закончена. Следующим этапом будет создание самого инвентаря.

Общие принципы работы инвентаря

  • Инвентарями мы сможем наделять как объект игрока, так и другие объекты (сундук, мусорное ведро на улице, другой NPC и т.д.);
  • Так как инвентарь, который мы создаем, будет сетчатым, мы воспользуемся встроенной в GameMaker структурой данных ds_grid, которая, по сути, является двумерным массивом с некоторыми улучшениями для упрощения работы. Наш инвентарь и будет являться контейнером ds_grid, просто мы напишем дополнительные функции для работы с ним;
  • Предметы, размещаемые в сетке, могут иметь разные размеры (1x1, 1x2, 2x2 и т.д.), нам нужно это учитывать при добавлении, перемещении и удалении этих предметов. В структуре предмета есть поля, отвечающие за его размер (Width и Height);
  • В одной и той же ячейке может находиться несколько предметов одного типа, но не более, чем значение поля MaxStack структуры предмета.

Учитывая все перечисленное, составим примерное описание того, как будет выглядеть хранение предметов в инвентаре:

  • Левая верхняя (основная) ячейка предмета в инвентаре — структура, содержащая поля itemID и quantity. Обращаясь к какому-либо предмету в инвентаре, мы будем обращаться именно к основной ячейке этого предмета, тем самым получать его идентификатор и количество таких предметов в этой ячейке, а так как мы знаем идентификатор, то можем и узнать всю информацию об этом предмете через функцию getItemData(_itemID);
  • Все остальные ячейки предмета инвентаря, кроме основной — структуры со значениями refX и refY, которые являются координатами основной ячейки. Через побочные ячейки мы будем получать доступ к основной;
  • Если ячейка в сетке инвентаря ничем не занята, она будет принимать значение noone.
Система сетчатого инвентаря в игре на GameMaker

Скрипт scInventory

Создадим скрипт scInventory. В этом скрипте будут все функции для работы с сеткой инвентаря. На каждую функцию я оставил подробные комментарии. Если при чтении кода вы заметили не определенную функцию, значит, она определена позже в этом или другом скрипте.

Создаем инвентарь

//создание инвентаря размерами _width на _height //возвращает созданную сетку инвентаря function inventoryCreate(_width, _height) { var grid = ds_grid_create(_width, _height); ds_grid_clear(grid, noone); //инициализируем все ячейки значением 'noone' (пусто) return grid; }

Уничтожаем инвентарь

//уничтожение инвентаря //возвращает true в случае успешного удаления и false, если указанный инвентарь не существует function inventoryDestroy(_inventory) { //структуры, хранившиеся в сетке, будут удалены автоматически if (ds_exists(_inventory, ds_type_grid)) { ds_grid_destroy(_inventory); //просто уничтожаем саму сетку return true; } else { return false; } }

Проверяем, пуст ли инвентарь

//функция, проверяющая, пустой ли инвентарь //возвращает true или false function inventoryIsEmpty(_inventory) { for (var i = 0; i < ds_grid_height(_inventory); ++i) for (var j = 0; j < ds_grid_width(_inventory); ++j) if (ds_grid_get(_inventory, j, i) != noone) return false; return true; }

Функция, проверяющая возможность размещения предмета по указанным координатам. Аргументы _cellX и _cellY — координаты основной ячейки. Мы проверяем все ячейки, которые будет занимать предмет. Таким образом, если мы, например, размещаем предмет размером 2x2 в ячейке (3;4), то функция вернет true только в том случае, если ячейки (3;4), (4;4), (3;5) и (4;5) будут свободны

//можем ли разместить предмет //возвращает true, если можем разместить предмет в указанном месте и false, если нет //считается, что разместить предмет возможно, даже если он добавляется частично function inventoryCanPlaceAt(_inventory, _itemID, _quantity, _cellX, _cellY) { var itemSize = itemGetSize(_itemID); if (itemSize == undefined) return false; var gridW = ds_grid_width(_inventory); var gridH = ds_grid_height(_inventory); //проверка выхода за границы if (_cellX < 0 || _cellY < 0 || _cellX + itemSize.w > gridW || _cellY + itemSize.h > gridH) return false; //ПРОВЕРКА СТАКОВАНИЯ var targetCell = inventoryGetCellData(_inventory, _cellX, _cellY); if (targetCell != noone && targetCell != undefined) //ячейка занята предметом { var mainItemCell = targetCell; if (mainItemCell.itemID == _itemID) //этот предмет тот же самый, что и добавляемый { var maxStack = getItemData(_itemID).MaxStack; if (mainItemCell.quantity < maxStack) return true; //если в стаке есть место хотя бы для одной штуки, разрешаем } return false; //занято другим предметом или стак уже полон } //СТАКОВАТЬ НЕЛЬЗЯ, проверяем, все ли ячейки пустые в области размещения for (var xx = _cellX; xx < _cellX + itemSize.w; ++xx) for (var yy = _cellY; yy < _cellY + itemSize.h; ++yy) if (_inventory[# xx, yy] != noone) return false; return true; //можно разместить }

Функция, проверяющая наличие предмета в инвентаре в указанном количестве

//проверяет наличие предмета в инвентаре в указанном количестве //возвращает true, если в инвентаре есть этот предмет в нужно количестве //возвращает false, если предмета нет в нужном количестве или нет вообще function inventoryHasItem(_inventory, _itemID, _quantity) { var totalFound = 0; //счетчик: сколько всего найдено var gridW = ds_grid_width(_inventory); var gridH = ds_grid_height(_inventory); //проходимся по всем основным ячейкам, суммируем quantity каждого предмета в инвентаре //если набирается нужно количество, возвращаем true for (var yy = 0; yy < gridH; ++yy) { for (var xx = 0; xx < gridW; ++xx) { var cellData = _inventory[# xx, yy]; if (is_struct(cellData) && variable_struct_exists(cellData, "itemID")) //ячейка — основная { if (cellData.itemID == _itemID) //предмет тот, что мы ищем { totalFound += cellData.quantity; if (totalFound >= _quantity) return true; } } } } return false; }

Функция, ищущая место для размещения предмета

//ищет координаты для размещения предмета заданного размера //возвращает координаты { posX, posY }, если нашлось место; //возвращает { -1, -1 }, если места не нашлось function inventoryFindFreeSpace(_inventory, _itemID, _quantity) { var gridW = ds_grid_width(_inventory); var gridH = ds_grid_height(_inventory); var itemSize = itemGetSize(_itemID); for (var yy = 0; yy <= gridH - itemSize.h; ++yy) for (var xx = 0; xx <= gridW - itemSize.w; ++xx) if (inventoryCanPlaceAt(_inventory, _itemID, _quantity, xx, yy)) return { posX : xx, posY : yy }; return { posX : -1, posY : -1 }; }

Функция для расширения инвентаря

//функция расширяет инвентарь на столько клеток, чтобы можно было вместить предмет размером (_itemW, _itemH) function inventoryExpand(_inventory, _itemW, _itemH) { var oldW = ds_grid_width(_inventory); var oldH = ds_grid_height(_inventory); //сохраняем квадратную форму инвентаря var newW = oldW, newH = oldH; if (oldW > oldH) newH = oldH + _itemH; else newW = oldW + _itemW; ds_grid_resize(_inventory, newW, newH); //ds_grid_resize инициализирует новые ячейки нулем, переписываем на noone for (var _x = 0; _x < newW; ++_x) for (var _y = 0; _y < newH; ++_y) if (_x >= oldW || _y >= oldH) //старые ячейки не трогаем _inventory[# _x, _y] = noone; }

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

//находим основную ячейку предмета по указанным координатам //возвращает: //undefined, если вышли за пределы сетки; //noone, если ячейка пустая; //структуру, содержащую координаты основной ячейки, если по указанным координатам есть предмет function inventoryGetItemMainCellCoords(_inventory, _cellX, _cellY) { //находим размеры сетки в ячейках var gridW = ds_grid_width(_inventory); var gridH = ds_grid_height(_inventory); //проверяем, не вышли ли мы за пределы сетки if (_cellX < 0 || _cellY < 0 || _cellX >= gridW || _cellY >= gridH) return undefined; //смотрим содержимое ячейки var cellData = _inventory[# _cellX, _cellY]; //проверяем, есть ли что-то в ячейке if (cellData == noone) return noone; //ячейка пуста var mainX_, mainY_; //проверяем, является ли содержимое ячейки ссылкой на основную ячейку (структурой с ключами "refX" и "refY") if (variable_struct_exists(cellData, "refX") && variable_struct_exists(cellData, "refY")) { //находим координаты основной ячейки через побочную ячейку mainX_ = cellData.refX; mainY_ = cellData.refY; } else { //содержимое — и есть основная ячейка mainX_ = _cellX; mainY_ = _cellY; } return { mainX: mainX_, mainY: mainY_ } }

Функция, возвращающая данные ячейки

//получаем данные ячейки (_cellX;_cellY) //возвращает: //undefined, если вышли за пределы сетки; //noone, если ячейка пустая; //основную ячейку предмета (структуру, содержащую поля itemID и quantity), если по указанным координатам есть предмет function inventoryGetCellData(_inventory, _cellX, _cellY) { //пытаемся найти предмет по указанным координатам var itemMainCellCoords = inventoryGetItemMainCellCoords(_inventory, _cellX, _cellY); var cellData; //если нажали на пустую ячейку или вышли за пределы сетки if (itemMainCellCoords == noone || itemMainCellCoords == undefined) { cellData = itemMainCellCoords; } //данные ячейки — предмет else { var mainX = itemMainCellCoords.mainX; var mainY = itemMainCellCoords.mainY; cellData = _inventory[# mainX, mainY]; } return cellData; //возвращаем данные ячейки }

Функция, добавляющая предмет в инвентарь по указанным координатам. Здесь прописана логика как для стакования, так и для добавления предмета в пустую ячейку

//добавляем предмет в инвентарь по конкретным координатам //возвращает число (остаток), которое не удалось положить (0 — если влезло всё) function inventoryAddItemTo(_inventory, _itemID, _quantity, _cellX, _cellY) { //не можем разместить if (!inventoryCanPlaceAt(_inventory, _itemID, _quantity, _cellX, _cellY)) return _quantity; //говорим, что не смогли разместить ничего var maxStack = getItemData(_itemID).MaxStack; //стакуем var targetCellData = inventoryGetCellData(_inventory, _cellX, _cellY); if (is_struct(targetCellData)) { if (targetCellData.itemID == _itemID) //предмет тот же { var spaceAvailable = maxStack - targetCellData.quantity; var amountToAdd = min(_quantity, spaceAvailable); targetCellData.quantity += amountToAdd; return _quantity - amountToAdd; //возвращаем число предметов, которое не влезло } } //если не стакуем, то кладем предмет в пустую область else { var itemSize = itemGetSize(_itemID); //размеры предмета var amountToAdd = min(_quantity, maxStack); //не кладем больше значения MaxStack _inventory[# _cellX, _cellY] = { itemID : _itemID, quantity : amountToAdd }; for (var xx = _cellX; xx < _cellX + itemSize.w; ++xx) { for (var yy = _cellY; yy < _cellY + itemSize.h; ++yy) { if (xx == _cellX && yy == _cellY) //пропускаем основную ячейку continue; _inventory[# xx, yy] = //остальные ячейки — ссылки на основную { refX: _cellX, refY: _cellY }; } } return _quantity - amountToAdd; //возвращаем число предметов, которое не влезло } }

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

//добавляем предмет в инвентарь в свободное место function inventoryAddItem(_inventory, _itemID, _quantity) { var remainder = _quantity; var maxStack = getItemData(_itemID).MaxStack; var gridW = ds_grid_width(_inventory); var gridH = ds_grid_height(_inventory); //сначала дозаполняем существующие неполные стаки for (var yy = 0; yy < gridH; ++yy) { for (var xx = 0; xx < gridW; ++xx) { var cellData = inventoryGetCellData(_inventory, xx, yy); if (cellData == noone || cellData == undefined) continue; if (!is_struct(cellData) || !variable_struct_exists(cellData, "itemID")) continue; if (cellData.itemID == _itemID && cellData.quantity < maxStack) { remainder = inventoryAddItemTo(_inventory, _itemID, remainder, xx, yy); if (remainder <= 0) return true; } } } //раскидываем остаток по новым пустым ячейкам while (remainder > 0) { var freeSpace = inventoryFindFreeSpace(_inventory, _itemID, remainder); if (freeSpace.posX != -1 && freeSpace.posY != -1) remainder = inventoryAddItemTo(_inventory, _itemID, remainder, freeSpace.posX, freeSpace.posY); else break; //места в инвентаре больше нет } //выбрасываем остатки if (remainder > 0) { itemDrop(_itemID, remainder); return false; } return true; }

Функция, принимающая координаты ячейки и удаляющая предмет в этой ячейке через основную. Здесь мы проходимся по всем ячейкам этого предмета и присваиваем им noone, что в дальнейшем будет сигнализировать о том, что ячейки пустые

//убираем предмет в ячейке (_cellX;_cellY) //возвращает: //false, если передали координаты пустой ячейки или вышли за пределы сетки; //основную ячейку удаленного предмета, если предмет успешно удален function inventoryRemoveItemAt(_inventory, _cellX, _cellY) { //ищем основную ячейку этого предмета var itemMainCellCoords = inventoryGetItemMainCellCoords(_inventory, _cellX, _cellY); //если нажали на пустую ячейку или вышли за пределы сетки if (itemMainCellCoords == noone || itemMainCellCoords == undefined) return false; //нашли координаты основной ячейки var mainX = itemMainCellCoords.mainX; var mainY = itemMainCellCoords.mainY; var cellData = _inventory[# mainX, mainY]; //содержимое ячейки var itemSize = itemGetSize(cellData.itemID); //получаем размеры предмета для очистки всех ячеек //очищаем все ячейки, занятые этим предметом (ставим noone) for (var xx = mainX; xx < mainX + itemSize.w; ++xx) for (var yy = mainY; yy < mainY + itemSize.h; ++yy) _inventory[# xx, yy] = noone; return cellData; //возвращаем данные удаленного предмета }

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

//удаляем предмет из инвентаря в указанном количестве function inventoryRemoveItem(_inventory, _itemID, _quantity) { //проверяем, есть ли нужное количество if (!inventoryHasItem(_inventory, _itemID, _quantity)) return false; var remainder = _quantity; var gridW = ds_grid_width(_inventory); var gridH = ds_grid_height(_inventory); for (var yy = 0; yy < gridH; ++yy) { for (var xx = 0; xx < gridW; ++xx) { var cellData = inventoryGetCellData(_inventory, xx, yy); if (cellData == undefined || cellData == noone) continue; if (cellData.itemID == _itemID) { if (cellData.quantity <= remainder) { //удаляем стак полностью (функция очистит ячейку и все связанные с ней) remainder -= cellData.quantity; inventoryRemoveItemAt(_inventory, xx, yy); } else { //отнимаем часть от стака cellData.quantity -= remainder; remainder = 0; } if (remainder == 0) return true; //успешно удалили все } } } }

Наш инвентарь будет отображаться как элемент интерфейса в виде сетки в левом верхнем (или любом другом) углу, поэтому нам понадобится функция, переводящая экранные координаты (в пикселях) в координаты сетки (в ячейках), чтобы мышью управлять инвентарем: при наведении мыши на инвентарь мы будем получить координаты той ячейки, над которой она “висит”. Самой обработки движений и нажатий мыши здесь еще нет, ее логика будет прописана в другом месте. Функция принимает в качестве аргументов сам инвентарь, координаты точки в пикселях, координаты начала инвентаря и размер ячейки. Так как функция учитывает расположение сетки инвентаря, саму сетку мы сможем размещать как угодно (об отрисовке сетки позже), и нам не нужно будет менять или дополнять код в этой функции

//переводим экранные координаты (в пикселях) в координаты сетки (в ячейках) //возвращает: //undefined, если рассматриваемые координаты за пределами области сетки инвентаря; //структуру, хранящую координаты ячейки, которым соответствуют экранные координаты, если координаты точки расположены внутри сетки //(например, при наведении мыши на инвентарь мы получаем координаты той ячейки, над которой "висит" мышь) function inventoryScreenToGridCoords(_inventory, _screenX, _screenY, _gridStartX, _gridStartY, _cellSize) { //находим размеры сетки инвентаря (в ячейках) var gridW = ds_grid_width(_inventory); var gridH = ds_grid_height(_inventory); //находим размеры сетки инвентаря (в пикселях) var totalW = gridW * _cellSize; var totalH = gridH * _cellSize; //если рассматриваемые координаты за пределами области сетки инвентаря if (_screenX < _gridStartX || _screenX >= _gridStartX + totalW || _screenY < _gridStartY || _screenY >= _gridStartY + totalH) return undefined; //вычисляем координаты в ячейках var cellX_ = floor((_screenX - _gridStartX) / _cellSize); var cellY_ = floor((_screenY - _gridStartY) / _cellSize); return { posX : cellX_, posY : cellY_ }; }

Функция для отрисовки указанного инвентаря по указанным координатам

//ф-я для отрисовки указанного инвентаря function inventoryDraw(_inventory, _startX, _startY) { var gridW = ds_grid_width(_inventory); var gridH = ds_grid_height(_inventory); //рисуем фон сетки for (var xx = 0; xx < gridW; ++xx) { for (var yy = 0; yy < gridH; ++yy) { var drawX = _startX + xx * cellSize; var drawY = _startY + yy * cellSize; draw_set_color(c_black); draw_rectangle(drawX, drawY, drawX + cellSize, drawY + cellSize, false); draw_set_color(c_dkgray); draw_rectangle(drawX, drawY, drawX + cellSize, drawY + cellSize, true); } } //отрисовка всех предметов в сетке for (var xx = 0; xx < gridW; ++xx) { for (var yy = 0; yy < gridH; ++yy) { var cellData = _inventory[# xx, yy]; //рисуем, только если это структура с ключом "itemID" if (is_struct(cellData) && variable_struct_exists(cellData, "itemID")) { var itemID = cellData.itemID; var quantity = cellData.quantity; var itemData = getItemData(itemID); var sprite = itemData.Sprite; var itemWidth = itemData.Width; var itemHeight = itemData.Height; var drawX = _startX + xx * cellSize; var drawY = _startY + yy * cellSize; var drawW = itemWidth * cellSize; var drawH = itemHeight * cellSize; //рисуем спрайт draw_set_alpha(1); if (sprite_exists(sprite)) draw_sprite(sprite, 0, drawX, drawY); else draw_sprite(spItemError, 0, drawX, drawY); //рисуем количество if (itemData.MaxStack > 1 && quantity > 1) { draw_set_color(c_white); draw_set_halign(fa_right); draw_set_valign(fa_bottom); draw_text_color(drawX + drawW + 1, drawY + drawH + 1, string(quantity), c_black, c_black, c_black, c_black, 1); //тень draw_text(drawX + drawW, drawY + drawH, string(quantity)); } } } } }

Скрипт scItem

Функция для выбрасывания предмета. Предмет выбрасываем в мешок oBag. Если мешок уже есть рядом с игроком, выбрасываем в него. Если мешка нет, создаем его

объект oBag (мешок) пока не определен, мы создадим его чуть позже, пока просто представьте, что он существует

function itemDrop(_itemID, _quantity) { var spawnX = oPlayer.x; var spawnY = oPlayer.y; var itemSize = itemGetSize(_itemID); var remainder = _quantity; //ищем мешок в радиусе InteractionDistance var nearestBag = instance_nearest(spawnX, spawnY, oBag); var targetBag = noone; if (nearestBag != noone && point_distance(spawnX, spawnY, nearestBag.x, nearestBag.y) <= InteractionDistance) targetBag = nearestBag; else targetBag = instance_create_layer(spawnX, spawnY, "Instances", oBag, { inventory : inventoryCreate(itemSize.w, itemSize.h) }); while (remainder > 0) { var bagFreeSpace = inventoryFindFreeSpace(targetBag.inventory, _itemID, remainder); if (bagFreeSpace.posX != -1 && bagFreeSpace.posY != -1) remainder = inventoryAddItemTo(targetBag.inventory, _itemID, remainder, bagFreeSpace.posX, bagFreeSpace.posY); else //в мешке нет места: увеличиваем инвентарь мешка var oldSize = inventoryExpand(targetBag.inventory, itemSize.w, itemSize.h); } }

Функция, возвращающая размеры предмета

//получаем размеры предмета в клетках (ячейках) //возвращает структуру, описывающую размеры предмета или undefined, если указанного предмета не существует function itemGetSize(_itemID) { var itemData = getItemData(_itemID); //ищем предмет в ItemDB if (itemData != undefined) //нашли предмет в ItemDB return { w : itemData.Width, h : itemData.Height }; else //не нашли (предмета нет) return undefined; }

Создание инвентарей и добавление предметов в них

Теперь мы можем добавлять инвентари объектам. В событии Create объекта игрока напишите следующую строчку:

inventory = inventoryCreate(_width, _height);

Замените _width и _height на значения, соответствующие желаемым размерам инвентаря.

И обязательно в событии Clean Up необходимо добавить эту строку:

inventoryDestroy(inventory);

Без этой строки при уничтожении экземпляра объекта сетка продолжит существовать, а так как переменная, ссылающаяся на нее (inventory), будет удалена вместе с экземпляром объекта, получить доступ к этой сетке вы больше не сможете, и это приведет к утечке памяти.

Для oContainer объявим инвентарь другим способом — в разделе Variable Definitions. Также добавим булеву переменную isPersistent, по умолчанию равной True — она будет нужна для проверки, надо ли удалять контейнер, когда он опустошается. Чуть позже мы напишем код, который будет автоматически удалять из комнаты все пустые контейнеры, у которых isPersistent равен False.

default-значение для inventory можно написать любым, я решил сделать значением по умолчанию инвентарь размером 1x1
default-значение для inventory можно написать любым, я решил сделать значением по умолчанию инвентарь размером 1x1

И, точно так же, как и для объекта игрока, для oContainer создайте событие Clean Up и в ней напишите функцию удаления инвентаря:

inventoryDestroy(inventory);

Для каждого отдельного экземпляра объекта контейнера в комнате вы можете переопределить переменную inventory, определенную в Variable Definitions, то есть изменить размер инвентаря. Я сделаю инвентарь контейнеру размером 7x3.

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

Создадим объект oBag. Это тот самый мешок, в который будут выбрасываться предметы, которые не влезли в инвентарь при попытке добавления. Нужно установить ему родителя oContainer, чтобы он унаследовал его переменные. Добавьте ему небольшой спрайт, чтобы его было видно. В окне Variable Definitions установите переменную isPersistent равной False, чтобы когда игрок забирал все предметы из этого мешка, мешок сам уничтожался (код для этого напишем чуть позже).

Система сетчатого инвентаря в игре на GameMaker

Для добавления предметов в инвентарь, необходимо использовать ранее написанную функцию inventoryAddItemTo(_inventoryGrid, _itemID, _quantity, _cellX, _cellY) или inventoryAddItem(_inventoryGrid, _itemID, _quantity).

Для инвентаря объекта игрока я добавлю в событии Create следующие строки:

inventory = inventoryCreate(7, 5); inventoryAddItem(inventory, "water_bottle", 1); inventoryAddItemTo(inventory, "apple", 1, 3, 3); inventoryAddItemTo(inventory, "mysterious_package", 1, 3, 0); inventoryAddItemTo(inventory, "apple", 2, 4, 4); inventoryAddItemTo(inventory, "apple", 3, 5, 4);
чуть позже, когда мы напишем код для отрисовки сетки, код выше приведет к такому результату
чуть позже, когда мы напишем код для отрисовки сетки, код выше приведет к такому результату

Также создадим инвентарь для сундука и добавим туда несколько предметов

Система сетчатого инвентаря в игре на GameMaker
inventoryAddItem(inventory, "apple", 2); inventoryAddItemTo(inventory, "water_bottle", 2, 1, 1);
сундук
сундук

Менеджер инвентаря

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

Создаем oInventoryManager. НЕ делаем его Persistent: он будет существовать только когда мы открываем инвентарь, а в момент закрытия он будет удаляться. В событии Create напишем такой код:

//ссылка на инвентарь игрока invPlayer = noone; //ссылка на объект, с которым взаимодействуем, у которого есть инвентарь invObjectOther = noone; //инвентарь и ячейка, над которыми "висит" курсор hoverInv = noone; hoverCell = undefined; //размер ячейки cellSize = 32; //позиции для отрисовки инвентарей на экране (левые верхние точки инвентарей) playerInvX = 50; playerInvY = 50; otherInvX = 50; otherInvY = 250; //ДЛЯ ПЕРЕТАСКИВАНИЯ //перетаскиваемый предмет dragged = noone; isDragging = function() { if (dragged != noone) return true; return false; } //исходный инвентарь перетаскиваемого предмета draggedItemOriginalInv = noone; //исходная позиция перетаскиваемого предмета draggedItemOriginalX = 0; draggedItemOriginalY = 0;

Теперь в событии Destroy напишем следующее:

//если закрыли инвентарь в момент перетаскивания, перетаскиваемый предмет возвращаем в то же место, откуда взяли его if (isDragging()) { var inv = draggedItemOriginalInv; var itemID = draggedItem.itemID; var quantity = draggedItem.quantity; var targetX = draggedItemOriginalX; var targetY = draggedItemOriginalY; inventoryAddItemTo(inv, itemID, quantity, targetX, targetY); } if (invObjectOther != noone) { //если контейнер временный (например, мешочек), удаляем его при условии, что в нем нет никаких предметов if (!invObjectOther.isPersistent) { if (inventoryIsEmpty(invObjectOther.inventory)) { inventoryDestroy(invObjectOther.inventory); instance_destroy(invObjectOther); } } }

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

Теперь нам понадобятся функции, которые будут отвечать за создание/уничтожение oInventoryManager. Эти функции мы не будем писать в самом oInventoryManager, потому что в таком случае мы не сможем вызвать функцию создания этого объекта (его ведь еще не существует.), так что создадим скрипт scInventoryManager и напишем там следующее:

function inventoryManagerCreate(_invObjectOther = noone) { if (instance_exists(oInventoryManager)) { inventoryManagerDestroy(); return; } //создаем менеджер var _manager = instance_create_layer(0, 0, "UI", oInventoryManager); _manager.invPlayer = oPlayer.inventory; //если передали второй инвентарь — подключаем его if (_invObjectOther != noone) _manager.invObjectOther = _invObjectOther; } function inventoryManagerDestroy() { if (instance_exists(oInventoryManager)) instance_destroy(oInventoryManager); }

Визуальное отображение и обработка нажатий инвентаря

Визуальное отображение инвентаря мы будем реализовывать в событии Draw GUI менеджера инвентаря oInventoryManager, а обработку нажатий — в событии Step.

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

Система сетчатого инвентаря в игре на GameMaker

Функцию для отрисовки сетки инвентаря и предметов, находящихся в ней, мы написали ранее в скрипте scInventory (inventoryDraw(_inventory, _startX, _startY)). Эта функция рисует как саму сетку, так и все предметы, находящиеся в инвентаре.

Нам необходимо вызывать эту функцию дважды: для инвентаря игрока и инвентаря другого объекта, с которым взаимодействуем (если он существует).

Также нужно отрисовать перетаскиваемый предмет.

В конце добавим код для дебага: рядом с курсором будут отображаться значения некоторых переменных для теста.

Вот, что получится:

draw_set_font(fArial); //ОТРИСОВЫВАЕМ ИНВЕНТАРИ: ИГРОКА И ДРУГОЙ, ЕСЛИ ОН ЕСТЬ //отрисовка для инвентаря игрока if (invPlayer != noone) { inventoryDraw(invPlayer, playerInvX, playerInvY); //пишем название инвентаря сверху от сетки draw_set_color(c_white); draw_set_halign(fa_left); draw_set_valign(fa_bottom); draw_text(playerInvX, playerInvY - 5, "Player"); } //отрисовка для другого инвентаря (если он есть) if (invObjectOther != noone) { inventoryDraw(invObjectOther.inventory, otherInvX, otherInvY); //пишем название инвентаря сверху от сетки draw_set_color(c_white); draw_set_halign(fa_left); draw_set_valign(fa_bottom); draw_text(otherInvX, otherInvY - 5, "Other"); } //ОТРИСОВКА ПЕРЕТАСКИВАЕМОГО ПРЕДМЕТА //рисуем проекцию перетаскиваемого предмета //(белые квадраты будут сигнализировать о возможности расположить предмет) if (isDragging() && hoverInv != noone) { if (inventoryCanPlaceAt(hoverInv, dragged.itemID, dragged.quantity, hoverCell.posX, hoverCell.posY)) { var draggedItemData = getItemData(dragged.itemID); var invX, invY; if (hoverInv == invPlayer) { invX = playerInvX; invY = playerInvY; } else { invX = otherInvX; invY = otherInvY; } var itemToAttachCoords = inventoryGetItemMainCellCoords(hoverInv, hoverCell.posX, hoverCell.posY); var xx, yy; if (itemToAttachCoords == undefined || itemToAttachCoords == noone) { xx = hoverCell.posX; yy = hoverCell.posY; } else { xx = itemToAttachCoords.mainX; yy = itemToAttachCoords.mainY; } for (var i = xx; i < xx + draggedItemData.Width; ++i) { for (var j = yy; j < yy + draggedItemData.Height; ++j) { var drawX = invX + i * cellSize; var drawY = invY + j * cellSize; draw_set_alpha(0.5); draw_set_color(c_white); draw_rectangle(drawX, drawY, drawX + cellSize - 1, drawY + cellSize - 1, false); } } } } //отрисовка перетаскиваемого предмета if (isDragging()) { var itemID = dragged.itemID; var quantity = dragged.quantity; var itemData = getItemData(itemID); var drawX = device_mouse_x_to_gui(0); var drawY = device_mouse_y_to_gui(0); //рисуем спрайт draw_set_alpha(0.5); draw_set_halign(fa_left); draw_set_valign(fa_top); draw_sprite(itemData.Sprite, 0, drawX, drawY); //рисуем кол-во, если больше одного предмета if (itemData.MaxStack > 1 && quantity > 1) { var textX = drawX + itemData.Width*cellSize; var textY = drawY + itemData.Height*cellSize; draw_set_alpha(1); draw_set_halign(fa_right); draw_set_valign(fa_bottom); draw_set_color(c_white); draw_text_color(textX + 1, textY + 1, string(quantity), c_black, c_black, c_black, c_black, 1); //тень draw_text(textX, textY, string(quantity)); } } draw_set_halign(fa_left); draw_set_valign(fa_top); draw_set_color(c_white); draw_set_alpha(1); //ДЕБАГ if (global.inventoryDebugMode) { draw_text(mouse_x+20, mouse_y+20, string_concat("mouseX: ", device_mouse_x_to_gui(0), " mouseY: ", device_mouse_y_to_gui(0))); if (hoverInv == noone) draw_text(mouse_x+20, mouse_y+20+20, "hoverInv: noone"); else if (hoverInv == invPlayer) draw_text(mouse_x+20, mouse_y+20+20, "hoverInv: invPlayer"); else if (hoverInv == invObjectOther.inventory) draw_text(mouse_x+20, mouse_y+20+20, "hoverInv: invObjectOther.inventory"); if (hoverCell == undefined) draw_text(mouse_x+20, mouse_y+40+20, "hoverCell: undefined"); else draw_text(mouse_x+20, mouse_y+40+20, string_concat("hoverCell: [", hoverCell.posX, "; ", hoverCell.posY, "]")); draw_text(mouse_x+20, mouse_y+60+20, string_concat("isDragging: ", isDragging())); }

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

В событии Step все того же oInventoryManager напишем такие строки:

//получаем координаты мыши в GUI var mouseX = device_mouse_x_to_gui(0); var mouseY = device_mouse_y_to_gui(0); //определяем, над какой сеткой и ячейкой висит курсор hoverInv = noone; hoverCell = undefined; //проверяем инвентарь игрока if (invPlayer != noone) { hoverCell = inventoryScreenToGridCoords(invPlayer, mouseX, mouseY, playerInvX, playerInvY, cellSize); if (hoverCell != undefined) hoverInv = invPlayer; } //если не над инвентарем игрока, то проверяем другой (если он есть) if (hoverInv == noone && invObjectOther != noone) { hoverCell = inventoryScreenToGridCoords(invObjectOther.inventory, mouseX, mouseY, otherInvX, otherInvY, cellSize); if (hoverCell != undefined) hoverInv = invObjectOther.inventory; } //если курсор не висит над каким-либо инвентарем, останавливаем выполнение события if (hoverInv == noone) return; //ЛОГИКА ПЕРЕТАСКИВАНИЯ //начало перетаскивания (нажатие левой кнопки мыши по предмету) if (mouse_check_button_pressed(mb_left) && !isDragging()) { //координаты ячейки, куда нажали var cellX = hoverCell.posX; var cellY = hoverCell.posY; var cellData = inventoryGetCellData(hoverInv, cellX, cellY); if (cellData == noone || cellData == undefined) //останавливаем событие, если предмета в этой ячейке нет return; cellData = inventoryRemoveItemAt(hoverInv, cellX, cellY); //убираем предмет из сетки if (cellData == false) //останавливаем событие, если не удалось удалить предмет return; //запоминаем перетаскиваемый предмет, инвентарь, откуда он был взят и его исходную позицию dragged = cellData; draggedOriginalInv = hoverInv; draggedOriginalX = cellX; draggedOriginalY = cellY; } //конец перетаскивания (нажатие лкм при наличии перетаскиваемого предмета) else if (mouse_check_button_pressed(mb_left) && isDragging()) { var cellX = hoverCell.posX; var cellY = hoverCell.posY; var cellData = inventoryGetCellData(hoverInv, cellX, cellY); if (cellData == undefined) return; //пытаемся положить предмет //inventoryAddItemTo сама все посчитает и вернет остаток var remainder = inventoryAddItemTo(hoverInv, dragged.itemID, dragged.quantity, cellX, cellY); if (remainder < dragged.quantity) { //если мы успешно положили хотя бы часть, обновляем количество в руке dragged.quantity = remainder; //если в руке ничего не осталось — завершаем перетаскивание if (dragged.quantity <= 0) dragged = noone; } }

Здесь мы сначала смотрим на расположение курсора и ищем ячейку, над которой “висит” курсор, используя ранее написанную функцию для перевода экранных координат в координаты инвентаря, после чего определяем инвентарь, над которым “висит” курсор.

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

Далее пишем саму логику перетаскивания. Оно осуществляется с помощью нажатий левой кнопки мыши.

Последний штрих

Предполагаю, что передвижение игрока вы уже реализовали сами. Тогда последний штрих перед запуском — обработка нажатия клавиши открытия инвентаря. В событии Step объекта oPlayer напишем:

if (keyboard_check_pressed(ord("I"))) { if (!instance_exists(oInventoryManager)) { //ищем контейнер рядом var _container = instance_nearest(x, y, oContainer); var _otherInv = noone; if (_container != noone && point_distance(x, y, _container.x, _container.y) < InteractionDistance) _otherInv = _container; //открываем инвентарь (одинарный или двойной) inventoryManagerCreate(_otherInv); } else { inventoryManagerDestroy(); } }

При нажатии на “I” мы будем проверять наличие менеджера инвентаря. Если он не создан, то создаем его. Если он уже был создан, уничтожаем его.

После создания ищем контейнер рядом с нами. Если не находим, то просто открываем инвентарь.

Итог

Запускаем проект и видим результат.

Теперь давайте проверим, как работает oBag. Создадим ситуацию, при которой происходит попытка добавления предмета в переполненный инвентарь игрока. Для этого добавим следующие строки в Step Event объекта oPlayer:

while (inventoryFindFreeSpace(inventory, "mysterious_package", 1).posX != -1) inventoryAddItem(inventory, "mysterious_package", 1); inventoryAddItem(inventory, "mysterious_package", 1); inventoryAddItem(inventory, "mysterious_package", 1);

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

Система сетчатого инвентаря в игре на GameMaker

Может быть проблема, когда все спрайты предметов в инвентарях становятся равными spItemError. Это происходит потому, что спрайты spApple, spWaterBottle и spMysteriousPackage явно не упоминаются в самом коде игры (на этапе компиляции), в результате чего движок думает, что эти спрайты вообще не используются, и удаляет их из игры для оптимизации. Решается это следующим образом:

Система сетчатого инвентаря в игре на GameMaker

Проект

Скачать исходники проекта можно здесь, он создан на версии v2024.14.3.260 (Steam).

19
2 комментария