Ethereum Game Programming

Ethereum Game Programming

In this tutorial, we will be creating a simple 2D arcade-style game where highscores are recorded in a smart contract on the Ethereum blockchain. We will guide you through setting up the project, programming the game using Phaser.js, writing the smart contract in the Solidity language using the Remix IDE, deploying the smart contract to Ethereum testnet, and finally connecting our game to Ethereum using MetaMask and Web3.js.

What kind of game are we making? We’ll keep it simple. The screen will have a bunch of randomly placed coins, and the player must collect as many of them as they can before the timer runs out. The score will then be recorded in the smart contract.

This tutorial will give you a great introduction to Ethereum Game Development. This guide will require you to have basic Javascript understanding.

If you don’t know basic Javascript and want to learn Ethereum Game Programming for real we recommend taking “Blockchain Game Developer” track in Ivan on Tech Academy.

The academy will teach you everything from scratch – no prior programming knowledge needed.

Setup

Before we begin, make sure that you have the MetaMask extension installed on your web browser.

https://metamask.io/

Now, in your terminal make a new directory on your hard drive:

mkdir ivan-game
cd ivan-game

Next, save this file to our game directory:

https://raw.githubusercontent.com/ethereum/web3.js/1.x/dist/web3.min.js

We will need a web server to test our game locally. You can use whatever you like (Node, etc.), but for the simple purposes of this tutorial we will be using Python 3’s http.server module.

Finally, let’s get our game assets. Let’s download the follwing:

Coin.png from this page: https://fatalgames.itch.io/2d-pixel-coin
pixel guy.png from this page: https://theleggies.itch.io/simple-pixel-guy?download

Game Programming with Phaser.js

Let’s begin programming our game. First, open a new file in this directory named index.html with your text editor of choice.

Enter this skeleton:

<!DOCTYPE html>
<html>

  <head>
    <script type="text/javascript" src="web3.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/phaser/3.16.2/phaser.min.js"></script>
  </head>

  <body>


    <script>
      /* our game code will go in here */
    </script>

  </body>

</html>

This line imports the Web3.js library into our project which we will use later:

<script type="text/javascript" src="web3.min.js"></script>

This line imports the Phaser.js library which we will use to program our game:

<script src="https://cdnjs.cloudflare.com/ajax/libs/phaser/3.16.2/phaser.min.js"></script>

Structure of a Phaser game

A game written in Phaser.js requires three functions, preload(), create(), and update(). We can define these functions right inside the <script>...</script> tags in the body of our index.html file:

    <script>
      function preload () {
        // Here we preload game assets
      }

      function create () {
        // Here we define game objects
      }

      function update () {
        // Here we program engine loop of the game
      }

      var config = {
        type: Phaser.WEBGL,
        parent: 'ivan-game',
        width: 800,
        height: 600,
        backgroundColor: 'black',
        scene: {
            preload: preload,
            create: create,
            update: update
        },
        pixelArt: true,
        audio: {
            disableWebAudio: true}};

      game = new Phaser.Game(config);
    </script>

Don’t worry about the details of the lines of code after the `update()` function definition. Suffice it to say, these lines give our game basic configuration then start the game.

Preloading game assets

We don’t want to load a game asset right when we need them in the game because this can interrupt the flow of our game. This is what the preload() function does. We can load our game assets before the game is started. Enter these lines of code inside our preload() function definition:

      function preload () {

        // this loads our Coin graphic and gives it the name 'coin'
        this.load.image('coin', './Coin.png');

        // this loads our character graphic and gives it the name 'player'
        this.load.spritesheet('player', './pixel guy.png',
          { frameWidth: 32, frameHeight: 32 }
        );
      }

Adding our Player Sprite

Now, that we have our assets loaded, let’s put them into our game. Enter these lines of code into our create() function definition:

      function create () {
        
        // this places the player sprite in the center of the screen
        player = this.add.sprite(this.cameras.main.width / 2, this.cameras.main.height / 2, 'player');

        // this scales the sprite to look bigger
        player.setScale(4);

        // this sets the position of the player sprite to be at the center of the graphic
        player.setOrigin(.5);
        
      }

We can now start up a local web server so we can test our game app. In your terminal, run this command:

python -m http.server

Now that you have a local server running, open this URL in your web browser: http://0.0.0.0:8000/ You should see our player sprite in the center of the screen.

We will play as this character in our game.

This is a great start! But let us now create the animation for the sprite.

Animating our Player Sprite

