Предисловие

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

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

Такой скрижалью хочу поделиться и я, на примере простой системы инвентаря.

Реализация будет на версии Godot 4.x.x. Будет проще, если есть базовые знания работы c Нодами, Сценами и Синглтонами.

Репозиторий с исходниками вот тут

Пункты

  1. Импортируем данные предметов из JSON

  2. Инвентарь для хранения предметов игрока

  3. Строим UI инвентаря

  4. Захватываем input игрока

  5. Прикручиваем Pick and Drop с помощью мышки

  6. Stack and Split предметов

  7. Отображаем всплывающую подсказку на предмете

Импорт данных из JSON

Для хранения данных о предметах я буду использовать формат JSON'a, хранить, модифицировать и переносить его проще. Есть много вариантов, где мы можем хранить информацию о предметах, это может быть словарь или обычный двумерный массив.

{
  "orange": {
    "name": "Апельсин",
    "description": "Сочный вкусный желтый апельсин!",
    "icon": "orange_icon.png",
    "stackable": true,
    "quantity": 1,
  },
  "potion": {
    "name": "Зелье здоровья",
    "description": "Прибавляет +5 к здоровью",
    "icon": "health_icon.png",
    "stackable": true,
    "quantity": 1,
  },
  "sword": {
    "name": "Меч",
    "description": "Старый ржавый меч",
    "icon": "sword_icon.png",
    "stackable": false,
  }
}

Создаём небольшой список предметов и назовём его items.json, в моём случае это — «orange», «potion» и «sword». Главное здесь это:

  • Stackable — True/False если предмет будет stackable

  • Quantity — Количество предметов и использований доступных нам

Теперь у нас есть файл, с перечислением доступных нам предметов, необходимо их перенести для использования и хранения. Для начала создаём скрипт Global.gd и папку scripts , где мы будем хранить наши скрипты.

extends Node

var items

func _ready():
  items = read_from_JSON("res://data/items.json")
  for key in items.keys():
    items[key]["key"] = key

func read_from_JSON(path: StringName):
  var file
  var data
  if (FileAccess.file_exists(path)):
    file = FileAccess.open(path, FileAccess.READ)
    data = JSON.parse_string(file.get_as_text())
    file.close()
    return data
  else:
    printerr("File doesn't exist")

Создаём функцию read_from_JSON с параметром path, где определяем путь к нашему JSON файлу. Используем встроенный метод FileAccess для доступа к чтению. Если обработка прошла без ошибок, возвращаем наши спарсенные данные, если файл обрабатывается некорректно, возвращаем ошибку.

В функции _ready вызываем read_from_JSON и передаём путь к нашему списку предметов res://data/items.json, сохраняем значения в переменной items. Внутри цикла мы сохраняем ключ как свойство элемента, поскольку каждый предмет идентифицируется уникальным ключом, для того, чтобы в будущем определить, относятся ли два элемента к одному и тому же типу.

Далее, нам необходимо добавить наш скрипт Global.gd в автозагрузку как синглтон. Для этого переходим в ПроектНастройки проекта, выбираем пункт Автозагрузка и добавляем наш скрипт Global.gd. Называем его Global и ставим чекбокс Глобальная переменная на включить. Теперь мы имеем доступ к классу Global глобально.

Очень хорошо расписано про паттерны игрового программирования в книге «Шаблоны игрового программирования» Роберта Найстрома.

Должно получится вот так
Должно получится вот так

Добавим ещё одну полезную функцию для получения предмета по ключу. Добавим код в конец файла Global.gd.

func get_item_by_key(key):
	if items && items.has(key):
		return items[key].duplicate(true)
	else:
		printerr("Item doesn't exist")

Эта функция принимает ключ предмета как параметр и возвращает его дубликат.

Для тестирования, создаём сцену Node2D, переименовываем в Main. Создадим папку scenes для хранения наших сцен. Сохраняем сцену как main.tscn и запускаем проект (F5), выбираем нашу сцену главной.

Теперь добавим вывод нашей функции get_item_by_key в конец _ready файла Global.gd для проверки нашего парсера.

