Build your first fully on-chain game
December 3rd, 2023

The core principle of a fully on-chain game is to use contracts to implement game data and game logic. So how to implement the simplest a fully on-chain game? This tutorial does not use any development framework or game engine. You only need to know some of the simplest solidity codes.

Entity Component System

ECS is a design pattern that improves code reusability by separating data from behavior. It is often used in game development. A minimal ECS consists of:

  • Entity: a unique identifier.

  • Component: a reusable data container attached to an entity.

  • System: the logic for operating entity components.

  • World: a container for an entity component system.

ECS Workflow
ECS Workflow

Implement minimal ECS with a few lines of solidity code

contract World{
    uint256 entityId; //Entity
    mapping(uint256=>uint256) HPs; //Component
    function play()public{} //System
}

Start by building the world

In this tutorial we will build a magical world. You will play a warrior and a mage to fight against the monster boss. This is a very old-fashioned story and game mode.

contract World{

}

Create characters in the game

Each character is an entity and has a unique number.

  • Warrior: entity id is 0

  • Mage: entity id is 1

  • Monster: entity id is 2

uint256 warriorEntityId;
uint256 mageEntityId;
uint256 monsterEntityId;

function createEntity() public {
   warriorEntityId = entityId; //create warrior, warriorEntityId=0
   entityId++;
   mageEntityId = entityId; //create mage, mageEntityId=1
   entityId++;
   monsterEntityId = entityId; //create monster, monsterEntityId=2
   entityId++;
}

Assign attributes to characters

The container of each attribute is called a component. There are four types of components in your world:

  • HP: represents the character’s health value

  • ATK: represents the character’s attack power

  • MP: represents the character’s mana

  • DEF: represents character’s defense

mapping(uint256 => uint256) HPs;
mapping(uint256 => uint256) ATKs;
mapping(uint256 => uint256) MPs;
mapping(uint256 => uint256) DEFs;

Different characters (entities) have different attributes (components), so you need to assign different components to different characters.

Add components to entities
Add components to entities
function addComponent() public {
  HPs[warriorEntityId] = 100;
  ATKs[warriorEntityId] = 20;

  HPs[mageEntityId] = 100;
  MPs[mageEntityId] = 30;

  HPs[monsterEntityId] = 100;
  ATKs[monsterEntityId] = 30;
  DEFs[monsterEntityId] = 5;
 }

Add game logic

There are 4 steps in this game logic(system).

Game logic
Game logic

The warrior attacks the monster, note that in order to enhance the fun of the game, we use keccak256 to obtain a pseudo-random number, which is not a safe way.

        uint256 warriorRandomAttack = (uint256(
            keccak256(abi.encodePacked(block.timestamp, msg.sender))
        ) % (ATKs[warriorEntityId] - 10)) + 10;
        HPs[monsterEntityId] = (warriorRandomAttack - DEFs[monsterEntityId]) >
            HPs[monsterEntityId]
            ? 0
            : HPs[monsterEntityId] -
                (warriorRandomAttack - DEFs[monsterEntityId]);

The monster attacks the warrior.

 uint256 monsterRandomAttack = (uint256(
            keccak256(
                abi.encodePacked(
                    block.timestamp,
                    msg.sender,
                    warriorRandomAttack
                )
            )
        ) % (ATKs[monsterEntityId] - 10)) + 10;
        HPs[warriorEntityId] = monsterRandomAttack > HPs[warriorEntityId]
            ? 0
            : (HPs[warriorEntityId] - monsterRandomAttack); 

Mage heals warrior.

        if (MPs[mageEntityId] > 0 && HPs[mageEntityId] > 0) {
            MPs[mageEntityId] -= 10;
            HPs[warriorEntityId] += 10;
        }

The monster attacks the Mage.

  HPs[mageEntityId] = monsterRandomAttack > HPs[mageEntityId]
            ? 0
            : (HPs[mageEntityId] - monsterRandomAttack);

        return (HPs[warriorEntityId], HPs[mageEntityId], HPs[monsterEntityId]);      

After running the play repeatedly, until one party's HP reaches 0.

return (HPs[warriorEntityId], HPs[mageEntityId], HPs[monsterEntityId]);

Package them together

// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.0;

contract World {
    uint256 entityId;
    mapping(uint256 => uint256) HPs;
    mapping(uint256 => uint256) ATKs;
    mapping(uint256 => uint256) MPs;
    mapping(uint256 => uint256) DEFs;
    
    uint256 warriorEntityId;
    uint256 mageEntityId;
    uint256 monsterEntityId;

    function createEntity() public {
        warriorEntityId = entityId; //create warrior, warriorEntityId=0
        entityId++;
        mageEntityId = entityId; //create mage, mageEntityId=1
        entityId++;
        monsterEntityId = entityId; //create monster, monsterEntityId=2
        entityId++;
    }

    function addComponent() public {
        HPs[warriorEntityId] = 100;
        ATKs[warriorEntityId] = 20;
        HPs[mageEntityId] = 100;
        MPs[mageEntityId] = 30;
        HPs[monsterEntityId] = 100;
        ATKs[monsterEntityId] = 30;
        DEFs[monsterEntityId] = 5;
    }

    function play()
        public
        returns (
            uint256,
            uint256,
            uint256
        )
    {
        //warrior attacks monster
        uint256 warriorRandomAttack = (uint256(
            keccak256(abi.encodePacked(block.timestamp, msg.sender))
        ) % (ATKs[warriorEntityId] - 10)) + 10;
        HPs[monsterEntityId] = (warriorRandomAttack - DEFs[monsterEntityId]) >
            HPs[monsterEntityId]
            ? 0
            : HPs[monsterEntityId] -
                (warriorRandomAttack - DEFs[monsterEntityId]);

        //monster attacks warrior
        uint256 monsterRandomAttack = (uint256(
            keccak256(
                abi.encodePacked(
                    block.timestamp,
                    msg.sender,
                    warriorRandomAttack
                )
            )
        ) % (ATKs[monsterEntityId] - 10)) + 10;
        HPs[warriorEntityId] = monsterRandomAttack > HPs[warriorEntityId]
            ? 0
            : (HPs[warriorEntityId] - monsterRandomAttack);

        //mage heals warrior
        if (MPs[mageEntityId] > 0 && HPs[mageEntityId] > 0) {
            MPs[mageEntityId] -= 10;
            HPs[warriorEntityId] += 10;
        }

        //monster attacks mage
        HPs[mageEntityId] = monsterRandomAttack > HPs[mageEntityId]
            ? 0
            : (HPs[mageEntityId] - monsterRandomAttack);

        return (HPs[warriorEntityId], HPs[mageEntityId], HPs[monsterEntityId]);
    }
}

Finale

Thank you for reading to the end. This is my first article about fully on-chain game. I hope you can understand FOCG through the simplest code. All codes are for testing only. If you have any questions you can contact me on twitter. Although you don't use any game engine, you can still build your own world, but you must keep it real. Make sure your data and game logic are really on the chain.

If you are interested in AW and FOCG, these are two products I am building.

Subscribe to Rickey
Receive the latest updates directly to your inbox.
Nft graphic
Mint this entry as an NFT to add it to your collection.
Verification
This entry has been permanently stored onchain and signed by its creator.
More from Rickey

Skeleton

Skeleton

Skeleton