Enter these lines of code at the end of our create() function definition:

      function create () {

	/* --snip-- */

        // this creates the player sprite's animation
        this.anims.create({
          key: 'player_anim',
          frames: this.anims.generateFrameNumbers('player', { start: 0, end: 8 }),
          repeat: -1
        });

        // this plays the sprite's animation
        player.anims.play('player_anim');
      }

Now refresh your browser page. Our player sprite is now animated. Cool, huh? Now let’s program the controls for the player.

Player controls

Enter this line to the end of our create() function definition:

      // this creates keyboard input for our game
      function create() {

	/* --snip-- */

	// this creates keyboard input for our game
        cursors = this.input.keyboard.createCursorKeys();
      }

Next, enter these lines to our update() function definition:

      function update () {
        
        // move the player up when up arrow key is pressed
        if (cursors.up.isDown) {
          player.y -= 2;
        }

        // move the player down when down arrow key is pressed
        if (cursors.down.isDown) {
          player.y += 2;
        }

        // move the player left when left arrow key is pressed
        if (cursors.left.isDown) {
          player.x -= 2;
        }

        // move the player right when right arrow key is pressed
        if (cursors.right.isDown) {
          player.x += 2;
        }
      }

Reload the page and try pressing the arrow keys on your keyboard. How cool is that?

Randomly place coins to collect

We will now randomly place coins on the screen. Once a coin is collected, the player’s score will go up, and another coin will be randomly placed on screen.

We will implement this by using an array of coins. Enter these lines to the end of our create() function definition:

      function create() {

	/* --snip-- */

	// this array will contain our coins
        coins = new Array();

        // this will populate our array with coin sprites randomly placed on screen
        for(var i = 0; i < 10; i++) {

          rand_x = Math.floor(Math.random() * Math.floor(800));
          rand_y = Math.floor(Math.random() * Math.floor(600));

          coin = this.add.sprite(rand_x, rand_y, 'coin');

          // this sets the position of the coin sprite to be at the center of the graphic
          coin.setOrigin(.5);

          coins.push(coin);      
        }
      }

Refresh your browser, and now you see coins on the screen.

Now let’s collect those coins!

Collision detection

When a coin is touched, we will increase the score of the player, and then we will replace that coin to another random location on the screen. First, let’s initialize a score variable in our create() function definition:

      function create () {
        
        // this initializes the player's score
        score = 0;

        // this places a text label which will display the player's score to the top left corner of the screen
        score_text = this.add.text(10, 10, score, null);

        /* --snip-- */
      }

