Draggable inventory slots, OOP

Let’s create an inventory, where item slots can be dragged around and swap places.
I will also create some of the UI through code, which is generated by Codify plugin (this plugin takes a GUI element, and outputs the exact code to create it through a script).

Here’s what you’ll need to follow along.
We’ll be working in a modulescript, in ReplicatedStorage.
b761284ea0c8a53cf833afb704beb1de

e154bb593ddf6bdb9a59f5c6a67559fc

I suggest you just copy this for now, for implementing the tutorial easily.

local function generate_slot() -- This'll generate all the Instances needed for a slot.
	local slot_Holder = Instance.new("Frame")
	slot_Holder.Name = "Slot_Holder"
	slot_Holder.BackgroundColor3 = Color3.fromRGB(115, 115, 115)

	local drag_Frame = Instance.new("Frame")
	drag_Frame.Name = "Drag_Frame"
	drag_Frame.BackgroundColor3 = Color3.fromRGB(150, 150, 150)
	drag_Frame.BackgroundTransparency = 0.3
	drag_Frame.Size = UDim2.fromScale(1, 1)
	
	local uIStroke = Instance.new("UIStroke")
	uIStroke.Name = "UIStroke"
	uIStroke.Thickness = 2
	uIStroke.Parent = drag_Frame

	drag_Frame.Parent = slot_Holder

	local uIPadding = Instance.new("UIPadding")
	uIPadding.PaddingBottom = UDim.new(0, 4)
	uIPadding.PaddingLeft = UDim.new(0, 4)
	uIPadding.PaddingRight = UDim.new(0, 4)
	uIPadding.PaddingTop = UDim.new(0, 4)
	uIPadding.Parent = slot_Holder
	
    local iD = Instance.new("TextLabel")
	iD.Name = "ID"
	iD.FontFace = Font.new("rbxasset://fonts/families/SourceSansPro.json")
	iD.Text = ""
	iD.TextColor3 = Color3.fromRGB(0, 0, 0)
	iD.TextSize = 32
	iD.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
	iD.BackgroundTransparency = 1
	iD.BorderColor3 = Color3.fromRGB(0, 0, 0)
	iD.BorderSizePixel = 0
	iD.Size = UDim2.fromScale(1, 1)
    iD.Parent = drag_Frame

	return slot_Holder
end

We’ll be making an InventorySlot class, which’ll represent every individual slot.
We’ll also make a DragInventory class, which’ll represent the parent container.

We’ll start with the InventorySlot.
The slot needs the following fields:

  • self.UI, which’ll be the background of the slot.
  • self.DragFrame, which will be a frame inside the background (this’ll do all the dragging)

and the following methods:

  • Drag, for starting a drag
  • EndDrag, for ending it

We also need a way to tell the DragInventory that we are hovering over this slot. We’ll do that by passing the DragInventory to the slot, so that it can simply set DragInventory.CurrentlyHovering.


-- Define the InventorySlot class
local InventorySlot = {}
InventorySlot.__index = InventorySlot

-- Constructor
function InventorySlot.new(parent)
	local self = setmetatable({
		UI = generate_slot(), -- Creating a slot using the function from before
		DragFrame = self.UI.Drag_Frame -- This is a reference, not really needed but handy.
	}, InventorySlot)

	-- Connect UI events
	self.UI.MouseEnter:Connect(function()
		parent.CurrentlyHovering = self -- We will set the CurrentlyHovering to self (this InventorySlot), so that we can easily call it's methods. 
		self.DragFrame.BackgroundColor3 = Color3.fromRGB(94, 94, 94) -- Some quality of life things, to make it easier to see what's going on.
		self.DragFrame.BackgroundTransparency = 0.6
	end)

	self.UI.MouseLeave:Connect(function() -- Note: we're using the background as the input area, and the DragFrame as the frame part that moves
		parent.CurrentlyHovering = nil 
		self.DragFrame.BackgroundColor3 = Color3.fromRGB(150, 150, 150)
		self.DragFrame.BackgroundTransparency = 0.3
	end)

