5.11 A Shop and Currency System

Returning to our merchant example, let’s extend that by creating a rudimentary shop and currency system.

First, we need a way to acquire gold. Let’s start with that first.

This can be implemented by scattering gold coins throughout the map and listening for touch events to pick them up.

Create a folder named ‘Coins’ and add to it a cylinder part. Set the size to (0.25, 2, 2) or whatever size you deem most coin-like. Just don’t make them too small otherwise the touch event might fail to detect.

For the material, I made mine metal and ‘Deep Orange’ for the BrickColor.

Then make lots of copies of the part and distribute them throughout the map. I also positioned them at a height of 200, which will cause the coins to rain down and scatter when the game starts.

For some practice, you can use a loop to create the gold pieces at random heights and positions.


Our merchant will be a bit square. Create a (4, 4, 1) Block part and name it ‘MerchantBlock.’

Then add a SurfaceGui and to that, a Frame. You may need to adjust the ‘Face’ property on the SurfaceGui to to make it face the correct direction.

Set the frame size scale to (1, 1). This will make it fill the face of the block and then add an ImageLabel and a TextLabel to the Frame.

The ImageLabel will be the product icon for the giant beetle and the TextLabel will display its price. We will set both later in code.

For the ImageLabel size, set the scale also to (1, 1). This will make it fill the Frame.

On the TextLabel, set the anchor and the position scale to (0.5, 1) which will center it horizontally and shift it all the way down vertically.

Then for the size, set the X-scale to 1 and Y-offset to 50. This makes the TextLabel the full width of the block but limits it to 50 pixels in height.

Finally set the Z-index on the TextLabel to 5. This will render it after the ImageLabel and make it display in front of the ImageLabel, otherwise the TextLabel will be hidden behind the ImageLabel.


Now let’s introduce some new classes.

Since we want to be able to click the merchant to make a purchase, we need to connect some event which can detect that. But unlike touched events, parts do not have a click detect event.

The simplest way to implement a part clicked event is via the ClickDetector. Add one to the block.

Also add a StringValue to the block.

StringValue, NumberValue, ObjectValue, Vector3Value, etc. are designed to store a single value, as indicated by their class name.

Because that value can be directly set in the properties window, one of their most useful functionalities is to hold configuration settings during development. This also makes it easy to collect those settings at a single place, such as a in configuration folder.

If placed in a shared directory such as ReplicatedStorage, values that are updated by the server will replicate to other clients, making them for automatically syncing some value such as, say, the gold or points each player currently has.

We’ll use the StringValue to set the item name for this merchant is selling. Rename it ‘ItemName’ and then in the properties window click the ‘Value’ property and type in “giant beetle.”


For the scripts, we will need a ModuleScript for the currency system and another for the merchant.

As for where ModuleScripts should be placed?

Because a client can install anything on their own machine, including applications that allow them to extract and take any assets stored there. So the best way to prevent theft is to not store it there in the first place.

It is unlikely feasible to create a game where no scripts are ever replicated to client machines, but you can take reasonable steps to not replicate any more than you have to. Because ServerStorage and ServerScriptService don’t replicate to clients, those are generally the best locations to store ModuleScripts that clients don’t need.

Insert two ModuleScripts into ServerStorage and name them ‘CurrencySystem’ and ‘Merchant.’

Then also add a Script into ServerScriptService. This is the main script and will require the other two scripts. This will also handle store duties, coin collecting, and currency transactions.


Let’s write the currency module first.

We will need a table to keep track of players. This may be a local table or returned as part of the module. Since we don’t want other scripts to be able to modify this table directly, we’ll make it local and just provide functions for other scripts to call.

The interface will be kept minimal. There just needs to be a function to add new players, a function to add funds, and one to withdraw.

Lua
--/ServerStorage/CurrencySystem (ModuleScript)
local currentPlayers = {}
local startingGold = 10

local CurrencySystem = {}

function CurrencySystem.addPlayer(player)
	if not currentPlayers[player.Name] then
		local leaderstats = Instance.new("Folder")
		leaderstats.Name = "leaderstats"
		leaderstats.Parent = player
		
		local intValue = Instance.new("IntValue")
		intValue.Name = "Gold"
		intValue.Value = startingGold
		intValue.Parent = leaderstats
		
		currentPlayers[player.Name] = {["gold"] = intValue, ["leaderstats"] = leaderstats}
	end
end

function CurrencySystem.addGold(playerName, amount)
	if not playerName or not currentPlayers[playerName] or not amount then return false end

	currentPlayers[playerName]["gold"].Value =  currentPlayers[playerName]["gold"].Value + amount
	return currentPlayers[playerName]["gold"].Value
end

function CurrencySystem.withdraw(playerName, amount)
	local returnAmount = 0

	if currentPlayers[playerName]["gold"].Value >= amount then
		returnAmount = amount
		currentPlayers[playerName]["gold"].Value = currentPlayers[playerName]["gold"].Value - amount

	else
		returnAmount = currentPlayers[playerName]["gold"].Value
		currentPlayers[playerName]["gold"].Value = 0

	end

	return returnAmount, currentPlayers[playerName]["gold"].Value
end

return CurrencySystem

For the player table, we are using the player’s name for the key.

Per a game session, this is usually a unique identifier and adequate for this purpose. However, since players can change their name, if you intend to save the data long term between game sessions, it is best to use Player.UserId.

