In this tutorial, we will learn how to create an NFT game with a Solidity smart contract deployed on Avalanche Network and a NextJS front-end to interact with the game.
We will be creating an Avengers-themed NFT game with an ERC20 token, which can be used to mint characters in the game and buy special powers from the marketplace. Yes, we will be creating a marketplace where players can buy additional powers for their character. The game will have the following features -
- Each user can mint a character of their choice using the EPIC token.
- One can get the EPIC tokens from the faucet we will be creating in the dApp.
- Once the user has a character, they can enter the fight arena to fight against the boss (Thanos).
- Each character will have Health Points(HP), Damage Points, and Attack Modes (Kick, Punch, etc...).
- Users can buy special attacks from the marketplace for their character using EPIC tokens.
- Each time a character hits the boss, the boss hits back and reduces the HP of the player.
- Users can claim more health over time using EPIC tokens.
- If a character's health reaches 0, they cannot play the game until they claim new health.
- Boss will have very high HP, but boss's HP does not increase over time.
- The boss character will be the same for all the players with the same HP and everyone has to collectively try to defeat the boss.
Sounds interesting? This is what the game we will be creating looks like:
To successfully follow along with this tutorial, you will need a good understanding of ERC721 standards, Solidity language, the Hardhat framework, and the Next.js framework.
We will be using ethers.js library to interact with the smart contract and Next.js for the frontend.
- Hardhat - Hardhat provides a local development environment for creating and testing blockchain applications.
- Metamask - You will need a Metamask wallet installed in your browser.
- Next.js - A web framework to create a user interface.
- Arweave - We will store images related to a proposal on Arweave.
- TailwindCSS - A CSS utility framework.
We will be using nextjs-hardhat-tailwind-starter to get the initial setup. Run the following code to get started with boilerplate code.
This starter kit comes with all the packages you will need to get started with a dApp. You can see the list of the package in the starter kit here.
We will need a few more packages to create awesome UI for our game. Run the following code to add additional packages.
EPICToken.soland paste the following code.
EPICTokencontract, we will accept the token name and token symbol from the constructor and mint some tokens for the contract owner.
We also have a faucet method that will airdrop up to 20 EPIC tokens in the caller's account. Note that the method call will revert if more than 20 tokens are requested.
Create a file called
NFTEpicGame.solin the contracts folder and write the following code -
I know it's a lot to take in, let's go ahead and see what's happening in the smart contract.
Here we are importing all the necessary Openzeppelin contracts. We need ERC721 to mint character NFTs and IERC20 to call ERC20 methods on our EPIC token. We will be using some utility contracts like
console.solis provided by hardhat, used to add some debug statements in the smart contract which will be removed when the contract is compiled for deployment.
We will inherit from ERC721, ReentrancyGuard, and Ownable contract in our NFTEpicGame. ERC721 is used to mint NFT characters for our users. ReentrancyGuard is a modifier that can prevent reentrancy during certain functions (read more about ReentrancyGuard here). Ownable provides a basic access control mechanism where an owner account can be granted exclusive access to specific functions.
_tokenIdsis used to keep track of all NFTs,
epicTokenis the contract address of our ERC20 token, and
regenTimeis the time in seconds user have to wait to reclaim the health (briefly explained below).
CharacterAttributesstruct is used to store all the character attributes of a player. An instance of
CharacterAttributesis created when the user mints the character for the first time.
lastRegenTimewhich stores that last time player has requested to regenerate the health for their character.
AttackTypestores the data related to each attack. It includes attack name, attack damage, and attack image displayed in the game.
SpecialAttackTypeis similar to
AttackTypebut has one extra attribute i.e. -
price. Since players have to buy special attacks from the marketplace, each special attack has a price marked.
allSpecialAttackskeeps the track of all the attacks and special attacks in the game. Keeping these in a map helps in reducing contract size as we will not store all the data associated with an attack multiple times for each character but we will store only the attack index which in effect reduces the contract transaction costs.
BigBossstruct contains all the data for the boss character.
defaultCharactersarray contains the array of all the mintable characters that the player can choose from.
nftHoldersis a map that holds the token ID for each address/player.
CharacterAttributesstruct for each token ID minted by the players.
CharacterNFTMintedis an event that is emitted when a new character is minted by the player.
AttackCompleteis emitted when a player performs a normal attack or a special attack and
RegenCompletedis emitted when a user has successfully called the
- In the constructor we are accepting arrays of
- We will loop over these arrays and create
CharacterAttributesinstances and add them to the
- Note that the size/length of all these arrays should be the same.
- We also accepts boss attributes like
bossAttackDamageand create an instance of
- We increment the
_tokenIdsso that it starts from 1.
- At last, we need to accept the EPIC token address so that we can accept that token when the user mints the NFT or made any purchase from the marketplace.
- Since we are accepting the EPIC token address in the
NFTEpicGamecontract, we have to deploy the token first and then use that address to deploy
addSpecialAttacksare used to add attacks and special attacks to the game.
- Note that both these methods have an
onlyOwnermodifier, which means only the deploying address can call these methods and add new attacks and special attacks.
- Both these methods accept arrays of data like name, image, damage, indexes, and loop over them to create respective struct instances and add them to the
allAttacksarray or the
mintCharacterNFTis called by all the players once they join the game. In
mintCharacterNFTwe have defined that to mint a new character user has to pay 10 EPIC tokens. This is what happens in the
mintCharacterNFTaccepts the character index that the player wants to mint and makes sure that the index is not out of bounds.
- Next, it checks the
epicTokenis greater than or equal to 10 EPIC tokens, if not methods revert with an error message.
- If the contract has allowance then we call the
transferFrommethod of ERC20 token and transfer 10 EPIC tokens from the player's account to the contract address.
- Once all the checks are completed, we get the current token ID and call the
_safeMintmethod of the
msg.senderand token Id. This call will essentially mint a new NFT for the player.
- After minting a new NFT character we have to create a new
CharacterAttributesinstance and use the
defaultCharactersto fetch the metadata of that player.
- Now that we have a new instance of
CharacterAttributeswe have to add it to
nftHolderAttributescorresponding to the current token ID.
- Finally, we can map the player's address to the token ID in the
nftHoldersmap and increment the
claimHealthcan be called by any character holder to gain some health for their player.
claimHealthprovides 1 HP for each minute since the player has called the
claimHealthmethod. This means if the user has last called the
claimHealthmethod after 20 minutes, then the player's character will have 20 new HP points. Note that the addition of these new HP points cannot go more than the
maxHpof that character.
Each time a player wants to call the
claimHealthmethod they have to pay a fee of 0.1 EPIC tokens.
claimHealthfirst, we make sure that the calling player has a character minted and the player has approved the 0.1 EPIC token for the contract.
- We have to call the
transferFrommethod of the ERC20 token to transfer 0.1 EPIC tokens from the player's wallet to the contract address. Once the tokens are transferred we have to retrieve the tokenId of the player from the
tokenIdwe can fetch the
lastRegenTimewhich can be used to calculate the time elapsed since the player has called the
- If it's been more than
regenTimewhich is 60 seconds in our case, we allow them to claim the health.
- To calculate the new HP for the character we have to divide
timeSinceLastRegenby 60 since
timeSinceLastRegenis the seconds elapsed since the player has claimed the health and add that number to the current HP of the player.
- If the
newHpis more than the
maxHpof that player we give
maxHpto that character. At the end we have to update the
attackBossis called when a player uses any normal attack on the boss. Any player can only attack the boss if the player and boss both have more than 0 HP points.
- First, we retrieve the current character instance of
nftHolderAttributesand check all the requirements.
attackIndexwhich corresponds to the attack player wants to perform on the boss.
- To get the
attackIndexwe have to loop over all the available attacks of that character and make sure that the player has called the method with the correct
- Once we have
attackDamagewe have to make sure that
attackDamageis more than 0 or else revert the method call.
- The only condition
attackDamagecan be 0 is when
attackIndexis not present in allAttacks.
- In the end, reduce
attackDamagefrom the boss's HP and reduce the boss's attack damage from the character's HP.
attackSpecialBossis similar to
attackBoss. Only difference is instead of fetching
allAttacks, we fetch it from
buySpecialAttackis the method called from the marketplace, where the player can buy and special attack for their character. Each special attack has a price associated with it in EPIC tokens and the player has to approve that price to purchase the special attack.
Once we confirm that the user has approved the special attack's price we make a
transferFromcall to transfer the token and push the special attack index in the
specialAttacksarray of the player's character.
These all are the helper functions to read the data from the contract:
checkIfUserHasNFTchecks if the player has minted NFT before, if yes then it returns the
CharacterAttributesinstance of the minted NFT or else returns an empty instance of
getAllDefaultCharactersmethod returns an array of all the available characters that the user can mint.
getAllSpecialAttacksreturns arrays of
getBigBosssimply returns the
The tokenURI on an NFT is a unique identifier of what the token "looks" like. A URI could be an API call over HTTPS, an IPFS hash, or anything else unique.
tokenURIwill return something like this -
The JSON show what an NFT looks like and its attributes. The image section points to a URI of what the NFT looks like. In our case, we will be using Arweave to store all the images and use the URI provided by Arweave. This makes it easy for NFT marketplace platforms like Opensea, Rarible, and Mintable to render NFTs on their platform and show all the attributes of those NFTs since they are all looking for this metadata.
In this function, we do string manipulation magic to create a JSON string and then convert it to Base64 and attach
data:application/json;base64,at the from so that our browser knows how to handle the base64 string. Note that this format is recommended by big NFT marketplaces to render the NFT with all the metadata.
In the end, we will quickly deploy this contract on Rinkby Testnet, mint a new character, and check the NFT on the Opensea marketplace.
This is all we need to do in
NFTEpicGame.sol. Now let's write a script for hardhat to deploy this contract in a local blockchain to develop the frontend app.
scripts/run.ts, delete all the contracts and paste the following code -
In run.ts we will deploy out both contracts on local blockchain for development.
First, we get the Contract Factory for
EPICTokenand then call the deploy method and pass-in token name and token symbol in the constructor. Once the EPIC token is deployed we can start the deployment process of
const gameContract = await gameContractFactory.deploy()call we are passing all the required data we need in the constructor of the
NFTEpicGamecontract. If you notice for
characterImageURIwe are using
arweave.netURLs. In the next section, we will cover how to upload images in arweave and get the image URLs.
NFTEpicGameis deployed we need to make two contract calls before anyone can use this contract. We need to add attacks and special attacks. You might think we should have accepted this data in the contract constructor, but solidity only accepts up to 15 arguments in the contract constructor, hence we have created two separate methods to add attacks and special attacks. Doing so gives us the flexibility of updating special attacks in the marketplace anytime we want.
That's all we are doing in
run.ts, deploying two smart contracts and calling
addSpecialAttackswhich sets up the game characters.
All the assets used in this game can be found here.
We will use ArDrive to upload images manually on the Arweave network and use the URLs provided ArDrive in our contracts. To use ArDrive, you will need an Arweave account and Arweave keys json file. If you don't have an Arweave account, go to faucet.arweave.net and create an account. You will have to upload the keys in
ArDriveto log in and start uploading images.
When you use Arweave Faucet to create an account you also get 0.2 AR tokens which are more than enough to upload more than 2000 Images on ArDrive.
Login to ArDrive, if you are a new user it will ask for username and password. Once you have access to the dashboard you can upload the image in the dashboard and click on the preview button to get the image URL. Use these URLs in the
run.tsfor characters and attacks.
To run the contracts locally run the following command.
This will start a local blockchain and print out 20 testing account addresses and their private keys.
Copy one of these private keys, go to Metamask and click on the
Import Accountbutton and paste the private key. Make sure you are connected to the
localhost:8454blockchain in Metamask.
Note: chain id of hardhat blockchain is
31337and default chain id for
localhost:8454in Metamask is
1337. To change the chain id, go to Setting > Networks > Localhost 8454. There you can change
To deploy the contacts on the hardhat local blockchain, we need to run the
This will deploy both contracts and print the contract address in the terminal. Note these addresses.
In our game, we will have four pages.
pages/index.tsxis the home page that shows features and some information about the game. Nothing much is happening here.
pages/faucet.tsxis the faucet page where players can mint new tokens.
pages/market-place.tsxis the place where players can buy new special attacks for their characters.
pages/play.tsxis the game arena where if the user has character minted they can fight with the boss or else they can mint a new character.
In this tutorial, we will try to separate business logic with our frontend UI code. We will create a global context that will have all the necessary data fetched from the
NFTEpicGamecontract as well as all the method calls.
Create a file
contexts/DappContext.tsxand paste the following code -
Typescript might be yelling for some imports errors, but bear with me, we will solve all the errors. Let's break the
DappContextand see what's happening -
checkIfWalletIsConnectedcheck if the metamask is installed in the user's browser and if it does then get the account and set the
connectWalletActionis used when the user has rejected to connect the wallet when the website opens and then chooses to connect the wallet from the
fetchBalanceis used to fetch the EPIC token balance of the user. In the function, we will check if the token contract is present in the state variable or not. If we found a token contract instance then we will use it and make a
balanceOfcall to fetch the user's balance or else we will create a new instance of token contract, set the state variable and then fetch the token balance. Once we have the token balance we can set the
fetchDatais used to fetch all the necessary data from the
NFTEpicGamecontract. This is what's happening here -
- Make a contract call to
checkIfUserHasNFTto check if the user has minted character NFT or not.
checkIfUserHasNFTreturns empty struct then we fetch the default characters by making method call to
- If we get a valid struct from
checkIfUserHasNFTthen we send the returned data to
parseDefaultCharacterwhich takes the raw data and returns the object with the
CharacterPropstype. We are doing this just to make typescript happy.
- Next, fetch the attributes of the boss by making a method call to
- At the end we fetch all the attacks and special attacks via respective contract method calls, parse the objects, and set the state variables.
- Don't worry, we will write all the parse functions shortly.
faucetmethod is used to call the
faucetmethod of EPIC token. Before making a call, we have to make sure that the user has less than 20 EPIC tokens in their wallet, or else the method call to the contract will revert with an error.
Here we will show a toast notification until new tokens are minted and once tokens are minted we make a call to
fetchBalance, which will fetch the balance again and update the state variable.
mintCharacterNFTis used to mint new character NFT for users on the
- Each time a user mints an NFT they have to pay 10 EPIC tokens, hence we verify that the user has more than 10 EPIC tokens present in their wallet.
- Since the EPIC token is an ERC20 token, we will have to call approve a method for 10 EPIC tokens, so that our contract can call the
transferFrommethod and accept these tokens. If you are confused about how this works, refer to this video.
- After approving the tokens, we have to make and call to
mintCharacterNFTmethod from the
NftEpicGamecontract and pass in the
- After the method call, we have minted a new NFT for that user, so we need to make a call to
fetchDatawhich will fetch all the data again, and set the necessary state variable which allows us to change the UI dynamically for better user experience.
attackBossWithSpecialAttackboth accepts the attack index and call the
attackSpecialBossrespectively to perform attack action. Note that we are making a call to
fetchDatabecause we need to update the HP point of both player's character and boss.
claimHealthmethod does two things, make an
approvecall for 0.1 EPIC token and then call the
claimHealthmethod call from the
NFTEpicGamecontract. In the end, we need to call
fetchDataagain since the HP of the user is updated and we have to update the UI.
buySpecialAttackare used on the marketplace screen.
fetchSpecialAttacksmakes a call to the
getAllSpecialAttacksmethod and returns the parsed data.
buySpecialAttackaccepts the price and index of the special attack user wishes to buy and checks if the user has enough EPIC tokens to make the purchase.
- If the user has enough balance, we have to make an
approvecall for the attack's price and then call
indexvalue to add the special attack for that player's character.
useEffectis called when the webpage is loaded or when a specific variable changes its value. Here in the first
useEffectwill run when the page is loaded and calls
checkIfWalletIsConnectedwill fetch the user account from metamask and set the state variable. The second
useEffectwill only run when we have the
currentAccountvariable set. In this, we get create an instance of the game contract and fetch the EPIC token balance.
useEffectwill only run when we have
hasCharactervalue changes. We need to check
hasCharacterbecause when the user has minted a new character then
hasCharacterchanges and we need to fetch all the new data.
That's all the business logic we need. Let's look at some helper functions and types needed in our context.
parseBigBossare used to convert the raw data from contract call into typed objects for typescript.
getAttackAnimationis used to get the Lottie animation file that we show when the user performs any action.
This file contains all the interfaces we will need in our code.
This is where we store our contract addresses. When we deploy to either local blockchain or mainnet/testnet, we have to update the contract address in this file.
Before we start, we will have t use the DappContext that we have created on our game. Open
_app.tsxand paste the following code.
_app.tsxwe are wrapping our
DappProvideras we need all the variables and functions from our
DappContext. Notice that we are only wrapping with
DappProviderif the current route is not
/, i.e current page is not the home page. As we are not doing anything except showing some images and data about the website on the home page we don't need any data from the context there.
Nothing cool happening here, just loading some UI components.
Here, we are making a common UI for the navbar and a container to show the current account address connected with the website.
The faucet page is used to get some EPIC tokens. On this page, we show the user their EPIC token balance and a button to request more. Users can only request new EPIC tokens if they have less than 20 tokens. With the click of the Mint button, we are calling the
On the marketplace, we are just fetching all the special attack lists, rendering the UI for each attack, and making a
buySpecialAttackcall from the
useDapphook to approve and buy the special attack. Nothing fancy here.
play.tsis the game arena where players can mint a new character and fight against the boss. On this page, if the user has minted a character then we will show them
GameArenaor else we will show them the
On this page, we have two components,
MintCharacterjust has the UI for the background and faucet button. We are fetching the
defaultCharactersList, looping over it, and rendering the
CharacterItemreturns the image of that character and the name. In this component, we are also adding a confirmation modal for each character, which will show the price of that character and allow the user to call the mint method of the smart contract.
MintConfirmationModalwe show a button to mint the character and on click of that button, we call the
mintCharacterNFTmethod from out
GameArenais the main page where all the magic happens. We have four components here -
GameArenarenders the background, health bar, title, and claim health button.
AttackAnimationis the animation that is shown when an attack is in progress.
SpecialAttackItemare just attack buttons that users can click on and make respective contract calls to act.
Let's break this down. When a player performs an attack action, we have to do two things, make a method call to contract and show the animation on top of characters. We have developed our attack method in such a way that when a player attacks the boss, the boss will attack the character in the same transaction to remove that extra transaction and save on transaction fees. This means we first have to show an attack animation on the boss and then show it on the player as well.
To handle this animation display logic, we have three state variables,
attackOnCharacterare boolean variables then will be set when we want to show the animation. By default both of these variables are false.
- When we make an api call, we make the
attackOnBosstrue, and in our UI, we have made a check that if
truewe will render the Lottie animation.
- Each attack can have a different animation. We will be using the
getAttackAnimationmethod from our
helper.tsfile to get the relevant animation for that attack.
- Once the contract call is completed we can set the
truefor a few seconds. This will essentially remove the Lottie animation from the boss and show it on the character.
- We are also maintaining the
attackIndexwhich will be used to get the relevant Lottie animation for the attack.
When an attack is in progress we give a little shake effect on the boss, and for that, we are using
react-spring. You can learn more about
react-springand small animations here.
HealthBarsimply shows a progress bar for the HP point of character and boss. We are using
react-step-progress-barfor rendering the bar.
ClaimHealthModalis a modal, that shows the HP that can be claimed by the player. To calculate the claimable HP, we have to subtract the current timestamp with the
lastRegenTimeof that character and convert it to minutes. The minutes that have been passed since the user has claimed the health is the claimable HP. Once the user chooses to claim the health we can make a
claimHealthcall, which will approve 0.1 EPIC token and call the
claimHealthmethod of our contract.
The last component that we have is the
Loadercomponent. Create a file called
Loader.tsxin the components folder and add the following code.
Loader.tsxwe are simply rendering a Lottie animation in the center of the page.
This sums up our UI code. Let's deploy our contract on ``Avalanche FUJI C-Chain.
Create a file called
scriptsfolder and paste the following code.
We already have a
run.tsscript with almost the same code but it's always a better idea to create a separate script for deployment and local development.
Next this is to add Avalanche FUJI C-Chain in the metamask. Follow this article if you are facing difficulty adding network in metamask.
Now we need to export our metamask account's private key. Create a new file called
.envin the root folder and paste the private key as shown below.
Next, we need to configure
hardhat.config.tsto point to Avalanche testnet. Open
hardhat.config.tsand paste the following code.
Here we have configured Avalanche and Ethereum testnet for deployment. If you wish to test your NFTs on Opensea, you can deploy our contract on the Rinkby network, mint a character and check the Opensea for the NFT details. Here is an example for you.
To deploy the contract on Avalanche testnet, run the following code in the terminal.
This command will print the contract address for both our contracts. Copy these addresses and update
utils/constants.tsfor the contract to work in the desired network.
Here comes the end of this long tutorial. Here is our game in action.
Video: The Epic Nft Game Demo
You can find the source code of the game here.
Congratulation on making it to the end of this very long tutorial and thank you for taking this journey with me. In this tutorial, we have learned how to create NFT based gaming contract. how to create an ERC20 token, how to create a beautiful UI for the game, and deploy our contract on Avalanche testnet.
Keep building on Web3. #WAGMI
I'm Viral Sangani, a tech enthusiast working on blockchain projects & love the Web3 community. Feel free to connect with me on Github.