Minting KIP-7 Tokens using Unity with ChainSafe SDK

Klaytn
Klaytn
Published in
13 min readSep 23, 2022

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
chainsafeD.png
  • Click on the latest release
chainsafeR.png
  • Click on web3.unitypackage to download the package
chainsafeWUP.png

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.
unityDarc.png
  • 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.
successUnity.png

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.
newUnityPro.png

Step 4. Import the ChainSafe SDK into your project

  • Drag the downloaded web3.unitypackage file in the Installation section into the Unity project
web3unitypack.png
  • 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
unityError.png
  • Import the package as follows: Window -> Package Manager -> My Assets -> JSON. NET For Unity -> Import
importpackage.png

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
webLogin.png
  • Go to File → Build Settings → WebGL → Switch Platform
webGL.png
  • 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.
addOpenScene.png
  • 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).
useWebGL.png

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

sampleScene.png

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 )

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
createBtn.png

ii. Repeat the above step to create for other buttons

  • You should have your buttons created and named as seen below.
buttonNaming.png
  • 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()
descrBtn.png
  • 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.
nameButton.png

Step 11. Set the chainID

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

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
buildARun.png
  • When the project builds and run, it opens a tab in your browser — webLogin scene
WebLogin.png
  • Click on Login to connect Metamask
webLMeta.png
  • Once connected, click on Mint to execute the mint function
unityMint.png
  • Finally, click on Transfer to execute the KIP7 token transfer
unityTransfer.png

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

--

--

Klaytn
Klaytn

Published in Klaytn

A Sustainable & Verifiable Blockchain Built For All

Klaytn
Klaytn

Written by Klaytn

The Ground for All Blockchain Services.

No responses yet