Using ethereum blockchain to build decentralized games, the theme of the game is Three English Lu Bu, choose the role of casting NFT to fight with Lu Bu, through simple game rules to gradually understand the method of creating decentralized games using Ethereum public blockchain:

  • Write smart Contract Language: Solidity, an object-oriented high-level language for implementing smart contracts.
  • Hardhat:
  • Vue.js
  • Ethers.js: With its ease of use and rich features,Ethers.jsIt even goes beyond web3.js, previously known as the first ETH library. This universal Ethereum library is associated withParity,Geth,CrowdsaleAnd so on the popular wallet perfect match.

The code address involved in the article: github.com/QuintionTan…

Solidity

For Solidity beginners, follow the tutorial at Buildspace.

Here the smart contract needs to complete the user character creation, cast the chosen character, and then use it to fight the BOSS.

Begin to build

Open a terminal and create a folder in the project folder using the following command:

mkdir vue-game-dapp
Copy the code

Go to the project folder you created:

cd vue-game-dapp
Copy the code

Run the following command to initialize the project information:

npm init
Copy the code

Fill in project information and install related dependencies:

npm install @openzeppelin/contracts --save
npm install hardhat chai @nomiclabs/hardhat-waffle @nomiclabs/hardhat-ethers ethers ethereum-waffle --save-dev 
Copy the code

Now write smart contracts and create the smart contracts folder under the project root:

mkdir contracts
Copy the code

Create a new file in the contracts folder and name it Epicgame.sol and write the following code to it:

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;
Copy the code

This is always the first line in the smart contract file to specify the version of solidity. Now to write the code to make the complete EpicGame smart contract:

// SPDX-License-Identifier: MIT
pragma solidity >=0.4.22 <0.9.0;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/utils/Strings.sol";

import "./libraries/Base64.sol";

import "hardhat/console.sol";

