Blockchain Virtual World Development

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Blockchain Virtual World Development
Complex
from 2 weeks to 3 months
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Blockchain Virtual World Development

Blockchain metaverse is not just "3D game with NFT". This is persistent virtual world where ownership of digital assets (lands, buildings, items, avatars) is cryptographically guaranteed, and economy managed by code, not centralized operator. Technically one of most complex products in Web3: intersects real-time 3D rendering, multiplayer networking, smart contract systems, decentralized data storage and complex tokenomics. Let's break down each layer.

Architecture: Layers of Virtual World

┌─────────────────────────────────────────────┐
│             Client (browser/desktop)        │
│        Three.js / Babylon.js / Unity WebGL  │
└─────────────────┬───────────────────────────┘
                  │ WebSocket / WebRTC
┌─────────────────▼───────────────────────────┐
│            Multiplayer Layer                 │
│     Colyseus / Photon / custom              │
│     Positions, movement, sync               │
└─────────────────┬───────────────────────────┘
                  │
┌─────────────────▼───────────────────────────┐
│         Game/World Logic Layer               │
│    Node.js / Go services                    │
│    Scene management, chunk loading          │
└──────────┬──────────────────────┬────────────┘
           │                      │
┌──────────▼──────────┐  ┌────────▼─────────────┐
│  Smart Contracts    │  │  Decentralized Store│
│  Ownership, Economy │  │  IPFS / Arweave     │
│  Governance         │  │  3D assets, metadata│
└─────────────────────┘  └──────────────────────┘

Key principle: blockchain is not game engine. Everything requiring low latency (positions, animations, movement) stays off-chain. Blockchain manages ownership and economy.

Land NFT: Virtual Land System

Land is foundational asset in most metaverses. The Sandbox, Decentraland, Otherside—all use NFT land as primary ownership mechanism.

Coordinate-based Land System

// 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 parcel
        bool buildable;
    }

    mapping(uint256 => LandData) public landData;
    mapping(bytes32 => uint256) public coordinateToTokenId; // hash(x,y) → tokenId
    uint256 private _nextTokenId = 1;

    // Secondary market royalties
    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);
    }

    // Owner sets 3D content on parcel
    function setContent(uint256 tokenId, string calldata contentURI) external {
        require(ownerOf(tokenId) == msg.sender, "Not owner");
        landData[tokenId].contentURI = contentURI;
        emit ContentUpdated(tokenId, contentURI);
    }

    // Batch request data for rendering map chunk
    function getLandChunk(
        int16 fromX, int16 fromY,
        int16 toX, int16 toY
    ) external view returns (LandData[] memory lands, uint256[] memory tokenIds) {
        // For small chunks (e.g., 20x20 = 400 parcels)
        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++;
            }
        }
    }
}

Land Rental (ERC-4907)

ERC-4907 adds user role to ERC-721: temporary user can use NFT until certain time, but cannot trade it. Perfect for land rental:

import "@openzeppelin/contracts/token/ERC721/extensions/ERC4907.sol";

contract MetaverseLandWithRent is MetaverseLand, ERC4907 {
    // Owner rents parcel for certain time
    function rentLand(
        uint256 tokenId,
        address tenant,
        uint64 expires
    ) external {
        require(ownerOf(tokenId) == msg.sender, "Not owner");
        setUser(tokenId, tenant, expires);
    }

    // Who can build on parcel: owner or tenant
    function canBuild(uint256 tokenId) external view returns (address) {
        address user = userOf(tokenId);
        return user != address(0) ? user : ownerOf(tokenId);
    }
}

3D Engine and Rendering

Three.js + React Three Fiber

For browser metaverse—Three.js via React Three Fiber (R3F) + Rapier for physics:

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 /> {/* FPS controls */}
      <Environment preset="sunset" />

      <Physics>
        {/* Terrain */}
        <RigidBody type="fixed" colliders="trimesh">
          <TerrainMesh heightMap={worldHeightmap} />
        </RigidBody>

        {/* Dynamically load only nearby parcels */}
        {nearbyLands.map(land => (
          <LandParcel
            key={land.tokenId}
            position={[land.x * PARCEL_SIZE, 0, land.y * PARCEL_SIZE]}
            contentURI={land.contentURI}
            owner={land.owner}
          />
        ))}

        {/* Player */}
        <PlayerController initialPosition={playerPosition} />
      </Physics>
    </Canvas>
  );
}

// Lazy load 3D content of parcel from IPFS
function LandParcel({ contentURI, position }: LandParcelProps) {
  const { scene } = useGLTF(ipfsToHttp(contentURI));
  return <primitive object={scene} position={position} />;
}

Level of Detail (LOD) and Chunked Loading

Metaverse with thousands of parcels can't render entirely. Architecture:

  • Immediate zone (0–50 meters)—full detail, physics active
  • Near zone (50–200 meters)—LOD1 models (50% polygons)
  • Far zone (200–500 meters)—LOD2 (10% polygons), no physics
  • Very far—2D billboard sprites only
  • Beyond—not loaded
const CHUNK_SIZE = 10; // 10x10 parcels per chunk

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));

    // Load 3x3 chunks around player
    const chunksToLoad = [];
    for (let dx = -1; dx <= 1; dx++) {
      for (let dz = -1; dz <= 1; dz++) {
        chunksToLoad.push(`${chunkX + dx},${chunkZ + dz}`);
      }
    }

    // Unload distant chunks to save memory
    const newLoaded = new Set(chunksToLoad);
    setLoadedChunks(newLoaded);
  }, [Math.floor(playerPosition.x / 50), Math.floor(playerPosition.z / 50)]);

  return loadedChunks;
}

