Developing a Blockchain Virtual World: Architecture and Technologies

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 resu

Blockchain Development Services

Frequently Asked Questions

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1441
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1301
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    998
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1267
  • image_logo-advance_0.webp
    B2B Advance company logo design
    713
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    1003

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

  1. Concept and Tokenomics: 4–6 weeks.
  2. Smart Contracts: 6–10 weeks.
  3. 3D Engine (MVP): 8–12 weeks.
  4. Multiplayer: 4–6 weeks.
  5. Content System: 6–8 weeks.
  6. Marketplace: 4–6 weeks.
  7. Audit: 4–8 weeks.
  8. Alpha Testing: 4–6 weeks.
  9. 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.