Розробка системи віртуальних земельних ділянок (virtual land)

Проєктуємо та розробляємо блокчейн-рішення повного циклу: від архітектури смарт-контрактів до запуску DeFi-протоколів, NFT-маркетплейсів та криптобірж. Аудит безпеки, токеноміка, інтеграція з наявною інфраструктурою.
Показано 1 з 1Усі 1306 послуг
Розробка системи віртуальних земельних ділянок (virtual land)
Складний
від 1 тижня до 3 місяців
Часті запитання

Напрямки блокчейн-розробки

Етапи блокчейн-розробки

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1285
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1198
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    902
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1120
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    588
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    854

Розробка системи віртуальних земельних ділянок

Віртуальні земельні ділянки—NFT, що представляють координатний простір у віртуальному світі. Decentraland, The Sandbox, Otherside реалізували це на Ethereum. Архітектура не тривіальна: координатна система, сусідство ділянок, ефекти сусідства, система прав та побудови, рендеринг.

Розробка системи віртуальних земельних ділянок з нуля—це перетин кількох технічних завдань: смарт-контракти (NFT, власність, дозволи), просторова база даних (сховище індексоване координатами), ігровий рушій або 3D рендерер, та економічна модель (дефіцит, бонуси сусідства, механіка районів).

Координатна система та зберігання

Земля зазвичай представлена як 2D сітка—цілочислові координати (x, y). Кожна ділянка—унікальний NFT з координатами як ключовий атрибут.

contract VirtualLand is ERC721 {
    // Упаковані координати як tokenId: x (int16) + y (int16) → uint32
    // Це обмежує світ: x від -32768 до 32767, y аналогічно
    
    struct LandInfo {
        int16 x;
        int16 y;
        address owner;
        bool developed; // є ли побудови
        uint32 districtId; // якому району належить
    }
    
    mapping(uint256 => LandInfo) public lands;
    mapping(bytes32 => uint256) public coordsToTokenId; // hash(x,y) → tokenId
    
    function _coordsToId(int16 x, int16 y) internal pure returns (uint256) {
        return uint256(uint32(uint16(int16(x))) | (uint32(uint16(int16(y))) << 16));
    }
    
    function _hashCoords(int16 x, int16 y) internal pure returns (bytes32) {
        return keccak256(abi.encodePacked(x, y));
    }
    
    function mint(int16 x, int16 y, address to) external onlyMinter {
        bytes32 coordHash = _hashCoords(x, y);
        require(coordsToTokenId[coordHash] == 0, "Land already exists");
        
        uint256 tokenId = _coordsToId(x, y);
        _safeMint(to, tokenId);
        
        lands[tokenId] = LandInfo({
            x: x,
            y: y,
            owner: to,
            developed: false,
            districtId: _getDistrictId(x, y)
        });
        coordsToTokenId[coordHash] = tokenId;
    }
    
    function getLandAtCoords(int16 x, int16 y) external view returns (LandInfo memory) {
        uint256 tokenId = coordsToTokenId[_hashCoords(x, y)];
        require(tokenId != 0, "No land at these coords");
        return lands[tokenId];
    }
}

Estate: об'єднання ділянок

Estate (маєток)—кілька суміжних ділянок об'єднаних в одного NFT. Спрощує управління великим суміжним простором. Реалізується як окремий контракт:

contract Estate is ERC721 {
    mapping(uint256 => uint256[]) public estateLands; // estateId → список tokenId ділянок
    mapping(uint256 => uint256) public landToEstate;  // landTokenId → estateId
    
    function createEstate(uint256[] calldata landTokenIds, string calldata name) external {
        // Перевіримо що всі ділянки належать caller
        // Перевіримо що ділянки суміжні (adjacency check)
        for (uint i = 0; i < landTokenIds.length; i++) {
            require(landContract.ownerOf(landTokenIds[i]) == msg.sender, "Not owner");
            require(!landToEstate[landTokenIds[i]], "Land in estate");
        }
        
        require(_areAdjacent(landTokenIds), "Lands not adjacent");
        
        uint256 estateId = _nextId++;
        _safeMint(msg.sender, estateId);
        estateLands[estateId] = landTokenIds;
        
        for (uint i = 0; i < landTokenIds.length; i++) {
            landToEstate[landTokenIds[i]] = estateId;
            landContract.transferFrom(msg.sender, address(this), landTokenIds[i]);
        }
    }
}

On-chain перевірка сусідства

Перевірка що набір ділянок утворює зв'язний граф—дорогостояча on-chain операція при великій кількості ділянок. Оптимізація: перевірити що кожна ділянка має принаймні одного сусіда в наборі (не повна зв'язність, достатньо для estate):

