Web3 Elden Ring boss battle using Moralis and Unity
Search
Generic filters

Moralis Projects – Web3 Elden Ring Clone

Yes, you read that right. 😎

For this week’s project, we cloned a boss fight in Elden Ring using Unity and Moralis!

You’re going to learn how to add Web3 elements to the gameplay, turning that sweet boss loot into ERC-721 and ERC-20 tokens.

First, we’ll learn how to create the loot system. Next, we’ll cover how to transform these loot items to ERC tokens, and lastly, look at how to retrieve the now on-chain data to visualize it in the game menu.

As always, go through the prerequisites to make sure you’re ready to start. Let’s do it!

PREREQUISITES

GET STARTED

Clone the project GitHub repository:

git clone https://github.com/MoralisWeb3/unity-web3-sample-elden-ring

Or download it as a ZIP file:

Once cloned/downloaded, open the unity-web3-sample-elden-ring folder using Unity Hub. By default, it’s always recommended to use the same Unity version as used originally (2021.3.2f1 LTS).

With the project open in Unity, we’re ready to start!

1. Setup Moralis Dapp

If you open the Unity project, the first thing you’ll find is the Moralis Web3 Setup panel:


Let’s follow the instructions detailed below the input fields. First, go to https://admin.moralis.io/login and log in (sign up if you are not already registered). Now click on Create a new Server:

Next, click on Testnet Server:

Choose a name for your server and your closest region. Then select Polygon (Mumbai), as we’re going to be deploying the smart contract to this chain. Finally, click on Add Instance:

The server (Dapp) will now be created, and if you click on View Details you’ll find the Server URL and the Application ID. Start by copying the Server URL, followed by the Application ID:


Next, paste them to the Moralis Web3 Setup panel input fields here:


If you closed the panel for any reason, you can find it again in the top toolbar under Window → Moralis → Web3 Unity SDK → Open Web3 Setup.

Hit Done and you will have the Moralis Server set up. Nice!

There’s another option to fill the server (Dapp) information too: in the Assets project tab go to Moralis Web3 Unity SDK → Resources and select MoralisServerSettings:

2. Loot system setup

Before we dive into the loot system setup, head to Assets → _Project. Here, you’ll find all the assets I created for this particular project, including the scripts and the Game scene. Go to that scene if you’re not already there, and we can take a look at the Hierarchy:



As you can see, we’re using the new AuthenticationKit that’s now available thanks to my colleague Sam and the Moralis Unity SDK team. You can find a specific scene showing its functionality under Samples → Moralis Web3 Unity SDK → Demos → Introduction → Introduction (scene):

It’s a super-powerful tool that will handle the authentication on any platform we chose to build our unity project on. If you want to know exactly how it works, take a look at this video from my colleague Sam:


For this tutorial, the main thing you need to know is that the AuthenticationKit has events you can subscribe to. So, for example, when it connects or disconnects we’re going to execute this:

As you can see, we’re calling some functions from GameManager.cs. This script is the StateMachine of the project and it will be controlling small States, each one with its own functionality. The one that handles the loot system is called Victory:

The victory state will be activated after the boss dies through this function in the Combating state:

private void OnCreatureDamaged(float damage)
{
   creatureCurrentHeath.fillAmount -= damage;
   if (creatureCurrentHeath.fillAmount <= 0)
   {
       creature.Death();
       hud.gameObject.SetActive(false);
       ChangeState("Victory");
   }
}

As we can see on the Start() method of Victory state, we call PopulateItemFromDB and PopulateRunes():
private async void Start()

{
   _gameManager = GetComponentInParent<GameManager>(); //Assuming this is under GameManager
   _audioSource = GetComponent<AudioSource>();
   _populatePosition = creature.transform.position;
   _gameItemsQuery = await Moralis.GetClient().Query<DatabaseItem>();
   PopulateItemsFromDB();
   PopulateRunes();
}

If we go and check PopulateItemsFromDB() we’ll see that we are executing a query to the Moralis Database to get DatabaseItem objects:

