Skip to content

Intro

In this second part, we will add features to our working inventory system created with gooey to make it more powerful. In particular, we want to add the following:

  • Add a tooltip that shows information about the held items
  • Make it so that we can reorder stuff in the inventory
  • Make it so that we can drop items to the ground

Let's go!

Adding tooltips

We can think of a tooltip as a gooey UIPanel that shows up whenever we hover on a sprite (in this case) and disappears when we stop hovering over it. With this paradigm, we can go ahead and create a tooltip panel with a simple text widget that will hold our text.

For this example, we want the tooltip to always be to the right of the mouse cursor. So we will create it at 0,0, but then update it every frame so it moves with the mouse, hovering at 30px right of the mouse cursor location in the GUI layer. We will use .setDimensions for that.

This is the code:

Game / Draw GUI
    if !(ui_exists("Panel_Tooltip")) {
        var _panel = new UIPanel("Panel_Tooltip", 0, 0, 210, 80, glass_panel);
        _panel.setResizable(false).setMovable(false);
        _panel.setVisible(false);

        _panel.setPreRenderCallback(function() {
            ui_get("Panel_Tooltip").setDimensions(device_mouse_x_to_gui(0)+30, device_mouse_y_to_gui(0));
        });

        var _txt = new UIText("Text_Tooltip", 30, 0, "", UI_RELATIVE_TO.MIDDLE_LEFT);
        _txt.setTextFormat("[c_white][fnt_UI][fa_middle][fa_left][scale,2]");
        _panel.add(_txt);
    }

Now, let's add some code to our sprites in the inventory, so that we achieve the desired effect. We can use the MOUSE_ENTER and MOUSE_EXIT events and add the corresponding callbacks.

For this example, we want to show the item name in the tooltip text. In order to get this, recall that, in the previous tutorial, we added four sprites, initially set to undefined, with a double for loop (corresponding to the maximum inventory size of 4 we have set), and we mapped the row and column for each index to determine where each item is rendered in the inventory.

We can use the same trick (in reverse) to get, for each hovered sprite, its index in the player inventory: for each row and column, we can determine the array index by multiplying its row index times the number of columns and then adding the column name. In order to do that, let's add user data to our inventory sprites:

Game / Draw GUI
    _sprite.setUserData("row", _row).setUserData("col", _col);

Now, let's code our event callbacks, adding them to each sprite. Take a look at MOUSE_ENTER first. We set visibility to true for the tooltip panel, make sure it's the focused panel (so it renders on top of everything else) and sets the tooltip text. To get the corresponding text, as discussed above, we calculate the index corresponding to this row and column. In order to do this, we need to create a kind of "closure" (see this for more info, in case you don't know what that's about) so we can "use" the local variables _row and _col inside our callback function, by using method:

For MOUSE_EXIT, we just reset visibility to false and text to the empty string:

Game / Draw GUI
    _sprite.setCallback(UI_EVENT.MOUSE_ENTER, method({_row, _col, _num_cols}, function() {
        ui_get("Panel_Tooltip").setVisible(true);
        ui_set_focused_panel("Panel_Tooltip");
        var _idx = _row * _num_cols + _col;
        var _item = obj_Player.inventory[_idx];
        ui_get("Text_Tooltip").setText(_item.item_name, true);
    }));
    _sprite.setCallback(UI_EVENT.MOUSE_EXIT, function() {
        ui_get("Panel_Tooltip").setVisible(false);
        ui_get("Text_Tooltip").setText("", true);
    });

We're done! Our tooltips are working!

Tooltips
Working tooltips in our inventory.

Changing the inventory array and methods

For making the following two parts easier, we will make some changes to our player's inventory array and inventory_add and inventory_remove methods. We want to make it so that the player array does not grow and shrink, but rather stays the same size always (4 in this case). This will allow items to maintain their slot in the array regardless of whether an item was deleted, and thus we will be able to use this for rendering the gooey inventory and coding the ability to reorder the items as well.

The changes basically resort to initializing the inventory to a fixed size, finding the first empty slot of the array and adding the item there, and set it to undefined instead of deleting the item:

