Skip to content

NFT Cosmetics

Overview

In this tutorial, you will learn how to use NFTs (Non-fungible Tokens) as cosmetics. You will create a system that allows players to walk into a trigger and get a random cosmetic attached to them. You will be using an NFT cosmetics collection that has already been created, however, creating your own can also be done and used for this tutorial.

  • Skills you will learn:
    • Reading NFTs using the Blockchain API.
    • Attaching objects to a socket on a player.
    • Changing the colors of a static mesh.
    • Creating a 3D progress bar.

Using an Existing NFT Collection

This tutorial will use an existing NFT collection that uses the cardboard boxes from Core Content. For example, looking at NFT #3 in the collection, we can see it has attributes for Box and Color. The Box attribute is the row index from the data table of templates. This could be your data table of templates that make up how the cosmetic looks.

https://opensea.io/collection/core-box-cosmetic

Properties

NFT Token Data Table

Because the NFTs in the collection are lazy minted, there isn't a way to get all NFTs from a collection due to the contract address being a global address for lazy minted NFTs. This means if you try to retrieve NFTs using the contract address, it may return a random NFT created by someone else. The solution to this would be to put your NFTs on the blockchain, but this costs gas.

Another way we can solve this is by having a data table with the collection of NFT Token IDs, and we randomly pick a Token ID from the data table.

Create Cosmetic Tokens Data Table

Create a new data table called Cosmetic Tokens. This table will have 1 column called TokenID which is a string type.

Add as many tokens from the collection by either copying the token ID from the URL or the Details section on the NFT page.

Cosmetic Tokens

Create Cosmetics Data Table

We need a data table that will hold the templates that link to each Box attribute.

Create a data table called Cosmetics and add 1 column called Template that is an Asset Reference type.

Cosmetics

Create Templates

Create some cosmetic templates that will be spawned and attached to the player when they walk through the trigger. For this tutorial, we are using the cardboard boxes from Core Content.

Box

Add Templates to Data Table

When you have created your templates for the boxes, they need to be added to the Cosmetics data table.

Rows

Create Trigger

To apply a random cosmetic to the player, you will need to set up an area that has a trigger so that players can enter it and receive a cosmetic. In this case, we will make it so it applies a random one to the player.

  1. Create a Client Context.
  2. Create a trigger and scale it up so that players can enter it easily.
  3. Add some geo so players know where to go to use and change their cosmetics.
  4. Turn off the collision on the trigger.

Trigger

Create Loading Bar

NFTs can take a little while to load. Having a progress bar to show the player will be a good idea so they know something is happening.

There are a few ways a loading bar could be done. In this section, we will create a 3D loading bar that will update as NFTs are loaded.

  1. Create a group called Loading.
  2. Adding 3D text objects that make up the word Loading.
  3. Add a bottom aligned cube below the Loading text by setting the Z position to -50.
  4. Rename the bottom aligned cube to Bar.
  5. Set the scale of the bar to X 4, Y 0.35, Z 0.35.
  6. Duplicate the bar and rename it to Progress Bar.
  7. Set position Y to 200.
  8. Set the rotation Y to -90.
  9. Set the scale X to 0.35, Y to 0.35, and Z to 0.

Bar

Progress Bar

Create NFTRandomCosmeticClient Script

Create a new script called NFTRandomCosmeticClient and place it into a Client Context. This script will update the progress bar when the NFTs are loading, and send off a broadcast to the server when the player enters the trigger so a random cosmetic is attached to them.

The script will need some custom properties set.

  1. Add the Trigger as a custom property called Trigger.
  2. Add the Loading group that holds all the text letters and the progress bar as a custom property called Loading.
  3. Add the Cosmetic Tokens data table as a custom property called CosmeticTokens.
  4. Add the Progress Bar as a custom property called ProgressBar.

Client

Create Variables

Add the following variables so you have references to the custom properties. The tokens variable will hold the loaded tokens so we can access how many have loaded, and if needed do more with this later as the tokens will be cached into the table.

local COSMETIC_TOKENS = require(script:GetCustomProperty("CosmeticTokens"))

local TRIGGER = script:GetCustomProperty("Trigger"):WaitForObject()
local LOADING = script:GetCustomProperty("Loading"):WaitForObject()
local PROGRESS_BAR = script:GetCustomProperty("ProgressBar"):WaitForObject()