private async void PopulateItemsFromDB()
{
   IEnumerable<DatabaseItem> databaseItems = await _gameItemsQuery.FindAsync();
   var databaseItemsList = databaseItems.ToList();
   if (!databaseItemsList.Any()) return;
   foreach (var databaseItem in databaseItemsList)
   {
       // databaseItem.metadata points to a JSON URL. We need to get the result of that URL first
       StartCoroutine(GetMetadataObject(databaseItem.metadata));
   }
}

We need to go to the top of the GameManager script to see what a DatabaseItem is:
public class DatabaseItem : MoralisObject

{
   public string metadata { get; set; }
   public DatabaseItem() : base("DatabaseItem") {}
}

It is a custom MoralisObject that has a string metadata field. What we’re going to do here is add some DatabaseItem objects to the Moralis Database, each one with an IPFS URL as the metadata field. Each URL will contain the name, the description, and the URL of an image. 

You can use this project to load image data to IPFS using Unity, but we already did it for you! If you go to Assets → _Project → IPFS → ItemsURLs you’ll find these:

Elven Helmet:
https://ipfs.moralis.io:2053/ipfs/QmUEPzw3pkxptNQd7as3JgUhiJPg6ZabQm6dC2y28WhCXN/ElvenHelmet_637905029204254360.json

Carlin Sword:
https://ipfs.moralis.io:2053/ipfs/QmVUXZ5dRVyKTLeiFVCUpp45iMqw9eTQjnuKWruVVJiGsL/CarlinSword_637905030139390627.json

Iron Ingot:
https://ipfs.moralis.io:2053/ipfs/QmUydnyXg7AL26jyztuVjGAzVa8sKx9mSwvuii2gm6QRpg/IronIngot_637905030867477762.json 

So time to go to the Moralis Admin Panel and go to the dashboard of your server:




Once there, click on the + button to create a new class:


Name the class DatabaseItem and click on Add columns. Name the new column metadata and click on Add column:

You should now see the newly created table/class with empty rows:


Now it’s time to add some rows, adding an IPFS url to each of them. Just click on Add a row and fill the metadata column with the IPFS url you want:


Finally, click on Add. You can create as many rows as you want. These will be the items that will show up when you kill the boss.

Now, to understand how that happens, we need to go back to the Victory script and check again at the PopulateItemsFromDB() method:


private async void PopulateItemsFromDB()
{
   IEnumerable<DatabaseItem> databaseItems = await _gameItemsQuery.FindAsync();
   var databaseItemsList = databaseItems.ToList();
   if (!databaseItemsList.Any()) return;
   foreach (var databaseItem in databaseItemsList)
   {
       // databaseItem.metadata points to a JSON URL. We need to get the result of that URL first
       StartCoroutine(GetMetadataObject(databaseItem.metadata));
   }
}

As you can see, now that we created the DatabaseItem class in the DB and added some rows, we will get those items using gameItemsQuery.FindAsync().

We will list them, and transform the metadata for each one, which is an IPFS URL, to a MetadataObject declared in the Unity project. If we take a quick look at the top of the GameManager script, we’ll see this object:


public class MetadataObject
{
   public string name;
   public string description;
   public string image;
}

Now let’s get back to the Victory script. GetMetadataObject() is the method that takes care of this conversion. Using a UnityWebRequest and then the JsonUtility.FromJson method we receive the metadata object:


private IEnumerator GetMetadataObject(string metadataUrl)
{
   // We create a GET UWR passing that JSON URL
   using UnityWebRequest uwr = UnityWebRequest.Get(metadataUrl);
   yield return uwr.SendWebRequest();
   if (uwr.result != UnityWebRequest.Result.Success)
   {
       Debug.Log(uwr.error);
       uwr.Dispose();
   }
   else
   {
       // If successful, we get the JSON content as a string
       var uwrContent = DownloadHandlerBuffer.GetContent(uwr);
       // Finally we need to convert that string to a MetadataObject
       MetadataObject metadataObject = JsonUtility.FromJson<MetadataObject>(uwrContent);
       // And voilà! We populate a new GameItem passing the metadataObject
       PopulateGameItem(metadataObject, metadataUrl);
       uwr.Dispose();
   }
}

