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.
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.
Implement minimal ECS with a few lines of solidity code
contract World{
uint256 entityId; //Entity
mapping(uint256=>uint256) HPs; //Component
function play()public{} //System
}
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{
}
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++;
}
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.
function addComponent() public {
HPs[warriorEntityId] = 100;
ATKs[warriorEntityId] = 20;
HPs[mageEntityId] = 100;
MPs[mageEntityId] = 30;
HPs[monsterEntityId] = 100;
ATKs[monsterEntityId] = 30;
DEFs[monsterEntityId] = 5;
}
There are 4 steps in this game logic(system).
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]);
// 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]);
}
}
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.