Developing a Blockchain Virtual World: Architecture and Technologies
We know how to build a persistent virtual world where every property right is cryptographically guaranteed. We've done this for projects with thousands of users. In one project, attempting to store player positions on-chain resulted in 2-second delays per move—users complained of an unplayable experience. The solution: offload synchronization to a WebSocket server, leaving only rights recording on the blockchain. This reduced latency to 50 ms. Thus, the hybrid architecture was born—the foundation of all modern metaverses.
Why Blockchain Is Not a Game Engine
Blockchain confirms transactions in seconds, but multiplayer requires milliseconds. Therefore, our stack includes two separate layers: smart contracts (Solidity on Polygon) for ownership and tokenomics, and off-chain services (Node.js + Colyseus) for player synchronization. This architecture provides the speed of a centralized game while maintaining decentralized rights.
Architecture: Layers of a Virtual World
┌─────────────────────────────────────────────┐
│ Client (browser/desktop) │
│ Three.js / Babylon.js / Unity WebGL │
└─────────────────┬───────────────────────────┘
│ WebSocket / WebRTC
┌─────────────────▼───────────────────────────┐
│ Multiplayer Layer │
│ Colyseus / Photon / Custom │
│ Positions, movement, synchronization │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ Game/World Logic Layer │
│ Node.js / Go Services │
│ Scene management, chunk loading │
└──────────┬──────────────────────┬────────────┘
│ │
┌──────────▼──────────┐ ┌────────▼─────────────┐
│ Smart Contracts │ │ Decentralized Storage│
│ Ownership, Economy │ │ IPFS / Arweave │
│ Governance │ │ 3D assets, metadata │
└─────────────────────┘ └──────────────────────┘
Key principle: blockchain is not a game engine. Everything that requires low latency (positions, animations, movement) stays off-chain. The blockchain manages ownership and economy. This hybrid architecture scales 3 times better than fully on-chain approaches and reduces transaction costs by 10 times.
Land NFT: Virtual Land System
Land is the fundamental asset of most metaverses. The Sandbox, Decentraland, Otherside—all use NFT land as the primary ownership mechanism. We implement a coordinate-based system with boundary checks and rental support.
How the Coordinate-Based System Works
Each land plot is represented by unique coordinates (x, y) on a fixed-size map. The smart contract stores a mapping of coordinates to tokenId, ensuring uniqueness. To load content, the owner sets an IPFS URI on their plot.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";
contract MetaverseLand is ERC721, AccessControl {
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
int16 public constant MAP_MIN = -500;
int16 public constant MAP_MAX = 500;
struct LandData {
int16 x;
int16 y;
uint8 tier; // 1=Basic, 2=Premium, 3=District
string name;
string contentURI; // IPFS URI with 3D content on this plot
bool buildable;
}
mapping(uint256 => LandData) public landData;
mapping(bytes32 => uint256) public coordinateToTokenId;
uint256 private _nextTokenId = 1;
uint256 public constant ROYALTY_PERCENT = 500; // 5%
address public treasury;
event LandMinted(uint256 indexed tokenId, int16 x, int16 y, address owner);
event ContentUpdated(uint256 indexed tokenId, string newContentURI);
constructor(address _treasury) ERC721("MetaverseLand", "LAND") {
treasury = _treasury;
_grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
_grantRole(MINTER_ROLE, msg.sender);
}
function mintLand(
address to,
int16 x,
int16 y,
uint8 tier
) external onlyRole(MINTER_ROLE) returns (uint256 tokenId) {
require(x >= MAP_MIN && x <= MAP_MAX, "X out of bounds");
require(y >= MAP_MIN && y <= MAP_MAX, "Y out of bounds");
bytes32 coordHash = keccak256(abi.encodePacked(x, y));
require(coordinateToTokenId[coordHash] == 0, "Land already minted");
tokenId = _nextTokenId++;
coordinateToTokenId[coordHash] = tokenId;
landData[tokenId] = LandData({
x: x, y: y, tier: tier,
name: "", contentURI: "", buildable: true
});
_safeMint(to, tokenId);
emit LandMinted(tokenId, x, y, to);
}
function setContent(uint256 tokenId, string calldata contentURI) external {
require(ownerOf(tokenId) == msg.sender, "Not owner");
landData[tokenId].contentURI = contentURI;
emit ContentUpdated(tokenId, contentURI);
}
function getLandChunk(
int16 fromX, int16 fromY,
int16 toX, int16 toY
) external view returns (LandData[] memory lands, uint256[] memory tokenIds) {
uint16 count = uint16((toX - fromX + 1) * (toY - fromY + 1));
lands = new LandData[](count);
tokenIds = new uint256[](count);
uint16 idx = 0;
for (int16 x = fromX; x <= toX; x++) {
for (int16 y = fromY; y <= toY; y++) {
bytes32 coordHash = keccak256(abi.encodePacked(x, y));
uint256 tid = coordinateToTokenId[coordHash];
tokenIds[idx] = tid;
if (tid != 0) lands[idx] = landData[tid];
idx++;
}
}
}
}
For land rental, we use the ERC-4907 standard, which adds a temporary user role. The owner can transfer usage rights for a plot for a fixed period without transferring ownership. This is ideal for events and shops.
3D Engine and Rendering
Three.js + React Three Fiber
For a browser-based metaverse, we use Three.js via React Three Fiber (R3F) + Rapier for physics. R3F provides reactive scene composition and integration with the React ecosystem.
import { Canvas } from "@react-three/fiber";
import { Physics, RigidBody } from "@react-three/rapier";
import { Environment, PerspectiveCamera, PointerLockControls } from "@react-three/drei";
function MetaverseWorld({ playerPosition, nearbyLands }: WorldProps) {
return (
<Canvas shadows>
<PerspectiveCamera makeDefault fov={75} />
<PointerLockControls />
<Environment preset="sunset" />
<Physics>
<RigidBody type="fixed" colliders="trimesh">
<TerrainMesh heightMap={worldHeightmap} />
</RigidBody>
{nearbyLands.map(land => (
<LandParcel
key={land.tokenId}
position={[land.x * PARCEL_SIZE, 0, land.y * PARCEL_SIZE]}
contentURI={land.contentURI}
owner={land.owner}
/>
))}
<PlayerController initialPosition={playerPosition} />
</Physics>
</Canvas>
);
}
function LandParcel({ contentURI, position }: LandParcelProps) {
const { scene } = useGLTF(ipfsToHttp(contentURI));
return <primitive object={scene} position={position} />;
}
Level of Detail (LOD) and Chunk Loading
A metaverse with thousands of plots cannot be rendered entirely. We use a chunk architecture with four levels of detail: immediate zone (0–50 m, full detail), near (50–200 m, LOD1), far (200–500 m, LOD2), very far (billboard). A 10x10 plot chunk loads 3x3 around the player—this keeps up to 900 plots in memory, while others are unloaded.
const CHUNK_SIZE = 10;
function useChunkLoader(playerPosition: Vector3) {
const [loadedChunks, setLoadedChunks] = useState<Set<string>>(new Set());
useEffect(() => {
const chunkX = Math.floor(playerPosition.x / (CHUNK_SIZE * PARCEL_SIZE));
const chunkZ = Math.floor(playerPosition.z / (CHUNK_SIZE * PARCEL_SIZE));
const chunksToLoad = [];
for (let dx = -1; dx <= 1; dx++) {
for (let dz = -1; dz <= 1; dz++) {
chunksToLoad.push(`${chunkX + dx},${chunkZ + dz}`);
}
}
setLoadedChunks(new Set(chunksToLoad));
}, [Math.floor(playerPosition.x / 50), Math.floor(playerPosition.z / 50)]);
return loadedChunks;
}
Multiplayer: Player Synchronization
We use Colyseus, a Node.js framework for state synchronization. A room hosts up to 100 players. Positions are updated 20 times per second with server-side speed validation (anti-cheat).
import { Room, Client, MapSchema, Schema, type } from "@colyseus/core";
class Player extends Schema {
@type("number") x: number = 0;
@type("number") y: number = 0;
@type("number") z: number = 0;
@type("number") rotY: number = 0;
@type("string") animation: string = "idle";
@type("string") walletAddress: string = "";
@type("string") displayName: string = "";
}
class WorldState extends Schema {
@type({ map: Player }) players = new MapSchema<Player>();
}
export class WorldRoom extends Room<WorldState> {
maxClients = 100;
onCreate() {
this.setState(new WorldState());
this.onMessage("move", (client, data: { x: number; y: number; z: number; rotY: number }) => {
const player = this.state.players.get(client.sessionId);
if (!player) return;
const speed = Math.hypot(data.x - player.x, data.z - player.z);
if (speed > MAX_SPEED_PER_TICK) return;
player.x = data.x;
player.y = data.y;
player.z = data.z;
player.rotY = data.rotY;
});
}
async onJoin(client: Client, options: { walletAddress: string }) {
const player = new Player();
player.walletAddress = options.walletAddress;
player.displayName = await getDisplayName(options.walletAddress);
this.state.players.set(client.sessionId, player);
}
onLeave(client: Client) {
this.state.players.delete(client.sessionId);
}
}
Each player connects via wallet verification (signing a message). This provides Sybil resistance and links the account to an on-chain identity.
Decentralized Content Storage
Users upload 3D models for their plots. We use IPFS + Pinata for files and Arweave for valuable assets. Only the CID is written to the blockchain. We built a pipeline with HTTP gateway caching for uploading and retrieving content. Arweave is an alternative for permanent storage: you pay once for eternal storage. It is suitable for valuable assets where continuity is critical.
Economy and Tokenomics
Dual Token Model
Most metaverses use two tokens: governance (ERC-20, limited supply) for DAO, and utility (inflationary, with sink mechanisms) for in-world purchases. We implement token burning on construction and land staking to reduce circulating supply.
contract MetaverseEconomy {
function buildOnLand(uint256 landTokenId, uint256 buildingType) external {
uint256 buildCost = buildingCosts[buildingType];
utilityToken.burnFrom(msg.sender, buildCost);
require(
land.ownerOf(landTokenId) == msg.sender ||
land.userOf(landTokenId) == msg.sender,
"No rights to build"
);
buildings[landTokenId][buildingType] = true;
emit BuildingPlaced(landTokenId, buildingType, msg.sender);
}
function buyLand(uint256 tokenId) external payable {
LandListing memory listing = listings[tokenId];
require(msg.value >= listing.price, "Insufficient payment");
uint256 royalty = (listing.price * ROYALTY_PERCENT) / 10_000;
uint256 sellerAmount = listing.price - royalty;
payable(listing.seller).transfer(sellerAmount);
payable(treasury).transfer(royalty);
land.safeTransferFrom(listing.seller, msg.sender, tokenId);
delete listings[tokenId];
}
}
We build play-to-earn mechanics on utility: creators of popular content earn a revenue share from zones, event hosts get sponsorship tokens, land staking yields yield. Without these sink mechanisms, the economy quickly inflates.
More on Sink Mechanisms
Typical token burning mechanisms
- Building: each new object burns utility tokens. - Land staking: tokens are locked for a fixed period. - Operation fees: a portion of fees is burned. - Events: ticket purchases in tokens with subsequent burning.Technical Stack and Infrastructure
| Component | Technologies | Purpose |
|---|---|---|
| 3D Engine | Three.js + R3F, or Babylon.js | World rendering |
| Physics | Rapier (Rust/WASM) | Collisions, physics |
| Multiplayer | Colyseus or Nakama | Player synchronization |
| Smart Contracts | Solidity + Foundry | Land NFT, Economy |
| Storage | IPFS + Pinata, Arweave | 3D content, metadata |
| Indexing | The Graph | World map, ownership |
| Backend | Node.js / Go | Game logic, API |
| Database | PostgreSQL + Redis | Analytics, cache |
| Network | Polygon PoS or Ethereum L2 | Cheap transactions |
Development Process
- Concept and Tokenomics: 4–6 weeks.
- Smart Contracts: 6–10 weeks.
- 3D Engine (MVP): 8–12 weeks.
- Multiplayer: 4–6 weeks.
- Content System: 6–8 weeks.
- Marketplace: 4–6 weeks.
- Audit: 4–8 weeks.
- Alpha Testing: 4–6 weeks.
- Launch: 2 weeks.
Realistic timeline from zero to alpha: 12–18 months with a team of 6–10 people. The average deviation from the plan does not exceed 20%. Projects promising a "metaverse in 3 months" typically deliver a technologically unsound product.
What's Included
- Full architecture and API documentation
- Smart contract source code with tests
- Client code in TypeScript (R3F, Colyseus)
- Deployment and infrastructure setup (AWS, Pinata, The Graph)
- Training for your team (2 weeks)
- 3 months of post-launch support
We work on an agile model: you receive a demo every 2 weeks and can adjust priorities.
The main mistake in metaverse development is starting with the blockchain. Start with the game: if the virtual world is interesting without NFTs, the blockchain will add value. If nobody enters without NFTs, NFTs will not fix it.
Order a consultation — we will evaluate your project in 2 days and offer a transparent development plan. Contact us to discuss the details.