print(get_item_by_key("orange"))

Снова запускаем проект (F5) и смотрим в вывод.

Получаем такой вывод
Получаем такой вывод

Инвентарь для хранения предметов игрока

В этом разделе мы сделаем инвентарь для хранения предметов игрока. Создадим новый скрипт Inventory.gd, добавим следующий код:

extends Node

signal items_changed(indexes)

var cols: int = 6
var rows: int = 6
var slots: int = cols * rows
var items: Array = []

func _ready():
  for i in range(slots):
    items.append({})

func set_item(index, item):
  var previos_item = items[index]
  items[index] = item
  items_changed.emit([index])
  return previos_item

func remove_item(index):
  var previos_item = items[index].duplicate()
  items[index].clear()
  items_changed.emit([index])
  return previos_item

func set_item_quantity(index, amount):
  items[index].quantity += amount
  if items[index].quantity <= 0:
    remove_item(index)
  else:
    items_changed.emit([index])

Сначала мы определяем переменные:

  • cols — Количество столбцов в инвентаре, сейчас 6

  • rows — Количество строк в инвентаре, сейчас 6

  • slots — Общее количество ячеек в инвентаре, 6 x 6 = 36

  • items — Массив в котором будем хранить предметы игрока

Сигнал items_changed будет вызываться каждый раз, когда мы будем работать с индексами нашего инвентаря. Мы передаём массив индексов вместе с сигналом, указывая, какая ячейка инвентаря изменилась. Этот сигнал нам понадобиться для работы с UI инвентаря в дальнейшем.

Изначально инвентарь будет пуст, в функции _ready мы заполняем наш массив items пустым словарём перебирая количество ячеек.

Наш каждый элемент массива items представляет собой индекс в позиции ячеек инвентаря. Доступ к предмету мы можем получить так: items[0]

Для взаимодействия с инвентарём определим несколько функций:

  • set_item — Функция принимает два параметра, index — индекс ячейки инвентаря и item — предмет из списка предметов, добавляемый в массив предметов игрока. Функция возвращает предмет, ранее сохранённый в данной ячейке.

  • remove_item — Функция принимает параметр index — индекс ячейки инвентаря, удаляя предмет из массива предметов игрока. Так же возвращает ранее сохранённый предмет в данной ячейке.

  • set_item_quantity — Функция принимает два параметра, index — индекс ячейки инвентаря и amount — сумму, которую нужно вычесть или прибавить. Если количество предмета будет равняться нулю или меньше, мы удаляем предмет из массива предметов игрока.

По аналогии с прошлым пунктом, нам необходимо добавить наш скрипт Inventory.gd в Автозагрузку как синглтон. Получаем два глобальных класса Global и Inventory.

Получаем такой результат на выходе
Получаем такой результат на выходе

Строим UI инвентаря

Теперь мы готовы к разработке интерфейса для нашего инвентаря. Для начала создадим папку interface для хранения наших сцен.

Наш интерфейс будет заполняться через GridContainer, что даст нам большую адаптивность заполнения в зависимости от количества строк и столбцов.

Создадим сцену items_slot.tscn, выберем главный узел ColorRect и назовём его ItemRect. Зададим пресет якоря для нашей сцены на Центр. Установим свойство Transform → Size по X и Y 100px и добавим аналогичные значение в свойство Custom Minimum Size. Теперь наш родительский контейнер центрирован, что лишает нас неожиданных сюрпризов в будущем.

Добавим дочерний узел типа TextureRect с именем ItemIcon. В этом узле будет отображаться иконка нашего предмета. Устанавливаем свойства Transform → Size по X и Y → 80px, а Position по X и Y → 10px. Теперь наша иконка центрирована внутри родительского узла. Так же установим свойство Expand Mode → Fit Width, для корректного отображения пропорций иконки.

Добавим ещё один узел типа Label с именем LabelQuantity. В этом узле будем отображать количество предметов из свойства quantity. Задаём пресет якоря на Справа снизу. Так же можем изменить настройки размера шрифта в свойстве Label Settings → Font → 18px.

