Technology

Minting KIP-7 Tokens using Unity with ChainSafe SDK

Introduction

The existence of blockchain features such as decentralization, immutability has brought about numerous use cases for practically all industries not excluding the gaming industry. As a game developer or a game enthusiast, you would want to understand and tap into these great benefits and build games that entertain players.

Recently, the gaming industry has experienced a major shift with blockchain. Many play-to-earn and metaverse use cases have evolved and we have seen them go mainstream. These platforms often require players to buy items with cryptocurrency in order to play the game and collect unique, in-game items. These rare in-game items such as swords, potions, and shields reside on-chain using NFTs. Items can be sold according to their rarity and functionality within a game.

This sounds great right? All these functionalities are possible when you build your game with Unity and integrate with a gaming SDK as ChainSafe. In this guide we would take a step-by-step approach to minting fungible tokens (KIP-7) on unity with the ChainSafe gaming SDK. So by the end of this article you would understand how to bridge your Unity game to the blockchain – Klaytn.

Prerequisites

Why Unity?

If you want to make games

Unity software is used for the development of 2D and 3D games for computers, mobile, etc. It is developed by Unity Technologies in 2005 and is written in C# and C++ language. It is a free and open-source software. It was first showcased at Apple Inc’s Worldwide Developers Conference as a Mac OS X-exclusive game engine.

Why ChainSafe?

If you want to bridge Unity games to the blockchain

ChainSafe is an 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, ERC 1155.
  • Build an in-game marketplace.
  • Import NFT’s and more.

Getting Started

By the end of this article you would understand how to bridge your Unity game to the blockchain, mint and transfer tokens on Klaytn testnet in your game projects. To get started, let’s follow through this step by step guide:

Step 1. Install ChainSafe SDK

  • Go to the ChainSafe Gaming SDK from their GitHub repository called web3.unity on ChainSafe
  • Click on the latest release
  • Click on web3.unitypackage to download the package

Step 2. Install Unity and Unity Hub

  • Go to Download Unity: Create a Unity ID and Download Unity Hub or follow this guide here
  • Choose Version: Having created a Unity ID and downloaded Unity Hub, lets download Unity. For the sake of this project, we would be using version 2020.3.30f1. To download this version, navigate to download archive, scroll to select version 2020.3.30f1 then download.
  • On click of the download button, it opens up the installs tab in your Unity Hub. Add the WebGL Module and Vs Code if not installed. Your UI should look like this after installation.

Step 3. Create a new Unity3D project

To create a new project, do these:

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

Step 4. Import the ChainSafe SDK into your project

  • Drag the downloaded web3.unitypackage file in the Installation section into the Unity project
  • To have a smooth run using the ChainSafe SDK, you might want to install NewtonSoft package. This is important to avoid this error as seen below
  • Import the package as follows: Window -> Package Manager -> My Assets -> JSON. NET For Unity -> Import

Step 6. Use the WebLogin prefab to enable web3 wallet connection

  • Under Assets → Web3Unity → Scenes, double-click on WebLogin. This is the prefab used to connect a wallet in a WebGL project
  • Go to File → Build Settings → WebGL → Switch Platform
  • From the same window, click on Add Open Scenes (top right) to add the Login scene as the first scene to appear when we run the project.
  • From the same window, click on Player Settings → Player → Resolution and Presentation, under WebGL Template, select the one with the same as our Unity version (WebGL 2020 for our case).

Step 7. Create a SampleScene

  • Go back to the Unity project. Under Assets, select Scenes and double-click on SampleScene to use it as our second scene (FYI the first one is the login scene)
  • Go to File → Build Settings → Add Open Scenes. The SampleScene will appear under the WebLogin scene. The SampleScene is where we will create the buttons to read and write to the contract. This scene will pop-up after the WebLogin.

Note: Make sure the WebLogin scene is at the top because the order matters

Step 8. Create your contract

COPY

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@klaytn/contracts/KIP/token/KIP7/KIP7.sol";
import "@klaytn/contracts/access/Ownable.sol";