contract EpicGame is ERC721 {
    struct CharacterAttributes {
        uint256 characterIndex;
        string name;
        string imageURI;
        uint256 hp;
        uint256 maxHp;
        uint256 attackDamage;
    }

    struct BigBoss {
        string name;
        string imageURI;
        uint256 hp;
        uint256 maxHp;
        uint256 attackDamage;
    }

    BigBoss public bigBoss;

    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    CharacterAttributes[] defaultCharacters;

    mapping(uint256 => CharacterAttributes) public nftHolderAttributes;

    mapping(address => uint256) public nftHolders;

    event CharacterNFTMinted(
        address sender,
        uint256 tokenId,
        uint256 characterIndex
    );
    event AttackComplete(uint256 newBossHp, uint256 newPlayerHp);

    constructor(
        string[] memory characterNames,
        string[] memory characterImageURIs,
        uint256[] memory characterHp,
        uint256[] memory characterAttackDmg,
        string memory bossName,
        string memory bossImageURI,
        uint256 bossHp,
        uint256 bossAttackDamage
    ) ERC721("Heroes", "HERO") {
        for (uint256 i = 0; i < characterNames.length; i += 1) {
            defaultCharacters.push(
                CharacterAttributes({
                    characterIndex: i,
                    name: characterNames[i],
                    imageURI: characterImageURIs[i],
                    hp: characterHp[i],
                    maxHp: characterHp[i],
                    attackDamage: characterAttackDmg[i]
                })
            );

            CharacterAttributes memory c = defaultCharacters[i];
            console.log(
                "Done initializing %s w/ HP %s, img %s",
                c.name,
                c.hp,
                c.imageURI
            );
        }

        bigBoss = BigBoss({
            name: bossName,
            imageURI: bossImageURI,
            hp: bossHp,
            maxHp: bossHp,
            attackDamage: bossAttackDamage
        });

        console.log(
            "Done initializing boss %s w/ HP %s, img %s",
            bigBoss.name,
            bigBoss.hp,
            bigBoss.imageURI
        );

        _tokenIds.increment();
    }

    function mintCharacterNFT(uint256 _characterIndex) external {
        uint256 newItemId = _tokenIds.current();

        _safeMint(msg.sender, newItemId);

        nftHolderAttributes[newItemId] = CharacterAttributes({
            characterIndex: _characterIndex,
            name: defaultCharacters[_characterIndex].name,
            imageURI: defaultCharacters[_characterIndex].imageURI,
            hp: defaultCharacters[_characterIndex].hp,
            maxHp: defaultCharacters[_characterIndex].hp,
            attackDamage: defaultCharacters[_characterIndex].attackDamage
        });

        console.log(
            "Minted NFT w/ tokenId %s and characterIndex %s",
            newItemId,
            _characterIndex
        );

        nftHolders[msg.sender] = newItemId;

        _tokenIds.increment();
        emit CharacterNFTMinted(msg.sender, newItemId, _characterIndex);
    }

    function attackBoss() public {
        uint256 nftTokenIdOfPlayer = nftHolders[msg.sender];
        CharacterAttributes storage player = nftHolderAttributes[
            nftTokenIdOfPlayer
        ];
        console.log(
            "\nPlayer w/ character %s about to attack. Has %s HP and %s AD",
            player.name,
            player.hp,
            player.attackDamage
        );
        console.log(
            "Boss %s has %s HP and %s AD",
            bigBoss.name,
            bigBoss.hp,
            bigBoss.attackDamage
        );

        require(player.hp > 0, "Error: character must have HP to attack boss.");
        require(bigBoss.hp > 0, "Error: boss must have HP to attack boss.");

        if (bigBoss.hp < player.attackDamage) {
            bigBoss.hp = 0;
        } else {
            bigBoss.hp = bigBoss.hp - player.attackDamage;
        }

        if (player.hp < bigBoss.attackDamage) {
            player.hp = 0;
        } else {
            player.hp = player.hp - bigBoss.attackDamage;
        }

        console.log("Boss attacked player. New player hp: %s\n", player.hp);
        emit AttackComplete(bigBoss.hp, player.hp);
    }

    function checkIfUserHasNFT()
        public
        view
        returns (CharacterAttributes memory)
    {
        uint256 userNftTokenId = nftHolders[msg.sender];
        if (userNftTokenId > 0) {
            return nftHolderAttributes[userNftTokenId];
        } else {
            CharacterAttributes memory emptyStruct;
            return emptyStruct;
        }
    }

    function getAllDefaultCharacters()
        public
        view
        returns (CharacterAttributes[] memory)
    {
        return defaultCharacters;
    }

    function getBigBoss() public view returns (BigBoss memory) {
        return bigBoss;
    }

    function tokenURI(uint256 _tokenId)
        public
        view
        override
        returns (string memory)
    {
        CharacterAttributes memory charAttributes = nftHolderAttributes[
            _tokenId
        ];

        string memory strHp = Strings.toString(charAttributes.hp);
        string memory strMaxHp = Strings.toString(charAttributes.maxHp);
        string memory strAttackDamage = Strings.toString(
            charAttributes.attackDamage
        );

        string memory json = Base64.encode(
            bytes(
                string(
                    abi.encodePacked(
                        '{"name": "',
                        charAttributes.name,
                        " -- NFT #: ",
                        Strings.toString(_tokenId),
                        '", "description": "This is an NFT that lets people play in the game", "image": "',
                        charAttributes.imageURI,
                        '", "attributes": [ { "trait_type": "Health Points", "value": ',
                        strHp,
                        ', "max_value":',
                        strMaxHp,
                        '}, { "trait_type": "Attack Damage", "value": ',
                        strAttackDamage,
                        "} ]}"
                    )
                )
            )
        );

        string memory output = string(
            abi.encodePacked("data:application/json;base64,", json)
        );

        return output;
    }
}
Copy the code

The contract refers to Base64.sol, which is used to encode data as Base64 strings.

test

Before deploying, test the contract to make sure the logic is correct. Create a folder test in the project root, which can contain both client tests and Ethereum tests.

Add the test.js file to the test folder, which will contain the contract tests in a file.