After that, we call PopulateGameItem() passing both the recently created metadataObject and the original metadataUrl.

Then, PopulateGameItem() is the function that finally takes care of instantiating new GameItem objects to the game world:

private void PopulateGameItem(MetadataObject metadataObject, string metadataUrl)
{
   GameItem newItem = Instantiate(gameItemPrefab, _populatePosition, Quaternion.identity);
   newItem.Init(metadataObject, metadataUrl);
}

If we now go to the GameItem script we’ll see that it uses the information received to set the name and the description, and to retrieve the texture through a UnityWebRequestTexture (as well as converting it to a Sprite afterwards):

public void Init(MetadataObject mdObject, string mdUrl)
{
   metadataObject = mdObject;
   metadataUrl = mdUrl;
   // We get the texture from the image URL in the metadata
   StartCoroutine(GetTexture(metadataObject.image));
}
private IEnumerator GetTexture(string imageUrl)
{
   using UnityWebRequest uwr = UnityWebRequestTexture.GetTexture(imageUrl);
   yield return uwr.SendWebRequest();
   if (uwr.result != UnityWebRequest.Result.Success)
   {
       Debug.Log(uwr.error);
       uwr.Dispose();
   }
   else
   {
       var tex = DownloadHandlerTexture.GetContent(uwr);
       // After getting the texture, we create a new sprite using the texture height (or width) to set the sprite's pixels for unit
       spriteRenderer.sprite = Sprite.Create(tex, new Rect(0.0f, 0.0f, tex.width, tex.height), new Vector2(0.5f, 0.5f), tex.height);
       uwr.Dispose();
   }
}

Now go to Unity and hit Play. After authenticating with your MetaMask wallet and killing the boss, you should see all the items you added in the DB as loot:

The orange one is not a GameItem, it’s a Rune, and will always drop as it’s not database-dependent. If we go back to the Victory script, we will see that PopulateRunes() is a much simpler function, directly instantiating a runePrefab which is a Rune object:

private void PopulateRunes()
{
   Instantiate(runePrefab, _populatePosition, Quaternion.identity);
}

Nice! Now we know how the loot system works. 🙂

3. Deploying Smart Contracts (Hardhat)

Now that the boss drops items when it dies, we need to pick them up. Picking them up means converting these “local” items to ERC tokens, and to do that we need to deploy two smart contracts first. One ERC-721 contract for the Game Items and one ERC-20 for the Rune.

Before continuing, remember you need to have MetaMask installed on your browser with the Mumbai Testnet imported and with some test MATIC in it. Check this link:

You’ll also need to have installed Node.js before continuing: https://nodejs.org/en/download/


Alright, let’s get into it! Create a folder in your desktop and name it as you like. I’m going to name it hardhat-elden-ring. Open Visual Studio Code and open the folder that we just created:



Now we’re going to execute some hardhat commands. It’ll be easy because I’ve already prepared the instructions.

Go to Unity and under Assets →  _Project → SmartContracts you’ll find the instructions and the contract files that we’ll need later.

The INSTRUCTIONS.txt are there for you if you need them later, but now let’s follow them in here:

Open the terminal in VS Code and make sure you are under the hardhat-elden-ring folder. If not, you can move to the desired folder through the cd command. Now let’s install hardhat by executing these two commands, one after the other:

npm i -D hardhat
npx hardhat

When executing the second one you will need to select Create a basic sample project and hit enter multiple times:


After doing so, you will have Hardhat installed and will have created a basic sample project:


Great! Now it’s time to install the dependencies. Execute all these commands one after another:

npm i -D @openzeppelin/contracts
    npm i -D @nomiclabs/hardhat-waffle
    npm i -D @nomiclabs/hardhat-etherscan

