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.
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