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

Проектируем и разрабатываем блокчейн-решения полного цикла: от архитектуры смарт-контрактов до запуска DeFi-протоколов, NFT-маркетплейсов и криптобирж. Аудит безопасности, токеномика, интеграция с существующей инфраструктурой.
Показано 1 из 1 услугВсе 1306 услуг
Разработка системы виртуальных земельных участков (virtual land)
Сложная
от 1 недели до 3 месяцев
Часто задаваемые вопросы
Направления блокчейн-разработки
Этапы блокчейн-разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1221
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1163
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    855
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1056
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    561
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    828

Разработка системы виртуальных земельных участков

Виртуальные земельные участки (virtual land) — NFT, представляющие координатное пространство в виртуальном мире. Decentraland, The Sandbox, Otherside — реализовали это на Ethereum. Архитектура не тривиальна: координатная система, соседство участков, эффекты adjacency, система прав и строительства, рендеринг.

Разработка virtual land системы с нуля — это пересечение нескольких технических задач: смарт-контракты (NFT, ownership, permissions), пространственная база данных (coordinate-indexed storage), игровой движок или 3D рендерер, и экономическая модель (scarcity, adjacency bonuses, district mechanics).

Координатная система и хранение

Земля обычно представлена как 2D Grid — целочисленные координаты (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; // какому district принадлежит
    }
    
    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]);
        }
    }
}

Adjacency check 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 система и экономика adjacency

District — административная единица, объединяющая группу участков. Может иметь свой governance, shared revenue из торговли внутри района, общие параметры:

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

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

Adjacency bonus — популярная механика: участки рядом с 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;
    }
}

Content Layer: что строится на участке

Постройки на участке — это 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 — стандарт хранения scene контента. Для больших 3D сцен (десятки MB) — дополнительно Arweave для permanent storage, или кастомный CDN с content hash верификацией.

Marketplace и Royalties

Встроенный marketplace для land торговли с 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 для virtual land. Два подхода:

2D Map (top-down view) — SVG или Canvas рендеринг координатной сетки. Каждый участок — квадрат с цветом по статусу (free/owned/for_sale/developed). Кликабельность, zoom, pan. Реализуется через react-konva или pixi.js. Производительность на 10 000+ участков — требует viewport culling (рендерить только видимые).

3D World View — Three.js или Babylon.js для рендеринга 3D сцен с участков. Загрузка scene контента с IPFS при приближении к участку. Это значительно сложнее — streaming загрузка, LOD (Level of Detail), collision 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 — стандартный choice. Subgraph для virtual land:

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 + adjacency logic
Marketplace Solidity (кастомный или Seaport)
Индексер The Graph / кастомный + PostGIS
2D карта React + Pixi.js / Konva.js
3D рендеринг Three.js / Babylon.js
Content storage IPFS + Pinata / Arweave
Сеть Polygon / Immutable zkEVM

Экономика и scarcity

Размер мира критически важен. Слишком большой — земля дешевеет, нет scarcity. Слишком маленький — барьер входа для новых игроков.

Decentraland: 90 601 участков (301×301 grid). The Sandbox: 166 464 участка. Оба показали: primary sale создаёт ажиотаж, secondary рынок существует, но activity падает без compelling use case.

Ключевой вопрос не в смарт-контрактах, а в retention: зачем игроки посещают мир? Без контента — земля бесполезна. Стратегия: first-party контент от команды (game experiences, concerts, brand activations) как anchor для начального трафика.

Сроки

MVP (Land NFT, базовый marketplace, 2D карта, content upload): 2-3 месяца.

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

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

Аудит контрактов обязателен — land NFT может стоить значительные суммы, ошибки в transfer логике или marketplace — прямой financial risk.