Розробка віртуального світу на блокчейні
Blockchain метавселенна—це не просто «3D гра з NFT». Це постійний віртуальний світ, де право власності на цифрові активи (землі, будівлі, предмети, аватари) криптографічно гарантовано, а економіка керується кодом, а не централізованим оператором. Технічно один з найбільш складних продуктів у Web3: перетинаються real-time 3D рендеринг, мережа мультиплеєра, системи смарт-контрактів, децентралізоване сховище даних та складна токеноміка. Розберемо кожний шар.
Архітектура: шари віртуального світу
┌─────────────────────────────────────────────┐
│ Клієнт (браузер/десктоп) │
│ Three.js / Babylon.js / Unity WebGL │
└─────────────────┬───────────────────────────┘
│ WebSocket / WebRTC
┌─────────────────▼───────────────────────────┐
│ Шар мультиплеєра │
│ Colyseus / Photon / користувацький │
│ Позиції, рух, синхронізація │
└─────────────────┬───────────────────────────┘
│
┌─────────────────▼───────────────────────────┐
│ Шар логіки гри/світу │
│ Node.js / Go сервіси │
│ Управління сценами, завантаження чанків │
└──────────┬──────────────────────┬────────────┘
│ │
┌──────────▼──────────┐ ┌────────▼─────────────┐
│ Смарт-контракти │ │ Децентр. сховище │
│ Власність, економіка│ │ IPFS / Arweave │
│ Управління │ │ 3D активи, метадані│
└─────────────────────┘ └──────────────────────┘
Ключовий принцип: блокчейн—не ігровий рушій. Усе, що вимагає низької затримки (позиції, анімації, рух), залишається off-chain. Блокчейн керує власністю та економікою.
Land NFT: система віртуальних земель
Земля—фундаментальний актив більшості метавселень. The Sandbox, Decentraland, Otherside—всі використовують NFT землю як основний механізм власності.
Система земель на основі координат
// 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 з 3D контентом на ділянці
bool buildable;
}
mapping(uint256 => LandData) public landData;
mapping(bytes32 => uint256) public coordinateToTokenId; // hash(x,y) → tokenId
uint256 private _nextTokenId = 1;
// 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);
}
// Власник встановлює 3D контент на ділянці
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) {
// Для малих чанків (наприклад, 20x20 = 400 ділянок)
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++;
}
}
}
}
Оренда землі (ERC-4907)
ERC-4907 додає роль user до ERC-721: тимчасовий користувач може користуватися NFT до певного часу, але не може його торгувати. Ідеально для оренди землі:
import "@openzeppelin/contracts/token/ERC721/extensions/ERC4907.sol";
contract MetaverseLandWithRent is MetaverseLand, ERC4907 {
// Власник здає ділянку на певний час
function rentLand(
uint256 tokenId,
address tenant,
uint64 expires
) external {
require(ownerOf(tokenId) == msg.sender, "Not owner");
setUser(tokenId, tenant, expires);
}
// Хто може будувати на ділянці: власник або орендатор
function canBuild(uint256 tokenId) external view returns (address) {
address user = userOf(tokenId);
return user != address(0) ? user : ownerOf(tokenId);
}
}
3D рушій та рендеринг
Three.js + React Three Fiber
Для браузерної метавселенної—Three.js через React Three Fiber (R3F) + Rapier для фізики:
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 управління */}
<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>
);
}
// Ліниво завантажуємо 3D контент ділянки з IPFS
function LandParcel({ contentURI, position }: LandParcelProps) {
const { scene } = useGLTF(ipfsToHttp(contentURI));
return <primitive object={scene} position={position} />;
}
Level of Detail (LOD) та завантаження за чанками
Метавселенну з тисячами ділянок не можна рендерити повністю. Архітектура:
- Immediate zone (0–50 метрів)—повна деталізація, фізика активна
- Near zone (50–200 метрів)—LOD1 моделі (50% полігонів)
- Far zone (200–500 метрів)—LOD2 (10% полігонів), без фізики
- Very far—тільки 2D billboard спрайти
- Beyond—не завантажується
const CHUNK_SIZE = 10; // 10x10 ділянок на чанк
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));
// Завантажуємо 3x3 чанки навколо гравця
const chunksToLoad = [];
for (let dx = -1; dx <= 1; dx++) {
for (let dz = -1; dz <= 1; dz++) {
chunksToLoad.push(`${chunkX + dx},${chunkZ + dz}`);
}
}
// Вивантажуємо далекі чанки для економії пам'яті
const newLoaded = new Set(chunksToLoad);
setLoadedChunks(newLoaded);
}, [Math.floor(playerPosition.x / 50), Math.floor(playerPosition.z / 50)]);
return loadedChunks;
}
Мультиплеєр: синхронізація гравців
Colyseus: синхронізація стану
Colyseus—Node.js фреймворк для мультиплеєр ігор із real-time синхронізацією стану:
// Сервер: 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; // до 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);
}
}
// Клієнт: підключитися до 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),
});
// Отримуємо оновлення стану всіх гравців
room.state.players.onAdd((player, sessionId) => {
addPlayerAvatar(sessionId, player);
player.onChange(() => updatePlayerPosition(sessionId, player));
});
room.state.players.onRemove((player, sessionId) => {
removePlayerAvatar(sessionId);
});
// Надсилаємо свою позицію (throttled до 20 разів/сек)
const sendPosition = throttle((position: Vector3) => {
room.send("move", { x: position.x, y: position.y, z: position.z, rotY: camera.rotation.y });
}, 50);
Децентралізоване сховище контенту
Користувачі завантажують 3D моделі для своїх ділянок. Це не йде в блокчейн—тільки 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> {
// Завантажуємо 3D модель
const modelUpload = await pinata.upload.file(gltfFile);
// Завантажуємо текстури
const textureUploads = await Promise.all(
textureFiles.map(f => pinata.upload.file(f))
);
// Створюємо JSON метаданих із посиланнями
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}`;
}
// Конвертуємо IPFS URI до HTTP gateway для браузера
function ipfsToHttp(uri: string): string {
if (uri.startsWith("ipfs://")) {
return `https://gateway.pinata.cloud/ipfs/${uri.slice(7)}`;
}
return uri;
}
Arweave—альтернатива для постійного сховища. Платиш один раз за вічне сховище. Підходить для цінних активів, де безперервність критична. Інтеграція через Bundlr/Irys.
Економіка та токеноміка
Модель з двома токенами
Більшість метавселень використовують два токени:
Токен управління (ERC-20)—обмежений supply, для DAO голосувань, staking. Наприклад, MANA в Decentraland, SAND у The Sandbox.
Utility токен / Credits—для in-world транзакцій, покупок, створення контенту. Може бути інфляційним із механізмами балансування sink.
contract MetaverseEconomy {
// Створення контенту на ділянці вимагає спалювання utility токена
function buildOnLand(uint256 landTokenId, uint256 buildingType) external {
uint256 buildCost = buildingCosts[buildingType];
utilityToken.burnFrom(msg.sender, buildCost); // sink механізм
// Верифікуємо власність або оренду
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 торговля з 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 механіки
Стійкі P2E механіки повинні мати реальний utility за кожною награєю:
- Створення контенту—користувачі, які створюють популярні ділянки, отримують частку трафіку/продаж
- Хостинг подій—проведення подій на ділянці приносить токени від спонсорів
- Staking землі—залочити ділянку на період для yield (зменшує циркуляцію, дефляційний)
Технічний стек та інфраструктура
| Компонент | Технології | Призначення |
|---|---|---|
| 3D рушій | Three.js + R3F, або Babylon.js | Рендеринг світу |
| Фізика | Rapier (Rust/WASM) | Колізії, фізика |
| Мультиплеєр | Colyseus або Nakama | Синхронізація гравців |
| Смарт-контракти | Solidity + Foundry | Land NFT, Economy |
| Сховище | IPFS + Pinata, Arweave | 3D контент, метаданні |
| Індексування | The Graph | Карта, власність |
| Бекенд | Node.js / Go | Логіка гри, API |
| БД | PostgreSQL + Redis | Аналітика, кеш |
| Мережа | Polygon PoS або Ethereum L2 | Дешеві транзакції |
Етапи та реалістичні сроки
| Фаза | Вміст | Сроки |
|---|---|---|
| Концепція та дизайн | Дизайн світу, токеноміка, tech spec | 4–6 тиж |
| Смарт-контракти | Land NFT, Economy, Governance | 6–10 тиж |
| 3D рушій (MVP) | Базовий світ, рух, LOD | 8–12 тиж |
| Мультиплеєр | Colyseus інтеграція, presence | 4–6 тиж |
| Content system | Upload pipeline, IPFS, builder tools | 6–8 тиж |
| Marketplace | P2P торговля, аукціони | 4–6 тиж |
| Аудит смарт-контрактів | — | 4–8 тиж |
| Альфа-тестування | — | 4–6 тиж |
| Launch | — | 2 тиж |
Реалістичні сроки від нуля до альфи: 12–18 місяців із командою 6–10 людей. Бюджет: $500k–$2M. Проекти, що обіцяють «метавселенну за 3 місяці», зазвичай доставляють технологічно неспроможний продукт.
Головна помилка при розробці метавселень—почнеяти з блокчейну. Почніть з гри: якщо віртуальний світ цікавий без NFT—блокчейн додає цінність. Якщо без NFT ніхто не входить—NFT це не виправить.