const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("EpicGame", function () { let gameContract; before(async () => { const gameContractFactory = await ethers.getContractFactory("EpicGame"); GameContract = await gameContractFactory. Deploy ([" liu ", "guan yu", "zhang fei"]. [ "https://resources.crayon.dev/suangguosha/liubei.png", "https://resources.crayon.dev/suangguosha/guanyu.png", "Https://resources.crayon.dev/suangguosha/zhangfei.png",], [100, 200, 300], [100, 50, 25], "lyu3 bu4," "https://resources.crayon.dev/suangguosha/lvbu.png", // boss 1000, 50 ); await gameContract.deployed(); }); it("Should have 3 default characters", async () => { let characters = await gameContract.getAllDefaultCharacters(); expect(characters.length).to.equal(3); }); it("Should have a boss", async () => { let boss = await gameContract.getBigBoss(); Expect (boss. Name). To. Equal (" lyu3 bu4 "); }); });Copy the code

Then execute the script in the project root directory:

npx hardhat test
Copy the code

Select Create an empty hardhat.config.js:

/** * @type import('hardhat/config').HardhatUserConfig */ require("@nomiclabs/hardhat-waffle"); Const config = {alchemy: "nine aa3d95b3bc440fa88ea12eaa4456161", / / test network token privateKey: "", / / wallet private key}; Module.exports = {solidity: "0.8.4", networks: {ropsten: {url: `https://ropsten.infura.io/v3/${config.alchemy}`, accounts: [config.privateKey], chainId: 3, }, }, };Copy the code

Execute again:

npx hardhat test
Copy the code

Deployment (to Ropsten test network)

Creating the folder scripts under the project root, and then creating the file deploy.js in that folder will create three default roles and a Boss from the contract constructor.

const main = async () => { const gameContractFactory = await hre.ethers.getContractFactory("EpicGame"); Const gameContract = await gameContractFactory. Deploy ([" liu ", "guan yu", "zhang fei"]. [ "https://resources.crayon.dev/suangguosha/liubei.png", "https://resources.crayon.dev/suangguosha/guanyu.png", "Https://resources.crayon.dev/suangguosha/zhangfei.png",], [100, 200, 300], [100, 50, 25], "lyu3 bu4," "https://resources.crayon.dev/suangguosha/lvbu.png", // boss 1000, 50 ); const [deployer] = await ethers.getSigners(); console.log("Deploying contracts with the account: ", deployer.address); console.log("Account balance: ", (await deployer.getBalance()).toString()); await gameContract.deployed(); console.log("Contract deployed to: ", gameContract.address); }; const runMain = async () => { try { await main(); process.exit(0); } catch (error) { console.log(error); process.exit(1); }}; runMain();Copy the code

To deploy the contract, run the command in the project root directory:

npx hardhat run scripts/deploy.js --network ropsten
Copy the code

When the execution is complete, you can see the result:

Deploying contracts with the account:  0xDC13b48Cf2a42160f820A255Ad79B39E695C0c84
Account balance:  4807257090844068484
Contract deployed to:  0x0006544b9c915Ab3cb0e8aC5d21000E4a4ABE746
Copy the code

Having completed the smart contract section, it’s time to create the front end interface using VUE.

VUE part

Start by creating the project:

vue create game
Copy the code

With VUE2, the front end will also use Ethers for Web3 interaction, Vuex for state management, and install dependencies:

npm install --save vuex ethers
Copy the code

Ok, now that the project is ready to start, the VUE part of the front-end application needs to do the following:

  • Connect to the user’s wallet
  • Choose a character
  • The characters take on Lu Bu

Connect the purse

In order for the user to interact with the application, Metamask must be installed and the Ropsten network selected.

Open the app.vue file and create a button with a link that will open a prompt in Metamask to allow the application to select the user’s wallet:

<template> <div class="app" id="app"> <div class="container mx-auto"> <div class="header-container"> <p class="header Gradient-text "> ⚔️ Metaverse Slayer ⚔️ </p> <p class="sub-text"> Team up to protect the Metaverse! </p> <div class="connect-wallet-container"> <img src="<https://64.media.tumblr.com/tumblr_mbia5vdmRd1r1mkubo1_500.gifv>" alt="Monty Python Gif" /> <button </button> </div> <div class="footer-container"> <img alt="Twitter Logo" class="twitter-logo" src="./assets/twitter-logo.svg" /> <a class="footer-text" :href="twitter_link" target="_blank" rel="noreferrer" >built by @{{ twitter_handle }}</a > </div> </div> </div> </template> <script> export default { name: "App", data() { return { twitter_handle: "DevPointCn", twitter_link: "<https://twitter.com/DevPointCn>", }; }, methods: { async connect() { await this.$store.dispatch("connect", true); }, }, async mounted() { await this.$store.dispatch("connect", false); }}; </script>Copy the code

The connect button has a click event, which will send an event to the Vuex Store. Here is the Store structure:

import Vue from "vue"; import Vuex from "vuex"; import { ethers } from "ethers"; import MyEpicGame from ".. /utils/MyEpicGame.json"; Vue.use(Vuex); const transformCharacterData = (characterData) => { return { name: characterData.name, imageURI: characterData.imageURI, hp: characterData.hp.toNumber(), maxHp: characterData.maxHp.toNumber(), attackDamage: characterData.attackDamage.toNumber(), }; }; export default new Vuex.Store({ state: { account: null, error: null, mining: false, characterNFT: null, characters: [], boss: null, attackState: null, contract_address: "0 x0006544b9c915ab3cb0e8ac5d21000e4a4abe746", / / contract address}, getters: { account: (state) => state.account, error: (state) => state.error, mining: (state) => state.mining, characterNFT: (state) => state.characterNFT characters: (state) => state.characters, boss: (state) => state.boss, attackState: (state) => state.attackState, }, mutations: { setAccount(state, account) { state.account = account; }, setError(state, error) { state.error = error; }, setMining(state, mining) { state.mining = mining; }, setCharacterNFT(state, characterNFT) { state.characterNFT = characterNFT; }, setCharacters(state, characters) { state.characters = characters; }, setBoss(state, boss) { state.boss = boss; }, setAttackState(state, attackState) { state.attackState = attackState; }, }, actions: {}, });Copy the code

Data structure description:

  • account: Stores connection account information
  • error: Exception information
  • mining: A Boolean value used to check whether a transaction is being mined
  • characterNFT: Stores selected role information
  • characters: Will save the position of the default character
  • boss: The BOSS that fights the character
  • attackState: Changes the status of trades being mined when attacking bosses
  • contract_address: contract address when the contract is deployed toRopstenThe address returned during network time.

And don’t forget to import EpicGame.json from the build after deploying the contract, which will be needed to make web3 calls using the contract in the blockchain.

Getters and setters are created for the state. First, to implement the join operation:

actions: { async connect({ commit, dispatch }, connect) { try { const { ethereum } = window; if (! ethereum) { commit("setError", "Metamask not installed!" ); return; } if (! (await dispatch("checkIfConnected")) && connect) { await dispatch("requestAccess"); } await dispatch("checkNetwork"); } catch (error) { console.log(error); commit("setError", "Account request refused."); } }, async checkNetwork({ commit, dispatch }) { let chainId = await ethereum.request({ method: "eth_chainId" }); const rinkebyChainId = "0x4"; if (chainId ! == rinkebyChainId) { if (! (await dispatch("switchNetwork"))) { commit( "setError", "You are not connected to the Rinkeby Test Network!" ); } } }, async switchNetwork() { try { await ethereum.request({ method: "wallet_switchEthereumChain", params: [{ chainId: "0x4" }], }); return 1; } catch (switchError) { return 0; } }, async checkIfConnected({ commit }) { const { ethereum } = window; const accounts = await ethereum.request({ method: "eth_accounts" }); if (accounts.length ! == 0) { commit("setAccount", accounts[0]); return 1; } else { return 0; } }, async requestAccess({ commit }) { const { ethereum } = window; const accounts = await ethereum.request({ method: "eth_requestAccounts", }); commit("setAccount", accounts[0]); }},Copy the code

First, check if Metamask is installed:

const { ethereum } = window; if (! ethereum) { commit("setError", "Metamask not installed!" ); return; }Copy the code

If all is well, check to see if the user has granted the application access to Metamask, then only connect to the accounts, and if not, return 0, the number of accounts found. This means that access must be requested from the user:

if (! (await dispatch("checkIfConnected")) && connect) { await dispatch("requestAccess"); }Copy the code

Note: The connect variable knows whether it is clicking the button or actually calling its mount function.

After checking the selected network, if it is not a Ropsten network, send a request to change it:

await dispatch("checkNetwork");
Copy the code

Once the account is found, submit the account to mutation to save it in state:

// in checkIfConnected action
commit("setAccount", accounts[0]);
Copy the code

The operations related to this connection are complete.

An action will now be created to get the default character for the user to select from the smart contract:

async getCharacters({ state, commit, dispatch }) { try { const connectedContract = await dispatch("getContract"); const charactersTxn = await connectedContract.getAllDefaultCharacters(); const characters = charactersTxn.map((characterData) => transformCharacterData(characterData) ); commit("setCharacters", characters); } catch (error) { console.log(error); }}Copy the code

To call a function from a contract, you need to get the contract by creating an action for it and then return it. Provider, contract ABI and signer:

async getContract({ state }) { try { const { ethereum } = window; const provider = new ethers.providers.Web3Provider(ethereum); const signer = provider.getSigner(); const connectedContract = new ethers.Contract( state.contract_address, EpicGame.abi, signer ); return connectedContract; } catch (error) { console.log(error); console.log("connected contract not found"); return null; }}Copy the code

You can then call a function in the smart contract that returns the default character and map each character data with the help of a function that converts the character data into objects available to JavaScript:

const charactersTxn = await connectedContract.getAllDefaultCharacters();
const characters = charactersTxn.map((characterData) =>
    transformCharacterData(characterData)
);
Copy the code

The transformCharacterData function is added to vuex.store initialization. It converts HP and attackDamage from bigNumber to a readable number:

const transformCharacterData = (characterData) => {
    return {
        name: characterData.name,
        imageURI: characterData.imageURI,
        hp: characterData.hp.toNumber(),
        maxHp: characterData.maxHp.toNumber(),
        attackDamage: characterData.attackDamage.toNumber(),
    };
};
Copy the code

The front-end part of the code is mainly to implement the logic of the game, choose a character to cast the hero NFT, here do not continue to interpret the code, see the code repository:

Github.com/QuintionTan…