Проставим узлам ItemIcon и LabelQuantity доступ по уникальному имени.

interface/items_slot.tscn
interface/items_slot.tscn

Дальше нам необходимо добавить ItemRect в группу. Для этого переходим в Узел → Группы и добавляем новую группу items_slot. Это поможет нам в дальнейшем получить все ячейки элементов в массиве.

Теперь добавим скрипт к нашей сцене:

extends ColorRect

@onready var item_icon = %ItemIcon
@onready var item_quantity = %LabelQuantity

func display_item(item):
	if item:
		item_icon.texture = load("res://textures/Items/%s" % item.icon)
		item_quantity.text = str(item.quantity) if item.stackable else ""
	else:
		item_icon.texture = null
		item_quantity.text = ""

Мы получаем ссылку на дочерние узлы с помощью ключевого слова @onready. Вызов функции display_item обновляет иконку и количество предмета. Путь к изображениям наших предметов res://textures/Items/[item.icon]


Далее добавим скрипт наследуемый от класса GridContainer, который мы будем использовать в нашем меню инвентаря. Создаём новый ContainerSlot.gd скрипт. Добавляем код:

extends GridContainer
class_name ContainerSlot

var ItemSlot = load("res://scenes/interface/items_slot.tscn")
var slots

func display_item_slot(cols: int, rows: int):
  var item_slot
  columns = cols
  slots = cols * rows
  
  for index in range(slots):
    item_slot = ItemSlot.instantiate()
    add_child(item_slot)
    item_slot.display_item(Inventory.items[index])
  Inventory.items_changed.connect(_on_Inventory_items_changed)

func _on_Inventory_items_changed(indexes):
  var item_slot

  for index in indexes:
    if index < slots:
      item_slot = get_child(index)
      item_slot.display_item(Inventory.items[index])

Для начала, мы определили class_name чтобы в будущем обратиться к классу по имени ContainerSlot. В переменной ItemSlot передаем экземпляр ранее созданной сцены items_slot.tscn.

В функции display_items_slot мы передаём, сколько столбцов и строк будет отображено в интерфейсе нашего инвентаря. Цикл создаёт экземпляр сцены и добавляет его в GridContainer. Наконец мы передаём сигнал обновления items_changed, созданный ранее в синглтоне Inventory, для обновления индекса ячейки предмета в инвентаре.


Добавим еще один маленький срипт (точно последний на этом этапе) InventoryMenu.gd. Он уже послужит нам функцией вывода отображения наших ячеек.

extends ContainerSlot

func _ready():
  display_item_slot(Inventory.cols, Inventory.rows)
  position = (get_viewport_rect().size - size) / 2

Наследуемся от класса ContainerSlot и вызываем функцию display_item_slotsдля отображения ячеек.


Последним этапом, нам необходима сцена, куда мы будем выводить наши ячейки инвентаря. Создаем новую сцену inventory.tscn, добавляем родительский узел Control с названием Inventory. Устанавливаем пресет якоря на Полный прямоугольник. Затем добавим узел PanelContainer с названием InventoryContainer. Это наш контейнер, в который мы будем добавлять все остальные будущие обновления для нашего инвентаря. Установим пресет якоря на Полный прямоугольник. Последним добавляем узел GridContainer с названием InventoryMenu. Устанавливаем горизонтальное и вертикальное выравнивание в статус Прижать к центру. Добавляем скрипт InventoryMenu.gd к узлу InventoryMenu. Наша сцена должна выглядеть вот так:

interface/inventory.tscn
interface/inventory.tscn

На этом мы закончили настройку отображения нашего инвентаря!

Захватываем input игрока

Отлично, мы настроили инвентарь для хранения предметов и его отображение, теперь необходимо все это перенести в главную сцену. Переходим в ранее созданную сцену Main.tscn и добавляем узел CanvasLayer, переименовываем в UI. Далее добавляем нашу сцену Inventory.tscn и активируем возможность редактирование потомков сцены. CanvasLayer позволяет нам рендерить элементы интерфейса поверх игры. Итог нашей сцены:

interface/main.tscn
interface/main.tscn