One of the reasons for using a DragFrame instead of the background is because we’ll use a UIGridLayout instance. This prevents us from moving the affected objects, but doesn’t affect their children. This can be inconvenient in some cases, because the Position of the background will actually be the same for every slot. Luckily we have AbsolutePosition, which’ll work still.

When dragging, we can pass the input object (we’ll use UIS.InputChanged to call it). This has a Position field, giving us the mouse’s screen position (as a Vector3, with z being 0). We will use it to calculate the difference between the place where we started dragging from, and the current position of the mouse. Then we can simply move the dragframe’s offset by that difference. This results in the dragframe moving the exact same path as the mouse, which is what we want.

We also want the dragframe to snap to a slot, if we hover over it. We are already tracking what slot we are hovering over on the parent, so we can simply read if that is nil or has a slot. If it has a slot, we should move the dragframe to the difference of the CurrentlyHovered’s absoluteposition, and the dragging’s background absoluteposition.



-- Drag method
	function InventorySlot:Drag(input)
		self.DragFrame.ZIndex = 2
		self.DragFrame.ID.ZIndex=3
		self.DragFrame.BackgroundTransparency = 0
		local distanceMovedX = self.InitialX - input.Position.X
		local distanceMovedY = self.InitialY - input.Position.Y
		
		if parent.CurrentlyHovering then
			local offset = parent.CurrentlyHovering.UI.AbsolutePosition - self.UI.AbsolutePosition
			local x,y = offset.X,offset.Y
			self.DragFrame.Position = UDim2.new(0,x,0,y)
		else
			self.DragFrame.Position = -UDim2.new(0, distanceMovedX, 0, distanceMovedY)
		end
		
	end

	-- EndDrag method
	function InventorySlot:EndDrag()
		self.DragFrame.ZIndex = 1
		self.DragFrame.ID.ZIndex=1
		self.DragFrame.BackgroundTransparency = 0.3
		if self.CurrentlyHovering and self.CurrentlyHovering ~= self.UI then
			-- Implement item merging logic here
			print("Merging items")
      self.DragFrame.Position = UDim2.new(0, 0, 0, 0)
		else
			-- If not dropped onto another slot, reset the position
			  self.DragFrame.Position = UDim2.new(0, 0, 0, 0)
		end

		parent.InputChangedConn:Disconnect()
	end

	return self
end

Next up, the DragInventory.

It needs the following variables:

  • A Holding variable, which’ll be nil or an InventorySlot. This variable will hold the InventorySlot currently being dragged.
  • A CurrentlyHovering variable, also nil/InventorySlot. This variable will change to whatever InventorySlot we’re hovering over.

and the following methods:

  • Mouse1down method, for setting Holding to CurrentlyHovering, and starting to drag the InventorySlot.
  • Mouse1up method, for ending drag and resetting variables.

It also needs to instantiate the InventorySlots. For that, we’ll need to feed it the number of slots we want.

local UIS = game:GetService()
local DragInventory = {}
DragInventory.__index = DragInventory


function DragInventory.new(inventory_frame,slots) -- This method will create a new DragInventory.
  local self = setmetatable({},DragInventory)
  
  self.Holding = false
  self.CurrentlyHovering = nil
  self.Content = {}
  
  -- Let's instantiate some slots here
  

	for i = 1,slots do
		local new_slot = InventorySlot.new(self)  -- This'll call the InventorySlot.new() to create a slot. We will pass self, so that we can access it from the slot.
		new_slot.UI.Parent = game.Players.LocalPlayer.PlayerGui.ContainerGui:WaitForChild("Inventory")
		new_slot.UI.LayoutOrder = i
		new_slot.UI.Drag_Frame.ID.Text = new_slot.UI.LayoutOrder
		table.insert(self.Content,new_slot)
	end