obj_Player / Create
    self.inventory_max_size = 4;
    self.inventory = array_create(self.inventory_max_size, undefined);
    self.inventory_add = function(_item_name, _qty=1) {
        var _idx = array_find_index(self.inventory, method({_item_name}, function(_elem, _i) {
            return _elem != undefined && _elem.item_name == _item_name;
        }));
        if (_idx != -1) {
            self.inventory[_idx].qty += _qty;
            return true;
        }
        else {
            var _first_empty_slot = 0;
            while (_first_empty_slot < self.inventory_max_size) {
                if (self.inventory[_first_empty_slot] == undefined) break;
                else _first_empty_slot++;
            }
            if (_first_empty_slot < self.inventory_max_size) {
                self.inventory[_first_empty_slot] = {item_name: _item_name, qty: _qty};
                return true;
            }
            else {
                return false;
            }
        }
    }

    self.inventory_remove = function(_item_name, _qty=999999999) {
        var _idx = array_find_index(self.inventory, method({_item_name}, function(_elem, _i) {
            return _elem != undefined && _elem.item_name == _item_name;
        }));
        if (_idx != -1) {
            self.inventory[_idx].qty = max(0, self.inventory[_idx].qty - _qty);
            if (self.inventory[_idx].qty == 0) {
                self.inventory[_idx] = undefined;
            }
            return true;
        }
        else {
            return false;
        }
    }

In the obj_NPC quest completion checking logic, we also need to check that the element being processed in the player's inventory is not undefined to avoid errors:

obj_NPC / Create
var _idx = array_find_index(obj_Player.inventory, method({_this}, function(_elem, _i) {
    return _elem != undefined && _elem.item_name == _this.thing && _elem.qty >= _this.qty;
}));

Now that we have this structure (and everything still works as expected) we can get to coding the desired mechanics.

Dragging items

Our end goal is to implement item reordering in the inventory Specifically, we want to be able to click and drag an item in the inventory and drop it in another inventory slot. If that inventory slot is empty, the item gets moved; if not, the items are swapped.

However, we need to implement dragging items first.

To achieve this effect, we will store a variable in obj_Player that will keep track of whether we are dragging any item:

obj_Player / Create
    self.currently_dragged_item = undefined;

Then, we can add a LEFT_CLICK callback for our item sprites, that starts the "drag" action of the item by setting the aforementioned variable to its index.

We could think of doing the opposite with LEFT_RELEASE. However, note that the player can stop dragging the item anywhere on the screen - not just over a sprite. So we will need to manage this separately. Probably, if the player drags the item to a place which is not an inventory slot, their intention is not to swap / reorder the items, but to cancel the action or do otherwise. We will still need the LEFT_RELEASE callback to be set to handle the cases when the player does want to swap / reorder items. Let's create it, but leave it empty for the time being:

Game / Draw GUI
    _sprite.setCallback(UI_EVENT.LEFT_CLICK, method({_row, _col, _num_cols}, function() {
        var _idx = _row * _num_cols + _col;
        obj_Player.currently_dragged_item = _idx;
    }));
    _sprite.setCallback(UI_EVENT.LEFT_RELEASE, method({_row, _col, _num_cols}, function() {
        // we'll complete this at a later point of the tutorial
    }));

Note we haven't cleared the dragged item index when the player releases the mouse button. Also, there's no indicator whatsoever that the player is dragging something - we probably want to draw the item next to the mouse cursor when this happens. To do both things, let's add a piece of code to our Draw GUI event:

Game / Draw GUI
if (obj_Player.currently_dragged_item != undefined) {
    var _item = obj_Player.inventory[obj_Player.currently_dragged_item];
    if (_item != undefined) {
        var _sprite = sprite_exists(asset_get_index(_item.item_name)) ? asset_get_index(_item.item_name) : asset_get_index(_item.item_name+"_05");
        draw_sprite_ext(_sprite, 0, device_mouse_x_to_gui(0), device_mouse_y_to_gui(0), 3, 3, 0, c_white, 0.5);

        if (InputReleased(INPUT_VERB.USE_TOOL)) {
            obj_Player.currently_dragged_item = undefined;
        }
    }
}

With this code, we are drawing the same item with an alpha of 0.5 at the cursor location, and we're also setting the currently_dragged_item variable to undefined whenever the player releases the mouse button.

Once we run this, we can see we can drag items around, and the dragged item is visible:

Dragging items
The rendering of our dragged item. No reorder/swap yet.

Reordering items

Once we have the drag functionality set up, we can handle what happens when dragging an item and dropping it into another item. Since our inventory updates every frame, it suffices to swap the corresponding items in the obj_Player inventory. Furthermore, we already have our stub created, so let's just complete the code:

