Unity Game Development on Kaia: Minting Fungible Tokens with ChainSafe

Introduction

Unity Game Development on Kaia: Minting Fungible Tokens with ChainSafe

Introduction

Blockchain technology has opened exciting new possibilities for game developers. Features like decentralization, immutability, and programmable money are transforming how we think about games and player experiences.

The gaming industry has seen tremendous growth in blockchain integration. Play-to-earn(P2E) games and metaverse platforms are becoming mainstream. These platforms let players buy items with cryptocurrency and collect unique digital assets. Rare items like swords, potions, and shields now exist on the blockchain as NFTs, where players can trade them based on rarity and utility.

Sounds exciting, right? You can build these features into your Unity game by integrating the ChainSafe gaming SDK. This guide will walk you through creating and integrating fungible tokens (KIP7 tokens) in your game on Unity. By the end, you’ll know how to connect your Unity game to the Kaia blockchain.

Prerequisites

Why Choose Unity?

Unity is an excellent choice for game development. It’s a powerful platform for creating 2D and 3D games across computers, mobile devices, and more. Unity Technologies created it in 2005, and it uses C# and C++ programming languages. A key advantage is its accessibility, with free versions available for many users.

Why Choose ChainSafe?

ChainSafe is an open source SDK that provides the base layer for blockchain-enabled games. It helps us build games that interact with blockchains. With ChainSafe, you can:

  • Connect your game to any EVM compatible blockchain
  • Create different token types in Unity (ERC20, ERC721, ERC1155)
  • Build in game marketplaces
  • Import NFTs and much more

Getting Started

Create a new Unity3D project

To create a new project, do these:

  • Navigate to the Projects tab,
  • Click on the New project button.
  • Select All templates. We will use a 3D template,
  • Click on Create project.

Setup web3.unity

  1. Navigate to chainsafe docs and follow the getting started instructions to download the web3.unity SDK.

:::note
While setting up your Unity project, you are required to register your project with ChainSafe in order to successfully build with the SDK. In the case where you get a ChainSafeServerSettings prompt while importing the ChainSafe SDK in the step above, complete the next steps.

:::

  1. Navigate to the chainsafe dashboard to create your project and grab your project ID.
  1. Paste your project ID into your network settings and configure the chain to Kaia Kairos Testnet

Setting Up the Game Scene

We need to prepare our scene for Web3 functionality:

Loading the Scene

  • Under Assets > Scenes, double click on SampleScene. This will be where all our token functionality lives

Adding Web3Unity Prefab

  1. Navigate to Packages > web3.unity SDK > Runtime > Prefabs
  2. Drag the Web3Unity Prefab into your scene hierarchy
  3. Go to Connection Handler and expand the connection provider dropdown
  4. Click “Add Provider” under MetaMask to add MetaMask as the wallet provider

Creating the UI Canvas

  1. Right click in the scene hierarchy
  2. Navigate to GameObject > UI > Canvas
  1. Copy the ConnectToWallet prefab from Packages > web3.unity SDK > Runtime > Prefabs
  2. Paste it as a child of the Canvas

This prefab handles all blockchain connections and wallet management

Building Your Token Interface

Now let’s create the user interface components for our token functionality:

Token Name Component

We’ll create a button to fetch the token name and a text field to display it:

Name Button:

  1. Right click on Canvas > UI > Button (TextMeshPro)
  2. Rename it to “NameButton”
  3. Set the button text to “Name”

Token Name Display:

  1. Right click on Canvas > UI > Text (TextMeshPro)
  2. Rename it to “TokenNameText”
  3. Set the initial text to “Token Name: ”

Note about TextMeshPro: When you first create a TextMeshPro element, Unity will prompt you to import TMP Essentials. This includes the shaders, fonts, and materials needed for text to display properly. If you skip this prompt, you can import it later through Window > TextMeshPro > Import TMP Essentials.

Balance Checker Component

This lets users check token balances for any wallet address:

Wallet Address Input:

  1. Right click on Canvas > UI > Input Field (TextMeshPro)
  2. Rename to “WalletAddressInputField”
  3. Set placeholder text to “Enter wallet address…”