contract MyToken is KIP7, Ownable {
    constructor() KIP7("Test Token", "TST") {     
        _mint(msg.sender, 100000 * 10 ** 18);        
    }

     function mintToken(address account, uint256 amount) public onlyOwner {
        _safeMint(account, amount);
    }
}
  • Compile your contract and deploy it to baobab testnet (get your faucet here Klaytn Wallet ) ABI formatting:  We would copy our ABI from the IDE to be included in our Unity Script. The ABI has a special formatting. Replace the   with \” , otherwise it will not be recognised by the SDK.
  • Example:

COPY

string abi = “[ { \”inputs\”: [ { \”internalType\”: \”uint8\”, \”name\”: \”_myArg\”, \”type\”: \”uint8\” } ], \”name\”: \”addTotal\”,
\”outputs\”: [], \”stateMutability\”: \”nonpayable\”, \”type\”: \”function\” }, { \”inputs\”: [], \”name\”: \”myTotal\”, \”outputs\”:
[ { \”internalType\”: \”uint256\”, \”name\”: \”\”, \”type\”: \”uint256\” } ], \”stateMutability\”: \”view\”, \”type\”: \”function\” } ]”;

Step 9. Create your C# script on Unity

  • Under Project window, right-click on Scenes, click on Create → C# Script and rename it to ERC20Custom
  • Open the script in VS Code and paste the code below.
  • You can change some values (contract address, account, toAccount) in the script to fit yours.
  • Change some arguments in the functions to fit yours

COPY

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
using System.Numerics;
using UnityEngine.UI;
using Newtonsoft.Json;

public class ERC20CUSTOM : MonoBehaviour
{


