Разработка метавселенной (Metaverse)
Метавселенная — перегруженный термин. Прежде чем проектировать, нужно выбрать конкретную модель: это persistent 3D world с real-time взаимодействием игроков (как Decentraland, The Sandbox), или это social layer поверх разных приложений, или виртуальные офисы для enterprise? Каждая модель — разный технологический стек.
Я опишу архитектуру web3-native persistent world: многопользовательская 3D среда с ownership NFT земли/ассетов, on-chain экономикой и decentralized governance. Это самый технически сложный и самый интересный вариант.
Архитектурные слои метавселенной
1. Блокчейн слой: ownership и экономика
Всё, что имеет ценность, существует on-chain:
- LAND NFT — участки виртуальной земли (ERC-721)
- Avatar NFT — персонажи с attributes
- Wearables NFT — одежда, предметы (ERC-1155 для fungible, ERC-721 для уникальных)
- Governance token — участие в DAO управлении
- In-world currency — ERC-20 токен для экономики
Всё остальное — off-chain (контент земельных участков, 3D ассеты, история взаимодействий).
2. Content layer: что находится на LAND
Владелец LAND деплоит на своём участке контент: 3D сцены (GLTF/GLB файлы), скрипты интерактивности, портали в другие миры. Контент хранится децентрализованно — IPFS или Arweave.
3. Real-time layer: multiplayer движок
Игроки видят друг друга и взаимодействуют в реальном времени — это задача real-time networking, не блокчейна. Game servers (или P2P peer relay) синхронизируют позиции и состояния аватаров.
4. Application layer: dApps и игры на LAND
Владелец земли разворачивает приложения — мини-игры, галереи NFT, виртуальные магазины, концертные площадки. Каждое приложение может иметь собственную on-chain логику.
LAND система: NFT земля и координатная сетка
Координатная система
Классический подход: карта как двумерная сетка координат. Decentraland использует (-150, -150) до (150, 150). The Sandbox — 408×408.
contract LandRegistry is ERC721 {
int16 public constant MIN_X = -100;
int16 public constant MAX_X = 100;
int16 public constant MIN_Y = -100;
int16 public constant MAX_Y = 100;
// Token ID кодирует координаты: id = (x + 100) * 201 + (y + 100)
function coordinatesToId(int16 x, int16 y) public pure returns (uint256) {
require(x >= MIN_X && x <= MAX_X, "X out of range");
require(y >= MIN_Y && y <= MAX_Y, "Y out of range");
return uint256(uint16(x - MIN_X)) * 201 + uint256(uint16(y - MIN_Y));
}
function idToCoordinates(uint256 tokenId) public pure returns (int16 x, int16 y) {
y = int16(int256(tokenId % 201)) + MIN_Y;
x = int16(int256(tokenId / 201)) + MIN_X;
}
// Adjacency check: нужно для Estate (объединение соседних участков)
function isAdjacent(uint256 tokenId1, uint256 tokenId2) public pure returns (bool) {
(int16 x1, int16 y1) = idToCoordinates(tokenId1);
(int16 x2, int16 y2) = idToCoordinates(tokenId2);
int16 dx = x1 - x2;
int16 dy = y1 - y2;
return (dx == 0 && (dy == 1 || dy == -1)) || (dy == 0 && (dx == 1 || dx == -1));
}
}
Estate: объединение соседних LAND
Владелец нескольких соседних участков может объединить их в Estate для постройки больших сцен:
contract EstateRegistry is ERC721 {
struct Estate {
uint256[] landIds; // входящие LAND токены
string name;
string ipfsHash; // metadata
}
mapping(uint256 => Estate) public estates;
function createEstate(
uint256[] calldata landIds,
string calldata name
) external returns (uint256 estateId) {
require(landIds.length >= 2, "Need at least 2 parcels");
// Верифицируем ownership и adjacency
for (uint256 i = 0; i < landIds.length; i++) {
require(landRegistry.ownerOf(landIds[i]) == msg.sender, "Not land owner");
}
require(_isConnectedGraph(landIds), "Parcels not adjacent");
// Переводим LAND в estate контракт (lock)
for (uint256 i = 0; i < landIds.length; i++) {
landRegistry.transferFrom(msg.sender, address(this), landIds[i]);
}
estateId = ++nextEstateId;
estates[estateId] = Estate({ landIds: landIds, name: name, ipfsHash: "" });
_mint(msg.sender, estateId);
emit EstateCreated(estateId, msg.sender, landIds);
}
// BFS проверка что все parcels связаны в один граф
function _isConnectedGraph(uint256[] calldata ids) internal view returns (bool) {
if (ids.length == 1) return true;
bool[] memory visited = new bool[](ids.length);
uint256[] memory queue = new uint256[](ids.length);
uint256 qHead = 0;
uint256 qTail = 0;
visited[0] = true;
queue[qTail++] = ids[0];
while (qHead < qTail) {
uint256 current = queue[qHead++];
for (uint256 i = 0; i < ids.length; i++) {
if (!visited[i] && landRegistry.isAdjacent(current, ids[i])) {
visited[i] = true;
queue[qTail++] = ids[i];
}
}
}
for (uint256 i = 0; i < ids.length; i++) {
if (!visited[i]) return false;
}
return true;
}
}
Content система: что деплоится на LAND
Scene descriptor
Каждый LAND имеет scene — набор 3D объектов, скриптов, порталов. Хранится на IPFS:
// Scene descriptor (IPFS JSON)
interface SceneDescriptor {
version: '2.0';
landId: number;
owner: string; // ethereum address
title: string;
description: string;
// 3D контент
models: Array<{
src: string; // ipfs://Qm... ссылка на GLTF/GLB
position: [number, number, number];
rotation: [number, number, number];
scale: [number, number, number];
}>;
// Скрипты интерактивности
scripts: Array<{
src: string; // ipfs://Qm... JavaScript модуль
entryPoint: string; // имя экспортированной функции
}>;
// Spawn points для аватаров
spawnPoints: Array<{
position: [number, number, number];
cameraTarget: [number, number, number];
}>;
// Ссылки на adjacent land / порталы
portals: Array<{
position: [number, number, number];
targetLandId: number;
targetUrl?: string; // или внешний URL
}>;
}
Публикация сцены:
contract LandContent {
// IPFS hash сцены для каждого LAND
mapping(uint256 => string) public sceneHash;
mapping(uint256 => uint256) public sceneVersion;
function publishScene(uint256 landId, string calldata ipfsHash) external {
require(landRegistry.ownerOf(landId) == msg.sender, "Not owner");
// Базовая валидация: непустой hash
require(bytes(ipfsHash).length == 46, "Invalid IPFS hash"); // Qm... = 46 chars
sceneHash[landId] = ipfsHash;
sceneVersion[landId]++;
emit ScenePublished(landId, msg.sender, ipfsHash, sceneVersion[landId]);
}
}
Scene Scripting SDK
Разработчики пишут скрипты для интерактивности на LAND. Это декентрализованная версия Unity/Unreal scripting:
// Metaverse Scene Script SDK (выполняется в sandbox iframe/WebWorker)
import { engine, Transform, MeshRenderer, OnPointerDown } from '@metaverse/sdk';
// Интерактивная дверь
const door = engine.addEntity();
engine.addComponentOrReplace(door, Transform, {
position: { x: 0, y: 0, z: 5 },
rotation: { x: 0, y: 0, z: 0, w: 1 },
scale: { x: 1, y: 1, z: 1 },
});
let isOpen = false;
engine.addComponentOrReplace(door, OnPointerDown, {
callback: async () => {
isOpen = !isOpen;
// Анимируем открытие/закрытие
const transform = engine.getComponent(door, Transform);
transform.rotation = isOpen
? Quaternion.fromEulerDegrees(0, 90, 0)
: Quaternion.fromEulerDegrees(0, 0, 0);
},
hoverText: isOpen ? 'Close door' : 'Open door',
});
// NFT gate: только владельцы определённого NFT могут войти
import { checkNFTOwnership } from '@metaverse/blockchain';
engine.addComponentOrReplace(nftGate, OnPointerDown, {
callback: async () => {
const userAddress = await engine.getUserAddress();
const hasNFT = await checkNFTOwnership(userAddress, NFT_CONTRACT, TOKEN_ID);
if (!hasNFT) {
engine.showNotification('You need the Golden Pass NFT to enter');
return;
}
engine.teleportPlayer({ x: INSIDE_X, y: 0, z: INSIDE_Z });
},
});
Scripting sandbox — изолированный WebWorker или iframe без прямого доступа к DOM. API работы с блокчейном предоставляется через постсообщения, не через прямой доступ к кошельку.
Real-time networking: синхронизация аватаров
Architecture: area servers
World делится на regions (каждый регион = N×N LAND). Каждый регион обслуживается одним game server. При переходе игрока между регионами — handoff на другой сервер.
┌─────────────────────────────┐
│ Load Balancer / Router │
│ (по координатам игрока) │
└──────────┬──────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Area Server │ │ Area Server │ │ Area Server │
│ Region A │ │ Region B │ │ Region C │
│ (-100,-100) │ │ (0,0) │ │ (100,100) │
│ to (0,0) │ │ to (100,100)│ │ ... │
└──────────────┘ └──────────────┘ └──────────────┘
// Area Server: управление аватарами в регионе
import { WebSocketServer } from 'ws';
import { World } from '@dimforge/rapier3d'; // физический движок
class AreaServer {
private players = new Map<string, PlayerState>();
private physicsWorld = new World({ x: 0, y: -9.81, z: 0 });
handlePlayerJoin(playerId: string, ws: WebSocket, position: Vector3) {
const state: PlayerState = {
id: playerId,
position,
rotation: Quaternion.identity(),
animation: 'idle',
ws,
};
this.players.set(playerId, state);
// Отправляем новому игроку состояния всех в регионе
const snapshot = this.getRegionSnapshot();
ws.send(JSON.stringify({ type: 'region_snapshot', players: snapshot }));
// Уведомляем остальных о новом игроке
this.broadcast({ type: 'player_joined', player: state }, playerId);
}
handleMovement(playerId: string, movement: MovementPacket) {
const player = this.players.get(playerId);
if (!player) return;
// Server-side validation движения (anti-cheat)
if (!this.isValidMovement(player, movement)) {
// Корректируем позицию
player.ws.send(JSON.stringify({
type: 'position_correction',
position: player.position,
}));
return;
}
player.position = movement.position;
player.rotation = movement.rotation;
player.animation = movement.animation;
// Broad-cast всем в регионе (delta compression)
this.broadcastMovement(playerId, movement);
}
// 20 updates/sec для smooth movement
private startTickLoop() {
setInterval(() => this.tick(), 50);
}
private tick() {
this.physicsWorld.step();
// Собираем dirty states и рассылаем батчем
const updates = this.getDirtyPlayerStates();
if (updates.length > 0) {
this.broadcast({ type: 'batch_update', updates });
}
}
}
Proximity-based broadcasting
Не нужно рассылать позицию игрока всем в регионе — только тем, кто рядом. Interest Management:
const VISIBILITY_RADIUS = 100; // метров
function getVisiblePlayers(playerId: string): string[] {
const player = players.get(playerId)!;
return Array.from(players.values())
.filter(p => p.id !== playerId)
.filter(p => distance(p.position, player.position) < VISIBILITY_RADIUS)
.map(p => p.id);
}
Это снижает bandwidth с O(N²) до O(N × K), где K = среднее число видимых игроков.
In-world экономика
Marketplace контракт
Владелец LAND продаёт виртуальные товары внутри своего мира:
contract InWorldMarketplace {
struct Listing {
address seller;
address nftContract;
uint256 tokenId;
uint256 price; // в in-world currency (ERC-20)
uint256 landId; // на каком LAND выставлен товар
bool active;
}
// Royalty для владельца LAND: 2.5% от продаж на его земле
uint256 public constant LAND_ROYALTY = 250; // basis points
function buy(uint256 listingId) external {
Listing storage listing = listings[listingId];
require(listing.active, "Not active");
uint256 landRoyalty = listing.price * LAND_ROYALTY / 10_000;
uint256 sellerProceeds = listing.price - landRoyalty;
// Покупатель платит in-world currency
worldToken.transferFrom(msg.sender, listing.seller, sellerProceeds);
worldToken.transferFrom(msg.sender, landRegistry.ownerOf(listing.landId), landRoyalty);
// Передаём NFT
IERC721(listing.nftContract).transferFrom(
address(this), msg.sender, listing.tokenId
);
listing.active = false;
emit Sale(listingId, msg.sender, listing.price);
}
}
Play-to-earn механики
In-world активности могут генерировать in-world currency:
- Посещение событий на LAND (check-in reward)
- Выполнение квестов, созданных владельцами LAND
- Участие в мини-играх
Важно: emission rate должен быть контролируемым для предотвращения инфляции. Рекомендация: weekly emission cap + halvening механизм по аналогии с Bitcoin.
DAO и Governance
// Governance через Compound-style voting
contract MetaverseDAO is Governor, GovernorTimelockControl {
// Holding LAND даёт voting power
function _getVotes(
address account,
uint256 blockNumber,
bytes memory
) internal view override returns (uint256) {
// 1 LAND = 1 vote + bonus за стейкинг governance token
uint256 landVotes = landRegistry.balanceOf(account); // at blockNumber
uint256 tokenVotes = govToken.getPastVotes(account, blockNumber);
return landVotes + tokenVotes;
}
}
Governance решает: размер карты (новые LAND), параметры экономики, whitelist контентных форматов, upgrade контрактов.
Технический стек
3D рендеринг
Three.js + React Three Fiber — для web-native метавселенной. Хорошая поддержка GLTF, PBR материалы, performance оптимизации через instancing и LOD.
Babylon.js — альтернатива, особенно хорошо с WebXR (VR/AR). Decentraland использует Babylon.js.
Unity WebGL — лучший graphics quality, но большой bundle size (50–200 MB), медленный initial load. Подходит для desktop-ориентированного продукта.
Полный стек
| Слой | Технология |
|---|---|
| Blockchain | Polygon PoS или Arbitrum (низкие gas для LAND transactions) |
| LAND/NFT | Solidity ERC-721 + ERC-1155, Foundry |
| Governance | OpenZeppelin Governor + TimelockController |
| Storage | IPFS (Pinata/Web3.Storage) + Arweave для permanent content |
| Real-time | Node.js + uWebSockets.js (высокая производительность) |
| Physics | Rapier3D (Rust/WASM, быстрее Cannon.js) |
| 3D Web | Three.js + React Three Fiber + Drei |
| Avatar system | ReadyPlayerMe SDK или кастомные VRM avatars |
| State | Redis для region state, PostgreSQL для persistent data |
| Indexing | The Graph (события LAND transfers, scene updates) |
Этапы разработки
| Фаза | Содержание | Срок |
|---|---|---|
| Foundation | LAND контракты, coordinate system, basic marketplace | 4–6 нед |
| Content system | Scene descriptor, IPFS storage, scene publishing | 3–4 нед |
| 3D Client | Three.js world, scene loading, basic navigation | 6–8 нед |
| Real-time | Area servers, avatar sync, proximity system | 6–8 нед |
| Economy | In-world token, marketplace, play-to-earn | 4–6 нед |
| Scripting SDK | Scene scripting sandbox, NFT gate APIs | 4–6 нед |
| Governance | DAO contracts, voting UI | 3–4 нед |
| Audit | LAND, marketplace, economy контракты | 5–8 нед |
| Alpha launch | Private alpha с ограниченной картой | 2–4 нед |
Реалистичный срок от нуля до public alpha: 12–18 месяцев для команды 8–12 человек (2–3 blockchain, 2–3 3D/frontend, 2 backend, 1 PM, 1 designer). Это один из наиболее scope-сложных проектов в Web3.
Главный риск не технический, а продуктовый: без контента на LAND и активного комьюнити мир будет пустым. Параллельно с разработкой нужна программа для early LAND holders и content creators.