Next, we will write a helper function for collecting coins. Place this function before our update() function:

      function coinCollection() {

        // we loop through our coins array to see if there is a collision somewhere
        for (var i = 0; i < coins.length; i++) {
          let coin = coins[i];

          if(player.x > (coin.x - coin.width/2)
             && player.x < (coin.x + coin.width/2)
             && player.y > (coin.y - coin.height/2)
             && player.y < (coin.y + coin.height/2)) {
              
              // there was a collision so first we replace the coin to another random location
              rand_x = Math.floor(Math.random() * Math.floor(800));
              rand_y = Math.floor(Math.random() * Math.floor(600));
              coin.x = rand_x;
              coin.y = rand_y;

              // next we increment the player's score
              score += 1;

              // finally, we update the score display
              score_text.setText(score);
          } 
        }
      } 

      function update () {

      	/* --snip */

Be sure to call this function at the end of our update() function:

      function update () {

      	/* --snip */

  	coinCollection();

      }

Go ahead and test this in your browser. Next, we will implement a counter to end the game.

Ending the game

Let’s initialize our timer. Phaser games are run at 60 frames per second. We will also have a frames variable which will count all the update loops our game runs. Add this code after our score definition:

      function create () {
        
        // this initializes the player's score
        score = 0;

        // this variable will count the frames our game has run
        frames = 0;

        // this initializes our timer to 20 seconds
        timer = 20;

        // this places a text label which will dispaly the timer at the top right corner of the scre
        timer_text = this.add.text(this.cameras.main.width - 25, 10, timer, null);

        /* --snip */

So let us decrement the timer after every 60 frames. When the timer reaches 0, we will end the game. Add this to the beginning of our update() function:

      function update () {

        // a new update is run so increment our frame counter
        frames++;

        // decrement our timer every second (or 60 frames)
        if (frames % 60 == 0) {
          timer--;
          timer_text.setText(timer);

          // end the game when timer reaches 0
          if (timer == 0) {
            window.alert("GAME OVER");

            // TODO: write a helper function to deal with game over logic
          }
        }

        // move the player up when up arrow key is pressed
        if (cursors.up.isDown) {
          player.y -= 2;
        }

        /* --snip-- */

Go test this out. Okay, now we have a game!

Next, we will take a break from Phaser and write some solidity code.

Solidity Programming with Remix IDE

We are now ready to begin programming the Ethereum smart contract for our game. Our contract will be used to record highscores. We will be using the Remix IDE which you can use in your browser by going here:

https://remix.ethereum.org/

Acquiring Ether for Testnet

Now would be a good time to make sure we have enough testnet ETH to make some transactions with our smart contract and to deploy our smart contract. There a several testnet faucets. You can use this one:

http://rinkeby-faucet.com/

What does our contract do?

Before we begin programming Solidity code for Ethereum, let’s break down what we need the smart contract to do for our game. In order for a player to play our game and record their highscores, they will need the MetaMask extension installed on their browser. Their currently selected address in MetaMask will act as their player account. In order to record their highscore, the player will require sufficient funds in their wallet to cover the transaction fee.

The main purpose of our smart contract is to store a mapping of addresses to scores. The contract will also contain a public function to set the player’s score and a public function to retrieve the player’s score which our game will be able to call once we connect our app to the contract.

Additionally, our contract will keep track of the total number players who have their highscore recorded in our contract along with a mapping of their addresses.

Programming our Smart Contract

In the Remix IDE, click on the + in the File Browser to open a new file. Name the new file Highscores.sol.

In the text editor field, enter this line:

pragma solidity 0.5.5;

That first line indicates which solidity compiler we will be using. Before moving any further, in Remix navigate to the Solidity compiler tab. In the drop down menu at the top labeled Compiler, select 0.5.5.

Now add these lines to the text editor field:

contract Highscores {

    // this is a mapping of player addresses to scores (unsigned integers)
    mapping (address => uint) private _highscores;

    // this will keep track of the total number of records we have stored in our contract
    uint public totalPlayers = 0;

    // this is a mapping of unsigned integers to addresses
    mapping (uint => address) public addresses;

}    

Note that it is convention that the names of private variables and functions in Solidity being with _.

Our Smart Contracts Private Functions

Next we will define some helper functions which are private, meaning they will only be called within our contract. Our game has no reason to use these functions directly which is why they remain private:

contract Highscores {

    /* --snip-- */

    // this function increments our total player count
    function _incrementPlayers() private returns (bool) {
        totalPlayers++;
        return true;
    }

    // this function adds the player's address to our 'addresses' mapping
    function _addPlayerAddress() private returns (bool) {
        addresses[totalPlayers] = msg.sender;
        _incrementPlayers();
        return true;
    }
}    

Notice that the _addPlayerAddress() makes a call to _incrementPlayers(). This results in our addresses mapping containing all the player addresses in the order in which they were recorded.

Our Smart Contracts Public Functions

Next we will define the public functions which our game will call:

contract Highscores {

    /* --snip-- */

    // this function will store the player's highscore in the _highscores mapping
    function setHighscore(uint score) public returns (bool) {

        //check if player already has a record. If not, we have to add their address to our mapping
        if(_highscores[msg.sender] <= 0) {
            _addPlayerAddress();
        }

        // set the player's highscore
        _highscores[msg.sender] = score;
        return true;
    }

    // this function will retrieve the highscore of the player with address a
    function getHighscore(address a) public view returns (uint) {
        return _highscores[a];
    }
}  

In Solidity, msg.sender refers to the user’s currently selected address in MetaMask. As you may have figured out, if you select a different address in MetaMask, then our contract will treat you as a different user. This is exactly how we will test our smart contract and enter multiple highscores.

Deploying Our Smart Contract

Now that we are done programming the Ethereum smart contract for our game, we can deploy the contract to the Rinkeby tesnet. First, make sure that you are connected to Rinkeby in your MetaMask extension. In the Remix IDE, navigate to the tab Deploy & run transactions In the drop down menu labeled Environment, select Injected Web3 (this means MetaMask in our case). In the field labeled Account, make sure that it matches your address which has the funds to make the deployment transaction. All other fields can be left as is. Now, hit the Deploy button. A MetaMask window will popup displaying the gas fee which you will be paying. Assuming you have sufficient funds, click Confirm.

Now, after waiting a bit, you will receive a notification that the smart contract was deployed. At the bottom of the Deploy & run transactions tab, you will see Deployed Contracts. Click > to expand, and you will see our contracts public variables and functions. We can test our smart contract here before we test it in our game.

Testing Our Smart Contract

First, click on totalPlayers. As expected, the result is 0. So let’s add some player highscores to the contract. Next, in the field next to the setHighscore button, enter 42, then click the button. MetaMask will ask you to confirm the transaction fee. Now when you click on totalPlayers again, you should get the result 1.

It is important to note that writing to a smart contract changes the state of the smart contract so it will require a transaction fee. The other public variables and functions in our smart contract are read-only, so they will not require a transaction fee.

Next, in the field next to the addresses button, enter 0 (referring to the first address entered into our contract), then click the button. As expected, the result is the address which is currently selected in your MetaMask. Now, copy that address to your clipboard and paste it into the field next to the getHighscore button, then click the button. As expected, we get the result 42. So far so good!

Now let’s try entering a new record. First, in your MetaMask send some funds to another one of your addresses. Then switch to that address. Next, in the field next to the setHighscore button, enter 21, then click the button. Next, in the field next to the addresses button, enter 1 (referring to the address you just added), then click the button. The result should be your currently selected address. Once again, copy this address, then paste it into the field next to the getHighscore button, then click the button. As expected, the result is 21. And obviously, totalPlayers will give you 2.

This works perfectly so far! If you like, you can test with a third address, but this should be enough for our purposes. So let’s go back to programming our game to make calls to the Ethereum smart contract.

Connecting our game to Ethereum

Switch back to index.html in your code editor of choice. Inside our <script>...</script> tags, enter this block of code to the top:

    <script>

      // here we will store our user's ethereum account as a global variable
      var account;

      // Checking if Web3 has been injected by the browser (MetaMask)
      if (typeof web3 !== 'undefined') {
        // Use provider
        web3js = new Web3(web3.currentProvider);
        console.log("web3 has been injected");

        // This will require the player to first login to MetaMask
        ethereum.enable()
          .then(function (a) {
            // ethereum.enable() returns a promise of an array of hex-prefixed ethereum address strings
            // we can store the first entry into our global account variable
            account = a[0];

            // the configuration and creation of our Phaser game is now here
            // the game won't start until the player logs in to MetaMask
            var config = {
              type: Phaser.WEBGL,
              parent: 'ivan-game',
              width: 800,
              height: 600,
              backgroundColor: 'black',
              scene: {
                  preload: preload,
                  create: create,
                  update: update
              },
              pixelArt: true,
              audio: {
                  disableWebAudio: true}};

            game = new Phaser.Game(config);
          })
          .catch(function (error) {
            // Handle error. Likely the user rejected the login
            console.error(error)
          });
      } else {

        // If the code goes here, this means that the player doesn't have MetaMask installed
        console.log('No web3? You should consider trying MetaMask!')
      }

      function preload () {

      /* --snip */

Note, that we have moved the chunk of code that configures and starts our Phaser game. Logout of MetaMask. Then refresh your page with the URL http://0.0.0.0:8000/ to see that this all works as expected.

Loading Our Smart Contract

Now let’s load our smart contract in our game. Still inside our <script>...</script> tags, just above our preload() function definition at this code:

HighscoresContract = web3.eth.contract(/* contract ABI will be inserted here */);

Next we have to get the application binary interface (ABI) of our contract. You can think of this as something like the API of our contract so our game can interact with it. Go back to the Remix IDE and navigate to the Solidity compiler tab. At the bottom of this tab, you will see a clipboard icon labelled ABI. Click this button, and you will have the contracts ABI copied in you clipboard. You can now paste this as the input to the function we just added to our game. It will look something like this:

HighscoresContract = web3.eth.contract([
      {
        "constant": false,
        "inputs": [
          {
            "name": "score",
            "type": "uint256"
          }
        ],

     /* --snip-- */

        ],
        "payable": false,
        "stateMutability": "view",
        "type": "function"
      }]);

Next, we will need the address of our smart contract on the Ropsten testnet. Back to the Remix IDE, navigate to the Deploy & run transactions tab. At the bottom where you see Highscores at 0x..., click on the clipboard icon to copy our contracts address to you clipboard.

Now back to our game’s code, add this immediately after the HighscoresContract ABI:

    Highscores = HighscoresContract.at(/* insert contract's address here */);

Paste the address string into input of the function call (Make sure it is enclosed in quotes). Our code should look something like this:

    /* --snip-- */

   "payable": false,
        "stateMutability": "view",
        "type": "function"
      }]);

      Highscores = HighscoresContract.at('0x...');

      function preload () {

      /* --snip-- */

Now our game can interact with our smart contract!

Interacting with the Smart Contract

In our game code, find the section in our update() function definition where we left a comment saying // TODO: write a helper function to deal with game over logic. We will do that now!

Let’s define a function called gameOver() which will first retrieve the player’s current highscore. It will then check if the new score beat the current highscore. If so, we will then write that score to the smart contract. Add this new function just above our update() function definition:

      function gameOver() {

        // this is the call to our contracts getHighscore() function
        Highscores.getHighscore(account, { from: account }, (error,result) => {

          // we initialize highscore to be 0
          let highscore = 0;

          if(!error) {

            // the result of the contract call is stored in result.c[0]
            // let's store that in our highscore variable
            highscore = result.c[0];

            // here we check if the player's current score beat the score recored on the smart contract
            if (score > highscore) {

              // the score indeed beat the previous highscore so we can call our contracts setHighscore() function
              Highscores.setHighscore(score, { from: account }, (error,result) => {
                if(!error) {

                  // we will give the player a message announcing their new highscore
                  window.alert("Congratulations! Your new highscore is " + score);

                  // once the alert is closed, we will reload the page so the player can play again
                  location.reload();
                } else {

                  // if we go here, the player likely rejected the transaction to setHighscore()
                  console.dir(error);
                }
              });
            } else {

              // the score was not beat so let the user know their current highscore
              // and reload the page
              window.alert("You did not beat your current highscore of " + highscore);
              location.reload();
            }
          } else {
            console.dir(error);
          }
        });
      }

Take a good look at that chunk of code. It may be confusing at first because of the nested callbacks. The idea is that we only want to call setHighscore() once the call to getHighscore() has finished AND the player’s score is greater than the highscore recorded in the smart contract.

Now, make sure you add the call to gameOver() in the update() function:

function update () {

        /* --snip-- */

          // end the game when timer reaches 0
          if (timer == 0) {
            window.alert("GAME OVER");
            gameOver();
          }
        }

        /* --snip-- */

Okay, now let’s test it! Refresh the game and try it out with your first account. Notice that when were testing the contract in Remix, all those highscores persist from in the game. You’ll see that you had to beat a score of 42. Nearly impossible! Now try it with your second account. If you beat a score of 12 then you will be asked to make a transaction. Now try with a new account. It will ask you to make a transaction no matter what score you get (unless it was 0, then that means you didn’t even try!).

Congratulations! You made a simple blockchain game!

Exercises

  • There were a lot of magic numbers used in our code. For example, the speed of the player could be defined in a constant called PLAYER_SPEED that we can assign an integer value to, instead of using the number 2 everywhere. If we want to change that value, we can just reassign our constant to a different value. Can you think of other ways we can use constants in our code?
  • We put all of our code in index.html. This is very disorganized. All the Phaser.js functions can be defined in a separate file called Game.js. All the constants can be defined in a file called Constants.js. Our helper functions can be defined in their own file. Can you think of other ways to keep our code organized?
  • The way our game shows the player highscores is through window.alert(). Can you figure out how to retrieve a player’s highscore before the game begins and show that on screen as the game is being played? Also, note that other players’ highscores are publicly accessible from within our game. Can you figure out how to make a highscore board which displays all the players’ scores as a ranked list?

Room for improvement

As is, this game is not very friendly to a casual no-coiner gamer. It requires a browser extension with an unconventional login system. Additionally, since recording scores on-chain requires transactions of ETH, the player is required to manage funds. Although a developer should be knowledgeable of these things going on behind the scenes when programming a game on Ethereum, a casual player should not have to worry about these things. We will not be covering these solutions in this tutorial but you can look into Portis as an option for a conventional login system and the Gas Station Network to pay for small transactions on behalf of the player.

Why program a game on Ethereum?

One of the mantras in the crypto space is to ask “Does it have to be on the blockchain or can it just be on a database?” Surely, this simple game can have highscores recorded in a database. But consider this situation where money is on the line. What if playing the game required a buy-in and the total buy-in of all the players contributed to a prize pot? This is a case where recording scores in a centralized database poses a security concern and programming a game to record scores on Ethereum (in other words, a decentralized blockchain) provides a real benefit.

We didn’t even talk about non-fungible tokens (NFTs) in this tutorial at all. There is a lot of excitement in the crypto space of using NFTs in gaming as a way for games to have a built-in marketplace for in game items.

Conclusion

Although this may be a very trivial game, I hope you found this tutorial useful and now understand the basics of programming a game in Ethereum. Good luck to you in your future of becoming a blockchain developer!

And in case you missed it, check out our previous post about Ethereum 2.0.

Leave a Reply

Your email address will not be published. Required fields are marked *