    // set chain: ethereum, polygon, klaytn, etc
    string chain = "klaytn";
    // set network mainnet, testnet
    string network = "testnet"; 
    // wallet address that deployed the contract
    private string account = "YOUR ACCOUNT";
    // set ABI
    private readonly string abi = "[ { \"inputs\": [], \"stateMutability\": \"nonpayable\", \"type\": \"constructor\" }, { \"anonymous\": false, \"inputs\": [ { \"indexed\": true, \"internalType\": \"address\", \"name\": \"owner\", \"type\": \"address\" }, { \"indexed\": true, \"internalType\": \"address\", \"name\": \"spender\", \"type\": \"address\" }, { \"indexed\": false, \"internalType\": \"uint256\", \"name\": \"value\", \"type\": \"uint256\" } ], \"name\": \"Approval\", \"type\": \"event\" }, { \"anonymous\": false, \"inputs\": [ { \"indexed\": true, \"internalType\": \"address\", \"name\": \"previousOwner\", \"type\": \"address\" }, { \"indexed\": true, \"internalType\": \"address\", \"name\": \"newOwner\", \"type\": \"address\" } ], \"name\": \"OwnershipTransferred\", \"type\": \"event\" }, { \"anonymous\": false, \"inputs\": [ { \"indexed\": true, \"internalType\": \"address\", \"name\": \"from\", \"type\": \"address\" }, { \"indexed\": true, \"internalType\": \"address\", \"name\": \"to\", \"type\": \"address\" }, { \"indexed\": false, \"internalType\": \"uint256\", \"name\": \"value\", \"type\": \"uint256\" } ], \"name\": \"Transfer\", \"type\": \"event\" }, { \"inputs\": [ { \"internalType\": \"address\", \"name\": \"owner\", \"type\": \"address\" }, { \"internalType\": \"address\", \"name\": \"spender\", \"type\": \"address\" } ], \"name\": \"allowance\", \"outputs\": [ { \"internalType\": \"uint256\", \"name\": \"\", \"type\": \"uint256\" } ], \"stateMutability\": \"view\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"address\", \"name\": \"spender\", \"type\": \"address\" }, { \"internalType\": \"uint256\", \"name\": \"amount\", \"type\": \"uint256\" } ], \"name\": \"approve\", \"outputs\": [ { \"internalType\": \"bool\", \"name\": \"\", \"type\": \"bool\" } ], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"address\", \"name\": \"account\", \"type\": \"address\" } ], \"name\": \"balanceOf\", \"outputs\": [ { \"internalType\": \"uint256\", \"name\": \"\", \"type\": \"uint256\" } ], \"stateMutability\": \"view\", \"type\": \"function\" }, { \"inputs\": [], \"name\": \"decimals\", \"outputs\": [ { \"internalType\": \"uint8\", \"name\": \"\", \"type\": \"uint8\" } ], \"stateMutability\": \"view\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"address\", \"name\": \"spender\", \"type\": \"address\" }, { \"internalType\": \"uint256\", \"name\": \"subtractedValue\", \"type\": \"uint256\" } ], \"name\": \"decreaseAllowance\", \"outputs\": [ { \"internalType\": \"bool\", \"name\": \"\", \"type\": \"bool\" } ], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"address\", \"name\": \"spender\", \"type\": \"address\" }, { \"internalType\": \"uint256\", \"name\": \"addedValue\", \"type\": \"uint256\" } ], \"name\": \"increaseAllowance\", \"outputs\": [ { \"internalType\": \"bool\", \"name\": \"\", \"type\": \"bool\" } ], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"address\", \"name\": \"account\", \"type\": \"address\" }, { \"internalType\": \"uint256\", \"name\": \"amount\", \"type\": \"uint256\" } ], \"name\": \"mintToken\", \"outputs\": [], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }, { \"inputs\": [], \"name\": \"name\", \"outputs\": [ { \"internalType\": \"string\", \"name\": \"\", \"type\": \"string\" } ], \"stateMutability\": \"view\", \"type\": \"function\" }, { \"inputs\": [], \"name\": \"owner\", \"outputs\": [ { \"internalType\": \"address\", \"name\": \"\", \"type\": \"address\" } ], \"stateMutability\": \"view\", \"type\": \"function\" }, { \"inputs\": [], \"name\": \"renounceOwnership\", \"outputs\": [], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"address\", \"name\": \"recipient\", \"type\": \"address\" }, { \"internalType\": \"uint256\", \"name\": \"amount\", \"type\": \"uint256\" } ], \"name\": \"safeTransfer\", \"outputs\": [], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"address\", \"name\": \"recipient\", \"type\": \"address\" }, { \"internalType\": \"uint256\", \"name\": \"amount\", \"type\": \"uint256\" }, { \"internalType\": \"bytes\", \"name\": \"_data\", \"type\": \"bytes\" } ], \"name\": \"safeTransfer\", \"outputs\": [], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"address\", \"name\": \"sender\", \"type\": \"address\" }, { \"internalType\": \"address\", \"name\": \"recipient\", \"type\": \"address\" }, { \"internalType\": \"uint256\", \"name\": \"amount\", \"type\": \"uint256\" } ], \"name\": \"safeTransferFrom\", \"outputs\": [], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"address\", \"name\": \"sender\", \"type\": \"address\" }, { \"internalType\": \"address\", \"name\": \"recipient\", \"type\": \"address\" }, { \"internalType\": \"uint256\", \"name\": \"amount\", \"type\": \"uint256\" }, { \"internalType\": \"bytes\", \"name\": \"_data\", \"type\": \"bytes\" } ], \"name\": \"safeTransferFrom\", \"outputs\": [], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"bytes4\", \"name\": \"interfaceId\", \"type\": \"bytes4\" } ], \"name\": \"supportsInterface\", \"outputs\": [ { \"internalType\": \"bool\", \"name\": \"\", \"type\": \"bool\" } ], \"stateMutability\": \"view\", \"type\": \"function\" }, { \"inputs\": [], \"name\": \"symbol\", \"outputs\": [ { \"internalType\": \"string\", \"name\": \"\", \"type\": \"string\" } ], \"stateMutability\": \"view\", \"type\": \"function\" }, { \"inputs\": [], \"name\": \"totalSupply\", \"outputs\": [ { \"internalType\": \"uint256\", \"name\": \"\", \"type\": \"uint256\" } ], \"stateMutability\": \"view\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"address\", \"name\": \"to\", \"type\": \"address\" }, { \"internalType\": \"uint256\", \"name\": \"amount\", \"type\": \"uint256\" } ], \"name\": \"transfer\", \"outputs\": [ { \"internalType\": \"bool\", \"name\": \"\", \"type\": \"bool\" } ], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"address\", \"name\": \"from\", \"type\": \"address\" }, { \"internalType\": \"address\", \"name\": \"to\", \"type\": \"address\" }, { \"internalType\": \"uint256\", \"name\": \"amount\", \"type\": \"uint256\" } ], \"name\": \"transferFrom\", \"outputs\": [ { \"internalType\": \"bool\", \"name\": \"\", \"type\": \"bool\" } ], \"stateMutability\": \"nonpayable\", \"type\": \"function\" }, { \"inputs\": [ { \"internalType\": \"address\", \"name\": \"newOwner\", \"type\": \"address\" } ], \"name\": \"transferOwnership\", \"outputs\": [], \"stateMutability\": \"nonpayable\", \"type\": \"function\" } ]";
    // set rpc endpoint url
    string rpc = "https://public-node-api.klaytnapi.com/v1/baobab";