-- Finally, we need to add the dragstart/end logic
   UIS.InputBegan:Connect(function(input) -- On input, checkif that input is MouseButton1/Touch
		if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
			self.Holding = self.CurrentlyHovering or false -- self.Holding is set to CurrentlHovering. If we're not hovering, it's set to false and the dragging won't  commence.
			if self.Holding then
				self.Holding.InitialX, self.Holding.InitialY = input.Position.X, input.Position.Y -- Setting the variables on the ItemSlot
				self.Holding.UIInitialPos = self.Holding.UI.Position

				self.InputChangedConn = UIS.InputChanged:Connect(function(input) -- Connect moving the mouse to the drag method of the itemslot. This is what updates the dragframe position.
					if input.UserInputType == Enum.UserInputType.MouseMovement then
						self.Holding:Drag(input)
					end
				end)
			end
		end
	end)

	UIS.InputEnded:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then 
			if self.Holding then
				print(self.CurrentlyHovering and self.CurrentlyHovering.UI.LayoutOrder)
				self.Holding.UI.LayoutOrder,self.CurrentlyHovering.UI.LayoutOrder = self.CurrentlyHovering.UI.LayoutOrder,self.Holding.UI.LayoutOrder
				self.Holding:EndDrag()
			end
			self.Holding = false
		end
	end)

  return self
end

return DragInventory

Final result:


The localscript in the ScreenUI:

local DragInventory = require(game.ReplicatedStorage.DragInventory).new(5)

Full module:

local UIS = game:GetService("UserInputService")

local function generate_slot()
	local slot_Holder = Instance.new("Frame")
	slot_Holder.Name = "Slot_Holder"
	slot_Holder.BackgroundColor3 = Color3.fromRGB(115, 115, 115)

	local drag_Frame = Instance.new("Frame")
	drag_Frame.Name = "Drag_Frame"
	drag_Frame.BackgroundColor3 = Color3.fromRGB(150, 150, 150)
	drag_Frame.BackgroundTransparency = 0.3
	drag_Frame.Size = UDim2.fromScale(1, 1)
	
	local uIStroke = Instance.new("UIStroke")
	uIStroke.Name = "UIStroke"
	uIStroke.Thickness = 2
	uIStroke.Parent = drag_Frame

	drag_Frame.Parent = slot_Holder

	local uIPadding = Instance.new("UIPadding")
	uIPadding.PaddingBottom = UDim.new(0, 4)
	uIPadding.PaddingLeft = UDim.new(0, 4)
	uIPadding.PaddingRight = UDim.new(0, 4)
	uIPadding.PaddingTop = UDim.new(0, 4)
	uIPadding.Parent = slot_Holder
	
	local iD = Instance.new("TextLabel")
	iD.Name = "ID"
	iD.FontFace = Font.new("rbxasset://fonts/families/SourceSansPro.json")
	iD.Text = ""
	iD.TextColor3 = Color3.fromRGB(0, 0, 0)
	iD.TextSize = 32
	iD.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
	iD.BackgroundTransparency = 1
	iD.BorderColor3 = Color3.fromRGB(0, 0, 0)
	iD.BorderSizePixel = 0
	iD.Size = UDim2.fromScale(1, 1)
	iD.Parent = drag_Frame
	
	return slot_Holder
end
-- Define the InventorySlot class
local InventorySlot = {}
InventorySlot.__index = InventorySlot