The player’s UserId is unique and tied to the player account so that even if they change their name, their UserId does not change.

It is also possible to use the player object directly as the key, heeding what we mentioned earlier about object keys.


Roblox has a built-in way to display values on the screen.

The GUI that often shows up on the right hand side of the screen and used for points is called the leaderboard.

This can be enabled by parenting a folder name ‘leaderstats‘ to the player object. Then any IntValue, StringValue, or BoolValue placed into the folder will be displayed on the leaderboard with the value’s name for the stat and populated with the value.

At line 13 we create the IntValue and set the name to “Gold” then parent it to the folder.

In development, sanity checks and error handling will comprise a lot of the code you will write. Line 23 checks to make sure a player was specified and exists and an amount was given.

In the player table, the “gold” element only references the IntValue. If we directly assign the amount to that element, it will override the reference to the Intvalue, which is not what we want. Assign the new value to the the ‘Value’ property.

The leaderboard will automatically update anytime the value changes.

The withdraw function checks to see if the player has sufficient funds and returns the requested amount or the player’s remaining funds; whichever is smallest.

A different way you might approach this is to return 0 and have the calling script handle an insufficient funds situation then decide if it wants to make a request for the remaining funds.

This script cannot remove players so for a larger game, you might also decide to add that ability.


Onto our merchant script, this one does not differ much from the one we saw in the previous section. We’ll add a way query for the price of an item and also a merchant gold variable.

Lua
--/ServerStorage/Merchant (ModuleScript)
local merchantGold = 25

local inventory = {
	["giant beetle"] = {["price"] = 10, ["stock"] = 8, ["description"] = {["brickColor"] = BrickColor.new("Burnt Sienna")}},
	--["wooden stick"] = {["price"] = 5, ["stock"] = 1, ["sell price"] = 2}
}

local function createItem(item, description)
	local newPart = Instance.new("Part")
	newPart.Name = item
	newPart.Size = Vector3.new(1, 1, 1)
	newPart.BrickColor = description["brickColor"]
	return newPart
end

local merchant = {}

merchant["buy item"] = function(item, gold)
	if not inventory[item] or inventory[item]["stock"] <= 0 then print("Out of stock") return false, gold end
	if gold < inventory[item]["price"] then print("Not enough gold") return false, gold end
	
	merchantGold = merchantGold + inventory[item]["price"]
	inventory[item]["stock"] = inventory[item]["stock"] - 1
	return createItem(item, inventory[item]["description"]), gold - inventory[item]["price"]
end

merchant["get price"] = function(item)
	if not inventory[item] then return end
	
	return inventory[item]["price"]
end
	
return merchant

Rather than returning just a string, this time we will return a physical object to represent the item. A local function is created to to handle that duty.


Since this isn’t a very complex game, we can just have the main script handle shop duties, initializing players, and managing gold.

We will need a listener that adds players into the currency system when they enter the game. We will also need to connect the coin touch events that will give the player gold.

Finally, we need to need setup the merchant, price and image, and connect the click detector to the merchant script.

Lua
--/ServerScriptService/Script
local Players = game:GetService("Players")

local currencySystem = require(game.ServerStorage.CurrencySystem)
local merchant = require(game.ServerStorage.Merchant)

local coinValueMin = 1
local coinValueMax = 6

for _, object in pairs(game.Workspace.Coins:GetChildren()) do
	if object.Name == "coin" then
		object.Touched:connect(function(otherPart)
			local player = Players:GetPlayerFromCharacter(otherPart.Parent)
			
			if player then
				object:Destroy()
				currencySystem.addGold(player.Name, math.random(coinValueMin, coinValueMax))
				
			end
		end)
	end
end


local merchantBlock = game.Workspace.MerchantBlock
local clickDetector = merchantBlock.ClickDetector
local frame = merchantBlock.Value.Value
frame.ImageLabel.Image = "http://www.roblox.com/asset/?id=17001559762"
frame.TextLabel.Text = "Giant Beetle =" .. merchant["get price"]("giant beetle") .. "💰"

clickDetector.MouseClick:Connect(function(player)
	local itemName = merchantBlock.ItemName.Value
	
	local price = merchant["get price"](itemName)
	local goldWithdrawn = currencySystem.withdraw(player.Name, price)
	local item
	
	if goldWithdrawn >= price then
		item, goldWithdrawn = merchant["buy item"](itemName, goldWithdrawn)
		
	else
		print("Not enough gold")
	end
	
	if item then
		item.Position = player.Character.PrimaryPart.Position
		item.Parent = game.Workspace
	end
	
	currencySystem.addGold(player.Name, goldWithdrawn)
end)


Players.PlayerAdded:Connect(function(player)
	currencySystem.addPlayer(player)
end)

The player service contains an event that fires whenever a player enters the game. This makes it useful for player initialization duties, including adding the player to the currency system.

A player leave event is also available—useful for removing the player from the currency system if you decide to implement that into your script.

From the players service we also have the function ‘GetPlayerFromCharacter’ which, you guessed it, returns the player object associated with a character model.

Since most part parts are immediate children of the model (i.e. character), the part’s parent is passed to the function to check for a player. A nil result means the part does not belong to a player and can be ignored. Otherwise, this is where we give the player gold and remove the part.

In some circumstances, the touch event can fire multiple times before the part is destroyed so if you were deploying this in an actual gave, it would be a good idea to implement a debouncer.