How we built a full-scale Web3 survival game in 3 weeks

A technical deep dive into what it takes to build a great onchain game

How we built a full-scale Web3 survival game in 3 weeks

Welcome to the Web3 Warriors technical blog post! In this article, we explore how we built the immersive gaming experience of Web3 Warriors using thirdweb's GamingKit.

Web3 Warriors is a onchain survival game that demonstrates how to best leverage the potential of blockchain technology.

This article details how we built “invisible” web3 features so that players can just focus on gameplay, and still get the full benefits of web3. No wallet connection for authentication, no gas fees or popups for transactions. And for game developers, no backend or servers required!

Why We Built Web3 Warriors

Gameplay from Web3 Warriors

The goal of Web3 Warriors is to show the power of Web3 technology in games. Traditional games often suffer from limitations such as lack of true ownership and a closed ecosystem.

With Web3 Warriors, we aimed to create an open and decentralized gaming ecosystem where players have full ownership and control over their in-game assets, can earn real value from their gameplay, claim items that are actually their own, and experience a new level of interactivity and interoperability outside of the game itself.



Onchain Components

Web3 Warriors does not use a backend. Instead, it incorporates various onchain components to enable its advanced functionalities. These components reside on the blockchain, specifically on the Base Goerli testnet. Leveraging Base as the game’s public cloud infrastructure ensures transparency, security, and immutability.

Invisible wallet

The first component is the player’s wallet, which will hold currency and assets obtained through the game. We created an “invisible wallet” experience by leveraging Local Wallets that are generated for each player, based on a username.

Invisible wallet flow in Web3 Warriors

The Invisible Wallet plays a crucial role in Web3 Warriors, allowing players to securely manage their in-game assets and interact with the blockchain seamlessly. We opted for a Local Wallet because it eliminates the need for transaction confirmations and provides encryption to safeguard user data. Moreover, the wallet allows users to easily save and restore their game progress from disk, ensuring a hassle-free and uninterrupted gaming experience.

Here’s how we generate a Local Wallet for the players using the Unity SDK:

using Thirdweb;

public async Task ConnectAccount(string username)
{
    Debug.Log("Connecting Account");

    // Cache it in PlayerPrefs as needed for future logins
    Username = username; 

    // Connect to a Local Wallet
		// Generates an encrypted account.json under Application.persistentDataPath
    ConnectedAccount = await ThirdwebManager.Instance.SDK.wallet.Connect(
			new WalletConnection() { 
				provider = WalletProvider.LocalWallet, 
				password = Username 
    });

    Debug.Log("Connected to Account: " + ConnectedAccount);
}

Local Wallet is a powerful tool that allows you to craft any wallet connection flow you can imagine. Local Wallets can be exported and loaded into other web3 wallets to take assets outside of the game.

Here’s our account dropdown script that lets you copy, view and export your in-game local wallet, you could of course have a different export flow, or ditch it entirely and use a custom asset transfer method to move assets to a different wallet:

using UnityEngine;
using Thirdweb;

public class AccountDropdown : MonoBehaviour
{
    public async void OnOptionSelected(GameObject obj)
    {
        if (obj.name.Contains("Copy"))
        {
            GUIUtility.systemCopyBuffer = InventoryManager.Instance.ConnectedAccount;
            Debugger.Instance.ShowPanel(
                "Copied to clipboard",
                "Your public account address was copied to your clipboard.",
                false,
                true
            );
        }
        else if (obj.name.Contains("View"))
        {
            Application.OpenURL($"<https://goerli.basescan.org/address/{InventoryManager.Instance.ConnectedAccount}>");
            Debugger.Instance.ShowPanel(
                "Redirected to BaseScan",
                "Your public account explorer has been opened.",
                false,
                true
            );
        }
        else if (obj.name.Contains("Export"))
        {
            Debugger.Instance.ShowPanel("Exporting...", "Please wait.", true, false);

            string text = System.IO.File.ReadAllText(Utils.GetAccountPath());
            var ipfsResult = await ThirdwebManager.Instance.SDK.storage.UploadText(text);
            Application.OpenURL(ipfsResult.IpfsHash.cidToIpfsUrl(true));

            Debugger.Instance.ShowPanel(
                "Export Successful",
                "You may import the opened JSON file into an EVM compatible wallet and unlock it with your username.",
                false,
                true
            );
        }
    }
}