Game / Draw GUI
    _sprite.setCallback(UI_EVENT.LEFT_RELEASE, method({_row, _col, _num_cols}, function() {
        if (obj_Player.currently_dragged_item != undefined) {
            var _idx = _row * _num_cols + _col;
            var _temp = obj_Player.inventory[_idx];
            obj_Player.inventory[_idx] = obj_Player.inventory[obj_Player.currently_dragged_item];
            obj_Player.inventory[obj_Player.currently_dragged_item] = _temp;
        }
    }));

If we now test this and we try to swap items - it works!

Reordering items
The basic swap functionality.

However, there's two issues with our code:

  1. We cannot drop items into an empty slot - it does not do anything.
  2. If we check closely, we need to click (and release) exactly in the sprite - not in the actual slot.

The first issue is happening because the sprite is set to undefined and its dimensions are 0x0, so the LEFT_RELEASE callback is not firing. The second is because we are assigning the callbacks to the sprites - so if we click (or drop) the slots instead of the items, nothing is happening.

To fix both, we can set the callbacks to the grid cells (UIGroup) instead of the sprites - but then clicking/releasing over the sprite would yield no effect. We could always abstract the code to a method and assign it to the callbacks of both the grid cells and the sprites, but we will do something else: we'll asssign it to the grid cells and use the drill-through functionality to make the sprites inherit these callbacks automatically. This way, no matter if we click/release over the slot or over the sprite, it will work seamlessly.

The changes are easy to make and our updated code looks as follows:

Game / Draw GUI
    _grid.getCell(_row, _col).setCallback(UI_EVENT.LEFT_CLICK, method({_row, _col, _num_cols}, function() {
        var _idx = _row * _num_cols + _col;
        obj_Player.currently_dragged_item = _idx;
    }));
    _grid.getCell(_row, _col).setCallback(UI_EVENT.LEFT_RELEASE, method({_row, _col, _num_cols}, function() {
        if (obj_Player.currently_dragged_item != undefined) {
            var _idx = _row * _num_cols + _col;
            var _temp = obj_Player.inventory[_idx];
            obj_Player.inventory[_idx] = obj_Player.inventory[obj_Player.currently_dragged_item];
            obj_Player.inventory[obj_Player.currently_dragged_item] = _temp;
        }
    }));
    _sprite.setDrillThroughLeftClick(true);
    _sprite.setDrillThroughLeftRelease(true);

VoilĂ ! We have item reorder/swap implemented!

Final reorder/swap item logic
The complete reorder and swap functionality, implemented with gooey.

Dropping items

We need a way to drop items to the ground - otherwise we will not be able to complete some quests, if we have already picked up some stuff!

Fortunately, we already have all the scaffolding needed to do this very easily. We can modify the block of code we added earlier, at the end of our Draw GUI event, that handles the release logic, create an obj_Item instance1 with the required properties in the ground, and remove the item from the inventory with the method we already have. This is the only thing required! Also, to allow the player to "cancel" the drag action, we only do this if the mouse button is released on the actual game world and not on a gooey window (by using ui_is_interacting).

The modified code is as follows:

Game / Draw GUI
if (obj_Player.currently_dragged_item != undefined) {
    var _item = obj_Player.inventory[obj_Player.currently_dragged_item];
    if (_item != undefined) {
        var _sprite = sprite_exists(asset_get_index(_item.item_name)) ? asset_get_index(_item.item_name) : asset_get_index(_item.item_name+"_05");
        draw_sprite_ext(_sprite, 0, device_mouse_x_to_gui(0), device_mouse_y_to_gui(0), 3, 3, 0, c_white, 0.5);

        if (InputReleased(INPUT_VERB.USE_TOOL)) {
            if (!ui_is_interacting()) {
                var _dir = point_direction(obj_Player.x, obj_Player.y, device_mouse_x(0), device_mouse_y(0));
                var _dx = lengthdir_x(16, _dir);
                var _dy = lengthdir_y(16, _dir);
                var _is_crop = !asset_get_index(_item.item_name);
                instance_create_layer(obj_Player.x+_dx, obj_Player.y+_dy, "lyr_Crops", obj_Item, {item_name: _item.item_name, qty: _item.qty, is_crop: _is_crop})
                obj_Player.inventory_remove(_item.item_name, _item.qty);
            }
            obj_Player.currently_dragged_item = undefined;
        }
    }
}

The final result

With little effort, we now have a fully functioning inventory system, with drag/drop, reorder, swap, item drop to the ground, tooltips etc.

This is how it looks fully finished:

Complete inventory system
Our final implementation of the inventory system with gooey.

  1. In case you haven't looked at this part of the game code yet, obj_Item is the object that handles objects in the ground, such as resources obtained from animals or crops.