Satchel // Open-source modern backpack system

Wonderful system! I found something interesting you should consider changing in your script:

Problem:
Tool does not show tool name when texture ID is set to empty during game

Repro:

  1. Change a tool’s texture ID
  2. Remove the texture ID
  3. It does not display the tool name. ~ slot is blank.

This is because, although you set ToolName.Visible to false, it is never set to true ever again!!

New changes:


With these new changes, the ToolName.Visible will toggle off and on depending on if the icon is set to “” or not.

Hey, thanks for the very detailed information about the bug. I believe that I’ve already made changes previously to another problem which fixes your problem too.

Please review commit 6a9cf5b for more about the fix.

For now, you can either copy the commit’s changes to your local copy or use Rojo to sync the main branch of Satchel which contains the fixes along with other unpublished changes.

I appreciate your detailed report and solution of your bug. If the issue still occurs then you can reply or message me.

1 Like

Hi, I have three suggestions and the last suggestion here in this post is something you should consider changing in your script, let’s begin!

Suggestions

Change the ScrollingFrame's BorderSizePixel to 0

Problem:
There’s a weird line that appears next to the Scrollbar whenever the player is Scrolling on a Computer or Mobile. (Tools are copied from Satchel Playground and duplicated for demonstration purposes.)

Video

This is because the ScrollingFrame’s BorderSizePixel is not set to 0, rather it is set to 1.

-- Make the ScrollingFrame, which holds the rest of the Slots (however many)
ScrollingFrame = NewGui("ScrollingFrame", "ScrollingFrame")
ScrollingFrame.Selectable = false
ScrollingFrame.ScrollingDirection = Enum.ScrollingDirection.Y
ScrollingFrame.ScrollBarThickness = 8
ScrollingFrame.ScrollBarImageColor3 = Color3.new(1, 1, 1)
ScrollingFrame.VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar
ScrollingFrame.CanvasSize = UDim2.new(0, 0, 0, 0)
ScrollingFrame.Parent = InventoryFrame

New Changes
To fix this, we have to set the ScrollingFrame’s BorderSizePixel to 0.
Code

-- Make the ScrollingFrame, which holds the rest of the Slots (however many)
ScrollingFrame = NewGui("ScrollingFrame", "ScrollingFrame")
ScrollingFrame.Selectable = false
ScrollingFrame.ScrollingDirection = Enum.ScrollingDirection.Y
ScrollingFrame.BorderSizePixel = 0 -- [[ADDED]]
ScrollingFrame.ScrollBarThickness = 8
ScrollingFrame.ScrollBarImageColor3 = Color3.new(1, 1, 1)
ScrollingFrame.VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar
ScrollingFrame.CanvasSize = UDim2.new(0, 0, 0, 0)
ScrollingFrame.Parent = InventoryFrame

See that weird black line next to the scrollbar has finally disappeared below, now I can relax without worrying about that anymore.
Video

