Perks Part 2
Overview
In this tutorial, you will be continuing with the project from Perks. You will be improving a few of the previous systems, and adding some new features.
For further information about the Perks Program, click here.
- Completion Time: ~1 hour
- Prerequisite: Completed Perk Systems Part 1
- Skills you will learn:
- Validating limited time perks.
- Limited time perk expiration reminder.
- Gifting resources to players in game.
- Gifting perks to players in game.
- Removing gifted perks from players in game.
Validating Limited Time Perks
If a limited time perk expires while the player is on the server, then the player would still receive the benefits of that limited time perk until they rejoin. For example, if your VIP package included double XP, then the player would continue to receive double XP even though the perk has expired.
A good solution to this problem, is to have a task check if all the players on the server still have the perk. If they don't, then the player is no longer flagged as being a VIP.
Update VIPShopServer Script
Open the VIPShopServer script.
A task will be spawned that will loop through all the players on the server and check if they each have a VIP perk. If they do not have a VIP perk, then the resource for that perk is set to 0
. This will also update on the client side for the player, because the player's resource is being watched for changes.
local perkCheckTask = Task.Spawn(function()
for index, player in ipairs(Game.GetPlayers()) do
if not player:HasPerk(VIP_BRONZE_PERK) then
player:SetResource("bronzevip", 0)
end
if not player:HasPerk(VIP_SILVER_PERK) then
player:SetResource("silvervip", 0)
end
if not player:HasPerk(VIP_GOLD_PERK) then
player:SetResource("goldvip", 0)
end
end
end)
perkCheckTask.repeatCount = -1
perkCheckTask.repeatInterval = (60 * 5)
The task doesn't need to run that often. In this case, it will run every 5 minutes.
perkCheckTask.repeatCount = -1
perkCheckTask.repeatInterval = (60 * 5)
The VIPShopServer Script
VIPShopServer
local VIP_BRONZE_PERK = script:GetCustomProperty("VIPBronze")
local VIP_SILVER_PERK = script:GetCustomProperty("VIPSilver")
local VIP_GOLD_PERK = script:GetCustomProperty("VIPGold")
local function PerkChanged(buyer, perk)
local gemsAmount = 0
local coinsAmount = 0
if perk == VIP_BRONZE_PERK then
buyer:SetResource("bronzevip", 1)
gemsAmount = 250
coinsAmount = 1000
elseif perk == VIP_SILVER_PERK then
buyer:SetResource("silvervip", 1)
gemsAmount = 500
coinsAmount = 3000
elseif perk == VIP_GOLD_PERK then
buyer:SetResource("goldvip", 1)
gemsAmount = 1000
coinsAmount = 7500
end
local playerData = Storage.GetPlayerData(buyer)
buyer:AddResource("gems", gemsAmount)
buyer:AddResource("coins", coinsAmount)
-- g (Gems)
if not playerData["g"] then
playerData["g"] = 0
end
-- c (Coins)
if not playerData["c"] then
playerData["c"] = 0
end
playerData["g"] = playerData["g"] + gemsAmount
playerData["c"] = playerData["c"] + coinsAmount
Storage.SetPlayerData(buyer, playerData)
end
local function OnJoined(player)
player.perkChangedEvent:Connect(PerkChanged)
if player:HasPerk(VIP_BRONZE_PERK) then
player:SetResource("bronzevip", 1)
end
if player:HasPerk(VIP_SILVER_PERK) then
player:SetResource("silvervip", 1)
end
if player:HasPerk(VIP_GOLD_PERK) then
player:SetResource("goldvip", 1)
end
end
local function OnChatMessage(speaker, params)
if speaker:HasPerk(VIP_GOLD_PERK) then
params.speakerName = "[Gold VIP] " .. params.speakerName
elseif speaker:HasPerk(VIP_SILVER_PERK) then
params.speakerName = "[Silver VIP] " .. params.speakerName
elseif speaker:HasPerk(VIP_BRONZE_PERK) then
params.speakerName = "[Bronze VIP] " .. params.speakerName
end
end
local perkCheckTask = Task.Spawn(function()
for index, player in ipairs(Game.GetPlayers()) do
if not player:HasPerk(VIP_BRONZE_PERK) then
player:SetResource("bronzevip", 0)
end
if not player:HasPerk(VIP_SILVER_PERK) then
player:SetResource("silvervip", 0)
end
if not player:HasPerk(VIP_GOLD_PERK) then
player:SetResource("goldvip", 0)
end
end
end)
perkCheckTask.repeatCount = -1
perkCheckTask.repeatInterval = (60 * 5)
Game.playerJoinedEvent:Connect(OnJoined)
Chat.receiveMessageHook:Connect(OnChatMessage)
Update VIPShopClient Script
Open the VIPShopClient script.
Because the status of a VIP can now change, you need to check to see if the status of each VIP resource is set to 1
. If it is not, then the VIP badge in the UI will have the visibility
property set to FORCE_OFF
.
local function UpdateStatus(player, resource, amount)
if resource == "bronzevip" or resource == "silvervip" or resource == "goldvip" then
local bronzeStatus = localPlayer:GetResource("bronzevip")
local silverStatus = localPlayer:GetResource("silvervip")
local goldStatus = localPlayer:GetResource("goldvip")
local hasStatus = false
if goldStatus == 1 then
ShowVIPStatus(VIP_GOLD_STATUS)
hasStatus = true
else
VIP_GOLD_STATUS.visibility = Visibility.FORCE_OFF
end
if not hasStatus and silverStatus == 1 then
ShowVIPStatus(VIP_SILVER_STATUS)
hasStatus = true
else
VIP_SILVER_STATUS.visibility = Visibility.FORCE_OFF
end
if not hasStatus and bronzeStatus == 1 then
ShowVIPStatus(VIP_BRONZE_STATUS)
hasStatus = true
else
VIP_BRONZE_STATUS.visibility = Visibility.FORCE_OFF
end
end
end
The VIPShopClient Script
VIPShopClient
local VIP_SHOP_UI = script:GetCustomProperty("VIPShopUI"):WaitForObject()
local VIP_SHOP_TRIGGER = script:GetCustomProperty("VIPShopTrigger"):WaitForObject()
local VIP_SHOP_BUTTON = script:GetCustomProperty("VIPShopButton"):WaitForObject()
local VIP_BRONZE_STATUS = script:GetCustomProperty("VIPBronzeStatus"):WaitForObject()
local VIP_SILVER_STATUS = script:GetCustomProperty("VIPSilverStatus"):WaitForObject()
local VIP_GOLD_STATUS = script:GetCustomProperty("VIPGoldStatus"):WaitForObject()
local vipStatuses = { VIP_BRONZE_STATUS, VIP_SILVER_STATUS, VIP_GOLD_STATUS }
local localPlayer = Game.GetLocalPlayer()
local inTrigger = false
local function CloseUI()
VIP_SHOP_UI.visibility = Visibility.FORCE_OFF
if inTrigger then
VIP_SHOP_TRIGGER.isInteractable = true
else
VIP_SHOP_TRIGGER.isInteractable = false
end
UI.SetCursorVisible(false)
UI.SetCanCursorInteractWithUI(false)
end
local function OnInteracted(trigger, obj)
if inTrigger and Object.IsValid(obj) and obj:IsA("Player") and obj == localPlayer then
VIP_SHOP_UI.visibility = Visibility.FORCE_ON
VIP_SHOP_TRIGGER.isInteractable = false
UI.SetCursorVisible(true)
UI.SetCanCursorInteractWithUI(true)
end
end
local function OnExitTrigger(trigger, obj)
if Object.IsValid(obj) and obj:IsA("Player") and obj == localPlayer then
inTrigger = false
CloseUI()
end
end
local function OnEnterTrigger(trigger, obj)
if Object.IsValid(obj) and obj:IsA("Player") and obj == localPlayer then
VIP_SHOP_TRIGGER.isInteractable = true
inTrigger = true
end
end
local function ShowVIPStatus(statusToShow)
for index, status in ipairs(vipStatuses) do
if status == statusToShow then
status.visibility = Visibility.FORCE_ON
else
status.visibility = Visibility.FORCE_OFF
end
end
end
local function UpdateStatus(player, resource, amount)
if resource == "bronzevip" or resource == "silvervip" or resource == "goldvip" then
local bronzeStatus = localPlayer:GetResource("bronzevip")
local silverStatus = localPlayer:GetResource("silvervip")
local goldStatus = localPlayer:GetResource("goldvip")
local hasStatus = false
if goldStatus == 1 then
ShowVIPStatus(VIP_GOLD_STATUS)
hasStatus = true
else
VIP_GOLD_STATUS.visibility = Visibility.FORCE_OFF
end
if not hasStatus and silverStatus == 1 then
ShowVIPStatus(VIP_SILVER_STATUS)
hasStatus = true
else
VIP_SILVER_STATUS.visibility = Visibility.FORCE_OFF
end
if not hasStatus and bronzeStatus == 1 then
ShowVIPStatus(VIP_BRONZE_STATUS)
hasStatus = true
else
VIP_BRONZE_STATUS.visibility = Visibility.FORCE_OFF
end
end
end
VIP_SHOP_BUTTON.clickedEvent:Connect(CloseUI)
VIP_SHOP_TRIGGER.interactedEvent:Connect(OnInteracted)
VIP_SHOP_TRIGGER.endOverlapEvent:Connect(OnExitTrigger)
VIP_SHOP_TRIGGER.beginOverlapEvent:Connect(OnEnterTrigger)
localPlayer.resourceChangedEvent:Connect(UpdateStatus)
for key, value in pairs(localPlayer:GetResources()) do
UpdateStatus(localPlayer, key, value)
end
Test the Game
Testing limited time perks is a little tricky due to the minimum duration being 60 minutes for all limited time perks in the editor. So to do a proper test, a few steps need to be done.
Enable a VIP Perk
From the Perk Testing window, enable the bronze VIP perk for testing.
Modify VIPShopServer Function
Modify the spawned task temporarily just while testing. The bronze VIP perk is being used for testing. The task will be delayed by 5 seconds (otherwise it will run as soon as you join), and check if the player owns the bronze VIP perk. If they do own it, then it is flagged as not being owned. This will simulate the limited time perk expiring while the player is on the server.
Play the game, and you will see the bronze VIP badge be hidden after 5 seconds, indicating that the perk has expired.
After the test was successful, revert the task back.
local perkCheckTask = Task.Spawn(function()
for index, player in ipairs(Game.GetPlayers()) do
if player:HasPerk(VIP_BRONZE_PERK) then
player:SetResource("bronzevip", 0)
end
if not player:HasPerk(VIP_SILVER_PERK) then
player:SetResource("silvervip", 0)
end
if not player:HasPerk(VIP_GOLD_PERK) then
player:SetResource("goldvip", 0)
end
end
end, 5)
PerkCheckTask
local perkCheckTask = Task.Spawn(function()
for index, player in ipairs(Game.GetPlayers()) do
if not player:HasPerk(VIP_BRONZE_PERK) then
player:SetResource("bronzevip", 0)
end
if not player:HasPerk(VIP_SILVER_PERK) then
player:SetResource("silvervip", 0)
end
if not player:HasPerk(VIP_GOLD_PERK) then
player:SetResource("goldvip", 0)
end
end
end)
Creating Perk Reminders
In this section you will implement a system to remind the player that their VIP package has expired. This can be a useful for reminding a player that may have not logged into your game for some time, and the limited time perk they previous purchased has expired. This could help encourage repeat buys of VIP packages.
Update VIPShopServer Script
The VIPShopServer script will need to be updated so you can keep track if a player has purchased a VIP package before. To do this, you will be saving a flag in the player's Storage that will be updated every time a VIP package is purchased, or when the expired notice is displayed to the player.
Update PerkChanged Function
The hasVIP
variable will get set to 1
if any of the VIP perks are owned by the player. This is then saved in the playerData
table, which is stored for the player persistently. This will allow you track when to show the reminder to the player.
local function PerkChanged(buyer, perk)
local gemsAmount = 0
local coinsAmount = 0
local hasVIP = 0
if perk == VIP_BRONZE_PERK then
buyer:SetResource("bronzevip", 1)
gemsAmount = 250
coinsAmount = 1000
hasVIP = 1
elseif perk == VIP_SILVER_PERK then
buyer:SetResource("silvervip", 1)
gemsAmount = 500
coinsAmount = 3000
hasVIP = 1
elseif perk == VIP_GOLD_PERK then
buyer:SetResource("goldvip", 1)
gemsAmount = 1000
coinsAmount = 7500
hasVIP = 1
end
local playerData = Storage.GetPlayerData(buyer)
buyer:AddResource("gems", gemsAmount)
buyer:AddResource("coins", coinsAmount)
-- g (Gems)
if not playerData["g"] then
playerData["g"] = 0
end
-- c (Coins)
if not playerData["c"] then
playerData["c"] = 0
end
playerData["g"] = playerData["g"] + gemsAmount
playerData["c"] = playerData["c"] + coinsAmount
playerData["vip"] = hasVIP
Storage.SetPlayerData(buyer, playerData)
end
Update OnJoined Function
When a player joins the game, the OnJoined
function will check to see if the player owns a VIP perk, and if so, set a new resource called showvipnotice
. This will be watched for in the client script, and if it equals 1
, the reminder UI will be shown to the player. The vip
property for the players data needs to be updated so they don't get a reminder each time they join the server.
local function OnJoined(player)
player.perkChangedEvent:Connect(PerkChanged)
local hasVIP = false
local playerData = Storage.GetPlayerData(player)
if player:HasPerk(VIP_BRONZE_PERK) then
player:SetResource("bronzevip", 1)
hasVIP = true
end
if player:HasPerk(VIP_SILVER_PERK) then
player:SetResource("silvervip", 1)
hasVIP = true
end
if player:HasPerk(VIP_GOLD_PERK) then
player:SetResource("goldvip", 1)
hasVIP = true
end
if playerData.vip ~= nil then
if not hasVIP and playerData.vip == 1 then
playerData.vip = 0
player:SetResource("showvipnotice", 1)
Storage.SetPlayerData(player, playerData)
end
end
end
The VIPShopServer Script
VIPShopServer
local VIP_BRONZE_PERK = script:GetCustomProperty("VIPBronze")
local VIP_SILVER_PERK = script:GetCustomProperty("VIPSilver")
local VIP_GOLD_PERK = script:GetCustomProperty("VIPGold")
local function PerkChanged(buyer, perk)
local gemsAmount = 0
local coinsAmount = 0
local hasVIP = 0
if perk == VIP_BRONZE_PERK then
buyer:SetResource("bronzevip", 1)
gemsAmount = 250
coinsAmount = 1000
hasVIP = 1
elseif perk == VIP_SILVER_PERK then
buyer:SetResource("silvervip", 1)
gemsAmount = 500
coinsAmount = 3000
hasVIP = 1
elseif perk == VIP_GOLD_PERK then
buyer:SetResource("goldvip", 1)
gemsAmount = 1000
coinsAmount = 7500
hasVIP = 1
end
local playerData = Storage.GetPlayerData(buyer)
buyer:AddResource("gems", gemsAmount)
buyer:AddResource("coins", coinsAmount)
-- g (Gems)
if not playerData["g"] then
playerData["g"] = 0
end
-- c (Coins)
if not playerData["c"] then
playerData["c"] = 0
end
playerData["g"] = playerData["g"] + gemsAmount
playerData["c"] = playerData["c"] + coinsAmount
playerData["vip"] = hasVIP
Storage.SetPlayerData(buyer, playerData)
end
local function OnJoined(player)
player.perkChangedEvent:Connect(PerkChanged)
local hasVIP = false
local playerData = Storage.GetPlayerData(player)
if player:HasPerk(VIP_BRONZE_PERK) then
player:SetResource("bronzevip", 1)
hasVIP = true
end
if player:HasPerk(VIP_SILVER_PERK) then
player:SetResource("silvervip", 1)
hasVIP = true
end
if player:HasPerk(VIP_GOLD_PERK) then
player:SetResource("goldvip", 1)
hasVIP = true
end
if playerData.vip ~= nil then
if not hasVIP and playerData.vip == 1 then
playerData.vip = 0
player:SetResource("showvipnotice", 1)
Storage.SetPlayerData(player, playerData)
end
end
end
local function OnChatMessage(speaker, params)
if speaker:HasPerk(VIP_GOLD_PERK) then
params.speakerName = "[Gold VIP] " .. params.speakerName
elseif speaker:HasPerk(VIP_SILVER_PERK) then
params.speakerName = "[Silver VIP] " .. params.speakerName
elseif speaker:HasPerk(VIP_BRONZE_PERK) then
params.speakerName = "[Bronze VIP] " .. params.speakerName
end
end
local perkCheckTask = Task.Spawn(function()
for index, player in ipairs(Game.GetPlayers()) do
if not player:HasPerk(VIP_BRONZE_PERK) then
player:SetResource("bronzevip", 0)
end
if not player:HasPerk(VIP_SILVER_PERK) then
player:SetResource("silvervip", 0)
end
if not player:HasPerk(VIP_GOLD_PERK) then
player:SetResource("goldvip", 0)
end
end
end)
perkCheckTask.repeatCount = -1
perkCheckTask.repeatInterval = (60 * 5)
Game.playerJoinedEvent:Connect(OnJoined)
Chat.receiveMessageHook:Connect(OnChatMessage)
Update VIPShopClient Script
The VIPShopClient script will be updated to show the reminder to the player. To do this, you will need to create a few custom properties so the VIPShopClient script has a reference to a few things.
In the UI Container for the VIP Shop in the Hierarchy, there is a VIP Reminder panel.
- Add the VIP Reminder panel as a custom property. Name the property
VIPReminderUI
. - Add the VIP Shop Button object as a custom property. Name the property
OpenVIPShopButton
. - Add the Close Button object as a custom property. Name the property
CloseReminderUIButton
.
When the reminder UI is shown, it will also include the button to the VIP Shop for quick access for the player.
Create UI Reminder Variables
Create these variables so you have a reference to the various UI components for the reminder UI.
local VIP_REMINDER_UI = script:GetCustomProperty("VIPReminderUI"):WaitForObject()
local OPEN_VIP_SHOP_BUTTON = script:GetCustomProperty("OpenVIPShopButton"):WaitForObject()
local CLOSE_REMINDER_UI_BUTTON = script:GetCustomProperty("CloseReminderUIButton"):WaitForObject()
Set Player Name in UI
The reminder message contains a replacement string that can be replaced with the players name.
The text {name}
will be replaced with the localPlayer.name
.
local reminderText = VIP_REMINDER_UI:FindChildByName("Notice")
reminderText.text = reminderText.text:gsub("%{name%}", localPlayer.name)
Update UpdateStatus Function
The UpdateStatus
function is the listener being called when a resource has changed for the player. By adding a check for the resource showvipnotice
, you can turn on the reminder UI.
local function UpdateStatus(player, resource, amount)
if resource == "bronzevip" or resource == "silvervip" or resource == "goldvip" then
local bronzeStatus = localPlayer:GetResource("bronzevip")
local silverStatus = localPlayer:GetResource("silvervip")
local goldStatus = localPlayer:GetResource("goldvip")
local hasStatus = false
if goldStatus == 1 then
ShowVIPStatus(VIP_GOLD_STATUS)
hasStatus = true
else
VIP_GOLD_STATUS.visibility = Visibility.FORCE_OFF
end
if not hasStatus and silverStatus == 1 then
ShowVIPStatus(VIP_SILVER_STATUS)
hasStatus = true
else
VIP_SILVER_STATUS.visibility = Visibility.FORCE_OFF
end
if not hasStatus and bronzeStatus == 1 then
ShowVIPStatus(VIP_BRONZE_STATUS)
hasStatus = true
else
VIP_BRONZE_STATUS.visibility = Visibility.FORCE_OFF
end
elseif resource == "showvipnotice" then
VIP_REMINDER_UI.visibility = Visibility.FORCE_ON
UI.SetCursorVisible(true)
UI.SetCanCursorInteractWithUI(true)
end
end
Create CloseReminder Function
The CloseReminder
function will close the reminder UI when the player clicks the close button, or VIP shop button.
local function CloseReminder()
VIP_REMINDER_UI.visibility = Visibility.FORCE_OFF
UI.SetCursorVisible(false)
UI.SetCanCursorInteractWithUI(false)
end
Create OpenVIPShop Function
The OpenVIPShop
function will be called when the player clicks on the VIP shop button in the reminder UI.
local function OpenVIPShop()
VIP_REMINDER_UI.visibility = Visibility.FORCE_OFF
VIP_SHOP_UI.visibility = Visibility.FORCE_ON
VIP_SHOP_TRIGGER.isInteractable = false
end
Connect Events
Connect the CloseReminder
function to the clickedEvent
for the CLOSE_REMINDER_UI_BUTTON
. When the CLOSE_REMINDER_UI_BUTTON
is clicked by the player, it will close the UI.
CLOSE_REMINDER_UI_BUTTON.clickedEvent:Connect(CloseReminder)
Connect the OpenVIPShop
function to the clickedEvent
for the OPEN_VIP_SHOP_BUTTON
. When the OPEN_VIP_SHOP_BUTTON
is clicked by the player, it will open the VIP shop UI. This gives the player a shortcut to the VIP Shop to make a purchase.
OPEN_VIP_SHOP_BUTTON.clickedEvent:Connect(OpenVIPShop)
The VIPShopClient Script
VIPShopClient
local VIP_SHOP_UI = script:GetCustomProperty("VIPShopUI"):WaitForObject()
local VIP_SHOP_TRIGGER = script:GetCustomProperty("VIPShopTrigger"):WaitForObject()
local VIP_SHOP_BUTTON = script:GetCustomProperty("VIPShopButton"):WaitForObject()
local VIP_BRONZE_STATUS = script:GetCustomProperty("VIPBronzeStatus"):WaitForObject()
local VIP_SILVER_STATUS = script:GetCustomProperty("VIPSilverStatus"):WaitForObject()
local VIP_GOLD_STATUS = script:GetCustomProperty("VIPGoldStatus"):WaitForObject()
local VIP_REMINDER_UI = script:GetCustomProperty("VIPReminderUI"):WaitForObject()
local OPEN_VIP_SHOP_BUTTON = script:GetCustomProperty("OpenVIPShopButton"):WaitForObject()
local CLOSE_REMINDER_UI_BUTTON = script:GetCustomProperty("CloseReminderUIButton"):WaitForObject()
local vipStatuses = { VIP_BRONZE_STATUS, VIP_SILVER_STATUS, VIP_GOLD_STATUS }
local localPlayer = Game.GetLocalPlayer()
local inTrigger = false
local reminderText = VIP_REMINDER_UI:FindChildByName("Notice")
reminderText.text = reminderText.text:gsub("%{name%}", localPlayer.name)
local function CloseUI()
VIP_SHOP_UI.visibility = Visibility.FORCE_OFF
if inTrigger then
VIP_SHOP_TRIGGER.isInteractable = true
else
VIP_SHOP_TRIGGER.isInteractable = false
end
UI.SetCursorVisible(false)
UI.SetCanCursorInteractWithUI(false)
end
local function OnInteracted(trigger, obj)
if inTrigger and Object.IsValid(obj) and obj:IsA("Player") and obj == localPlayer then
VIP_SHOP_UI.visibility = Visibility.FORCE_ON
VIP_SHOP_TRIGGER.isInteractable = false
UI.SetCursorVisible(true)
UI.SetCanCursorInteractWithUI(true)
end
end
local function OnExitTrigger(trigger, obj)
if Object.IsValid(obj) and obj:IsA("Player") and obj == localPlayer then
inTrigger = false
CloseUI()
end
end
local function OnEnterTrigger(trigger, obj)
if Object.IsValid(obj) and obj:IsA("Player") and obj == localPlayer then
VIP_SHOP_TRIGGER.isInteractable = true
inTrigger = true
end
end
local function ShowVIPStatus(statusToShow)
for index, status in ipairs(vipStatuses) do
if status == statusToShow then
status.visibility = Visibility.FORCE_ON
else
status.visibility = Visibility.FORCE_OFF
end
end
end
local function UpdateStatus(player, resource, amount)
if resource == "bronzevip" or resource == "silvervip" or resource == "goldvip" then
local bronzeStatus = localPlayer:GetResource("bronzevip")
local silverStatus = localPlayer:GetResource("silvervip")
local goldStatus = localPlayer:GetResource("goldvip")
local hasStatus = false
if goldStatus == 1 then
ShowVIPStatus(VIP_GOLD_STATUS)
hasStatus = true
else
VIP_GOLD_STATUS.visibility = Visibility.FORCE_OFF
end
if not hasStatus and silverStatus == 1 then
ShowVIPStatus(VIP_SILVER_STATUS)
hasStatus = true
else
VIP_SILVER_STATUS.visibility = Visibility.FORCE_OFF
end
if not hasStatus and bronzeStatus == 1 then
ShowVIPStatus(VIP_BRONZE_STATUS)
hasStatus = true
else
VIP_BRONZE_STATUS.visibility = Visibility.FORCE_OFF
end
elseif resource == "showvipnotice" then
VIP_REMINDER_UI.visibility = Visibility.FORCE_ON
UI.SetCursorVisible(true)
UI.SetCanCursorInteractWithUI(true)
end
end
local function CloseReminder()
VIP_REMINDER_UI.visibility = Visibility.FORCE_OFF
UI.SetCursorVisible(false)
UI.SetCanCursorInteractWithUI(false)
end
local function OpenVIPShop()
VIP_REMINDER_UI.visibility = Visibility.FORCE_OFF
VIP_SHOP_UI.visibility = Visibility.FORCE_ON
VIP_SHOP_TRIGGER.isInteractable = false
end
CLOSE_REMINDER_UI_BUTTON.clickedEvent:Connect(CloseReminder)
OPEN_VIP_SHOP_BUTTON.clickedEvent:Connect(OpenVIPShop)
VIP_SHOP_BUTTON.clickedEvent:Connect(CloseUI)
VIP_SHOP_TRIGGER.interactedEvent:Connect(OnInteracted)
VIP_SHOP_TRIGGER.endOverlapEvent:Connect(OnExitTrigger)
VIP_SHOP_TRIGGER.beginOverlapEvent:Connect(OnEnterTrigger)
localPlayer.resourceChangedEvent:Connect(UpdateStatus)
for key, value in pairs(localPlayer:GetResources()) do
UpdateStatus(localPlayer, key, value)
end
Test the Game
Test the game and make sure the reminder UI shows after the VIP perk has expired. Use the Perk Testing window to manually expire any VIP perks.
- Purchase a VIP Perk.
- Disable VIP perk from the Perk Testing window.
- Reminder UI will show to the player.
- Exit and enter preview to make sure the reminder UI isn't shown again.
Gifting Perks to Players
In this section you will be creating a perk gifting system that allows you to gift a perk to a player in game. For example, if you are holding a competition in your game, you may want to reward the top 3 players with a VIP package.
The gifting system will be done using the chat. You will enter specific commands that will be parsed on the server to make sure the person sending the command has permission.
Create GiftServer Script
Create a new script called GiftServer
, and place it into a new Server Context group called Perks Tutorial - Gift
.
Create CommandParser Custom Property
The perk tutorial asset that was imported from Community Content, comes with a small command parsing library that will help you create the commands. To use this library, you need to add a property so the GiftServer script has a reference to the library so you can use the functions.
Add the CommandParser script from Project Content as a custom property.
Require the CommandParser Library
With the custom property added for the Command Parser, you can now require the script so that the library is included and all of the exposed functions from the CommandParser script can be used.
The CommandParser library will do the heavy lifting by doing the following for you:
- Watch for any chat messages received.
- Check the player issuing the command has permission.
- Validate commands, including sub commands.
- Deliver a success or error message to the player who issued the command.
- Deliver a message to the player who successfully received the item (that is VIP Perk).
Add the require line to the top of the GiftServer script.
local CommandParser = require(script:GetCustomProperty("CommandParser"))
Set Admins
When a player sends a command in the chat, the Command Parser will validate if the player has permission to execute any the command. The Command Parser has a function called SetAdmins
, which requires a string of player names, separated by a comma.
Add your name to the string. Bot1
is added so that you can test commands in local multiplayer with the Bot1
player.
Only add player names that you trust. Before publishing your game, Bot1
should be removed.
CommandParser.SetAdmins("CommanderFoo,Bot1")
Create Give Resource Command
You will be creating a give
command that will have a sub command called resource
. This will allow you to give resource perks to players. Before you create the command, you need to specify some command data so that when the command is parsed, it knows what to give to the player.
Add Pumpkin Command Data
The Command Parser library has an AddCommandData
function that can be used to register data for the perk or resource.
Add the following code to the script. Notice that the resourceKey
and storageKey
match the values used in the VIP Resource Shop system. This will reuse those existing player resources without you needing to add extra code.
The name
property will contain the name of the perk or resource that will be displayed to the player in the chat when they receive the gift.
CommandParser.AddCommandData("pumpkins", {
name = "Pumpkins",
resourceKey = "pumpkins",
resourceAmount = 5,
storageKey = "p"
})
Create Give Resource Command
The Command Parser library has an AddCommand
function that can be used to register a command table. This table will have sub commands that perform different actions. For example, give resource
has a main command of give
, and a sub command of resource
.
To give player resources, you need a give
command, and a resource
sub command. The resource
sub command, is where you control what happens when the command is called.
For example, if you execute the command /give resource CommanderFoo pumpkins
, then the resource
sub command will look up the command data for pumpkins
, and attempt to give the resourceAmount
to the player specified. If the commands are valid, then the player's Storage is updated.
The status
parameter is a table that is passed by reference. Meaning that any change to that table done in the callee (resource sub command in this case), is visible outside. This is handy, as there is no need to return the status
table back to the caller, and simplifies the sub command function.
CommandParser.AddCommand("give", {
resource = function(receiver, item, status)
local commandData = CommandParser.GetCommandData(item)
if commandData ~= nil then
local playerData = Storage.GetPlayerData(receiver)
receiver:AddResource(commandData.resourceKey, commandData.resourceAmount or 0)
if not playerData[commandData.storageKey] then
playerData[commandData.storageKey] = commandData.resourceAmount
else
playerData[commandData.storageKey] = playerData[commandData.storageKey] + commandData.resourceAmount
end
Storage.SetPlayerData(receiver, playerData)
status.success = true
status.senderMessage = "Resource successfully given to " .. receiver.name .. "."
status.receiverMessage = "You have been gifted " .. tostring(commandData.resourceAmount) .. " " .. commandData.name .. "."
else
status.senderMessage = "Command data \"" .. item .. "\" does not exist."
end
end
})
The GiftServer Script
Make sure to update the admin names in the script.
GiftServer
local CommandParser = require(script:GetCustomProperty("CommandParser"))
CommandParser.SetAdmins("CommanderFoo,Bot1")
CommandParser.AddCommandData("pumpkins", {
name = "Pumpkins",
resourceKey = "pumpkins",
resourceAmount = 5,
storageKey = "p"
})
CommandParser.AddCommand("give", {
resource = function(receiver, item, status)
local commandData = CommandParser.GetCommandData(item)
if commandData ~= nil then
local playerData = Storage.GetPlayerData(receiver)
receiver:AddResource(commandData.resourceKey, commandData.resourceAmount or 0)
if not playerData[commandData.storageKey] then
playerData[commandData.storageKey] = commandData.resourceAmount
else
playerData[commandData.storageKey] = playerData[commandData.storageKey] + commandData.resourceAmount
end
Storage.SetPlayerData(receiver, playerData)
status.success = true
status.senderMessage = "Resource successfully given to " .. receiver.name .. "."
status.receiverMessage = "You have been gifted " .. tostring(commandData.resourceAmount) .. " " .. commandData.name .. "."
else
status.senderMessage = "Command data \"" .. item .. "\" does not exist."
end
end
})
Test the Game
Make sure to enter your own name into the SetAdmins
function, otherwise you will not have permission to execute the commands.
- Try the command
/give resource name pumpkins
, wherename
is your name. - Try incorrect commands, including a player that does exist to make sure an error message is received.
Create Give Perk Command
In this section, you will create a command that will allow you to give players Gold VIP. Because the Gold VIP perk is limited time, then additional work will need to be done to support custom time tracking so that the perk expires after a set amount of time, similar to how a real time limited perk works.
Add Gold VIP Perk Property
You will need to add the Gold VIP Perk to the GiftServer script as a custom property so that it can be registered with the Command Parser.
Drag the VIP Gold perk from the Perk Manager window, onto the GiftServer script as a custom property. Name the property VIPGoldPerk
.
Add Gold VIP Perk Command Data
Add new command data for the Gold VIP perk. The data for this command will be different to the Pumpkin data you added previous. In this case you need a reference to the perk, and a duration.
For testing, the duration has been set to 1
minute. You will want to increase this for your published game.
CommandParser.AddCommandData("goldvip", {
perk = script:GetCustomProperty("VIPGoldPerk"),
name = "Gold VIP",
resourceKey = "goldvip",
storageKey = "goldvip",
duration = 1
})
Create Give Perk Command
A sub command key for the give
table needs to be created called perk
. This command will check to see if the player already has the Gold VIP perk by checking GetPerkTimeRemaining is greater than 0. If they don't have an existing Gold VIP perk, then custom time tracking is done by getting the current time in seconds, and adding the duration
property of the command data. This is then persistently stored in player Storage that will be checked for in the VIPShopServer script.
By using the DateTime object, you can build a new DateTime
by using the FromSecondsSinceEpoch
function. With adding the perk duration time to the current time in seconds, you get an expiry time that can be stored.
perk = function(receiver, item, status)
local commandData = CommandParser.GetCommandData(item)
if commandData ~= nil then
local playerData = Storage.GetPlayerData(receiver)
if commandData.perk ~= nil then
if receiver:GetPerkTimeRemaining(commandData.perk) > 0 then
status.senderMessage = "Player already has this time limited perk."
else
local durationSeconds = commandData.duration * 60
local expireSeconds = DateTime.CurrentTime().secondsSinceEpoch + durationSeconds
local expireDateTime = DateTime.FromSecondsSinceEpoch(expireSeconds)
playerData[commandData.storageKey] = expireDateTime.secondsSinceEpoch
receiver:SetResource(commandData.resourceKey, 1)
Storage.SetPlayerData(receiver, playerData)
status.senderMessage = "Perk successfully given to " .. receiver.name .. "."
status.receiverMessage = "You have been gifted " .. commandData.name .. "."
status.success = true
end
end
else
status.senderMessage = "Command data \"" .. item .. "\" does not exist."
end
end
The GiftServer Script
GiftServer
local CommandParser = require(script:GetCustomProperty("CommandParser"))
CommandParser.SetAdmins("CommanderFoo,Bot1")
CommandParser.AddCommandData("pumpkins", {
name = "Pumpkins",
resourceKey = "pumpkins",
resourceAmount = 5,
storageKey = "p"
})
CommandParser.AddCommandData("goldvip", {
perk = script:GetCustomProperty("VIPGoldPerk"),
name = "Gold VIP",
resourceKey = "goldvip",
storageKey = "goldvip",
duration = 1
})
CommandParser.AddCommand("give", {
resource = function(receiver, item, status)
local commandData = CommandParser.GetCommandData(item)
if commandData ~= nil then
local playerData = Storage.GetPlayerData(receiver)
receiver:AddResource(commandData.resourceKey, commandData.resourceAmount or 0)
if not playerData[commandData.storageKey] then
playerData[commandData.storageKey] = commandData.resourceAmount
else
playerData[commandData.storageKey] = playerData[commandData.storageKey] + commandData.resourceAmount
end
Storage.SetPlayerData(receiver, playerData)
status.success = true
status.senderMessage = "Resource successfully given to " .. receiver.name .. "."
status.receiverMessage = "You have been gifted " .. tostring(commandData.resourceAmount) .. " " .. commandData.name .. "."
else
status.senderMessage = "Command data \"" .. item .. "\" does not exist."
end
end,
perk = function(receiver, item, status)
local commandData = CommandParser.GetCommandData(item)
if commandData ~= nil then
local playerData = Storage.GetPlayerData(receiver)
if commandData.perk ~= nil then
if receiver:GetPerkTimeRemaining(commandData.perk) > 0 then
status.senderMessage = "Player already has this time limited perk."
else
local durationSeconds = commandData.duration * 60
local expireSeconds = DateTime.CurrentTime().secondsSinceEpoch + durationSeconds
local expireDateTime = DateTime.FromSecondsSinceEpoch(expireSeconds)
playerData[commandData.storageKey] = expireDateTime.secondsSinceEpoch
receiver:SetResource(commandData.resourceKey, 1)
Storage.SetPlayerData(receiver, playerData)
status.senderMessage = "Perk successfully given to " .. receiver.name .. "."
status.receiverMessage = "You have been gifted " .. commandData.name .. "."
status.success = true
end
end
else
status.senderMessage = "Command data \"" .. item .. "\" does not exist."
end
end
})
Update VIPShopServer Script
Because you are doing custom time tracking for the Gold VIP perk, you will need to update the VIPShopServer script so that it can mark the player as Gold VIP if the timestamp (in seconds) stored for the player has not expired.
Create HasGoldVIP Function
The HasGoldVIP
function will check if the player has the Gold VIP perk by checking 2 places.
- If the player has the perk using HasPerk.
- If the player has the
goldvip
property, and the time is greater than the current time.
When you issue the command give perk
, a timestamp in seconds is stored for the player in the goldvip
property. This can be used to see if the current time is greater than the stored time. If it is less that the time in goldvip
, then the perk has expired.
local function HasGoldVIP(player)
local playerData = Storage.GetPlayerData(player)
if player:HasPerk(VIP_GOLD_PERK) or (playerData.goldvip ~= nil and playerData.goldvip > DateTime.CurrentTime().secondsSinceEpoch) then
return true
end
return false
end
Update OnJoined Function
The OnJoined
function needs to be updated so that it uses the HasGoldVIP
function to check if the player has the Gold VIP perk.
local function OnJoined(player)
player.perkChangedEvent:Connect(PerkChanged)
local hasVIP = false
local playerData = Storage.GetPlayerData(player)
if player:HasPerk(VIP_BRONZE_PERK) then
player:SetResource("bronzevip", 1)
hasVIP = true
end
if player:HasPerk(VIP_SILVER_PERK) then
player:SetResource("silvervip", 1)
hasVIP = true
end
if HasGoldVIP(player) then
player:SetResource("goldvip", 1)
hasVIP = true
end
if playerData.vip ~= nil then
if not hasVIP and playerData.vip == 1 then
playerData.vip = 0
player:SetResource("showvipnotice", 1)
Storage.SetPlayerData(player, playerData)
end
end
end
Update OnChatMessage Function
The OnChatMessage
function also needs to check if the player has the Gold VIP perk by calling the HasGoldVIP
function.
local function OnChatMessage(speaker, params)
if HasGoldVIP(speaker) then
params.speakerName = "[Gold VIP] " .. params.speakerName
elseif speaker:HasPerk(VIP_SILVER_PERK) then
params.speakerName = "[Silver VIP] " .. params.speakerName
elseif speaker:HasPerk(VIP_BRONZE_PERK) then
params.speakerName = "[Bronze VIP] " .. params.speakerName
end
end
Update perkCheckTask Spawned Task
The task that periodically runs to check all players perks will also need to use the HasGoldVIP
function.
local perkCheckTask = Task.Spawn(function()
for index, player in ipairs(Game.GetPlayers()) do
if not player:HasPerk(VIP_BRONZE_PERK) then
player:SetResource("bronzevip", 0)
end
if not player:HasPerk(VIP_SILVER_PERK) then
player:SetResource("silvervip", 0)
end
if not HasGoldVIP(player) then
player:SetResource("goldvip", 0)
end
end
end)
The VIPShopServer Script
VIPShopServer
local VIP_BRONZE_PERK = script:GetCustomProperty("VIPBronze")
local VIP_SILVER_PERK = script:GetCustomProperty("VIPSilver")
local VIP_GOLD_PERK = script:GetCustomProperty("VIPGold")
local function PerkChanged(buyer, perk)
local gemsAmount = 0
local coinsAmount = 0
local hasVIP = 0
if perk == VIP_BRONZE_PERK then
buyer:SetResource("bronzevip", 1)
gemsAmount = 250
coinsAmount = 1000
hasVIP = 1
elseif perk == VIP_SILVER_PERK then
buyer:SetResource("silvervip", 1)
gemsAmount = 500
coinsAmount = 3000
hasVIP = 1
elseif perk == VIP_GOLD_PERK then
buyer:SetResource("goldvip", 1)
gemsAmount = 1000
coinsAmount = 7500
hasVIP = 1
end
local playerData = Storage.GetPlayerData(buyer)
buyer:AddResource("gems", gemsAmount)
buyer:AddResource("coins", coinsAmount)
-- g (Gems)
if not playerData["g"] then
playerData["g"] = 0
end
-- c (Coins)
if not playerData["c"] then
playerData["c"] = 0
end
playerData["g"] = playerData["g"] + gemsAmount
playerData["c"] = playerData["c"] + coinsAmount
playerData["vip"] = hasVIP
Storage.SetPlayerData(buyer, playerData)
end
local function HasGoldVIP(player)
local playerData = Storage.GetPlayerData(player)
if player:HasPerk(VIP_GOLD_PERK) or (playerData.goldvip ~= nil and playerData.goldvip > DateTime.CurrentTime().secondsSinceEpoch) then
return true
end
return false
end
local function OnJoined(player)
player.perkChangedEvent:Connect(PerkChanged)
local hasVIP = false
local playerData = Storage.GetPlayerData(player)
if player:HasPerk(VIP_BRONZE_PERK) then
player:SetResource("bronzevip", 1)
hasVIP = true
end
if player:HasPerk(VIP_SILVER_PERK) then
player:SetResource("silvervip", 1)
hasVIP = true
end
if HasGoldVIP(player) then
player:SetResource("goldvip", 1)
hasVIP = true
end
if playerData.vip ~= nil then
if not hasVIP and playerData.vip == 1 then
playerData.vip = 0
player:SetResource("showvipnotice", 1)
Storage.SetPlayerData(player, playerData)
end
end
end
local function OnChatMessage(speaker, params)
if HasGoldVIP(speaker) then
params.speakerName = "[Gold VIP] " .. params.speakerName
elseif speaker:HasPerk(VIP_SILVER_PERK) then
params.speakerName = "[Silver VIP] " .. params.speakerName
elseif speaker:HasPerk(VIP_BRONZE_PERK) then
params.speakerName = "[Bronze VIP] " .. params.speakerName
end
end
local perkCheckTask = Task.Spawn(function()
for index, player in ipairs(Game.GetPlayers()) do
if not player:HasPerk(VIP_BRONZE_PERK) then
player:SetResource("bronzevip", 0)
end
if not player:HasPerk(VIP_SILVER_PERK) then
player:SetResource("silvervip", 0)
end
if not HasGoldVIP(player) then
player:SetResource("goldvip", 0)
end
end
end)
perkCheckTask.repeatCount = -1
perkCheckTask.repeatInterval = (60 * 5)
Game.playerJoinedEvent:Connect(OnJoined)
Chat.receiveMessageHook:Connect(OnChatMessage)
Test the Game
Test the game by issuing the give perk
command for yourself.
For example, /give perk CommanderFoo goldvip
.
Make sure the following work:
- Give perk command gives the Gold VIP perk.
- Gold VIP badge shows in the UI.
- Player receives a chat message they received the perk.
- Perk is persistent between game sessions.
- Perk expires after the duration time.
- Test in multiplayer preview.
Create Remove Perk Command
It may be handy to have an option to remove gifted perks from players. In this section, you will create a new remove
command that contains a perk
sub command, which will allow you to remove the Gold VIP perk from players that were gifted it.
This command will only remove the Gold VIP perk if it was not purchased by the player. If the perk was purchased by the player, then you will receive a message indicating that it can not be removed.
Create Remove Perk Command
Open up the GiftServer script.
Add a new remove
command, with a sub command of perk
. This will check to see if the player has remaining time left on the purchased perk. If they don't, then it will update the players resource and Storage to expire the gifted perk by setting the expiry value to 0
, and the resource value to 0
so that the badge is removed from the UI.
CommandParser.AddCommand("remove", {
perk = function(receiver, item, status)
local commandData = CommandParser.GetCommandData(item)
if commandData ~= nil then
local playerData = Storage.GetPlayerData(receiver)
if commandData.perk ~= nil then
if receiver:GetPerkTimeRemaining(commandData.perk) > 0 then
status.senderMessage = "Player has purchased this perk."
else
playerData[commandData.storageKey] = 0
receiver:SetResource(commandData.resourceKey, 0)
Storage.SetPlayerData(receiver, playerData)
status.senderMessage = "Perk successfully removed from " .. receiver.name .. "."
status.success = true
end
end
else
status.senderMessage = "Command data \"" .. item .. "\" does not exist."
end
end
})
The GiftServer Script
Make sure to update the admin names in the script.
GiftServer
local CommandParser = require(script:GetCustomProperty("CommandParser"))
CommandParser.SetAdmins("CommanderFoo,Bot1")
CommandParser.AddCommandData("pumpkins", {
name = "Pumpkins",
resourceKey = "pumpkins",
resourceAmount = 5,
storageKey = "p"
})
CommandParser.AddCommandData("goldvip", {
perk = script:GetCustomProperty("VIPGoldPerk"),
name = "Gold VIP",
resourceKey = "goldvip",
storageKey = "goldvip",
duration = 1
})
CommandParser.AddCommand("give", {
resource = function(receiver, item, status)
local commandData = CommandParser.GetCommandData(item)
if commandData ~= nil then
local playerData = Storage.GetPlayerData(receiver)
receiver:AddResource(commandData.resourceKey, commandData.resourceAmount or 0)
if not playerData[commandData.storageKey] then
playerData[commandData.storageKey] = commandData.resourceAmount
else
playerData[commandData.storageKey] = playerData[commandData.storageKey] + commandData.resourceAmount
end
Storage.SetPlayerData(receiver, playerData)
status.success = true
status.senderMessage = "Resource successfully given to " .. receiver.name .. "."
status.receiverMessage = "You have been gifted " .. tostring(commandData.resourceAmount) .. " " .. commandData.name .. "."
else
status.senderMessage = "Command data \"" .. item .. "\" does not exist."
end
end,
perk = function(receiver, item, status)
local commandData = CommandParser.GetCommandData(item)
if commandData ~= nil then
local playerData = Storage.GetPlayerData(receiver)
if commandData.perk ~= nil then
if receiver:GetPerkTimeRemaining(commandData.perk) > 0 then
status.senderMessage = "Player already has this time limited perk."
else
local durationSeconds = 15--commandData.duration * 60
local expireSeconds = DateTime.CurrentTime().secondsSinceEpoch + durationSeconds
local expireDateTime = DateTime.FromSecondsSinceEpoch(expireSeconds)
playerData[commandData.storageKey] = expireDateTime.secondsSinceEpoch
receiver:SetResource(commandData.resourceKey, 1)
Storage.SetPlayerData(receiver, playerData)
status.senderMessage = "Perk successfully given to " .. receiver.name .. "."
status.receiverMessage = "You have been gifted " .. commandData.name .. "."
status.success = true
end
end
else
status.senderMessage = "Command data \"" .. item .. "\" does not exist."
end
end
})
CommandParser.AddCommand("remove", {
perk = function(receiver, item, status)
local commandData = CommandParser.GetCommandData(item)
if commandData ~= nil then
local playerData = Storage.GetPlayerData(receiver)
if commandData.perk ~= nil then
if receiver:GetPerkTimeRemaining(commandData.perk) > 0 then
status.senderMessage = "Player has purchased this perk."
else
playerData[commandData.storageKey] = 0
receiver:SetResource(commandData.resourceKey, 0)
Storage.SetPlayerData(receiver, playerData)
status.senderMessage = "Perk successfully removed from " .. receiver.name .. "."
status.success = true
end
end
else
status.senderMessage = "Command data \"" .. item .. "\" does not exist."
end
end
})
Test the Game
Test the game to make sure that a gifted perk done by command is removed.
Summary
There are many ways you could implement perks in your game. With the knowledge gained from these tutorials, you can adapt them in many different ways to suit your own game.
Take a look at how other games implement perks to get ideas on the many ways you use the perks system.
Here are some ideas to challenge yourself:
- New commands for gifting different resources and perks.
- Implement a UI notification system to let the player know they received a gift.
- Implement player to player gifting of resources and perks.
- Create permanent perks the player can purchase (that is player titles, cosmetics).
Feedback and Questions
For feedback and questions, join the discussion on this tutorial's forum thread.
Learn More
Perks Program | UI Perk Purchase Button | UI Perk Purchase Button | Perks Systems Part 1 | HasPerk | GetPerkTimeRemaining | Storage | DateTime