    // set contract address
    private string contract = "YOUR CONTRACT ADDRESS";
    // set recipient address
    private string toAccount = "YOUR TO ACCOUNT";
    // set amount to transfer
    private string amount = "10000";

    // Use this if you want to display the balance of an account
    /*public Text balance;


     void Start() 
     {
         string account = PlayerPrefs.GetString("Account");
         balance.text = account;
    }
    */

    // call the "name" function
    async public void Name()
    {
        // function name
        string method = "name";
        // arguments
        string args = "[]";
        try
        {
        string response = await EVM.Call(chain, network, contract, abi, method, args,rpc);            
        Debug.Log("Token name: " + response);
        } catch(Exception e) 
        {
            Debug.LogException(e, this);
        }
    }

    // call the "totalSupply" function
    async public void TotalSupply()
    {
        // function name
        string method = "totalSupply";
        // arguments
        string args = "[]";
        try
        {
        string response = await EVM.Call(chain, network, contract, abi, method, args,rpc);            
            Debug.Log("Total Supply: " + response);
        } catch(Exception e) 
        {
            Debug.LogException(e, this);
        }
    }

    // call the "balanceOf" function
    async public void BalanceOf()
    {
        // function name
        string method = "balanceOf";
        // arguments
        string args = "[\"0x7b9B65d4ee2FD57fC0DcFB3534938D31f63cba65\"]";
        try
        {
        string response = await EVM.Call(chain, network, contract, abi, method, args,rpc);            
            Debug.Log("Balance of 0x7b9B65d4ee2FD57fC0DcFB3534938D31f63cba65: " + response);
        } catch(Exception e) 
        {
            Debug.LogException(e, this);
        }
    }

    // call the "transfer" function
    async public void Transfer()
    {
        // function name
        string method = "transfer";
        // put arguments in an array of string
        string[] obj = {toAccount, amount};
        // serialize arguments
        string args = JsonConvert.SerializeObject(obj);
        // value in ston (wei) in a transaction
        string value = "0";
        // gas limit: REQUIRED
        string gasLimit = "1000000";
        // gas price: REQUIRED
        string gasPrice = "250000000000";
        try
        {          
        string response = await Web3GL.SendContract(method, abi, contract, args, value, gasLimit, gasPrice);            
            Debug.Log(response);
        } catch(Exception e) 
        {
            Debug.LogException(e, this);
        }
    }

      // call the "safeTransfer" function
      async public void SafeTransfer()
    {
        string method = "safeTransfer";
        string[] obj = {toAccount, amount};
        string args = JsonConvert.SerializeObject(obj);
        string value = "0";
        // gas limit OPTIONAL
        string gasLimit = "1000000";
        // gas price OPTIONAL
        string gasPrice = "250000000000";
        try
        {
        //string response = await Web3GL.SendContract(chain, network, contract, abi, method, args,rpc);            
        string response = await Web3GL.SendContract(method, abi, contract, args, value, gasLimit, gasPrice);            
        Debug.Log(response);
        } catch(Exception e) 
        {
            Debug.LogException(e, this);
        }
    }