Balance Button:

  1. Create another button named “BalanceOfButton”
  2. Set text to “Check Balance”

Balance Display:

  1. Create text element “TokenBalanceText”
  2. Set initial text to “Balance: “

Mint Component

For creating new tokens:

Recipient Address Input:

  1. Create input field “ToAddressField”
  2. Placeholder: “Enter recipient address…”

Mint Amount Input:

  1. Create input field “MintAmountField”
  2. Placeholder: “Enter amount to mint…”

Mint Button and Status:

  1. Create button “MintButton” with text “Mint”
  2. Create text “MintStatusText” (leave empty initially)

Transfer Component

For sending tokens between wallets:

Transfer Components:

  1. Input field “RecipientAddressField” with placeholder “Enter recipient address…”
  2. Input field “TransferAmountField” with placeholder “Enter amount…”
  3. Button “TransferButton” with text “Transfer”
  4. Text “TransferStatusText” (leave empty initially)

After creating all components, arrange them neatly in your scene using the Move Tool.

Creating and deploying KIP7 smart contract

First, we’ll use Kaia Contract Wizard to generate our smart contract.

Step 1: Using Kaia Contract Wizard

  1. Navigate to Kaia Contract Wizard.
  2. Select KIP7 (Kaia’s token standard, similar to ERC20).
  3. Configure your token:
  • Name: UnityTestToken (or something else!)
  • Symbol: UTT (your token’s ticker)
  • Premint: 100 (initial token supply)
  • Features: Check ✅ Mintable

:::Note

For this tutorial, we’ll modify the contract to allow anyone to mint tokens (not just the owner). We will tweak the mint function not to have onlyOwner modifier. To do this, we have to remove the ownable.sol import, and Ownable inheritance. The final contract code should now look like this:

:::