Gasless Transactions

Gasless transactions are an essential feature in Web3 Warriors, enabling players to perform actions on the blockchain without needing initial funds or worrying about the costs of transaction fees.

The Unity SDK lets you specify a gasless relayer directly in the editor, which handles the payment of transaction fees on behalf of the players. This pairs really well with local wallets with don’t hold any funds initially. This combo enables a smooth and uninterrupted gaming experience, without the need for players to transfer Ether or worry about gas prices.

For Web3 Warriors, we opted to use OpenZeppelin Defender, which can be integrated simply in our main SDK Prefab - the ThirdwebManager - as seen below:

The ThirdwebManager is a simple yet powerful drag-and-drop prefab that persists throughout your game, maintaining your connection to the blockchain and the player’s wallet. In Web3 Warriors, we have a _preload Scene which is where this prefab resides. You can find how to setup a forwarder contract on your own gasless relayers here.



$BATTLE - ERC20 Token Contract

Battle Tokens serve as the in-game currency within Web3 Warriors. We used our pre-built Token contract to create and manage our in-game ERC20 tokens.

These tokens can be minted by the game's admin or by one or more dedicated minter-permissioned wallets. This in-game currency enables various mechanics, such as purchasing armors and weapons or unlocking special features. The contract provides a flexible and scalable solution for handling the game's economy, without needing any sort of backend!

You can find the $BATTLE Token Contract used by the game here.

To deploy your own, you can head to our Explore page and select “Token” - no code needed!

Building the in-game currency mechanics

One of the core aspects of Web3 Warriors is the in-game currency mechanics that rely on the Battle Tokens. Let's explore how these mechanics are implemented:

Reading Balance

Using our standard ERC20 contract extension, you can set up a background Coroutine or Async Task to periodically refresh the player’s balance, and in Web3 Warrior’s case, notify UI elements if any are listening. Here’s an example:

private async void RefreshBattleTokensLoop()
{
    while (Application.isPlaying)
    {
        await new WaitForSeconds(30f);
        if (SceneLoader.Instance.CurrentScene == SceneName.Scene_Game)
            continue;

        try
        {
            await RefreshBattleTokens();
        }
        catch (System.Exception e)
        {
            Debug.LogWarning("Error Refreshing Battle Tokens | " + e.Message);
        }
    }
}

Additionally, we hook up a refresh for related user actions that update their balance, as seen below:

EventManager.Instance.OnBattleTokensMinted.AddListener(
		async (data) => await RefreshBattleTokens()
);

EventManager.Instance.OnModelMinted.AddListener(
    async (data) =>
    {
        await RefreshBattleTokens();
        await RefreshModels();
    }
);

EventManager.Instance.OnWeaponMinted.AddListener(
    async (data) =>
    {
        await RefreshBattleTokens();
        await RefreshWeapons();
    }
);

Finally, this is how simple it is to read a user’s balance:

private async Task RefreshBattleTokens()
{
    Debug.Log("Refreshing Battle Tokens");

    // A CurrencyValue is a typed object that can represent any currency
    CurrencyValue result = await ThirdwebManager.Instance.SDK
        .GetContract(GameContracts.BATTLE_TOKENS_CONTRACT)
        .ERC20.BalanceOf(ConnectedAccount);

    // Cache it as needed for your game
    TotalBattleTokensOwned = result.displayValue;

    // Notify UI
    EventManager.Instance.OnBattleTokensRefreshed.Invoke(TotalBattleTokensOwned);

    Debug.Log("Total Battle Tokens Owned: " + TotalBattleTokensOwned);
}

Signature Minting Flow