Once the dependencies are installed, go to Unity and copy the code under Assets → _Project → SmartContracts → ERC-721 → GameItem:


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
contract GameItem is ERC721URIStorage {
   // Auto generates tokenIds
   using Counters for Counters.Counter;
   Counters.Counter private _tokenIds;
   address owner;
   constructor() ERC721("GameItem", "ITM") {
       owner = msg.sender;
   }
   function getItem(string memory tokenURI) public returns (uint256)
   {
       //DISCLAIMER -- NOT PRODUCTION READY CONTRACT
       //require(msg.sender == owner);
       uint256 newItemId = _tokenIds.current();
       _mint(msg.sender, newItemId);
       _setTokenURI(newItemId, tokenURI);
       _tokenIds.increment();
       return newItemId;
   }
}

Go back to VSCode, and under the folder contracts, rename the existing Greeter.sol to GameItem.sol and replace the existing code for the copied code:

DISCLAIMER – This is not a production-ready contract as it doesn’t have any ownership security, so the getItem() function could be called from anyone with access to the contract address.

As you can see, getItem() is the function that we’ll call from Unity, once the contract is deployed to mint the item and transfer it to the player address. It needs a tokenURI which will be the IPFS url that we got from the Moralis DB.

Now, under the folder scripts, rename the existing sample-script.js to deployGameItem.js:

This is the script that we’ll execute through a command later to deploy the GameItem.sol but in order for this to happen, we need to rename some existing fields in it named Greeter and greeter to GameItem and gameitem (respecting case). In the deployGameItem.sol file, press Ctrl + F and replace all fields as said. It’s important to select the AB option to respect the case:


After this, also make sure that the deploy() method call doesn’t contain any parameter as the GameItem.sol constructor doesn’t need any:


To end configuring deployGameItem.sol, add this code just after the console.log():

    await gameitem.deployTransaction.wait(5);
    // We verify the contract
    await hre.run("verify:verify", {
        address: gameitem.address,
        constructorArguments: [],
    });

So it looks like this:

We’re done with deployGameItem.js so now open hardhat.config.js and we will move this requirement to the top:

require("@nomiclabs/hardhat-etherscan");

So it looks like this:

Now we will add these fields just before the module.exports part:

const PRIVATE_KEY = "";
const MUMBAI_NETWORK_URL = "";
const POLYGONSCAN_API_KEY = "";

So it looks like this:

We’ll start by filling MUMBAI_NETWORK_URL which needs to be the RPC Node URL of the Mumbai Testnet. Go to the Moralis Admin Panel (https://admin.moralis.io/servers), where we created the server, go to Speedy Nodes Polygon Network Endpoints and copy the Mumbai one:

Paste it on MUMBAI_NETWORK_URL:

Now let’s fill POLYGONSCAN_API_KEY which needs to be filled with a PolygonScan API Key. Head to PolygonScan, create an account if you don’t have one and under “YourAccountName” → API Keys you will be able to create one:

Do that and copy it back to POLYGONSCAN_API_KEY:

Finally we need to fill PRIVATE_KEY which is the private key of your browser MetaMask wallet.

REMEMBER – Never give your private key to anybody!

You can follow this blog on how to get your private key so once you have it, paste it on PRIVATE_KEY:

Alright! The last thing we need to do before deploying the contract is to replace the module.exports part with this one:

    module.exports = {
      solidity: "0.8.7",
      networks: {
        mumbai: {
          url: MUMBAI_NETWORK_URL,
          accounts: [PRIVATE_KEY]
        }
      },
      etherscan: {
        apiKey: POLYGONSCAN_API_KEY 
      }
    };

So hardhat.config.js should look like this in the end:


Now it’s finally time to deploy the contract. We just need to run these commands one after another:

npm hardhat clean
npm hardhat compile
npx hardhat run scripts/deployGameItem.js –-network mumbai

And voilà! After a minute or so, we have our GameItem.sol contract deployed and verified! If we copy the contract address that appears as a log in the terminal and paste it on PolygonScan we should see the contract there:



Now go to GameManager script in Unity and paste it under GameItemContractAddress:

For the GameItemContractAbi, go back to PolygonScan and under Contract → Code scroll down until you find Contract ABI. Copy it:

Before pasting the value in GameManager.cs, we need to format it. Go to https://jsonformatter.org/ and paste the ABI on the left side. Then click on Minify/Compact:

After this, click on the right side, press to Ctrl + F and search for
We need to replace for \”

Click on All to replace it in all the text:


 Copy the formatted ABI, go back to GameManager.cs and paste it on GameItemContractAbi:


Great! We deployed the ERC-721 contract and configured GameManager.cs with its data. Now we need to do the same for the ERC-20, the Rune.sol contract. It’s going to be much much easier and faster now that we have the Hardhat project configured.

Return to VSCode and under contracts, duplicate the GameItem.sol file. Name the copy Rune.sol:

Now go to Unity, and copy the code under Assets → _Project → SmartContracts → ERC-20 → Rune.sol:


// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract Rune is ERC20 {
   address owner;
   constructor() ERC20("Rune", "RUNE") {
       owner = msg.sender;
   }
   function getExperience(uint256 amount) public {
       //DISCLAIMER -- NOT PRODUCTION READY CONTRACT
       //require(msg.sender == owner);
       _mint(msg.sender, amount);
   }
}