Multiplayer: Player Synchronization

Colyseus: State Synchronization

Colyseus—Node.js framework for multiplayer games with real-time state sync:

// Server: Colyseus Room
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; // up to 100 players in one chunk

  onCreate() {
    this.setState(new WorldState());

    // Receive position updates
    this.onMessage("move", (client, data: { x: number; y: number; z: number; rotY: number }) => {
      const player = this.state.players.get(client.sessionId);
      if (!player) return;

      // Basic server validation (not too fast movement)
      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 }) {
    // Verify wallet ownership via signature
    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);
  }
}
// Client: Connect to Colyseus
import Colyseus from "colyseus.js";

const client = new Colyseus.Client("wss://world.example.com");

const room = await client.joinOrCreate("world_room", {
  walletAddress: connectedWalletAddress,
  chunkId: getCurrentChunk(playerPosition),
});

// Get state updates for all players
room.state.players.onAdd((player, sessionId) => {
  addPlayerAvatar(sessionId, player);
  player.onChange(() => updatePlayerPosition(sessionId, player));
});

room.state.players.onRemove((player, sessionId) => {
  removePlayerAvatar(sessionId);
});

// Send own position (throttled to 20 times/sec)
const sendPosition = throttle((position: Vector3) => {
  room.send("move", { x: position.x, y: position.y, z: position.z, rotY: camera.rotation.y });
}, 50);

Decentralized Content Storage

Users upload 3D models for their parcels. This doesn't go on-chain—only CID (Content Identifier).

IPFS + Pinata

import { PinataSDK } from "pinata";

const pinata = new PinataSDK({ pinataJwt: PINATA_JWT });

async function uploadLandContent(
  gltfFile: File,
  textureFiles: File[],
  metadata: LandMetadata
): Promise<string> {
  // Upload 3D model
  const modelUpload = await pinata.upload.file(gltfFile);

  // Upload textures
  const textureUploads = await Promise.all(
    textureFiles.map(f => pinata.upload.file(f))
  );

  // Create metadata JSON with links
  const fullMetadata = {
    ...metadata,
    model: `ipfs://${modelUpload.IpfsHash}`,
    textures: textureUploads.map(u => `ipfs://${u.IpfsHash}`),
  };

  const metadataUpload = await pinata.upload.json(fullMetadata);
  return `ipfs://${metadataUpload.IpfsHash}`;
}

// Convert IPFS URI to HTTP gateway for browser
function ipfsToHttp(uri: string): string {
  if (uri.startsWith("ipfs://")) {
    return `https://gateway.pinata.cloud/ipfs/${uri.slice(7)}`;
  }
  return uri;
}

Arweave—alternative for permanent storage. Pay once for eternal storage. Suitable for valuable assets where continuity critical. Integration via Bundlr/Irys.

Economy and Tokenomics

Dual Token Model

Most metaverses use two tokens:

Governance token (ERC-20)—limited supply, for DAO votes, staking. E.g., MANA in Decentraland, SAND in The Sandbox.

Utility token / Credits—for in-world transactions, purchases, content creation. Can be inflationary with balancing sink mechanics.

contract MetaverseEconomy {
    // Creating content on parcel requires burning utility token
    function buildOnLand(uint256 landTokenId, uint256 buildingType) external {
        uint256 buildCost = buildingCosts[buildingType];
        utilityToken.burnFrom(msg.sender, buildCost); // sink mechanism

        // Verify ownership or rental
        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);
    }

    // Marketplace: P2P trading with royalty
    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];
    }
}

Play-to-Earn Mechanics

Sustainable P2E mechanics should have real utility behind each reward:

  • Content creation—users creating popular parcels get share of traffic/sales
  • Event hosting—hosting events on parcel earns tokens from sponsors
  • Staking land—lock parcel for period to earn yield (reduces circulation, deflationary)

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 sync
Smart contracts Solidity + Foundry Land NFT, Economy
Storage IPFS + Pinata, Arweave 3D content, metadata
Indexing The Graph Map, ownership
Backend Node.js / Go Game logic, API
Database PostgreSQL + Redis Analytics, cache
Network Polygon PoS or Ethereum L2 Cheap transactions

Stages and Realistic Timelines

Phase Content Timeline
Concept and design World design, tokenomics, tech spec 4–6 weeks
Smart contracts Land NFT, Economy, Governance 6–10 weeks
3D engine (MVP) Basic world, movement, LOD 8–12 weeks
Multiplayer Colyseus integration, presence 4–6 weeks
Content system Upload pipeline, IPFS, builder tools 6–8 weeks
Marketplace P2P trading, auctions 4–6 weeks
Smart contract audit 4–8 weeks
Alpha testing 4–6 weeks
Launch 2 weeks

Realistic timeline from zero to alpha: 12–18 months with 6–10 person team. Budget: $500k–$2M. Projects promising "metaverse in 3 months" typically deliver technologically unsound product.

Main mistake in metaverse development—starting with blockchain. Start with game: if virtual world is interesting without NFT—blockchain adds value. If no one enters without NFT—NFT won't fix that.