local LOCAL_PLAYER = Game.GetLocalPlayer()
local tokens = {}

Rotate Group

Rotating the group that contains the loading letters and progress bar so it faces the player would be nice. We can do this easily with the LookAtContinuous function. We pass true in as the second argument to lock the pitch.

LOADING:LookAtContinuous(LOCAL_PLAYER, true)

Create OnTriggerEnter Function

Create a function called OnTriggerEnter. When the player enters the trigger, a broadcast to the server will be done that will attach a random cosmetic to the player.

local function OnTriggerEnter(trigger, other)
    if other == LOCAL_PLAYER then
        Events.BroadcastToServer("Cosmetic.Attach")
    end
end

Fetch NFT Tokens

You need to fetch the NFTs using the Blockchain API. Because all of the NFTs being used are lazy minted, we have to use the contract address from OpenSea which is the same for everyone else who has lazy minted NFTs. It would be recommended to put your NFTs onto the blockchain, but it costs Gas which can be expensive. So this alternative will work for now.

When a token is successfully retrieved, the scale of the PROGRESS_BAR is increased based on the total tokens loaded. For each iteration, a wait of 1.6 seconds is done to prevent hitting the rate limits for OpenSea.

for index, row in ipairs(COSMETIC_TOKENS) do
    local token, success, msg = Blockchain.GetToken("0x495f947276749ce646f68ac8c248420045cb7b5e", tostring(row.TokenID))

    if success == BlockchainTokenResultCode.SUCCESS then
        tokens[#tokens + 1] = token

        local scale = PROGRESS_BAR:GetScale()

        scale.z = (4 / #COSMETIC_TOKENS) * #tokens
        PROGRESS_BAR:SetScale(scale)
    else
        warn(msg)
    end

    Task.Wait(1.6)
end

Clean Up

After the looping of the tokens has finished, you can stop the loading group from rotating, and turn the visibility off. The collision for the TRIGGER can be turned on to allow players to enter and get a random cosmetic equipped.

LOADING:StopRotate()
LOADING.visibility = Visibility.FORCE_OFF
TRIGGER.collision = Collision.FORCE_ON

Connect Trigger Event

Finally, connect up the TRIGGER so that the OnTriggerEnter function is called when the player enters the trigger.

TRIGGER.beginOverlapEvent:Connect(OnTriggerEnter)

The NFTRandomCosmeticClient Script

NFTRandomCosmeticClient
local COSMETIC_TOKENS = require(script:GetCustomProperty("CosmeticTokens"))

local TRIGGER = script:GetCustomProperty("Trigger"):WaitForObject()
local LOADING = script:GetCustomProperty("Loading"):WaitForObject()
local PROGRESS_BAR = script:GetCustomProperty("ProgressBar"):WaitForObject()

local LOCAL_PLAYER = Game.GetLocalPlayer()
local tokens = {}

LOADING:LookAtContinuous(LOCAL_PLAYER, true)

local function OnTriggerEnter(trigger, other)
    if other == LOCAL_PLAYER then
        Events.BroadcastToServer("Cosmetic.Attach")
    end
end

for index, row in ipairs(COSMETIC_TOKENS) do
    local token, success, msg = Blockchain.GetToken("0x495f947276749ce646f68ac8c248420045cb7b5e", tostring(row.TokenID))

    if success == BlockchainTokenResultCode.SUCCESS then
        tokens[#tokens + 1] = token

        local scale = PROGRESS_BAR:GetScale()

        scale.z = (4 / #COSMETIC_TOKENS) * #tokens
        PROGRESS_BAR:SetScale(scale)
    else
        warn(msg)
    end

    Task.Wait(1.6)
end

LOADING:StopRotate()
LOADING.visibility = Visibility.FORCE_OFF
TRIGGER.collision = Collision.FORCE_ON

TRIGGER.beginOverlapEvent:Connect(OnTriggerEnter)

Create NFTRandomCosmeticServer Script

Create a new script called NFTRandomCosmeticServer script and place it into a Server Context. This script will handle attaching a cosmetic to the player when they enter the trigger.

The script will need the 2 data tables added as custom properties.

  1. Add the Cosmetics data table as a custom property called Cosmetics.
  2. Add the Cosmetic Tokens data table as a custom property called CosmeticTokens.

Server

Create Variables

Add the following variables so you have references to the custom properties. The tokens variable will hold the loaded tokens so they are only loaded once. The players table will keep track of what cosmetic players have equipped so it can be removed when switched or when they leave the game.

local COSMETICS = require(script:GetCustomProperty("Cosmetics"))
local COSMETIC_TOKENS = require(script:GetCustomProperty("CosmeticTokens"))

local players = {}
local tokens = {}

Create AttachCosmetic Function

Create a function called AttachCosmetic. This function will attach a random cosmetic to the player's head. It will destroy an existing cosmetic if one is already attached by looking at the players table.

A random token is picked from the tokens table. Each token has 2 attributes. One for the color of the box, and one for the box template that the NFT uses. The first child of the item that is spawned has the color set.

The item is then attached to the player's head socket by using the AttachToPlayer function.

local function AttachCosmetic(player)
    if players[player] then
        players[player]:Destroy()
    end

    local token = tokens[math.random(#tokens)]

    local color = { CoreString.Split(token:GetAttribute("Color"):GetValue(), ", ") }
    local item = World.SpawnAsset(COSMETICS[tonumber(token:GetAttribute("Box"):GetValue())].Template, { networkContext = NetworkContextType.NETWORKED })

    item:GetChildren()[1]:SetColor(Color.New(color[1], color[2], color[3], 1))
    item:AttachToPlayer(player, "head")
    players[player] = item
end

Create OnPlayerLeft Function

Create a function called OnPlayerLeft. This function will clean up the cosmetic a player may have equipped by looking at the players table. If an entry for that player who is leaving the game exist, then the object is destroyed.

local function OnPlayerLeft(player)
    if players[player] then
        players[player]:Destroy()
        players[player] = nil
    end
end

Fetch NFTs

Loop through the cosmetic tokens and load each token. Each token is put into the tokens table for later use so you don't need to load them each time. The contract address that is passed to GetToken is where the tokens are stored. Because they are lazy minted tokens on OpenSea, then this address will be the same for others, so we need to grab the token by passing in the token id as well.

for index, row in ipairs(COSMETIC_TOKENS) do
    local token, success, msg = Blockchain.GetToken("0x495f947276749ce646f68ac8c248420045cb7b5e", tostring(row.TokenID))

    if success == BlockchainTokenResultCode.SUCCESS then
        tokens[#tokens + 1] = token
    end

    Task.Wait(1.6)
end

Connect Events

Connect up the events so that when the trigger event is fired, it tells the server to attach a cosmetic to the player who entered the trigger.

Events.ConnectForPlayer("Cosmetic.Attach", AttachCosmetic)
Game.playerLeftEvent:Connect(OnPlayerLeft)

The NFTRandomCosmeticServer Script

NFTRandomCosmeticServer
local COSMETICS = require(script:GetCustomProperty("Cosmetics"))
local COSMETIC_TOKENS = require(script:GetCustomProperty("CosmeticTokens"))

local players = {}
local tokens = {}

local function AttachCosmetic(player)
    if players[player] then
        players[player]:Destroy()
    end

    local token = tokens[math.random(#tokens)]

    local color = { CoreString.Split(token:GetAttribute("Color"):GetValue(), ", ") }
    local item = World.SpawnAsset(COSMETICS[tonumber(token:GetAttribute("Box"):GetValue())].Template, { networkContext = NetworkContextType.NETWORKED })


    item:GetChildren()[1]:SetColor(Color.New(color[1], color[2], color[3], 1))
    item:AttachToPlayer(player, "head")
    players[player] = item
end

local function OnPlayerLeft(player)
    if players[player] then
        players[player]:Destroy()
        players[player] = nil
    end
end

for index, row in ipairs(COSMETIC_TOKENS) do
    local token, success, msg = Blockchain.GetToken("0x495f947276749ce646f68ac8c248420045cb7b5e", tostring(row.TokenID))

    if success == BlockchainTokenResultCode.SUCCESS then
        tokens[#tokens + 1] = token
    end

    Task.Wait(1.6)
end

Events.ConnectForPlayer("Cosmetic.Attach", AttachCosmetic)
Game.playerLeftEvent:Connect(OnPlayerLeft)

Test the Game

Test the game to make sure the NFTs are loaded. The progress bar will grow as each NFT is loaded. Once all the NFTs have loaded, the player can enter the trigger to equip a random cosmetic.

Learn More

Blockchain API | NFT Slideshow Tutorial | NFT Inventory Loot Tutorial


Last update: September 1, 2022