In this week’s Moralis Project you will learn how to buy NFTs from an NPC Shop and how to create a full on-chain inventory system with Unity and Moralis.
We will create and then retrieve some items from an on-chain database and that will give us the ability to modify them or even add new ones in real time.
Then we will choose and mint one of the items as an NFT and transfer it to our wallet. After 1 or 2 minutes of syncing, we will have the NFT in our inventory. By clicking on it, the web browser will open and we’ll be able to see it on OpenSea.
PREREQUISITES
GET STARTED
Clone the youtube-tutorials GitHub Repository:
git clone https://github.com/MoralisWeb3/youtube-tutorials.git
Once downloaded, navigate to the unity folder and you will find all the unity tutorials. Open unity-web3-nft-shop with Unity Hub and you’re ready to go!
VIDEO STRUCTURE
Alright so just to summarize let’s see the structure that the video will follow:
- First, I’m gonna show you how the project is structured so you have a broader vision before going into specific features
- Second, we’re gonna setup a Moralis Server and get the credentials we need to log into Web3
- Third, we’re gonna add some items to the Moralis Database and we will learn how to populate them into the shop inventory. (ShopInventory.cs)
- Then, we will deploy a smart contract which will contain the mint function (Remix)
- After that, we will call that function from Unity and actually mint the NFT. (PurchaseItemController.cs)
- And Finally we will learn how to retrieve the NFT and how to see it on OpenSea.
1. Project structure overview
First we can take a look at how the project is structured:
If we look at the Assets tab:
- Under _Project you will find all the assets created for this project.
- Under Moralis Web3 Unity SDK → Resources you will find the MoralisServerSettings. We will need to fill this later.
- Under ThirdParty you will find all the free assets from the Unity Asset Store.
There’s only one scene in this project, located under _Project → Scenes → Game.unity so open it.
If we look at the Hierarchy tab:
- MoralisStandaloneAuth: Takes care of the authentication to Moralis and Web3.
- GameManager: Listens to most of the events in the game and manages game states.
- CameraManager: Manages the PlayerCamera and the NpcCamera.
- Player: All character related components and scripts are inside this prefab.
- World: All the room environment assets are contained here, but there’s one that has functionality apart from ornamentation:
- Room → ShopCounter: Triggers an event when the player collides with it.
- UI Elements: We have 3 important objects here:
- ShopInventory: It will populate the items from the Moralis Database.
- PlayerInventory: It will populate the NFTs owned by the player.
- PurchaseItemManager: It takes care of the minting/buying process of the items.
2. Setup Moralis Server
Before going right into Unity, let’s create a Moralis Server.
Go to https://admin.moralis.io/login and log in (sign up if you are not already registered). Click on Create a new Server:
Click on Testnet Server:
Select whatever name you want and also your closest region. Then select Polygon (Mumbai), as we’re going to be deploying the smart contract to this chain. Finally, click on Add Instance:
The server will be created so if you click on View Details you will find the Server URL and the Application ID so just start by copying the Server URL (you will need to do the same for Application ID):
Go to Unity and now you have to options:
- In the Assets tab go to Moralis Web3 Unity SDK → Resources and fill the MoralisServerSettings with both Server URL and Application ID:
- In the Unity Menu Bar go to Window → Moralis → Web3 Unity SDK → Open Web3 Setup and paste Server URL and Application ID using the wizard:
Hit on Done and you will have the Moralis Server set up. Nice!
3. Prepare shop items
So after setting up the Moralis Server, when we hit play MoralisStandaloneAuth will take care of the authentication process. If you scan the QR code with your MetaMask wallet and you authenticate successfully, MoralisStandaloneAuth.cs will trigger an event called Authenticated.
Because MoralisStandaloneAuth.cs inherits from MoralisAuthenticator.cs, to listen to this event from any other we will need to type it like this:
MoralisAuthenticator.Authenticated +=
One of the scripts that listens to that event and after that, takes care of the process that interests us in this section is ShopInventory.cs and is located under ShopInventory prefab:
So if we open the script we’ll see that on OnEnable() we listen to the event and we call SubscribeToDatabaseEvents():
private void OnEnable()
{
MoralisAuthenticator.Authenticated += SubscribeToDatabaseEvents;
ShopCounterCollider.PlayerEnteredShop += OpenShop;
ShopItem.Selected += OnItemSelectedHandler;
PurchaseItemManager.PurchaseCompleted += DeletePurchasedItem;
PurchaseItemManager.ItemPanelClosed += OpenShop;
}
Before going through this function, it’s important to see what private variables ShopInventory.cs has:
private MoralisQuery<ItemData> _getAllItemsQuery;
private MoralisLiveQueryCallbacks<ItemData> _callbacks;
We will use _getAllItemsQuery and _callbacks in the SubscribeToDatabaseEvents() to, like the method describes, subscribing to the events of the ItemData table in the Moralis Database:
private async void SubscribeToDatabaseEvents()
{
// 1. We create a new MoralisQuery targeting ItemData
_getAllItemsQuery = await Moralis.GetClient().Query<ItemData>();
// 2. We set a new MoralisLiveQueryCallbacks and we subscribe to events
_callbacks = new MoralisLiveQueryCallbacks<ItemData>();
_callbacks.OnConnectedEvent += (() => { Debug.Log("Connection Established."); });
_callbacks.OnSubscribedEvent += ((requestId) => { Debug.Log($"Subscription {requestId} created."); });
_callbacks.OnUnsubscribedEvent += ((requestId) => { Debug.Log($"Unsubscribed from {requestId}."); });
_callbacks.OnCreateEvent += ((item, requestId) =>
{
Debug.Log("New item created on DB");
PopulateShopItem(item);
});
_callbacks.OnUpdateEvent += ((item, requestId) =>
{
Debug.Log("Item updated");
UpdateItem(item.objectId, item);
});
_callbacks.OnDeleteEvent += ((item, requestId) =>
{
Debug.Log("Item deleted from DB");
DeleteItem(item.objectId);
});
// 3. We add a subscription to ItemData table using MoralisLiveQueryController and passing _getAllItemsQuery and _callbacks
MoralisLiveQueryController.AddSubscription<ItemData>("ItemData", _getAllItemsQuery, _callbacks);
}
So, detailing the process:
- We create a new MoralisQuery targeting ItemData
- We set a new MoralisLiveQueryCallbacks and we subscribe to events
- We add a subscription to ItemData table using MoralisLiveQueryController and passing _getAllItemsQuery and _callbacks
Now what we need to do is creating the ItemData table to the Moralis Database so to do that go back to https://admin.moralis.io and on your server, expand it by clicking on the arrow and then click on Dashboard:
This will bring you to the dashboard (database):
On the left sidebar, next to Browser, click on the plus button to create a new class (table). Call this class ItemData and click on Create Class:
Before adding some elements (rows) to the newly created class, we need to add some columns to match the fields that we set in our ItemData class in Unity.
So if we go to Unity and open InventoryItem.cs we will find the ItemData class:
public class ItemData : MoralisObject
{
public string name { get; set; }
public string description { get; set; }
public string imageUrl { get; set; }
public ItemData() : base("ItemData") {}
}
It inherits from MoralisObject and we have set 3 string properties for it:
So now it’s time to add three columns to the ItemData class in the dashboard using these same names. Once in the dashboard, click on Add a new column, call it name and click on Add column & continue. Do the same for the description and the imageUrl columns:
When adding the last column (imageUrl) just click on Add column and you should see the three columns added to the class:
Now go back to Unity and on the Assets tab, go to _Project and open IPFS.txt. Here you will find some item data already prepared for you (you won’t need full metadata url for this project but it’s there anyways):
So now go to the dashboard, add a new row and copy and paste the information you want. Repeat the process so you end up having at least 3 rows:
So to test this, go back to Unity and hit play. Scan the QR code with your MetaMask wallet. A sign message will pop up on your wallet so confirm it:
Once logged in successfully, walk to the shop counter and you should see the three items we just added to the Moralis Database. Nice!
Programmatically, what happened here is that ShopInventory.cs listened to the event triggered by ShopCounterCollider.cs named PlayerEnteredShop and it called OpenShop():
private void OnEnable()
{
MoralisAuthenticator.Authenticated += SubscribeToDatabaseEvents;
ShopCounterCollider.PlayerEnteredShop += OpenShop;
ShopItem.Selected += OnItemSelectedHandler;
PurchaseItemManager.PurchaseCompleted += DeletePurchasedItem;
PurchaseItemManager.ItemPanelClosed += OpenShop;
}
And OpenShop() itself calls the function GetItemsFromDB(), which is the function in charge of retrieving the items that we just added to the Database.
Foreach ItemData retrieved from the database using _getAllItemsQuery.FindAsync() we will call PopulateShopItem() passing ItemData as a parameter:
private async void GetItemsFromDB()
{
IEnumerable<ItemData> databaseItems = await _getAllItemsQuery.FindAsync();
var databaseItemsList = databaseItems.ToList();
if (!databaseItemsList.Any()) return;
foreach (var databaseItem in databaseItemsList)
{
PopulateShopItem(databaseItem);
}
}
PopulateShopItem() will then instantiate an InventoryItem and will call the Init() function in this class, also passing the ItemData:
private void PopulateShopItem(ItemData data)
{
InventoryItem newItem = Instantiate(item, itemsGrid.transform);
newItem.Init(data);
}
If we go to the Editor Hierarchy and we click on ShopInventory we will see the prefab that we’re instantiating, called ShopItem:
So if we open ShopItem.cs we see that it inherits from InventoryItem.cs. If we navigate to that class we will then be able to see the Init() function which gets the ItemData coming from the database and sets it as a private variable:
public void Init(ItemData newData)
{
_itemData = newData;
StartCoroutine(GetTexture(_itemData.imageUrl));
}
After that it uses its imageUrl to retrieve the texture that will become the item icon:
private IEnumerator GetTexture(string imageUrl)
{
using UnityWebRequest uwr = UnityWebRequestTexture.GetTexture(imageUrl);
_currentWebRequest = uwr;
yield return uwr.SendWebRequest();
if (uwr.result != UnityWebRequest.Result.Success)
{
Debug.Log(uwr.error);
uwr.Dispose();
}
else
{
var tex = DownloadHandlerTexture.GetContent(uwr);
myIcon.sprite = Sprite.Create(tex, new Rect(0.0f, 0.0f, tex.width, tex.height), new Vector2(0.5f, 0.5f), 100.0f);
//Now we are able to click the button and we will pass the loaded sprite :)
myIcon.gameObject.SetActive(true);
myButton.interactable = true;
uwr.Dispose();
}
}
So this is how we create, retrieve and populate our items in the shop inventory. Also remember that because we have subscribed to the events on the ItemData table in the database, if we add, delete, or modify any item there, the shop inventory will get automatically updated. Nice!
4. Deploy Smart Contract
Now it’s time to learn how to mint these items as NFT and transfer them to the player. But first, we will deploy the smart contract that will let us do that.
We’re going to use the web browser MetaMask wallet to deploy the contract (not your mobile MetaMask) so make sure that before we continue, you have imported the Mumbai Testnet and you have some funds on it like we stated in the pre-requisites:
Having MetaMask installed both on your browser and your mobile device, with the Mumbai Testnet imported and some funds in both:
https://moralis.io/mumbai-testnet-faucet-how-to-get-free-testnet-matic-tokens/
Once you’re ready, go to Assets → _Project → ShopContract.txt and copy the code inside:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
//Importing ERC 1155 Token contract from OpenZeppelin
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC1155/ERC1155.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/access/Ownable.sol";
import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Strings.sol";
contract NftShop is ERC1155 , Ownable {
string public name = "Unity NFT Shop";
mapping(uint256 => string) _tokenUrls;
constructor() ERC1155("") {}
function buyItem(uint256 _tokenId, string memory _tokenUrl, bytes memory data) public /*onlyOwner*/{
//IMPORTANT Implement own security (set ownership to users). Not production ready contract
_tokenUrls[_tokenId] = _tokenUrl;
_mint(msg.sender, _tokenId, 1, data);
}
function uri(uint256 id) public view virtual override returns (string memory) {
return _tokenUrls[id];
}
}
IMPORTANT – NOT PRODUCTION READY CONTRACT
As you can see, the buyItem function is public so you may need to add some kind of security/access control to it and make sure you restrict what addresses are able to call the function. You can check https://docs.openzeppelin.com/contracts/2.x/access-control to know more about it.
As the objective of this section is to learn how to call a smart contract function from Unity using Moralis, we will go ahead and leave the function public.
So go to https://remix.ethereum.org/, create a new file called NftShop.sol and paste the copied contract:
On the left sidebar, go to the Solidity Compiler and click on Compile NftShop.sol:
Below the solidity compiler on the left sidebar, go to the Deployer. Under ENVIRONMENT select Injected Web3. MetaMask will pop up and you will need to sign in.
Then, make sure NftShop.sol is selected under CONTRACT. Click on Deploy and you will need to confirm the transaction on MetaMask (make sure you are under the mumbai network):
That’s it! You have deployed your contract to the Mumbai Testnet and under Deployed Contracts you will find the contract address:
Copy the contract address, go to GameManager.cs in Unity and paste it on ContractAddress:
Alright! Now we need the ABI so go back to Remix → Solidity Compiler and copy the contract ABI below Compilation Details:
Before pasting the value in GameManager.cs, we need to format it. So go to https://jsonformatter.org/ and paste the ABI on the left side. Then click on Minify/Compact:
After this, click on the right side, press to Ctrl + F and type “
We need to replace “ for \”
Click on All to replace it in all the text:
Copy the formatted ABI, go back to GameManager.cs and paste it on ContractAbi:
ContractChain is already correct because we just deployed the contract to mumbai.
Alright, contract deployed and configured!
5. Mint items as NFTs
The script that takes care of converting these items to NFTs is PurchaseItemManager.cs and we find it here:
Open the script and you’ll see that it listens to the Selected event triggered by ShopItem.cs:
private void OnEnable()
{
ShopItem.Selected += ActivateItemPanel;
}
This event triggers when we select any item on the shop inventory. ActivateItemPanel() gets the InventoryItem and its data and displays a UI panel with the name, the description and the sprite of the item:
private void ActivateItemPanel(InventoryItem selectedItem)
{
_currentItemData = selectedItem.GetData();
_currentItemData.objectId = selectedItem.GetId();
itemName.text = selectedItem.GetData().name.ToUpper();
itemDescription.text = selectedItem.GetData().description;
itemIcon.sprite = selectedItem.GetSprite();
itemPanel.SetActive(true);
}
And here comes the interesting part; when we click on Buy we will call PurchaseItem() which is one of the main functions of this section. Let’s go through it step by step:
public async void PurchaseItem()
{
PurchaseStarted?.Invoke();
itemPanel.SetActive(false);
transactionInfoText.gameObject.SetActive(true);
transactionInfoText.text = "Creating and saving metadata to IPFS...";
var metadataUrl = await CreateIpfsMetadata();
if (metadataUrl is null)
{
transactionInfoText.text = "Metadata couldn't be saved to IPFS";
StartCoroutine(DisableInfoText());
PurchaseFailed?.Invoke();
return;
}
First it triggers an event called PurchaseStarted to let know all the other scripts that purchase is starting (GameManager.cs will use that to change the game state). After closing the itemPanel and updating the transactionInfoText we proceed to call CreateIpfsMetadata().
That function, using the name, description and imageUrl obtained from the InventoryItem passed on the Selected event, will save the information to IPFS in a JSON format and will return a URL pointing to that JSON data.
If that operation fails for some reason and the URL returned is null, we will trigger the PurchaseFailed event.
On the contrary, if we get a valid URL we will call PurchaseItemFromContract() passing a converted tokenId and the metadataUrl:
transactionInfoText.text = "Metadata saved successfully";
// I'm assuming that this is creating a different tokenId from the already minted tokens in the contract.
// I can do that because I know I'm converting a unique objectId coming from the MoralisDB.
long tokenId = MoralisTools.ConvertStringToLong(_currentItemData.objectId);
transactionInfoText.text = "Please confirm transaction in your wallet";
var result = await PurchaseItemFromContract(tokenId, metadataUrl);
Let’s go to PurchaseItemFromContract() before continuing with the current function, as this is the method that calls the buyItem function from the smart contract:
private async Task<string> PurchaseItemFromContract(BigInteger tokenId, string metadataUrl)
{
byte[] data = Array.Empty<byte>();
object[] parameters = {
tokenId.ToString("x"),
metadataUrl,
data
};
// Set gas estimate
HexBigInteger value = new HexBigInteger("0x0");
HexBigInteger gas = new HexBigInteger(0);
HexBigInteger gasPrice = new HexBigInteger("0x0");
string resp = await Moralis.ExecuteContractFunction(GameManager.ContractAddress, GameManager.ContractAbi, "buyItem", parameters, value, gas, gasPrice);
return resp;
}
First we create an object called parameters with the tokenId, the metadataUrl and an empty byte’s array called data.
Then we set a gas estimate and for this simple case we do that by setting the value, gas and gasPrice to a zero-value HexBigInteger.
Then, the magic happens when we call Moralis.ExecuteContractFunction() passing:
- The ContractAddress and the ContractAbi saved on GameManager.cs
- The name of the contract function → buyItem
- The object parameters. These are the parameters that the contract buyItem function requires.
- The gas-related variables value, gas and gasPrice.
Depending on the transaction result, going back to PurchaseItem(), we will trigger the PurchaseFailed or PurchaseCompleted events:
if (result is null)
{
transactionInfoText.text = "Transaction failed";
StartCoroutine(DisableInfoText());
PurchaseFailed?.Invoke();
return;
}
transactionInfoText.text = "Transaction completed!";
StartCoroutine(DisableInfoText());
PurchaseCompleted?.Invoke(_currentItemData.objectId);
This is the message that you should see in the game while this function is running:
If you confirm the message in your MetaMask wallet, the transaction will succeed and we will have minted the item as an NFT!
Before learning how to retrieve it, it’s important to know that ShopInventory.cs is listening to the PurchaseCompleted and will delete this item from the database which will then not be in the game shop inventory anymore:
private async void DeletePurchasedItem(string purchasedId)
{
MoralisQuery<ItemData> purchasedItemQuery = _getAllItemsQuery.WhereEqualTo("objectId", purchasedId);
IEnumerable<ItemData> purchasedItems = await purchasedItemQuery.FindAsync();
var purchasedItemsList = purchasedItems.ToList();
if (!purchasedItemsList.Any()) return;
await purchasedItemsList.First().DeleteAsync();
}
This is the function that ShopInventory.cs calls when listening to the PurchaseCompleted event.
Because we’re the purchaseId we can create a MoralisQuery selecting the database items that contain that purchasedId. As this id is unique we will only find one element which we will delete using DeleteAsync().
The item won’t appear next time we open the shop inventory. Awesome!
6. Get minted NFTs
After buying the item and waiting for about 1-2 minutes for the NFT to sync, let’s open the player inventory by pressing the “I” key on the keyboard. You should see the item that you just bought there:
That is because PlayerInventory.cs takes care of it:
If we open it, we see that LoadPurchasedItems() is the function that does the magic. Let’s go through it step by step:
To start, we the current logged user and its wallet address:
private async void LoadPurchasedItems()
{
//We get our wallet address.
MoralisUser user = await Moralis.GetUserAsync();
var playerAddress = user.authData["moralisEth"]["id"].ToString();
Then we try to get all the NFTs that the wallet address owns from a specific contract address by calling GetNFTsForContract(), a magic Moralis function:
try
{
NftOwnerCollection noc =
await Moralis.GetClient().Web3Api.Account.GetNFTsForContract(playerAddress.ToLower(),
GameManager.ContractAddress,
GameManager.ContractChain);
List<NftOwner> nftOwners = noc.Result;
If we get some, we will have access to the TokenId and to the Metadata and with that we will populate a new player item:
if (!nftOwners.Any())
{
Debug.Log("You don't own any NFT");
return;
}
foreach (var nftOwner in nftOwners)
{
var nftMetaData = nftOwner.Metadata;
NftMetadata formattedMetaData = JsonUtility.FromJson<NftMetadata>(nftMetaData);
PopulatePlayerItem(nftOwner.TokenId, formattedMetaData);
}
}
catch (Exception exp)
{
Debug.LogError(exp.Message);
}
}
PopulatePlayerItem() works very similar than PopulateShopItem() but here we’re passing an NftMetadata object as parameter instead of an ItemData one:
private void PopulatePlayerItem(string tokenId, NftMetadata data)
{
InventoryItem newItem = Instantiate(item, itemsGrid.transform);
newItem.Init(tokenId, data);
}
The good thing is that instantiating a PlayerItem is very similar to instantiating a ShopItem because both inherit from InventoryItem.cs. So when calling Init(), a PlayerItem will be created in a very similar way than a ShopItem would be.
So that’s almost it but now that we have retrieved the NFT, we can learn how to check it on OpenSea.
Super simple, go to PlayerItem.cs and there we have OnItemClicked(). This function will be called when we click on the item in the player inventory and it will call MoralisTools.CheckNftOnOpenSea() passing the ContractAddres, the ContractChain and the id of the item:
public void OnItemClicked()
{
MoralisTools.CheckNftOnOpenSea(GameManager.ContractAddress, GameManager.ContractChain.ToString(), GetId());
}
CheckNftOnOpenSea will use that parameters to fill the OpenSea Testnets URL:
public static void CheckNftOnOpenSea(string contractAddress, string contractChain, string tokenId)
{
string url = $"https://testnets.opensea.io/assets/{contractChain}/{contractAddress}/{tokenId}";
Application.OpenURL(url);
}
With the URL set, Application.OpenURL(url) will take care of opening our default web browser and showing the NFT on OpenSea:
Congratulations, you completed the Purchase NFTs from a NPC Shop Moralis Project and now you know how to create a full on-chain inventory system with Unity.
See you on the next one, Moralis Mage!