Go back to VSCode and paste it under the newly created Rune.sol contract:


DISCLAIMER – This is not a production-ready contract as it doesn’t have any ownership security so getExperience() function could be called from anyone with access to the contract address.

This contract is even simpler than the ERC-721 one. We’re creating a new token called Rune (RUNE) and we will call getExperience() from Unity to mint an amount of tokens to the player address, which will represent the amount of experience points.

Now under scripts, duplicate deployGameItem.js and rename it to deployRune.js:

Then repeat what we did before; replace some original fields for new ones. In this case, replace GameItem for Rune:

After this we just need to run these commands to run this script and deploy our Rune.sol contract:

npm hardhat clean
npm hardhat compile
npx hardhat run scripts/deployRune.js --network mumbai

Great! After succeeding, copy the contract address and the contract abi the same way we did after deploying GameItem.sol and copy the data to GameManager.cs:

Nice! We have now completed the deployment of the smart contracts and we’re ready to learn how to call them from Unity!

4. Mint items as ERC tokens

As we know, after defeating the boss, the items and the rune will drop and we will be in the Victory state. In that state, if we collide with an item and press P we will activate the PickingUpItem state. If we do the same while colliding with a rune, we will activate the PickingUpRune state. We can see that looking at the OnPickUp() function from Victory.cs:

private void OnPickUp(InputAction.CallbackContext obj)
{
   if (itemPanel.isActiveAndEnabled)
   {
       ChangeState("PickingUpItem");
       return;
   }
   if (runePanel.isActiveAndEnabled)
   {
       ChangeState("PickingUpRune");
   }
}

As you can imagine, these states will take care of calling the getItem() function in GameItem.sol and the getExperience() function in Rune.sol respectively. Let’s start with PickingUpItem:


Opening the script we can see that we call PickUp() on the OnEnable() handler, passing the metadata url of the current GameItem that we’re colliding with:


private void OnEnable()
{
   _gameManager = GetComponentInParent<GameManager>(); // We assume we are under GameManager
   _gameInput = new GameInput();
   _gameInput.PickingUp.Enable();
   _gameInput.PickingUp.Cancel.performed += CancelTransaction;
   player.input.EnableInput(false);
   PickUp(_gameManager.currentGameItem.metadataUrl);
}

But PickUp() is just a manager for the main function in charge of calling the smart contract which is GetItem(). Passing the metadata url and the GameItem.sol deployed contract data, we call the getItem() function in the contract:


private async UniTask<string> GetItem(string metadataUrl)
{
   object[] parameters = {
       metadataUrl
   };
   // Set gas estimate
   HexBigInteger value = new HexBigInteger(0);
   HexBigInteger gas = new HexBigInteger(0);
   HexBigInteger gasPrice = new HexBigInteger(0);
   string resp = await Moralis.ExecuteContractFunction(GameManager.GameItemContractAddress, GameManager.GameItemContractAbi, "getItem", parameters, value, gas, gasPrice);
   return resp;
}