Далее нам нужно привязать кнопку открытия и закрытия нашего инвентаря. Переходим в Проект → Настройки проекта → Список действий. Добавляем действие ui_inventory и привязываем кнопку Tab к нашему действию. Получаем такой результат:

Добавим скрипт InventoryHandler.gd к нашей Inventory сцене:

extends Control

func _unhandled_input(event):
  if event.is_action_pressed("ui_inventory"):
    visible = !visible

Мы используем функцию _unhandled_input, которая будет вызываться при действии ввода. Ловим момент нашего действия ui_inventory и меня свойство visible инвентаря.

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

Pick and Drop предметов

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

Мы сделаем новую сцену DragPreview. Эта сцена будет отвечать за предварительный просмотр перетаскиваемых предметов. Нам нужно будет обновлять её позицию каждый кадр для следования за мышью.

Создаём новую сцену drag_preview.tscn с корневым узлом Control и именем DragPreview. Устанавливаем свойство Mouse → Filter на Ignore. Добавляем дочерний узел Control с именем Item. Устанавливаем свойство transform на X и Y → 140px.

Дальше нам нужно добавить вывод иконки и количества нашего предмета. Добавляем узел TextureRect с именем ItemIcon дочерним от узла Item. Устанавливаем свойство transform на X и Y → 120px, ставим пресет якоря на Центр. Не забываем про свойство Expand Mode → Fit Width. Добавим узел Label с именем LabelQuantity. Установим в свойстве Label Settings → Font → Size на 18px и Horizontal Alignment → Right. Якорь пресета установим на Справа снизу. Сохраняем сцену и получаем вот такой результат:

interface/drag_preview.tscn
interface/drag_preview.tscn

Теперь добавим скрипт DragPreview.gd к нашей сцене:

extends Control

@onready var item_icon = $Item/ItemIcon
@onready var item_quantity = $Item/LabelQuantity

var dragged_item = {} : set = set_dragged_item

func _process(delta):
	if dragged_item:
		position = get_global_mouse_position()

func set_dragged_item(item):
	dragged_item = item
	if dragged_item:
		item_icon.texture = load("res://textures/Items/%s" % dragged_item.icon)
		item_quantity.text = str(dragged_item.quantity) if dragged_item.stackable else ""
	else:
		item_icon.texture = null
		item_quantity.text = ""

Здесь мы используем переменную dragged_item для хранения перетаскиваемого предмета. Изначально он пуст. Так же устанавливаем сеттер на функцию set_dragged_item. Он будет вызываться каждый раз при изменении перетаскиваемого предмета, обновляя иконку предмета и его количество.

В функции _process мы двигаем узел за мышью, если перетаскиваемый предмет не пуст.

Мы закончили настройку нашей сцены для предварительного просмотра перетаскиваемых предметов. Теперь добавим экземпляр сцены drag_preview.tscn в нашу сцену инвентаря Inventory.tscn. Должно получиться вот так:

interface/inventory.tscn
interface/inventory.tscn

Добавим код в наш скрипт InventoryHandler.gd :

@onready var drag_preview = $InventoryContainer/DragPreview

func _ready():
  for item_slot in get_tree().get_nodes_in_group("items_slot"):
	var index = item_slot.get_index()
	item_slot.connect("gui_input", _on_ItemSlot_gui_input.bind(index))

Для начала подключим сигнал gui_input к каждому слоту инвентаря. Мы можем использовать метод get_tree().get_nodes_in_group(), где передаём имя искомой группы items_slot, которую указали ранее. Внутри цикла мы подключаем сигнал gui_input к каждому слоту из искомой группы, который будет вызывать функцию on_ItemSlot_gui_input каждый раз, когда узел получит событие ввода. В него передаём index нашего слота и событие ввода.

Определим нашу функцию _on_ItemSlot_gui_input:

func _on_ItemSlot_gui_input(event, index):
  if event is InputEventMouseButton:
    if event.button_index == MOUSE_BUTTON_LEFT && event.pressed:
      if visible:
    	drag_item(index)

Если игрок нажмёт левой кнопкой мыши по слоту, мы вызываем функцию drag_item:

func drag_item(index):
  var inventory_item = Inventory.items[index]
  var dragged_item = drag_preview.dragged_item
	
  # Взять предмет
  if inventory_item && !dragged_item:
    drag_preview.dragged_item = Inventory.remove_item(index)
  # Бросить предмет
  if !inventory_item && dragged_item:
    drag_preview.dragged_item = Inventory.set_item(index, dragged_item)
  # Свапнуть предмет
  if inventory_item and dragged_item:
    drag_preview.dragged_item = Inventory.set_item(index, dragged_item)

Последним, немного модифицируем действие открытия и закрытия инвентаря в функции _unhandled_input:

func _unhandled_input(event):
  if event.is_action_pressed("ui_inventory"):
    # Не даем закрыть инвентарь пока взят предмет
    if visible && drag_preview.dragged_item: return
    visible = !visible

Для теста добавим предметы в инвентарь. Перейдём в скрипт Inventory.gd и добавим несколько предметов в конец функции _ready:

items[0] = Global.get_item_by_key("orange")
items[1] = Global.get_item_by_key("orange")
items[2] = Global.get_item_by_key("orange")
items[3] = Global.get_item_by_key("potion")
items[4] = Global.get_item_by_key("potion")
items[5] = Global.get_item_by_key("sword")

Запустим проект (F5) и убедимся что наш инвентарь открывается и закрывается, а так же доступен выбор предмета и его перенос в другой слот:

main.tscn
main.tscn

Stack and Split предметов

Теперь мы можем перемещать предметы в меню инвентаря, но остаётся ещё две проблемы. Первая, что одни и те же предметы не складываются вместе, а вторая, что нет возможности их разделить. Самое время это решить!

Откроем наш скрипт InventoryHandler.gd и добавим следующий код:

func drag_item(index):
  var inventory_item = Inventory.items[index]
  var dragged_item = drag_preview.dragged_item
	
  # Взять предмет
  if inventory_item && !dragged_item:
    drag_preview.dragged_item = Inventory.remove_item(index)
  # Бросить предмет
  if !inventory_item && dragged_item:
    drag_preview.dragged_item = Inventory.set_item(index, dragged_item)

  if inventory_item && dragged_item:
    # Стакнуть предмет
    if inventory_item.key == dragged_item.key && inventory_item.stackable:
      Inventory.set_item_quantity(index, dragged_item.quantity)
      drag_preview.dragged_item = {}
    # Свапнуть предмет
    else:
      drag_preview.dragged_item = Inventory.set_item(index, dragged_item)

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

Теперь добавим сплит предметов. Для этого обновляем функцию _on_ItemSlot_gui_input в том же скрипте, добавив новое условие:

func _on_ItemSlot_gui_input(event, index):
	if event is InputEventMouseButton:
		if event.button_index == MOUSE_BUTTON_LEFT && event.pressed:
			drag_item(index)
		if event.button_index == MOUSE_BUTTON_RIGHT && event.pressed:
			split_item(index)

На 5 строке мы добавили новое условие, если игрок нажмёт правую кнопку мыши на выделенном предмете, мы вызываем функцию split_item.

func split_item(index):
  var inventory_item = Inventory.items[index]
  var dragged_item = drag_preview.dragged_item
  var split_amount
  var item
  
  # Проверяем если предмет стакабл
  if !inventory_item || !inventory_item.stackable: return

  split_amount = ceil(inventory_item.quantity / 2.0)

  if dragged_item && inventory_item.key == dragged_item.key:
	drag_preview.dragged_item.quantity += split_amount
  	Inventory.set_item_quantity(index, -split_amount)
  if !dragged_item:
	item = inventory_item.duplicate()
    item.quantity = split_amount
	drag_preview.dragged_item = item
	Inventory.set_item_quantity(index, -split_amount)

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

Затем мы получаем split_amount, уменьшив количество предметов вдвое.

На строках 12-14, если предмет перетаскиваемый и имеет такой же ключ, что и ключ предмета в инвентаре, мы увеличиваем его количество на split_amount, а из предмета в инвентаре вычитаем.