To distribute tokens when a player finishes a round, we used the signature minting technique. A dedicated wallet with a Minter role is able to generate signatures in order to mint tokens to the player’s local wallet, if conditions are met.

This approach ensures secure and controlled token generation, preventing unauthorized minting. We recommend using a remote solution for private key management, but for demonstration purposes, we embedded a "minter only" private key that can be rotated.

Here’s how you use our signature minting Token contract functionality - we call this function when the player wins or gets defeated, ensure minting of tokens can only be done at the end of a completed run.

public async Task MintBattleTokens(int amount)
{
    if (amount == 0)
        return;

    Debug.Log("Crafting " + amount + " Battle Tokens");

		// Get the $BATTLE contract
    var contract = ThirdwebManager.Instance.SDK.GetContract(GameContracts.BATTLE_TOKENS_CONTRACT);
    
    // Generate a mint request
		var mintRequest = new ERC20MintPayload(ConnectedAccount, amount.ToString());
    
    // Generate a signed payload with an authorized wallet
		var signedPayload = await contract.ERC20.signature.Generate(mintRequest, _battleTokenMinterPk);
		
    // Mint with the signature, this will mint the tokens to the connected account
    await contract.ERC20.signature.Mint(signedPayload);

		// Cache as needed
    TotalBattleTokensOwned += amount;

		// Notify UI
    EventManager.Instance.OnBattleTokensMinted.Invoke(amount);
}

Signature minting can be extended to many use-cases since it allows distributing tokens and NFTs based on game state, such as score-based achievements, level criteria, quests completion and much more!

In Web3 Warriors, players complete runs to obtain BATTLE Tokens, which in turn can be spent to purchase armor and weapon skins from the shop. These skins are NFTs owned by the players, and are reflected inside and outside of the game!

Armors and Weapons - EditionDrop contracts

In Web3 Warriors, the acquisition of armors and weapons adds personalization and special mechanics to the gameplay. These are mainly cosmetic skins, represented by NFTs can be purchased directly in game using BATTLE tokens.

We used the pre-built Edition Drop contracts to manage the distribution and ownership of these NFTs. These contracts leverage the ERC1155 standard, enabling multiple copies of the same NFT while maintaining a controlled supply, which is perfect for skins and in-game items.

The Edition Drop contracts work hand in hand with the Battle Tokens, as players can only claim Armor and Weapon skins by spending their hard-earned in-game currency. This mechanism creates a balanced and fair economy within the game, where players are encouraged to explore, strategize, and accumulate Battle Tokens to enhance their characters.

Edition Drop contracts lets owners set up Claim Conditions onchain for each NFT, with custom pricing in any currency. This makes sure the requirements for acquiring these NFTs are  enforced on chain. No central server required.

Explore the $ARMOR and $WEAPON Edition Drop contracts used in the game, which shows you live data on how many items have been obtained and who owns them!

To deploy your own game contract, you can head to our Explore page and select “Edition Drop” - no code needed!



Building the Armory panel

To provide players with a seamless experience in managing their armors and weapons, we have developed an in-game Armory Screen. Let's take a closer look at its key features:

Fetching All/Owned NFTs

The Armory Screen allows players to view all the NFTs available in the game, as well as those they already own. All this data comes directly from the blockchain.

For simplicity’s sake, we map the description of the NFTs to their in-game identifiers, and fetch related data for each NFT. In this case, we update the price based on the NFT’s claim conditions.

Here’s how we load the available Weapon Skins in the armory:

private async Task LoadWeaponSkins()
{
		// Get the $WEAPON contract
    var weaponSkinsContract = ThirdwebManager.Instance.SDK.GetContract(GameContracts.WEAPON_SKINS_CONTRACT);
    // Fetch all NFTs
		var weaponNfts = await weaponSkinsContract.ERC1155.GetAll();
    Debug.Log("Total Weapons " + weaponNfts.Count);

		// loop through all NFTs and updated related game data to display in your UI
    foreach (var nft in weaponNfts)
    {
        foreach (var weaponSetup in PlayerModelManager.Instance.weaponSetups)
        {
            if (nft.metadata.description == weaponSetup.identifier)
            {
                Debug.Log("Fetching Weapon " + nft.metadata.description);
                // Fetch the active claim condition to display the weapon price
                var claimConditions = await weaponSkinsContract.ERC1155.claimConditions.GetActive(nft.metadata.id);
                weaponSetup.price = claimConditions.currencyMetadata.displayValue;
            }
        }
    }
}