Add TextTruncate to ToolName (No attachments because it's self-explanatory)

Previous Changes
Code

ToolName = NewGui("TextLabel", "ToolName")
ToolName.Size = UDim2.new(1, -SLOT_EQUIP_THICKNESS * 2, 1, -SLOT_EQUIP_THICKNESS * 2)
ToolName.Position = UDim2.new(0.5, 0, 0.5, 0)
ToolName.AnchorPoint = Vector2.new(0.5, 0.5)
ToolName.Parent = SlotFrame

New Changes
Code

ToolName = NewGui("TextLabel", "ToolName")
ToolName.Size = UDim2.new(1, -SLOT_EQUIP_THICKNESS * 2, 1, -SLOT_EQUIP_THICKNESS * 2)
ToolName.Position = UDim2.new(0.5, 0, 0.5, 0)
ToolName.AnchorPoint = Vector2.new(0.5, 0.5)
ToolName.TextTruncate = Enum.TextTruncate.AtEnd -- [[ADDED]]
ToolName.Parent = SlotFrame
Better OnUISChanged (or Keyboard Status, this one might be complicated)

Problem:
Whenever a player goes using Keyboard whilst on Mobile, the hotkey labels (or SlotNumbers) appear, but if they go back into Touch (by using their hands or paws), the hotkey labels never disappear

Video

Code 1 (Optional, I recommend ignoring the mouse part)

local GAMEPAD_INPUT_TYPES: table = { -- These are the input types that will be used for gamepad
	[Enum.UserInputType.Gamepad1] = true,
	[Enum.UserInputType.Gamepad2] = true,
	[Enum.UserInputType.Gamepad3] = true,
	[Enum.UserInputType.Gamepad4] = true,
	[Enum.UserInputType.Gamepad5] = true,
	[Enum.UserInputType.Gamepad6] = true,
	[Enum.UserInputType.Gamepad7] = true,
	[Enum.UserInputType.Gamepad8] = true,
}

Code 2

local function OnUISChanged(property: string): ()
	if property == "KeyboardEnabled" or property == "VREnabled" then
		local on = UserInputService.KeyboardEnabled and not UserInputService.VREnabled
		for i = 1, NumberOfHotbarSlots do
			Slots[i]:TurnNumber(on)
		end
	end
end

Code 3

-- Listen to keyboard status, for showing/hiding hotkey labels
UserInputService.Changed:Connect(OnUISChanged)
OnUISChanged("KeyboardEnabled")

Video

New Changes
To fix this, we have to make a better one

Code 1 (Optional, I recommend ignoring the mouse part)

local MOUSE_INPUT_TYPES: table = { -- These are the input types that will be used for mouse -- [[ADDED]], Optional
	[Enum.UserInputType.MouseButton1] = true,
	[Enum.UserInputType.MouseButton2] = true,
	[Enum.UserInputType.MouseButton3] = true,
	[Enum.UserInputType.MouseMovement] = true,
	[Enum.UserInputType.MouseWheel] = true,
}

local GAMEPAD_INPUT_TYPES: table = { -- These are the input types that will be used for gamepad
	[Enum.UserInputType.Gamepad1] = true,
	[Enum.UserInputType.Gamepad2] = true,
	[Enum.UserInputType.Gamepad3] = true,
	[Enum.UserInputType.Gamepad4] = true,
	[Enum.UserInputType.Gamepad5] = true,
	[Enum.UserInputType.Gamepad6] = true,
	[Enum.UserInputType.Gamepad7] = true,
	[Enum.UserInputType.Gamepad8] = true,
}

Code 2

local function OnUISChanged(): () -- [[EDITED/FORKED]]
	-- Detect if player is using Touch
	if UserInputService:GetLastInputType() == Enum.UserInputType.Touch then
		for i = 1, NumberOfHotbarSlots do
			Slots[i]:TurnNumber(false)
		end
		return
	end

	-- Detect if player is using Keyboard
	if UserInputService:GetLastInputType() == Enum.UserInputType.Keyboard then
		for i = 1, NumberOfHotbarSlots do
			Slots[i]:TurnNumber(true)
		end
		return
	end

	-- Detect if player is using Mouse (Optional, I recommend ignoring the mouse part)
	for _, mouse in pairs(MOUSE_INPUT_TYPES) do
		if UserInputService:GetLastInputType() == mouse then
			for i = 1, NumberOfHotbarSlots do
				Slots[i]:TurnNumber(true)
			end
			return
		end

		-- Detect if player is using Controller
		for _, gamepad in pairs(GAMEPAD_INPUT_TYPES) do
			if UserInputService:GetLastInputType() == gamepad then
				for i = 1, NumberOfHotbarSlots do
					Slots[i]:TurnNumber(true)
				end
				return
			end
		end
	end
	
	--[[
	if property == "KeyboardEnabled" or property == "VREnabled" then -- replaced
		local on = UserInputService.KeyboardEnabled and not UserInputService.VREnabled
		for i = 1, NumberOfHotbarSlots do
			Slots[i]:TurnNumber(on)
		end
	end
	]]
end

Code 3

-- Listen to keyboard status, for showing/hiding hotkey labels
UserInputService.LastInputTypeChanged:Connect(OnUISChanged) -- [[EDITED/FORKED]], formerly UserInputService.Changed:Connect(OnUISChanged)
OnUISChanged()

Video


Here are all of the changes/suggestions in one Video

(Tools are copied from Satchel Playground and duplicated once again for demonstration purposes.)

and here’s the place file if you wanna try it out yourself.
Im_Andr3i’s Suggestions or Forks for Satchel.rbxl (471.1 KB)


To see the changes I’ve made in a SatchelScript, use the Find feature by clicking the Find button or by pressing Ctrl/Control + G then type “Added” or “Edited/Forked”.

I’m sure you can do the suggestions better than I and I’m planning to suggest another thing again so happy developing y’all! (Fun fact: I don’t have Github so I’m posting my suggestions here.)

2 Likes

Thank you so much for your detailed response and changes.

I will be reviewing these changes in the next few days and merging them into the public releases of Satchel.

Once again thanks for all your contributions. :grin:

2 Likes

Hello, I’m using Satchel for my game called Zombie Survival in Minot, North Dakota and I love the system. The UI is modernized and it makes a difference in the experience. So thank you for providing this resource.

I’m aware that the project is trying to stay close to functionality like the default backpack but I do think some minor changes would be appreciated, otherwise I can also modify the system myself. One of the features that I wish Satchel can have is the implementation of tool removal / drops for mobile or console devices.

I have a way to perform this modification myself, but I’m only inquiring to see if you’d be able to make the official change for the project. Other than that, this is almost the perfect backpack system for my game.

Have a wonderful weekend.

2 Likes

I’m always looking to help improve support for all platforms and I understand your concerns. It is a real bummer Roblox only allows tool dropping with a keyboard.

I would want to add the ability to drop tools but implementing it cleanly would be difficult. Xbox can bind to a button (this might mess up controls for pre-existing games) but for mobile a touch button would need to be added. Adding to the topbar or near the jump button would increase clutter and players can accidentally press it or cover existing UI. While I am actively looking towards a solution, adding too many buttons would just take away from Satchel’s vanilla nature.

If you have any other ideas or solution don’t hesitate to drop them below. But for now, I’m afraid that there isn’t a good way to implement tool dropping on platforms other than computer.

Yeah that’s understandable, there’s some aesthetic issues with this method. In that case, I could share my way of going about the mobile tool drop, which I was planning to add either way.

When dragging a tool outside of the backpack and releasing the hold, it will drop the tool. I’m planning to check the boundaries using size overlapping checks. Another way I would do this, which can help with directional drops, would be using a viewport raycast to detect Gui objects in the path, which will also return a position from 2D space to 3D space.

I’m not saying you should add this feature, but this is how I will be modifying the Satchel for my game. I’m willing to share the code by PR on Github once it’s ready and requested.

Edit: I’ve noticed that there’s already a function that does the check, so I’ll just modify that part on line 902 on SatchelScript. It looks like you already attempted to add this.

Any ETA on .Draggable usage being removed?
I use satchel in a project of mine but I constantly have the thought of the fact that deprecated stuff is being used. (Well just one thing in this case but still)
I’m somebody who spends hours just trying to fix typechecking issues so maybe thats just me but I’m still not happy about a deprecated property being used.

I would try to do it myself but I’m kinda busy doing other stuff, also I didnt write satchel so I don’t have a full enough understanding of what everything does.

Nicely done— just one suggestion: an additional button when an item is added to your inventory rather than your hotbar with an ellipses (…) icon which can be pressed to easily open the inventory.

1 Like

This is an interesting way of dropping the tool instead of just using the existing functions for dropping the tool.

I would be interested in seeing the PR and how it would be implemented. With Satchel we need to ensure compatibility with it’s target platforms: computer, phone, tablet, console, and VR. So heads up that the PR would need to be compatible with it’s targert platforms to be merged.

Currently there is no ETA on removal .Draggable. While I did plan to remove the deprecated property, I’ve ran into some issues implementing it, especially on VR platforms.

You can track the progress at the below GitHub issue.

That is unfortunate. However, your comment saying that there are no problems is incorrect. After trying it on a lower framerate (like 30FPS), this issue is more visible:

I believe it is necessary to use .InputBegan to your implementation.

local UserInputService = game:GetService("UserInputService")

local gui = script.Parent

local dragging
local dragInput
local dragStart
local startPos

gui:GetPropertyChangedSignal("CanvasPosition"):Connect(function()
	dragging = false
end)

local function update(input)
	local delta = input.Position - dragStart
	gui.Position = UDim2.new(startPos.X.Scale, startPos.X.Offset + delta.X, startPos.Y.Scale, startPos.Y.Offset + delta.Y)
end

gui.InputBegan:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then
		dragging = true
		dragStart = input.Position
		startPos = gui.Position
		input.Changed:Connect(function()
			if input.UserInputState == Enum.UserInputState.End then
				dragging = false
			end
		end)
	end
end)