На строках 15-19, если перетаскиваемого предмета еще нет, мы добавим предмет того же типа и изменим его количество, чтобы он имел такую же сумму как в split_amount. Наконец, мы вычитаем эту сумму из предмета в инвентаре.

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

Всплывающая подсказка

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

Создаём новую сцену inventory_tooltip.tscn, задаём главный узел ColorRect и переименовываем в InventoryTooltip. Добавим немного прозрачности нашему узлу задав свойство Color → #313 131cb. Пресет якоря назначаем на Слева вверху. Для свойства Custom Minimum Size установим размер по X → 300px, по Y → 100px. Так же нам необходимо установить горизонтальное и вертикальное выравнивание для дочерних узлов. В свойстве Container Sizing для Horizontal и Vertical ставим значение Прижать к началу. Не забываем установить свойство для Mouse → Ignore.

Добавим дочерний узел MarginContainer. Установим пресет якоря на Полный прямоугольник. Свойство Mouse → Ignore. Установим небольшие отступы для нашего контейнера, для этого переходим в свойства Theme Overrides → Constants, Margin Left → 20, Margin Top и Margin Bottom → 2, Margin Right → 4.

Создаём 2 узла Label с именем NameLabel и DescriptionLabel, задаём вертикальное выравнивание для NameLabel на Прижать к началу, для DescriptionLabel Прижать к центру. На выходе получаем вот такую сцену:

interface/inventory_tooltip.tscn
interface/inventory_tooltip.tscn

Создадим новый скрипт Tooltip.gd для нашей сцены inventory_tooltip.tscn:

extends ColorRect

@onready var name_label = $MarginContainer/NameLabel
@onready var description_label = $MarginContainer/DescriptionLabel

func _process(delta):
  position = get_global_mouse_position() + Vector2.ONE * 4

func display_info(item):
  name_label.text = item.name
  description_label.text = item.description

Мы обновляем позицию нашей подсказки каждый кадр, чтобы она следовала за позицией нашей мышки. В функции display_info передаем предмет, где будем отображать информацию о нашем предмете по ключам name и description.

Теперь добавим экземпляр сцены inventory_tooltip.tscn в нашу сцену с инвентарём Inventory.tscn. Так как по умолчанию подсказка должна быть скрыта, скроем её, нажав на значок глаза. Наша сцена должна выглядеть вот так:

interface/Inventory.tscn
interface/Inventory.tscn

Переходим в наш скрипт Inventory.gd и добавляем сцену с подсказкой:

@onready var tooltip = $InventoryContainer/InventoryTooltip

Для показа и скрытия подсказки, добавим сигналы mouse_entered и mouse_exited в функцию _ready.

func _ready():
  for item_slot in get_tree().get_nodes_in_group("items_slot"):
    var index = item_slot.get_index()
    item_slot.connect("gui_input", _on_ItemSlot_gui_input.bind(index))
    item_slot.connect("mouse_entered", show_tooltip.bind(index))
	item_slot.connect("mouse_exited", hide_tooltip)

Определим функции show_tooltip и hide_tooltip.

func show_tooltip(index):
  var inventory_item = Inventory.items[index]
	
  if inventory_item && !drag_preview.dragged_item:
    tooltip.display_info(inventory_item)
    tooltip.visible = true
  else:
    tooltip.visible = false

func hide_tooltip():
  tooltip.visible = false

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

Под конец добавим скрытие подсказки при переключении отображения инвентаря и в момент перетаскивания предмета.

func _on_ItemSlot_gui_input(event, index):
  if event is InputEventMouseButton:
    if event.button_index == MOUSE_BUTTON_LEFT && event.pressed:
    	hide_tooltip()
    	drag_item(index)
    if event.button_index == MOUSE_BUTTON_RIGHT && event.pressed:
    	hide_tooltip()
        split_item(index)

func _unhandled_input(event):
	if event.is_action_pressed("ui_inventory"):
    	# Не даем закрыть инвентарь пока взят предмет
    	if visible && drag_preview.dragged_item: return
        visible = !visible
        hide_tooltip()