function _areAdjacent(uint256[] calldata tokenIds) internal view returns (bool) {
    for (uint i = 1; i < tokenIds.length; i++) {
        LandInfo memory land = landContract.lands[tokenIds[i]];
        bool hasNeighbor = false;
        
        for (uint j = 0; j < i; j++) {
            LandInfo memory other = landContract.lands[tokenIds[j]];
            int16 dx = land.x - other.x;
            int16 dy = land.y - other.y;
            
            if ((dx == 0 && (dy == 1 || dy == -1)) ||
                (dy == 0 && (dx == 1 || dx == -1))) {
                hasNeighbor = true;
                break;
            }
        }
        if (!hasNeighbor) return false;
    }
    return true;
}

O(n²)—для estates до 20-30 ділянок прийнятно. Для більших estates—off-chain перевірка + Merkle proof або ZK proof.

Система прав та операторів

Власник ділянки контролює хто може будувати на землі. Система прав:

// Bitmap прав: bit 0 = BUILD, bit 1 = SCRIPT, bit 2 = VOICE, bit 3 = ADMIN
uint8 public constant RIGHT_BUILD  = 1 << 0;
uint8 public constant RIGHT_SCRIPT = 1 << 1;
uint8 public constant RIGHT_ADMIN  = 1 << 3;

mapping(uint256 => mapping(address => uint8)) public landOperators;
// landId → operator → bitmap прав

function grantRights(uint256 landId, address operator, uint8 rights) external {
    require(ownerOf(landId) == msg.sender, "Not owner");
    landOperators[landId][operator] |= rights;
    emit OperatorRightsGranted(landId, operator, rights);
}

function revokeRights(uint256 landId, address operator, uint8 rights) external {
    require(ownerOf(landId) == msg.sender, "Not owner");
    landOperators[landId][operator] &= ~rights;
}

function hasRight(uint256 landId, address operator, uint8 right) public view returns (bool) {
    return ownerOf(landId) == operator || 
           (landOperators[landId][operator] & right) != 0;
}

Система районів та економіка сусідства

District—адміністративна одиниця, що групує ділянки. Може мати власне управління, спільний дохід від торговлі в межах району, загальні параметри:

struct District {
    string name;
    uint32 id;
    int16 minX; int16 maxX;
    int16 minY; int16 maxY;
    address council;   // multisig управління районом
    uint256 floorPrice; // мінімальна ціна для листинга в районі
}

function _getDistrictId(int16 x, int16 y) internal view returns (uint32) {
    // Ітерувати по районам та знайти той, що містить координати
    // Для ефективності: райони в R-tree off-chain,
    // on-chain тільки хеш конфігурації
    return districtMap[_quantizeToSector(x, y)];
}

Бонус сусідства—популярна механіка: ділянки біля landmarks (центр міста, plaza) дорожче. Реалізується через маппінг координат landmark та розрахунок відстані:

mapping(bytes32 => uint8) public landmarkTier; // hash(x,y) → tier (0 = немає, 1-3 = важливість)

function getAdjacencyBonus(int16 x, int16 y) external view returns (uint8 maxBonus) {
    int16[4] memory dx = [int16(-1), 1, 0, 0];
    int16[4] memory dy = [int16(0), 0, -1, 1];
    
    for (uint8 i = 0; i < 4; i++) {
        uint8 tier = landmarkTier[_hashCoords(x + dx[i], y + dy[i])];
        if (tier > maxBonus) maxBonus = tier;
    }
}

Шар контенту: що будується на ділянці

Побудови на ділянці—off-chain дані (3D моделі, скрипти), посилання записуються on-chain:

struct LandContent {
    string contentHash;    // IPFS CID або хеш контенту
    string contentType;    // "scene", "model", "script"
    uint256 version;       // для версіонування
    address contentOwner;  // хто завантажив (може відрізнятись від owner)
}

mapping(uint256 => LandContent) public landContent;

function setContent(
    uint256 landId,
    string calldata contentHash,
    string calldata contentType
) external {
    require(
        hasRight(landId, msg.sender, RIGHT_BUILD),
        "No build rights"
    );
    
    landContent[landId] = LandContent({
        contentHash: contentHash,
        contentType: contentType,
        version: landContent[landId].version + 1,
        contentOwner: msg.sender
    });
    
    emit ContentUpdated(landId, contentHash, msg.sender);
}

IPFS—стандарт для зберігання контенту сцени. Для великих 3D сцен (десятки MB)—додатково Arweave для постійного сховища, або користувацький CDN з верифікацією хеша контенту.

Marketplace та Royalties

Вбудований marketplace для торговлі землею з EIP-2981 royalties:

struct Listing {
    uint256 tokenId;
    uint256 price;
    address seller;
    uint256 expiresAt;
}

mapping(uint256 => Listing) public listings;

function list(uint256 tokenId, uint256 price, uint256 duration) external {
    require(ownerOf(tokenId) == msg.sender);
    require(getApproved(tokenId) == address(this) || isApprovedForAll(msg.sender, address(this)));
    
    listings[tokenId] = Listing({
        tokenId: tokenId,
        price: price,
        seller: msg.sender,
        expiresAt: block.timestamp + duration
    });
}