And here’s how we display what weapons the player currently owns:

private async Task RefreshWeapons()
{
    Debug.Log("Refreshing Weapons");
		var weaponSkinsContract = ThirdwebManager.Instance.SDK.GetContract(GameContracts.WEAPON_SKINS_CONTRACT);
    var ownedWeapons = await weaponSkinsContract.ERC1155.GetOwned();

    foreach (var weapon in ownedWeapons)
        AddWeapon(weapon.metadata.description);

    Debug.Log("Total Weapons Owned: " + ownedWeapons.Count);
}

Purchasing Assets

The Armory screen enables players to purchase armor and weapon skins using their Battle Tokens. Let’s walk through how this works.

Edition Drop contracts allow users to “claim” NFTs as long as they meet the onchain conditions defined in the claim conditions of the contract.

First, we setup our claim conditions for each NFT on the thirdweb dashboard, setting the price of NFTs in BATTLE tokens. Once that’s done, we need to do is to execute the claim function of the contract when a user clicks on purchase in the armory.

In our Unity code, we hook up the purchase buttons of each UI item with the claiming function

ItemUnlockButton.onClick.AddListener(() =>
{
    OnMintWeapon(weaponSetup);
});

When the player clicks purchase, we then simply execute the claim function on the contract. All verifications happen onchain. If the conditions are met, the player will exchange some BATTLE tokens for a Armor or Weapon NFT.

private async void OnPurchaseWeapon(WeaponSetup weaponSetup)
{
    try
    {
        // Check the balance up front allows us to show nicer error messages
        if (InventoryManager.Instance.TotalBattleTokensOwned < weaponSetup.price)
        {
            Debugger.Instance.ShowPanel(
                "Insufficient Battle Tokens",
                $"You need {weaponSetup.price - InventoryManager.Instance.TotalBattleTokensOwned} more Battle Tokens to mint this weapon.",
                false,
                true
            );
            return;
        }
        Debugger.Instance.ShowPanel("Please Wait...", $"Crafting weapon: {weaponSetup.displayName}", true, false);
        
        // Get the contract
				var weaponSkinsContract = ThirdwebManager.Instance.SDK.GetContract(GameContracts.WEAPON_SKINS_CONTRACT);
        var quantity = 1;

				// Execute the claim transaction as the connected player wallet
        await weaponSkinContract.ERC1155.claim(weaponSetup.tokenId, quantity);

        // If succesfully claimed, equip the purchased weapon
        InventoryManager.Instance.EquipWeapon(PlayerModelManager.Instance.ParseWeaponSetup(weaponSetup));
        ShowPreview();
        MenuManager.Instance.RefreshShop();
        Debugger.Instance.ShowPanel("Success!", $"Crafted weapon: {weaponSetup.displayName}", false, true);
    }
    catch (System.Exception e)
    {
        Debugger.Instance.ShowPanel($"Unable to craft weapon: {weaponSetup.displayName}", e.Message, false, true);
    }
}

Other Unity SDK functionality used

The thirdweb Unity SDK excels at integrating smart contract in games and connecting different type of wallets, but that’s only a subset of the features included. Here’s some other noteworthy features that we used in web3 warriors:

Built-in IPFS Storage

The Unity SDK simplifies the process of uploading and downloading assets to and from IPFS (InterPlanetary File System), enabling decentralized storage of in-game content, at no cost.

All NFT metadata for weapons, armors and pets are stored on IPFS, the SDK handles it under the hood automatically.