gui.InputChanged:Connect(function(input)
	if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then --for VR, there is no dragging implementation, so remove this line
		dragInput = input
	end
end)

UserInputService.InputChanged:Connect(function(input)
	if input == dragInput and dragging then
		update(input)
	end
end)

This is a modified implementation of the one that Roblox staff have sent. I’m unsure if it works on VR, but if not, that may be a limitation of the inputObject.Position property and should not behave differently from GuiObject.Draggable

2 Likes

Hi again, get ready for another suggestion! This one will involve the ScrollingFrame, let’s begin!


(Tools are copied from Satchel Playground and duplicated for demonstration purposes.)

You may already notice that if a player scrolls down a little bit, they’ll start seeing the attachments below.


So if you want to change that and you want it to look like the attachments below


Here’s how we can do it, but keep in mind this might cause some problems in some platforms like VR and XBox, so don’t forget to fix them. (If it turns out it actually has problems)


Code 1

We are going to change this

local MainFrame = nil
local HotbarFrame = nil
local InventoryFrame = nil
local VRInventorySelector = nil
local ScrollingFrame: ScrollingFrame = nil
local UIGridFrame: Frame = nil
local UIGridLayout: UIGridLayout = nil
local ScrollUpInventoryButton = nil
local ScrollDownInventoryButton = nil