function buy(uint256 tokenId) external payable {
    Listing memory listing = listings[tokenId];
    require(block.timestamp <= listing.expiresAt, "Listing expired");
    require(msg.value >= listing.price, "Insufficient payment");
    
    delete listings[tokenId];
    
    // EIP-2981 royalty
    (address royaltyReceiver, uint256 royaltyAmount) = royaltyInfo(tokenId, listing.price);
    
    uint256 sellerProceeds = listing.price - royaltyAmount - (listing.price * PLATFORM_FEE / 10000);
    
    payable(royaltyReceiver).transfer(royaltyAmount);
    payable(PLATFORM_TREASURY).transfer(listing.price * PLATFORM_FEE / 10000);
    payable(listing.seller).transfer(sellerProceeds);
    
    if (msg.value > listing.price) {
        payable(msg.sender).transfer(msg.value - listing.price);
    }
    
    _safeTransfer(listing.seller, msg.sender, tokenId, "");
}

3D рендеринг та карта світу

Інтерактивна карта—core UI для віртуальної землі. Два підходи:

2D карта (вид зверху)—SVG або Canvas рендеринг координатної сітки. Кожна ділянка—квадрат, розфарбований за статусом (вільна/належить/на продаж/розроблена). Кліцкабельна, zoom, pan. Реалізується через react-konva або pixi.js. Продуктивність на 10,000+ ділянках—потребує viewport culling (рендерити тільки видимі).

3D World View—Three.js або Babylon.js для рендеринга 3D сцен з ділянок. Завантаження контенту сцени з IPFS при наближенні до ділянки. Значно складніше—streaming завантаження, LOD, detection колізій.

// Приклад viewport culling для 2D карти
function getVisibleLands(
  viewport: { x: number; y: number; width: number; height: number },
  tileSize: number
): [number, number][] {
  const startX = Math.floor(viewport.x / tileSize)
  const startY = Math.floor(viewport.y / tileSize)
  const endX = Math.ceil((viewport.x + viewport.width) / tileSize)
  const endY = Math.ceil((viewport.y + viewport.height) / tileSize)
  
  const visible: [number, number][] = []
  for (let x = startX; x <= endX; x++) {
    for (let y = startY; y <= endY; y++) {
      visible.push([x, y])
    }
  }
  return visible
}

Індексування: The Graph або користувацький індексер

On-chain дані (хто володіє якою ділянкою, історія транзакцій) неефективно читати напрямую. Потрібен індексер:

The Graph—стандартний вибір. Subgraph для віртуальної землі:

type Land @entity {
  id: ID!
  x: Int!
  y: Int!
  owner: Bytes!
  districtId: Int
  content: LandContent
  listings: [Listing!]! @derivedFrom(field: "land")
  transactions: [Transfer!]! @derivedFrom(field: "land")
}

type Transfer @entity {
  id: ID!
  land: Land!
  from: Bytes!
  to: Bytes!
  price: BigInt
  timestamp: BigInt!
}

Користувацький індексер (Node.js + PostgreSQL + PostGIS)—якщо потрібні геопросторові запити. PostGIS підтримує spatial indexes—пошук всіх ділянок у регіоні за O(log n).

Стек та інфраструктура

Компонент Технологія
Land NFT контракт ERC-721 + Solidity
Estate контракт ERC-721 + логіка сусідства
Marketplace Solidity (користувацький або Seaport)
Індексер The Graph / користувацький + PostGIS
2D карта React + Pixi.js / Konva.js
3D рендеринг Three.js / Babylon.js
Зберігання контенту IPFS + Pinata / Arweave
Мережа Polygon / Immutable zkEVM

Економіка та дефіцит

Розмір світу критично важливий. Занадто великий—земля дешева, немає дефіциту. Занадто маленький—бар'єр входу для нових гравців.

Decentraland: 90,601 ділянка (301×301 сітка). The Sandbox: 166,464 ділянки. Обидва показали: первинний продаж створює ажіотаж, вторинний ринок існує, але активність падає без переконливого use case.

Ключове питання не в смарт-контрактах, а в retention: чому гравці входять у світ? Без контенту—земля марна. Стратегія: контент першої партії від команди (ігрові досвіди, концерти, активації брендів) як якір для початкового трафіку.

Сроки

MVP (Land NFT, базовий marketplace, 2D карта, завантаження контенту): 2-3 місяці.

Повна система з Estate, Districts, 3D рендерингом, системою прав, The Graph індексером, механіками сусідства: 5-7 місяців.

3D world engine з підтримкою користувацького контенту, streaming завантаженням сцен, фізикою—окремий проект, 6-12 місяців. Це рівень Decentraland SDK—велика інженерна робота.

Аудит контрактів обов'язковий—land NFT може коштувати значні суми, помилки в логіці transfer або marketplace—прямий фінансовий ризик.