Using Moralis.ExecuteContractFunction(), is as simple as that! Pure magic. If we now take a look at the PickingUpRune.cs, we’ll see that it’s almost the same. However, this time we call GetExperience() and we pass the Rune.sol deployed contract data. Also an amount instead of the metadataUrl:


private async UniTask<string> GetExperience(int amount)
{
   BigInteger amountValue = new BigInteger(amount);
   object[] parameters = {
       amountValue.ToString("x")
   };
   // Set gas estimate
   HexBigInteger value = new HexBigInteger(0);
   HexBigInteger gas = new HexBigInteger(0);
   HexBigInteger gasPrice = new HexBigInteger(0);
   string resp = await Moralis.ExecuteContractFunction(GameManager.RuneContractAddress, GameManager.RuneContractAbi, "getExperience", parameters, value, gas, gasPrice);
   return resp;
}

This is how easy it is to call a contract function from Unity using Moralis! If we confirm the transaction in our wallet we will now have minted the items to our address. Let’s try that by hitting Play, defeating the boss and picking up an item and the rune:


5. Retrieve on-chain data

Now that we have one item and the experience, we can check they’re there by pressing M and opening the game menu. Try waiting at least 1 minute before doing so, and they should be appear.

The way we do this is through the Menu state, which activates when being in the Victory state and pressing M. To get the experience, on the OnEnable() handler we call Moralis.Web3Api.Account.GetTokenBalances() and if some of them have the same contract address as RuneContractAddress, meaning it is the right token, we get the balance through token.balance:

List<Erc20TokenBalance> listOfTokens = await Moralis.Web3Api.Account.GetTokenBalances(_walletAddress, Moralis.CurrentChain.EnumValue);
if (!listOfTokens.Any()) return;
foreach (var token in listOfTokens)
{
   // We make the sure that is the token that we deployed
   if (token.TokenAddress == GameManager.RuneContractAddress.ToLower())
   {
       runeAmountText.text = token.Balance;
       Debug.Log($"We have {token.Balance} runes (XP)");
   }
}

For the items we don’t do it directly in Menu.cs – we use the Inventory class to take care of that calling:


inventory.LoadItems(_walletAddress, GameManager.GameItemContractAddress, Moralis.CurrentChain.EnumValue);

So LoadItems() is the function that retrieves the minted items information by calling the Moralis.GetClient().Web3Api.Account.GetNFTsForContract(), passing the player address, the GameItemContractAddress and the deployed chain (mumbai):

public async void LoadItems(string playerAddress, string contractAddress, ChainList contractChain)
{
   try
   {
       NftOwnerCollection noc =
           await Moralis.GetClient().Web3Api.Account.GetNFTsForContract(playerAddress.ToLower(),
               contractAddress, contractChain);
       List<NftOwner> nftOwners = noc.Result;
       // We only proceed if we find some
       if (!nftOwners.Any())
       {
           Debug.Log("You don't own items");
           return;
       }
       if (nftOwners.Count == _currentItemsCount)
       {
           Debug.Log("There are no new items to load");
           return;
       }
       ClearAllItems(); // We clear the grid before adding new items
       foreach (var nftOwner in nftOwners)
       {
           var metadata = nftOwner.Metadata;
           MetadataObject metadataObject = JsonUtility.FromJson<MetadataObject>(metadata);
           PopulatePlayerItem(nftOwner.TokenId, metadataObject);
       }
   }
   catch (Exception exp)
   {
       Debug.LogError(exp.Message);
   }
}

We finally transform the metadata to a MetadataObject and we call PopulatePlayerItem(), which will use the MetadataObject to instantiate the items in the inventory:


Nice!! Now by clicking on the item you will be able to navigate to OpenSea and to PolygonScan by clicking on the Rune (experience).


Congratulations! You completed the Web3 Elden Ring Clone Tutorial. You’re more than ready to become a Moralis Mage 🙂

June 11, 2022
Build Web3 Projects
Level up your web3 development skills by building weekly projects.
Moralis 
Hackathons
Search
Generic filters
NFT Whale Watching Dapp
Related Articles
Become a Web3 Expert
Subscribe to our newsletter and get our E-book Web3 Speed run for FREE