How to mint an interactive NFT game.

This article was originally published on Monday, June 6th, 2022 @ 11:47a EST

Last night I’d shared some proof-of-concept NFT mini-games over on r/SuperStonk and there were quite a few people who were interested in learning how to do this, so I’ve decided to put together this tutorial in hopes of helping others to mint their own games.

Here’s a quick list of all the tools I’ll be using throughout this tutorial;

  • An IDE: personally I’m using BBEdit but you’re welcome to use whatever you like.
  • Photoshop: this is only to create the thumbnail displayed in the wallet.
  • Pinata: used to interact with IPFS.
  • Loopring: used to mint the NFT on L2.
  • GameStop Wallet: this is where the game can be played.

Getting Started

The first thing you’ll need is the source code for a game. 

However, since this isn’t a game-development tutorial (and in the interest of saving everyone some time) we’ll use an existing open-source game rather than writing one from scratch.

The game I’d selected for this example is a very simple brick breaker game and its repo can be found here, on GitHub.

For anyone that’s not familiar with GitHub, the screenshot above shows how you can download a copy of the source code locally which is the first step.

After you’ve downloaded the source’s .zip file locally you’ll need to extract (unzip) the content. 

Once unzipped you’ll see the following 4x files;

  • Index.html: this will be the entry point to your NFT. 
  • README.md: this is just some additional information about the game itself, no game logic is housed here.
  • Script.js: this is where the game logic is held.
  • Styles.css: this contains some minor additional styling options for the game.
  • Organizing Your Interactive NFT Files

    The second step is to create a new “src” directory (folder) which you’ll then place the files listed above into.

    In my case, I created a new “Brick Breaker” folder on my desktop and then created an “src” directory within that. 

    Note: I’ve dropped the README.md file from this example since that file doesn’t actually do anything.

    You’ll also notice a separate “Brick Breaker Meta” folder in the screenshot below, but we’ll circle back to that later.

    At this point, you can double click on the “index.html” file and it will launch in a browser window so you can preview (and play) the game.

    That’ll look something like this;

    Wow, cool! So now we’re ready to mint our game as a shiny new interactive NFT, right?

    Unfortunately, no, not quite yet… That’s because the canvas size is too large so we wouldn’t be able to see the entire game within GameStop’s wallet without having to scroll around.

    How to resize the game canvas.

    The canvas size for this game is held within the index.html file on line 9;

    				
    					<!DOCTYPE html>
    <html lang="en">
    <head>
    	<meta charset="UTF-8">
    	<title>Break out</title>
    	<link rel="stylesheet" href="styles.css">
    </head>
    <body>
    <canvas id="canvas1" width="730px" height="600px">
    </canvas>
    <script src="script.js"></script>
    
    </body>
    </html>
    
    				
    			

    Since GameStop’s wallet currently displays the NFT game scene at 320x320px we’ll need to modify these values. 

    All we’re doing here is changing the 730px & 600px values to 320px. 

    Once that’s done, we can re-launch the index.html file to see the canvas size has been updated (however our game still looks a bit wonky);

    Modifying the game display.

    The next step is to open the “script.js” file which holds the logic for the game, lines 4-22 need to be played around with a bit to ensure the bricks fit properly into the game scene.

    I’ve also changed the colors used and am including a copy of my edited “script.js” file for you below;

    				
    					var canvas=document.getElementById("canvas1");
    var ctx=canvas.getContext("2d");
    
    var x=canvas.width/2;
    var y=canvas.height-30;
    var dx=2;
    var dy=-2;
    var ballRadius=5;
    var paddleHeight=10;
    var paddleWidth=65;
    var paddleX=(canvas.width-paddleWidth)/2;
    var rightPressed=false;
    var leftPressed=false;
    var brickRowCount=6;
    var brickColumnCount=6;
    var brickWidth=40;
    var brickHeight=15;
    var brickPadding=10;
    var brickOffSetTop=40;
    var brickOffSetLeft=15;
    var score=0;
    var lives=3;
    
    var bricks=[];
    for(let c=0;c<brickColumnCount;c++)
    {
    	bricks[c]=[];
    	for(let r=0;r<brickRowCount;r++)
    	{
    		bricks[c][r]={x:0,y:0,status:1};
    	}
    }
    
    document.addEventListener("keydown",keyDownHandler);
    document.addEventListener("keyup",keyUpHandler);
    
    document.addEventListener("mousemove",mouseMoveHandler);
    
    function mouseMoveHandler(e)
    {
    	var relativeX = e.clientX-canvas.offsetLeft;
    	if(relativeX>0+paddleWidth/2 && relativeX < canvas.width-paddleWidth/2)
    	{
    		paddleX= relativeX-paddleWidth/2;
    	}
    }
    function drawBricks()
    {
    	for(let c=0;c<brickColumnCount;c++)
    	{
    		for(let r=0;r<brickRowCount;r++)
    		{
    			if(bricks[c][r].status==1)
    			{
    			var brickX=(c*(brickWidth+brickPadding)+brickOffSetLeft);
    			var brickY=(r*(brickHeight+brickPadding)+brickOffSetTop);
    			bricks[c][r].x=brickX;
    			bricks[c][r].y=brickY;
    			
    			ctx.beginPath();
    			ctx.rect(brickX,brickY,brickWidth,brickHeight);
    			ctx.fillStyle="#93186c";
    			ctx.fill();
    			ctx.strokeStyle='rgba(147,24,108,0.5)';
    			ctx.stroke();
    			ctx.closePath();
    			}
    
    		}
    	}
    }
    function keyDownHandler(e){
    	if(e.keyCode==39)
    	{
    		rightPressed=true;
    	}
    	else if(e.keyCode==37)
    	{
    		leftPressed=true;
    	}
    
    }
    
    function keyUpHandler(e){
    	if(e.keyCode==39)
    	{
    		rightPressed=false;
    	}
    	else if(e.keyCode==37)
    	{
    		leftPressed=false;
    	}
    
    }
    
    function drawBall()
    {
    	ctx.beginPath();
    	ctx.arc(x,y,ballRadius,0,Math.PI*2);
    	ctx.fillStyle="#93186c";
    	ctx.fill();
    	ctx.closePath();
    }
    
    function drawPaddle()
    {
    	ctx.beginPath();
    	ctx.rect(paddleX,canvas.height-(paddleHeight),paddleWidth,paddleHeight);
    	ctx.fillStyle="#93186c";
    	ctx.fill();
    	ctx.closePath();
    }
    function collisonDetection()
    {
    	for(var c=0;c<brickColumnCount;c++)
    	{
    		for(var r=0;r<brickRowCount;r++)
    		{
    			var b=bricks[c][r];
    			if(b.status==1)
    			{
    				if(x>b.x && x< b.x+brickWidth && y>b.y && y< b.y+brickHeight )
    				{
    					dy=-dy;
    					b.status=0;
    					++score;
    					if(brickColumnCount*brickRowCount==score)
    					{
    						alert("YOU WIN");
    						document.location.reload();
    					}
    
    				}
    			}
    		}
    	}
    }
    
    function drawScore()
    {
    	ctx.font="16px Arial";
    	ctx.fillStyle="#93186c";
    	ctx.fillText("Score: "+score,8,20);
    
    }
    
    function drawLives()
    {
    	ctx.font="16px Arial";
    	ctx.fillStyle="#93186c";
    	ctx.fillText("Lives: "+lives,canvas.width-65,20);
    
    }
    function draw()
    {
    	ctx.clearRect(0,0,canvas.width,canvas.height)
    	drawBricks();
    	drawLives();
    	drawBall();
    	drawPaddle();
    	drawScore();
    	collisonDetection();
    
    	if(y+dy < ballRadius){
    			dy=-dy;
    	}
    	else if(y+dy > canvas.height-2*ballRadius)
    	{
    
    		if(x>paddleX && x<paddleX +paddleWidth)
    		{
    			dy=-dy;
    		}
    		else{
    			lives=lives-1;
    			if(!lives)
    			{
    				alert("GAME OVER");
    		    	document.location.reload();
    			}
    			else
    			{
    				x=canvas.width/2;
    				y=canvas.height-30;
    				dx=2;
    				dy=-2;
    				 paddleX=(canvas.width-paddleWidth)/2;
    			}
    	    }
    	}
    
    	if((x+dx < ballRadius|| (x+dx > canvas.width-ballRadius)) ){
    			dx=-dx;
    	}
    	if(rightPressed && paddleX <canvas.width-paddleWidth)
    	{
    		paddleX+=7;
    	}
    	else if(leftPressed && paddleX>0){
    		paddleX-=7;
    	}
    	x += dx;
    	y += dy;
    }
    
    
    setInterval(draw,10);
    
    				
    			

    After saving these changes you can re-launch the index.html file and you’ll see the game now looks much better.

    Note: I’ve also edited the background color for my version of this game which is found on line 8 of the styles.css file.

    Minting the interactive NFT.

    The first thing you’ll need to do is create a cover photo / thumbnail that will be visible within the GameStop wallet. 

    Once you’ve got that created you’ll need to upload it to Pinata (or however you interact with IPFS).

    You’ll also need to upload the “Brick Breaker” folder to IPFS as well. 

    Once you’ve got the thumbnail + game content uploaded to IPFS you’re ready to create the metadata.json file for your interactive NFT.

    Here’s where the “Brick Breaker Meta” folder I’d mentioned earlier comes back into play… You’ll need to create an empty folder then create a metadata.json file inside of that. I am including an example of my metadata.json file below;

    				
    					{
      "description": "It's the classic game of breaking bricks using a paddle.", 
      "image": "ipfs://INSERT_PATH_TO_YOUR_THUMBNAIL_CID_HERE", 
      "animation_url": "ipfs://INSERT_PATH_TO_YOUR_SRC_DIRECTORY_HERE",
      "name": "Brick Breaker v1.0",
      "artist": "Nathan Ello",
      "royalty_percentage": 10
    }
    				
    			

    Once you’ve created your metadata.json file you’ll also need to upload that to IPFS, from there you can mint your interactive NFT.

    Minting your NFT on Loopring L2.

    I’ve used Loopring’s “advance create” minting option to mint my interactive NFT which can be seen in the screenshot below;

    Note: Once your interactive NFT has been minted you will not be able to play it within Loopring’s website.

    The next step is to transfer the L2 NFT to your GameStop wallet, once you’ve done that you’ll be able to click on the thumbnail image within GameStop’s wallet which will launch your game.

    Additional Resources

    Here’s another helpful guide about developing interactive NFT’s that was recently shared with me.

    If you have any questions about this process, please feel free to send me a DM over on Twitter.