The SDK also allows explicit use of decentralized storage. Here’s how we built the share flow: clicking a button takes a in-game screenshot, uploads it to IPFS and opens up a custom sharing link:

using System.IO;
using UnityEngine;

public class ShareButton : MonoBehaviour
{
    public async void OnShare()
    {
        try
        {
            Debugger.Instance.ShowPanel("Taking Screenshot...", $"Smile!", true, false);

            string fullPath = Application.persistentDataPath + "/sharedScreenshot.png";

            if (File.Exists(fullPath))
                File.Delete(fullPath);

            // Take the screenshot
            ScreenCapture.CaptureScreenshot(fullPath);
            Debugger.Instance.ShowPanel("Screenshot Saved! Uploading...", true, false);
						
						// Upload it and get back the IPFS hash
            var response = await ThirdwebManager.Instance.SDK.storage.UploadFromPath(fullPath);
            
            // Also get the player's local wallet address
            var playerAddress = await ThirdwebManager.Instance.SDK.wallet.GetAddress();

            // Open the share link
            Application.OpenURL($"<https://web3warriors.thirdweb.com/{playerAddress}?ipfs={response.IpfsHash}>");

            Debugger.Instance.ShowPanel("Success!", "You can now share your character on socials!", false, true);
        }
        catch (System.Exception e)
        {
            Debugger.Instance.ShowPanel("[Storage] Upload Error", $"Error uploading from path: {e.Message}", false, true);
        }
    }
}

Embedded RPC connection

The thirdweb SDK comes out of the box with direct connections to 750+ blockchains. To connect to the Base Goerli tesnet, all we needed was to enter the chain name in the Unity Editor. That’s it.

You can find all the supported chains on the thirdweb Chainlist:

Chainlist: RPCs, Block Explorers, Faucets
A list of EVM networks with RPCs, smart contracts, block explorers & faucets. Deploy smart contracts to all EVM chains with thirdweb.

ThirdwebManager Prefab

The ThirdwebManager prefab acts as your persistent reference to the Thirdweb SDK. This prefab significantly accelerates the integration of Web3 features into Unity games. It holds your connection to the blockchain, the connected wallet state and more. Drag and Drop into your scene, setup a chain from the inspector, and voila!

Other notable featuresThe Unity SDK comes with many more features that we haven’t used (yet) but are worth a mention:

  • Connect wallet UI prefab - drag and drop a fully functional connect wallet button into any scene. Supports all popular wallets.
  • NFT renderer prefab - a UI component that simplifies displaying NFTs in your games
  • Contract deployer - easily deploy contracts for your players directly from your games!
  • Marketplace contract - One of our most powerful contracts that allows to integrate marketplaces in your games, enabling player to player trading with custom rules
  • Pack contract - Add lootbox mechanics in your game, fully onchain

Why You Should Build Web3 Games

Web3 technology opens up a world of possibilities for game developers and players alike. By building Web3 games, you can:

  • Build Faster: Web3 games eliminate the need for complex backend infrastructure, enabling you to focus on the core gameplay experience and rapidly develop innovative gaming concepts.
  • Revenue Streams: Web3 games introduce new revenue streams through features such as royalties, gating mechanisms, and controlled supply. This provides game developers and players with unique economic opportunities and incentives.
  • Interoperability: Web3 technology fosters interoperability between games and platforms. Players can seamlessly transfer assets between different games and apps, creating a connected and expansive gaming ecosystem.

At thirdweb, we aim to accelerate the integration of Web3 technology into game development, providing tools, resources, and support to empower developers like you. We believe that open source tools are key to empower innovation.

The fastest way to build web3 games

By leveraging the thirdweb GamingKit & Unity SDK, we were able to expedite the development process. We focused on creating an great gaming experience and fun core loop. The web3 integration was the easy part!

The SDK is fully open-source and permission-less. Give it spin and let us know what you think!

Join us as we revolutionize the gaming landscape with Web3 Warriors and unlock the limitless possibilities of Web3 gaming!