-- Constructor
function InventorySlot.new(parent)
	local self = setmetatable({}, InventorySlot)
	self.UI = generate_slot()
	self.DragFrame = self.UI.Drag_Frame
	
	-- Connect UI events
	self.UI.MouseEnter:Connect(function()
		parent.CurrentlyHovering = self
		self.DragFrame.BackgroundColor3 = Color3.fromRGB(94, 94, 94)
		self.DragFrame.BackgroundTransparency = 0.6
	end)

	self.UI.MouseLeave:Connect(function()
		parent.CurrentlyHovering = nil
		self.DragFrame.BackgroundColor3 = Color3.fromRGB(150,150,150)
		self.DragFrame.BackgroundTransparency = 0.3
	end)
	
	-- Drag method
	function InventorySlot:Drag(input)
		self.DragFrame.ZIndex = 2
		self.DragFrame.ID.ZIndex=3
		self.DragFrame.BackgroundTransparency = 0
		local distanceMovedX = self.InitialX - input.Position.X
		local distanceMovedY = self.InitialY - input.Position.Y

		if parent.CurrentlyHovering then
			local offset = parent.CurrentlyHovering.UI.AbsolutePosition - self.UI.AbsolutePosition
			local x,y = offset.X,offset.Y
			self.DragFrame.Position = UDim2.new(0,x,0,y)
		else
			self.DragFrame.Position = -UDim2.new(0, distanceMovedX, 0, distanceMovedY)
		end

	end

	-- EndDrag method
	function InventorySlot:EndDrag()
		self.DragFrame.ZIndex = 1
		self.DragFrame.ID.ZIndex=1
		self.DragFrame.BackgroundTransparency = 0.3
		if parent.CurrentlyHovering and parent.CurrentlyHovering ~= self.UI then
			-- Implement item merging logic here
			print("Merging items")
self.DragFrame.Position = UDim2.new(0, 0, 0, 0)
		else
			-- If not dropped onto another slot, reset the position
			self.DragFrame.Position = UDim2.new(0, 0, 0, 0)
		end

		parent.InputChangedConn:Disconnect()
	end


	return self
end



local DragInventory = {}
DragInventory.__index = DragInventory


function DragInventory.new(slots)
	local self = setmetatable({},DragInventory)
	self.Holding = false
	self.CurrentlyHovering = nil
	
	self.Content = {}
	for i = 1,slots do
		local new_slot = InventorySlot.new(self)
		new_slot.UI.Parent = game.Players.LocalPlayer.PlayerGui.ContainerGui:WaitForChild("Inventory")
		new_slot.UI.LayoutOrder = i
		new_slot.UI.Drag_Frame:WaitForChild("ID").Text = new_slot.UI.LayoutOrder
		table.insert(self.Content,new_slot)
	end

	UIS.InputBegan:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
			self.Holding = self.CurrentlyHovering or false
			if self.Holding then
				self.Holding.InitialX, self.Holding.InitialY = input.Position.X, input.Position.Y
				self.Holding.UIInitialPos = self.Holding.UI.Position

				self.InputChangedConn = UIS.InputChanged:Connect(function(input)
					if input.UserInputType == Enum.UserInputType.MouseMovement then
						self.Holding:Drag(input)
					end
				end)
			end
		end
	end)

	UIS.InputEnded:Connect(function(input)
		if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
			if self.Holding then
				print(self.CurrentlyHovering and self.CurrentlyHovering.UI.LayoutOrder)
				-- Inventory action
				if self.CurrentlyHovering then
					self.Holding.UI.LayoutOrder,self.CurrentlyHovering.UI.LayoutOrder = self.CurrentlyHovering.UI.LayoutOrder,self.Holding.UI.LayoutOrder
				end
				--self.Holding.UI.Drag_Frame.ID.Text = self.Holding.UI.LayoutOrder
				--self.CurrentlyHovering.UI.Drag_Frame.ID.Text = self.CurrentlyHovering.UI.LayoutOrder
				self.Holding:EndDrag()
			end
			self.Holding = false
		end
	end)

	return self
end


return DragInventory
28 Likes

Cool resource.
Thanks for sharing! :slight_smile:

No worries, friend. Feel free to browse my profile here for some other tutorials.

Hi,

Does this work on all devices?

Thanks

No, this would only work on PC. It may accidentally work on other devices, but it’s definitely not made for that.

Hi there, how would you modify this for creating a system where you can drag the UI and drop into the correct position. An example is a jigsaw puzzle, where it snaps to the position where a piece is supposed to be, but otherwise is free to drag.

That would honestly be a significantly different issue, because you’d be dealing with non-uniform shapes. This system doesn’t even account for differently sized rectangles. I will keep your question in mind, and maybe someday I’ll make a tutorial for that, but that’d certainly cost me many hours which I do not have at the moment.

1 Like

This resource was very helpful as a base for my inventory system :slightly_smiling_face: Thanks for sharing this!