Здесь добавили вызовы функции hide_tooltip. Запускаем проект и тестируем наш супер навороченный инвентарь!

Финал

Надеюсь этот туториал (или записки сумасшедшего) помогли вам на пути к реализации той самой игры! Спасибо за уделённое время!

P.S. Вдохновение взято отсюда (сделано на Godot 3.x.x.)

Комментарии (9)


  1. pewpew
    29.03.2024 15:52
    +2

    Спасибо, туториал интересный, только русский интерфейс выглядит сильно непривычно. В статье почему-то перемешивается то "Проект -> Настройки проекта", то "Label Settings -> Font -> Size". Может кому это и удобно, но для себя я решил оставить англоязычный интерфейс как минимум чтобы меньше путаться, если смотришь какой туториал. А они в основном на английском.


    1. warmBy
      29.03.2024 15:52

      В годоте до сих пор не всё локализовано, из-за чего даже поставив язык на русский, какой-нибудь Label Settings не будет переведён, хотя это в некоторой степени и правильно, ведь это название класса/параметров


      1. pewpew
        29.03.2024 15:52
        +1

        Тем более в таком случае ставить русский язык даже вредно. Во всяком случае новичку. Запутаться - как два пальца. Не совсем понятно, зачем вообще разработчики решили сделать перевод. Ведь названия классов нельзя просто так взять и перевести. И даже если в одном месте нода называется "Узел", то класс при этом "Node". И зачем это всё тогда?


  1. NeiroNext
    29.03.2024 15:52

    Интересно, но так не привычно, что все по сути является какой-то сценой, даже подсказка.

    Никогда не занимался созданием игр, но просто интересно, примерно такой структурный подход используется и в других движках (Unity, Unreal) или это особенность именно данного игрового движка?


    1. stal1n63
      29.03.2024 15:52

      Можно конечно натянуть сову на глобус, что prefab в unity и blueprint в unreal в целом то же самое, но нет, в обоих случаях есть нюансы. Хотя префабы очень близки по замыслу.

      Так что конкретно фишка годо, что вообще всё загнано под единый *.tres формат. По факту там файл типа TOML, с описанием компонентов. К примеру, в unity можно увидеть схожее в связке с *.prefab и *.meta файлами. Но опять же, это не *.scene объекты, и между ними есть разница.


    1. Lekret
      29.03.2024 15:52

      Скорее особенность самого Godot, хотя подход от Unity и Unreal в целом не сильно отличается.

      Я по большей части работал в Unity и тут есть сцена, объекты, префаб (объект заготовка), и компоненты на объектах.
      В Unreal есть миры, акторы, компоненты акторов, блюпринты (префабы).
      В Godot все объекты на сцене является нодой, сама сцена это коллекция нод, префабы это по сути тоже сцена, объекты это ноды, и компоненты это тоже ноды.

      В подходе Godot наверное есть плюс, потому что в Unity часто нет большой разницы между тем чтобы загрузить дополнительную сцену с уровнем, или же просто собрать уровень в префабе и создать его, хотя workflow с ними отличается и сцена и префабы это вещи не взаимозаменяемые, как и объекты с компонентами. А в Godot получается что всё единообразно, то есть у тебя не несколько видов кирпичей, а один универсальный.


  1. ValeryIvanov
    29.03.2024 15:52
    +1

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


    1. ReverendWeaver
      29.03.2024 15:52
      +2

      Согласен с Валерием. Я сам долгое время заморачивался с JSON, пока не узнал, что в Godot есть более удачное решение для этого.

      Ресурсы позволяют не заморачиваться с сериализацией, на выходе объект с заполненными типизированными данными, а также ресурсы можно просматривать в инспекторе и заполнять с встроенными подсказками. В 4-ой версии ещё можно кастомные ресурсы вкладывать друг в друга, если необходимо.


  1. StepanDC
    29.03.2024 15:52
    +1

    Насколько я помню, если при проект при экспорте будет запакован в .pck, то FileAccess просто не откроет JSON, поэтому лучше попробовать ресурсы с их load() и preload()