Developing a Blockchain Virtual World: Architecture and Technologies

Building a virtual world where land and asset rights are protected by blockchain requires a balance between decentralization and speed. We develop metaverses with a hybrid architecture: smart contracts for NFT lands and off-chain services for smooth multiplayer. Our team delivers the project turnkey—from concept to support—ensuring a reliable solution that scales with your community.

Blockchain Development Services

Frequently Asked Questions

Latest works

  • Development of a web application for FEEDME
    Development of a web application for FEEDME
    1335
  • Development of an online store for the company FURNORO
    Development of an online store for the company FURNORO
    1293
  • B2B Advance company logo design
    B2B Advance company logo design
    738
  • Development of a web application for Enviok
    Development of a web application for Enviok
    1031
  • AIDER company logo development
    AIDER company logo development
    978
  • CRM development for Chasseurs
    CRM development for Chasseurs
    1087

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.