    // call the "mintToken" function
    async public void Mint()
    {
        // recipient
        string toAccount = "0x57468012dF29B5f1C4b5baCD1CD2F0e2eC323316";
        // amount to send
        string amount = "100";
        // function name
        string method = "mintToken";
        // put arguments in an array of string
        string[] obj = {toAccount, amount};
        // serialize arguments
        string args = JsonConvert.SerializeObject(obj);
        // value in ston (wei) in a transaction
        string value = "0";
        // gas limit: REQUIRED
        string gasLimit = "1000000";
        // gas price: REQUIRED
        string gasPrice = "250000000000";
        try
        {            
        string response = await Web3GL.SendContract(method, abi, contract, args, value, gasLimit, gasPrice);           
            Debug.Log(response);
        } catch(Exception e) 
        {
            Debug.LogException(e, this);
        }
    }

    // call the "approve" function
    async public void Approve()
    {
        // spender
        string spender = "0x57468012dF29B5f1C4b5baCD1CD2F0e2eC323316";
       // amount
        string amount = "100";
       // function name
        string method = "approve";
       // put arguments in an array of string
        string[] obj = {spender, amount};
       // serialize arguments
        string args = JsonConvert.SerializeObject(obj);
        string value = "0";
       // gas limit OPTIONAL
        string gasLimit = "1000000";
       // gas price OPTIONAL
        string gasPrice = "250000000000";
        try
        {            
        string response = await Web3GL.SendContract(method, abi, contract, args, value, gasLimit, gasPrice);           
        Debug.Log(response);
        } catch(Exception e) 
        {
            Debug.LogException(e, this);
        }
    }
}

Step 10. Create the buttons

  • We will create 5 buttons (Name, Total Supply, BalanceOf, Transfer, Mint) on the UI to interact with our KIP7 token.
  • To create each button, repeat the following steps:
    i. Right-click on the Sample Scene, click on GameObject → UI → Button and rename it to Name

ii. Repeat the above step to create for other buttons

  • You should have your buttons created and named as seen below.
  • Interact with the buttons:
    1. Click on Name button from the Hierarchy window
    2. Drag the ERC20Custom script into the right window (Add Co
    3. mponent tab)
    4. Add an On Click() function by clicking on the  button
    5. Drag the Name button from Hierarchy window into the On Click() function
    6. Click on No Function → ERC20Custom → Name()
  • Repeat the 5 steps above to link each button to the corresponding function in the contract. E.g here, we linked the button “Name” to the function “Name()” in the script, which calls the function “name()” in the contract.

Step 11. Set the chainID

  • Change the chainId of the network in the WebGL Templates folder to 1001 to connect to baobab testnet
  • Click on  to run the program and test the Name, Total Supply and BalanceOf buttons

Yay! We just read from our Smart Contract in our Unity game. Now let’s send and write transactions to the blockchain.

Step 12. Test the Mint and Transfer functions

  • To test the Mint and Transfer function, we need to build and run the project. So go to File → Build and Run
  • When the project builds and run, it opens a tab in your browser – webLogin scene
  • Click on Login to connect Metamask
  • Once connected, click on Mint to execute the mint function
  • Finally, click on Transfer to execute the KIP7 token transfer

Here is the details of the mint transaction on KlaytnScope

Here is the details of the transfer transaction on KlaytnScope

Congratulations on successfully bridging and interacting your unity game to the blockchain.

Conclusion

We have come to the end of connecting our unity game to Klaytn blockchain. In this guide, we learned how to make custom smart contract call in our Unity game:

  • Mint
  • Transfer our KIP-7 Tokens

This is the first step to different other possibilities in Web3 gaming. The world of web3 gaming is quite different from traditional gaming as the game needs to connect to the blockchain to verify in-game transactions. The speed and efficiency of these transactions will affect gameplay if they cannot keep up with the speed of the game engine.

The lag between input and blockchain transaction confirmation has been a significant hurdle in developing fab gaming experiences within web3 gaming. Klaytn can offer up to 4,000 transactions per second with immediate block finality making it a major chain to open up the metaverse to web3.

  • To learn more about connecting your Unity game to the blockchain, Visit the Chainsafe Docs

To get started building your gaming/metaverse project on Klaytn, Visit Klaytn Docs