To this, here we have added two instances/things; a UIListLayout, and a Frame called SpacingFrame

local MainFrame = nil
local HotbarFrame = nil
local InventoryFrame = nil
local VRInventorySelector = nil
local ScrollingFrame: ScrollingFrame = nil
local UIListLayout: UIListLayout = nil -- [[ADDED]]
local SpacingFrame: Frame = nil -- [[ADDED]]
local UIGridFrame: Frame = nil
local UIGridLayout: UIGridLayout = nil
local ScrollUpInventoryButton = nil
local ScrollDownInventoryButton = nil

Code 2

We are going to change this

local function UpdateScrollingFrameCanvasSize(): ()
	local countX = math.floor(ScrollingFrame.AbsoluteSize.X / (ICON_SIZE_PIXELS + ICON_BUFFER_PIXELS))
	local maxRow = math.ceil((#UIGridFrame:GetChildren() - 1) / countX)
	local canvasSizeY = maxRow * (ICON_SIZE_PIXELS + ICON_BUFFER_PIXELS) + ICON_BUFFER_PIXELS
	ScrollingFrame.CanvasSize = UDim2.new(0, 0, 0, canvasSizeY)
end

To this, here we have changed one line of code, which is the ScrollingFrame’s CanvasSize, I just added 35 into the code, 35 I believe is the most consistent, 36 is less consistent, and 40 is the most inconsistent but kind of looks fine (I tried my best to make ScrollingFrame’s CanvasSize constant), ehh I don’t know but moving on!

local function UpdateScrollingFrameCanvasSize(): ()
	local countX = math.floor(ScrollingFrame.AbsoluteSize.X / (ICON_SIZE_PIXELS + ICON_BUFFER_PIXELS))
	local maxRow = math.ceil((#UIGridFrame:GetChildren() - 1) / countX)
	local canvasSizeY = maxRow * (ICON_SIZE_PIXELS + ICON_BUFFER_PIXELS) + ICON_BUFFER_PIXELS
	ScrollingFrame.CanvasSize = UDim2.new(0, 0, 0, canvasSizeY + 35) -- [[EDITED/FORKED]], or 36 or 40, UDim2.new(0, 0, 0, canvasSizeY)
end

Code 3 (the longest code here)

We going to change this

local function UpdateBackpackLayout(): ()
	HotbarFrame.Size = UDim2.new(
		0,
		ICON_BUFFER_PIXELS + (NumberOfHotbarSlots * (ICON_SIZE_PIXELS + ICON_BUFFER_PIXELS)),
		0,
		ICON_BUFFER_PIXELS + ICON_SIZE_PIXELS + ICON_BUFFER_PIXELS
	)
	HotbarFrame.Position = UDim2.new(0.5, -HotbarFrame.Size.X.Offset / 2, 1, -HotbarFrame.Size.Y.Offset)
	InventoryFrame.Size = UDim2.new(
		0,
		HotbarFrame.Size.X.Offset,
		0,
		(HotbarFrame.Size.Y.Offset * NumberOfInventoryRows)
			+ INVENTORY_HEADER_SIZE
			+ (IsVR and 2 * INVENTORY_ARROWS_BUFFER_VR or 0)
	)
	InventoryFrame.Position = UDim2.new(
		0.5,
		-InventoryFrame.Size.X.Offset / 2,
		1,
		HotbarFrame.Position.Y.Offset - InventoryFrame.Size.Y.Offset
	)

	ScrollingFrame.Size = UDim2.new(
		1,
		ScrollingFrame.ScrollBarThickness + 1,
		1,
		-INVENTORY_HEADER_SIZE - (IsVR and 2 * INVENTORY_ARROWS_BUFFER_VR or 0)
	)
	ScrollingFrame.Position = UDim2.new(0, 0, 0, INVENTORY_HEADER_SIZE + (IsVR and INVENTORY_ARROWS_BUFFER_VR or 0))
	AdjustHotbarFrames()
	AdjustInventoryFrames()
end

To this, here we have made the ScrollingFrame’s Size to be the same height as the Inventory Frame, and we have made the ScrollingFrame’s Position to be completely Zero, or UDim2.new(0, 0, 0, 0). This is the part that could possibly cause problems with VR, but since I don’t have a headset (or VR headset), I don’t have a solution to fix the problem if I knew there was a problem, so keep note of that I guess, moving on

local function UpdateBackpackLayout(): ()
	HotbarFrame.Size = UDim2.new(
		0,
		ICON_BUFFER_PIXELS + (NumberOfHotbarSlots * (ICON_SIZE_PIXELS + ICON_BUFFER_PIXELS)),
		0,
		ICON_BUFFER_PIXELS + ICON_SIZE_PIXELS + ICON_BUFFER_PIXELS
	)
	HotbarFrame.Position = UDim2.new(0.5, -HotbarFrame.Size.X.Offset / 2, 1, -HotbarFrame.Size.Y.Offset)
	InventoryFrame.Size = UDim2.new(
		0,
		HotbarFrame.Size.X.Offset,
		0,
		(HotbarFrame.Size.Y.Offset * NumberOfInventoryRows)
			+ INVENTORY_HEADER_SIZE
			+ (IsVR and 2 * INVENTORY_ARROWS_BUFFER_VR or 0)
	)
	InventoryFrame.Position = UDim2.new(
		0.5,
		-InventoryFrame.Size.X.Offset / 2,
		1,
		HotbarFrame.Position.Y.Offset - InventoryFrame.Size.Y.Offset
	)

	ScrollingFrame.Size = UDim2.new(
		1,
		ScrollingFrame.ScrollBarThickness + 1,
		1,
		0) -- -INVENTORY_HEADER_SIZE - (IsVR and 2 * INVENTORY_ARROWS_BUFFER_VR or 0) -- [[EDITED/FORKED]]
	-- ) -- [[EDITED/FORKED]]
	ScrollingFrame.Position = UDim2.new(0, 0, 0, 0) -- UDim2.new(0, 0, 0, INVENTORY_HEADER_SIZE + (IsVR and INVENTORY_ARROWS_BUFFER_VR or 0)) -- [[EDITED/FORKED]]
	AdjustHotbarFrames()
	AdjustInventoryFrames()
end

Code 4 (and Lastly)

We are going to change this

-- Make the ScrollingFrame, which holds the rest of the Slots (however many)
ScrollingFrame = NewGui("ScrollingFrame", "ScrollingFrame")
ScrollingFrame.Selectable = false
ScrollingFrame.ScrollingDirection = Enum.ScrollingDirection.Y
ScrollingFrame.ScrollBarThickness = 8
ScrollingFrame.ScrollBarImageColor3 = Color3.new(1, 1, 1)
ScrollingFrame.VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar
ScrollingFrame.CanvasSize = UDim2.new(0, 0, 0, 0)
ScrollingFrame.Parent = InventoryFrame

To this, here we have added two of the things/instances I have mentioned in Code 1; a UIListLayout, and a Frame called SpacingFrame, I have also finally added their properties here. (I tried my best to make SpacingFrame’s Size consistent as well)

-- Make the ScrollingFrame, which holds the rest of the Slots (however many)
ScrollingFrame = NewGui("ScrollingFrame", "ScrollingFrame")
ScrollingFrame.Selectable = false
ScrollingFrame.ScrollingDirection = Enum.ScrollingDirection.Y
ScrollingFrame.ScrollBarThickness = 8
ScrollingFrame.ScrollBarImageColor3 = Color3.new(1, 1, 1)
ScrollingFrame.VerticalScrollBarInset = Enum.ScrollBarInset.ScrollBar
ScrollingFrame.CanvasSize = UDim2.new(0, 0, 0, 0)
ScrollingFrame.Parent = InventoryFrame

UIListLayout = Instance.new("UIListLayout") -- [[ADDED]]
UIListLayout.SortOrder = Enum.SortOrder.LayoutOrder
UIListLayout.HorizontalAlignment = Enum.HorizontalAlignment.Center
UIListLayout.Parent = ScrollingFrame

SpacingFrame = NewGui("Frame", "SpacingFrame") -- [[ADDED]]
SpacingFrame.Selectable = false
SpacingFrame.Size = UDim2.new(1, 0, 0, 40)
--SpacingFrame.Position = UDim2.new(0, ICON_BUFFER_PIXELS, 0, 0)
SpacingFrame.Parent = ScrollingFrame

And there you go, that’s how we can achieve it. Here’s another place file if you wanna see how it works
Im_Andr3i’s Suggestions or Forks for Satchel 2.rbxl (654.5 KB)
What’s new in there is there’s a new TestBackpack ScreenGui thing that doesn’t work, an AllForks + ScrollingFrame Fork Script, and a ScrollingFrame Fork Script.

To see the changes I’ve made in a SatchelScript, use the Find feature by clicking the Find button or by pressing Ctrl/Control + G then type “Added” or “Edited/Forked”.

I hope this is helpful and don’t forget to take breaks, take care!

1 Like

By the way, you can change:

gui.Changed:Connect(function(c)
	if c=="CanvasPosition" then
		dragging = false
	end
end)

To:

gui:GetPropertyChangedSignal("CanvasPosition"):Connect(function()
	dragging = false
end)
1 Like

Yes, the code I shared was made before that method was available, and I forgot to change it. Thank you for the information.

It was a mistake from my end while making that comment and not researching enough into how buggy .Draggable is.

Thanks for the code, I will look into it and test the compatibility across platforms.

The clipping is intentional behavior to help improve the visibility of the search bar. I do like how nicely it was put together but for now your solution won’t be implemented officially. Thanks for the help though.

1 Like

Hey, Is there any way I could make the hotbar always visible?
Thank you.

The hotbar should be visible by default. Can you specify what you mean?

I notice that the hotbar will go visible if you have a tool in backpack.