```sol 
// SPDX-License-Identifier: MIT 
pragma solidity ^0.8.4; 
import "@kaiachain/contracts/KIP/token/KIP7/KIP7.sol"; 
contract UnityTestToken is KIP7 { 
constructor() KIP7("UnityTestToken", "UTT") { 
_mint(msg.sender, 100 * 10 ** decimals()); 
} 
function supportsInterface(bytes4 interfaceId) 
public 
view 
virtual 
override 
returns (bool) 
{ 
return 
super.supportsInterface(interfaceId); 
} 
function mint(address to, uint256 amount) public { 
_mint(to, amount); 
} 
}

Step 2: Deploying via Remix IDE

  1. Copy and Paste the code above in a newly created file UnityTestToken.sol on Remix IDE.
  2. In Remix IDE:
  • Click the Compile contract button.
  • Activate the Kaia plugin in the plugin manager.
  • Under Environment in the Kaia Plugin tab, choose Injected Provider — Kaia Wallet.
  • Find your contract (UnityTestToken) in the Contract dropdown.
  • Click Deploy to launch your token!
  1. When your Kaia Wallet pops up:
  • Review the deployment details.
  • Click Confirm to deploy to Kaia Kairos Testnet.

:::IMPORTANT

Copy and save the deployed contract address. You’ll need it later in the tutorial.

:::

Converting solidity calls to C#

In this section, we will encapsulate all of our contract ABI into more user-friendly, statically typed methods:

  1. Navigate to Remix, click on the compile button, and copy the contract ABI for UnityTestToken.sol
  2. In Unity, navigate to ChainSafe SDK > Contract ABI To C# Generator
  3. Name your file UnityTestToken, create a folder called Scripts in your asset folder , drag and drop the Scripts folder in the Asset folder into the Select folder field in your Contract ABI to C# Converter Modal, and paste in your ABI.
  4. Click convert.

This creates all the C# scripts you need to interact with your smart contract.

Implementing TokenManager Script

In this step we will create a comprehensive script that handles all token operations. To do so, follow the steps below:

  • Create a TokenManager script in your Scripts folder.
  • Copy and paste the code below into your newly created scripts file:
using System; 
using System.Collections; 
using System.Collections.Generic; 
using UnityEngine; 
using UnityEngine.UI; 
using TMPro; 
using ChainSafe.Gaming.UnityPackage; 
using ChainSafe.Gaming.UnityPackage.Connection; 
using ChainSafe.Gaming.Web3; 
using ChainSafe.Gaming.Evm.Contracts.Custom; 
using System.Numerics; 
using System.Threading.Tasks; 
public class TokenManager : MonoBehaviour 
{ 
[SerializeField] private string ContractAddress; 
[SerializeField] private string DefaultWalletAddress; // Default address to check balance 
// Basic Token Info UI 
[SerializeField] private TMP_Text TokenNameText; 
[SerializeField] private TMP_Text TokenBalanceText; 
// New wallet address input field for checking balance of any address 
[SerializeField] private TMP_InputField WalletAddressInputField; 
// Transfer UI elements 
[SerializeField] private TMP_InputField RecipientAddressField; 
[SerializeField] private TMP_InputField TransferAmountField; 
[SerializeField] private Button TransferButton; 
[SerializeField] private TMP_Text TransferStatusText; 
// Minting UI elements 
[SerializeField] private TMP_InputField ToAddressField; 
[SerializeField] private TMP_InputField MintAmountField; 
[SerializeField] private Button MintButton; 
[SerializeField] private TMP_Text MintStatusText; // Optional - for showing mint status 
private UnityTestToken _tokenContract; 
private bool _isContractInitialized = false; 
private int _tokenDecimals = 18; // Default to 18, will be updated when contract is initialized 
private void Awake() 
{ 
Web3Unity.Web3Initialized += Web3UnityOnWeb3Initialized; 
// Disable buttons until contract is initialized 
if (TransferButton != null) 
{ 
TransferButton.interactable = false; 
} 
if (MintButton != null) 
{ 
MintButton.interactable = false; 
} 
// Clear status texts 
if (TransferStatusText != null) 
{ 
TransferStatusText.text = ""; 
} 
if (MintStatusText != null) 
{ 
MintStatusText.text = ""; 
} 
// Initialize wallet address input field with default wallet 
if (WalletAddressInputField != null) 
{ 
WalletAddressInputField.text = DefaultWalletAddress; 
} 
} 
private async void Web3UnityOnWeb3Initialized((Web3 web3, bool isLightWeight) obj) 
{ 
try 
{ 
// Initialize the token contract 
_tokenContract = await obj.web3.ContractBuilder.Build<UnityTestToken>(ContractAddress); 
_isContractInitialized = true; 
// Get and store token decimals for later use 
BigInteger decimals = await _tokenContract.Decimals(); 
_tokenDecimals = (int)decimals; 
// Enable buttons now that contract is initialized 
if (TransferButton != null) 
{ 
TransferButton.interactable = true; 
} 
if (MintButton != null) 
{ 
MintButton.interactable = true; 
} 
Debug.Log("Token contract initialized successfully. Decimals: " + _tokenDecimals); 
// Automatically get the token balance when initialized 
GetTokenBalance(); 
} 
catch (System.Exception ex) 
{ 
Debug.LogError($"Error initializing token contract: {ex.Message}"); 
if (TransferStatusText != null) 
{ 
TransferStatusText.text = "Failed to initialize contract"; 
TransferStatusText.color = Color.red; 
} 
} 
} 
private void OnDestroy() 
{ 
Web3Unity.Web3Initialized -= Web3UnityOnWeb3Initialized; 
// Dispose of the contract when the object is destroyed 
if (_tokenContract != null) 
{ 
_tokenContract.DisposeAsync().AsTask().Wait(); 
} 
} 
// Button click handler for Name button 
public async void GetTokenName() 
{ 
if (!_isContractInitialized) 
{ 
TokenNameText.text = "Contract not initialized yet. Please wait."; 
return; 
} 
try 
{ 
TokenNameText.text = "Loading token name…"; 
// Get token name 
string name = await _tokenContract.Name(); 
TokenNameText.text = $"Token Name: {name}"; 
Debug.Log($"Token name fetched successfully: {name}"); 
} 
catch (System.Exception ex) 
{ 
TokenNameText.text = "Error fetching token name."; 
Debug.LogError($"Error fetching token name: {ex.Message}"); 
} 
} 
// Button click handler for BalanceOf button - now uses input field if available 
public async void GetTokenBalance() 
{ 
if (!_isContractInitialized) 
{ 
TokenBalanceText.text = "Contract not initialized yet. Please wait."; 
return; 
} 
// Get the address to check - either from input field or default 
string addressToCheck = DefaultWalletAddress; 
// If we have the input field and it's not empty, use that address instead 
if (WalletAddressInputField != null && !string.IsNullOrEmpty(WalletAddressInputField.text)) 
{ 
addressToCheck = WalletAddressInputField.text; 
} 
// Validate the address format 
if (!IsValidEthereumAddress(addressToCheck)) 
{ 
TokenBalanceText.text = "Invalid wallet address format"; 
return; 
} 
try 
{ 
TokenBalanceText.text = "Loading token balance…"; 
// Get balance of the address 
BigInteger balance = await _tokenContract.BalanceOf(addressToCheck); 
// Format the balance considering decimals 
decimal formattedBalance = ConvertFromWei(balance, _tokenDecimals); 
TokenBalanceText.text = $"Balance: {formattedBalance}"; 
Debug.Log($"Token balance fetched successfully. Address: {addressToCheck}, Balance: {formattedBalance}"); 
} 
catch (System.Exception ex) 
{ 
TokenBalanceText.text = "Error fetching token balance."; 
Debug.LogError($"Error fetching token balance: {ex.Message}"); 
} 
} 
// Button click handler for Transfer button with balance-based verification 
public async void TransferTokens() 
{ 
if (!_isContractInitialized) 
{ 
UpdateTransferStatus("Contract not initialized yet", Color.red); 
return; 
} 
// Validate input fields 
if (string.IsNullOrEmpty(RecipientAddressField.text)) 
{ 
UpdateTransferStatus("Please enter a recipient address", Color.red); 
return; 
} 
if (string.IsNullOrEmpty(TransferAmountField.text)) 
{ 
UpdateTransferStatus("Please enter an amount to transfer", Color.red); 
return; 
} 
string recipientAddress = RecipientAddressField.text; 
// Validate recipient address format 
if (!IsValidEthereumAddress(recipientAddress)) 
{ 
UpdateTransferStatus("Invalid recipient address format", Color.red); 
return; 
} 
// Parse amount and validate 
decimal amount; 
if (!decimal.TryParse(TransferAmountField.text, out amount)) 
{ 
UpdateTransferStatus("Invalid amount format", Color.red); 
return; 
} 
if (amount <= 0) 
{ 
UpdateTransferStatus("Amount must be greater than 0", Color.red); 
return; 
} 
// Disable button to prevent multiple clicks 
if (TransferButton != null) 
{ 
TransferButton.interactable = false; 
} 
UpdateTransferStatus("Processing transfer…", Color.yellow); 
try 
{ 
// Get initial balance for comparison 
BigInteger initialBalance = await _tokenContract.BalanceOf(DefaultWalletAddress); 
// Convert amount to wei 
BigInteger amountInWei = ConvertToWei(amount, _tokenDecimals); 
Debug.Log($"Attempting to transfer {amount} tokens ({amountInWei} wei) to {recipientAddress}"); 
Debug.Log($"Initial balance: {initialBalance}"); 
// Attempt the transfer - even if it returns an error, the transaction might succeed 
try 
{ 
UpdateTransferStatus("Sending transaction…", Color.yellow); 
// Call the transfer function 
await _tokenContract.Transfer(recipientAddress, amountInWei); 
UpdateTransferStatus("Transaction submitted, waiting for confirmation…", Color.yellow); 
} 
catch (Exception ex) 
{ 
// Log the error but continue - this might be a UI callback error only 
Debug.LogWarning($"Transfer call reported an error: {ex.Message}"); 
Debug.LogWarning("Continuing to verify if the transaction actually went through…"); 
UpdateTransferStatus("Transaction may have failed, verifying…", new Color(1f, 0.6f, 0f)); // Orange 
} 
// Wait for transaction to be processed 
await VerifyTransferSuccess(initialBalance, recipientAddress, amount); 
} 
catch (Exception ex) 
{ 
// More serious error - couldn't even check balances 
UpdateTransferStatus($"Error: {ex.Message}", Color.red); 
Debug.LogError($"Transfer error: {ex.Message}"); 
} 
finally 
{ 
// Re-enable button 
if (TransferButton != null) 
{ 
TransferButton.interactable = true; 
} 
} 
} 
// Button click handler for Mint button 
public async void MintTokens() 
{ 
if (!_isContractInitialized) 
{ 
UpdateMintStatus("Contract not initialized yet", Color.red); 
return; 
} 
// Validate input fields 
if (string.IsNullOrEmpty(ToAddressField.text)) 
{ 
UpdateMintStatus("Please enter a recipient address", Color.red); 
return; 
} 
if (string.IsNullOrEmpty(MintAmountField.text)) 
{ 
UpdateMintStatus("Please enter an amount to mint", Color.red); 
return; 
} 
string mintToAddress = ToAddressField.text; 
// Validate address format 
if (!IsValidEthereumAddress(mintToAddress)) 
{ 
UpdateMintStatus("Invalid recipient address format", Color.red); 
return; 
} 
// Parse amount and validate 
decimal amount; 
if (!decimal.TryParse(MintAmountField.text, out amount)) 
{ 
UpdateMintStatus("Invalid amount format", Color.red); 
return; 
} 
if (amount <= 0) 
{ 
UpdateMintStatus("Amount must be greater than 0", Color.red); 
return; 
} 
// Disable button to prevent multiple clicks 
if (MintButton != null) 
{ 
MintButton.interactable = false; 
} 
UpdateMintStatus("Processing mint…", Color.yellow); 
try 
{ 
// Get initial balance of the recipient for comparison 
BigInteger initialBalance = await _tokenContract.BalanceOf(mintToAddress); 
// Convert amount to wei 
BigInteger amountInWei = ConvertToWei(amount, _tokenDecimals); 
Debug.Log($"Attempting to mint {amount} tokens ({amountInWei} wei) to {mintToAddress}"); 
Debug.Log($"Initial recipient balance: {initialBalance}"); 
// Attempt the mint - similar to transfer, it might report an error but still succeed 
try 
{ 
UpdateMintStatus("Sending mint transaction…", Color.yellow); 
// Call the mint function 
await _tokenContract.Mint(mintToAddress, amountInWei); 
UpdateMintStatus("Mint transaction submitted, waiting for confirmation…", Color.yellow); 
} 
catch (Exception ex) 
{ 
// Log the error but continue - this might be a UI callback error only 
Debug.LogWarning($"Mint call reported an error: {ex.Message}"); 
Debug.LogWarning("Continuing to verify if the mint actually went through…"); 
UpdateMintStatus("Mint may have failed, verifying…", new Color(1f, 0.6f, 0f)); // Orange 
} 
// Wait for transaction to be processed and verify 
await VerifyMintSuccess(initialBalance, mintToAddress, amount); 
} 
catch (Exception ex) 
{ 
// More serious error - couldn't even check balances 
UpdateMintStatus($"Error: {ex.Message}", Color.red); 
Debug.LogError($"Mint error: {ex.Message}"); 
} 
finally 
{ 
// Re-enable button 
if (MintButton != null) 
{ 
MintButton.interactable = true; 
} 
} 
} 
// Verify if a mint succeeded by checking balance changes 
private async Task VerifyMintSuccess(BigInteger initialBalance, string mintToAddress, decimal amount) 
{ 
// First delay to allow transaction to process 
for (int i = 0; i < 3; i++) 
{ 
await Task.Delay(1000); // Wait 1 second 
UpdateMintStatus($"Verifying mint… ({i+1}/3)", Color.yellow); 
} 
try 
{ 
// Check recipient balance after delay 
BigInteger newBalance = await _tokenContract.BalanceOf(mintToAddress); 
Debug.Log($"New balance after mint attempt: {newBalance}"); 
// Calculate expected balance after mint 
BigInteger expectedNewBalance = initialBalance + ConvertToWei(amount, _tokenDecimals); 
// If balance increased by approximately the mint amount, consider it successful 
if (newBalance >= expectedNewBalance) 
{ 
decimal changeAmount = ConvertFromWei(newBalance - initialBalance, _tokenDecimals); 
UpdateMintStatus($"Mint successful! Created {changeAmount} tokens", Color.green); 
// Clear amount field on success 
MintAmountField.text = ""; 
// Update the displayed balance if we're checking the same address 
GetTokenBalance(); 
} 
else if (newBalance > initialBalance) 
{ 
// Balance increased but not by the expected amount 
decimal changeAmount = ConvertFromWei(newBalance - initialBalance, _tokenDecimals); 
UpdateMintStatus($"Partial mint? Balance increased by {changeAmount} tokens", new Color(1f, 0.6f, 0f)); // Orange 
GetTokenBalance(); 
} 
else 
{ 
// Balance didn't increase 
UpdateMintStatus("Mint failed - no balance change detected", Color.red); 
} 
} 
catch (Exception ex) 
{ 
UpdateMintStatus($"Error verifying mint: {ex.Message}", Color.red); 
Debug.LogError($"Mint verification error: {ex.Message}"); 
} 
} 
// Verify if a transfer succeeded by checking balance changes 
private async Task VerifyTransferSuccess(BigInteger initialBalance, string recipientAddress, decimal amount) 
{ 
// First delay to allow transaction to process 
for (int i = 0; i < 3; i++) 
{ 
await Task.Delay(1000); // Wait 1 second 
UpdateTransferStatus($"Verifying transaction… ({i+1}/3)", Color.yellow); 
} 
try 
{ 
// Check sender balance after delay 
BigInteger newBalance = await _tokenContract.BalanceOf(DefaultWalletAddress); 
Debug.Log($"New balance after transfer attempt: {newBalance}"); 
// Calculate expected balance after transfer 
BigInteger expectedNewBalance = initialBalance - ConvertToWei(amount, _tokenDecimals); 
// If balance decreased by approximately the transfer amount, consider it successful 
if (newBalance <= expectedNewBalance) 
{ 
decimal changeAmount = ConvertFromWei(initialBalance - newBalance, _tokenDecimals); 
UpdateTransferStatus($"Transfer successful! Sent {changeAmount} tokens", Color.green); 
// Clear amount field on success 
TransferAmountField.text = ""; 
// Update the displayed balance 
GetTokenBalance(); 
// Also check recipient balance if it's a different address 
if (recipientAddress.ToLower() != DefaultWalletAddress.ToLower()) 
{ 
try 
{ 
BigInteger recipientBalance = await _tokenContract.BalanceOf(recipientAddress); 
Debug.Log($"Recipient balance: {ConvertFromWei(recipientBalance, _tokenDecimals)}"); 
} 
catch (Exception ex) 
{ 
Debug.LogWarning($"Failed to get recipient balance: {ex.Message}"); 
} 
} 
} 
else if (newBalance < initialBalance) 
{ 
// Balance decreased but not by the expected amount 
decimal changeAmount = ConvertFromWei(initialBalance - newBalance, _tokenDecimals); 
UpdateTransferStatus($"Partial transfer? Balance reduced by {changeAmount} tokens", new Color(1f, 0.6f, 0f)); // Orange 
GetTokenBalance(); 
} 
else 
{ 
// Balance didn't decrease 
UpdateTransferStatus("Transfer failed - no balance change detected", Color.red); 
} 
} 
catch (Exception ex) 
{ 
UpdateTransferStatus($"Error verifying transfer: {ex.Message}", Color.red); 
Debug.LogError($"Verification error: {ex.Message}"); 
} 
} 
// Helper method to update transfer status text 
private void UpdateTransferStatus(string message, Color color) 
{ 
if (TransferStatusText != null) 
{ 
TransferStatusText.text = message; 
TransferStatusText.color = color; 
} 
Debug.Log(message); 
} 
// Helper method to update mint status text 
private void UpdateMintStatus(string message, Color color) 
{ 
if (MintStatusText != null) 
{ 
MintStatusText.text = message; 
MintStatusText.color = color; 
} 
Debug.Log($"[MINT] {message}"); 
} 
// Helper method to convert from wei to token units based on decimals 
private decimal ConvertFromWei(BigInteger weiAmount, int decimals) 
{ 
if (decimals == 0) return (decimal)weiAmount; 
// Calculate divisor (10^decimals) 
BigInteger divisor = BigInteger.Pow(10, decimals); 
// Convert to decimal for division 
decimal result = (decimal)weiAmount / (decimal)divisor; 
return result; 
} 
// Helper method to convert from token units to wei based on decimals 
private BigInteger ConvertToWei(decimal amount, int decimals) 
{ 
if (decimals == 0) return new BigInteger(amount); 
// Calculate multiplier (10^decimals) 
decimal multiplier = (decimal)Math.Pow(10, decimals); 
// Convert to BigInteger after multiplication 
BigInteger result = new BigInteger(amount * multiplier); 
return result; 
} 
// Helper method to validate Ethereum address format 
private bool IsValidEthereumAddress(string address) 
{ 
// Basic validation - check if it's 42 characters long and starts with 0x 
return address.Length == 42 && address.StartsWith("0x", StringComparison.OrdinalIgnoreCase); 
} 
}

Setting Up the TokenManager GameObject

  1. Create the Manager Object:
  • Right-click in the Hierarchy window (root level).
  • Select “Create Empty Object”.
  • Name it “TokenManager”.
  1. Attach Your Script:
  • Select the TokenManager GameObject.
  • In Inspector, click Add Component.
  • Search for and select “TokenManager”.

3. Connect UI Elements:

  • With TokenManager selected, look in the Inspector.
  • Drag and drop your UI elements from the Hierarchy to the corresponding fields:
  • TokenNameText
  • TokenBalanceText
  • Mint, Transfer, Name, BalanceOf buttons
  • Input fields ….
  • Set your contract address and default wallet address.

Connecting Buttons to Script

In this section, we will connect the following buttons with their respective functions from the TokenManager script:

  • Select a button (like NameButton)
  • In the Inspector, find the OnClick() section and click the ➕ button
  • Drag the TokenManager Object from Hierarchy window into the None (object) field
  • Click on No Function → TokenManager → GetTokenName()
  • Repeat step 1–3 for the buttons below and change their reception to:
  • BalanceOf → GetTokenBalance()
  • Mint → MintTokens()
  • Transfer → TransferTokens()

WebGL Build Settings

In this section, we will configure our dApp for the web deployment:

  • Go to File → Build Settings → WebGL → Switch Platform
  • Unity will prompt you to install required components:
  • Async Utilities & WebGL Threading Patcher (click Yes)
  • web3.unity WebGL templates (click Yes)
  • From the same window, click on Add Open Scenes (top right) to add the SampleScene as the first scene to appear when we run the game.
  • From the same window, click on Player Settings → Player → Resolution and Presentation, under WebGL Template, select the one titled Web3.Unity.

Testing and running game application

In this section, we will test the various token functionalities in the game application. To see this in action, follow the steps below:

  • Build and run the project: Navigate to File → Build and Run
  • Save your WebGL build when prompted
  • If prompted about stripping level, select “Minimal”
  • When the project builds and run, it opens a tab in your browser — SampleScene
  • Click on Login to connect Metamask.
  • Once connected, test all the token functionalities:
  • Get the token name
  • Check balances
  • Mint new tokens
  • Transfer tokens between addresses

Conclusion

Congratulations! You’ve successfully created a Unity application that interacts with fungible tokens on the Kaia blockchain. This foundation opens up many possibilities:

  • In game economies powered by custom tokens
  • Player rewards through automated token minting
  • Item trading via secure token transfers
  • Cross-game compatibility using standardized tokens

This is just the beginning of your Web3 gaming journey. Traditional games run independently, but Web3 games connect to the blockchain to verify transactions. The speed of these transactions directly impacts gameplay. Kaia blockchain offers up to 4,000 transactions per second with immediate finality, making it ideal for responsive Web3 gaming experiences.

Visit ChainSafe Documentation to learn more about connecting Unity games to the blockchain and Kaia Dev Forum if you have any questions.