· Nacho Coll · Guides  · 16 分钟阅读

IPFS 用于游戏:在去中心化网络上存储游戏资产

游戏开发者如何使用 IPFS 存储道具、纹理和元数据。包括 Unity 和 Unreal 集成模式。

游戏开发者如何使用 IPFS 存储道具、纹理和元数据。包括 Unity 和 Unreal 集成模式。

现代游戏面临一个关键挑战:资产持久性。当游戏服务器关闭时,可下载内容消失,NFT 元数据变得无法访问,玩家失去了他们投入时间和金钱获得的数字物品。星际文件系统(IPFS)为游戏开发者提供了一个解决方案,确保他们的资产永远可访问,同时降低托管成本并提高全球性能。

IPFS Ninja

游戏开发者越来越多地转向 IPFS 来存储从纹理文件和 3D 模型到玩家生成的内容和 NFT 元数据的所有内容。与需要持续付款且可能一夜之间消失的传统云存储不同,IPFS 创建了一个分布式网络,只要有任何节点固定它们,文件就保持可用。本指南向您展示如何将 IPFS 集成到您的游戏开发工作流中。

为什么游戏资产需要去中心化存储

传统游戏资产存储依赖于发行商控制的中央服务器。当这些公司关闭服务器、改变商业模式或遇到技术故障时,资产会永久无法访问。这造成了几个问题:

资产长寿性:玩家在数字物品上投入大量时间和金钱。当服务器消失时,他们的资产也会消失。IPFS 确保即使原始游戏工作室关闭,物品仍然可访问。

全球性能:IPFS 的分布式网络自然提供类似 CDN 的性能,无需昂贵的基础设施。世界各地的玩家可以从最近的可用节点访问资产。

成本效率:初始上传后,与按月收取带宽和存储费用的传统云托管相比,IPFS 存储成本极低。

真正的所有权:对于区块链游戏和 NFT,IPFS 实现了真正的数字所有权。玩家的物品不依赖于单一公司服务器保持在线。

抗审查:没有单一实体可以删除或修改存储在 IPFS 上的资产,提供了对任意内容下架的保护。

完美适合 IPFS 的游戏资产类型

IPFS 对从永久可用性中受益的不可变游戏内容非常有效:

物品元数据:描述武器属性、角色特征和收藏品属性的 JSON 文件。非常适合元数据必须永久可访问的 NFT 游戏。

纹理文件:3D 模型、UI 元素和环境资产的高分辨率纹理。这些大文件受益于 IPFS 的去重和分布式传送。

3D 模型和动画:需要全球可访问性而无需供应商锁定的角色模型、武器网格和动画文件。

音频资产:增强游戏沉浸感同时降低服务器带宽成本的音效、音乐曲目和语音台词。

玩家生成的内容:受益于永久、去中心化托管的用户创建的关卡、模组和定制。

游戏配置:需要防篡改存储和全球可用性的平衡补丁、物品数据库和游戏规则。

为游戏开发设置 IPFS

在上传游戏资产之前,您需要一个 IPFS 固定服务来确保可靠性。什么是 IPFS 固定解释了固定服务如何在网络中永久保存您的文件。

首先,注册一个 IPFS 固定服务。IPFS Ninja 提供了一个对开发者友好的 API,带有专用网关,非常适合游戏应用。创建账户后,从仪表板生成 API 密钥。

以下是如何上传您的第一个游戏资产:

curl -X POST https://api.ipfs.ninja/upload/new \
  -H "X-Api-Key: bws_1234567890abcdef1234567890abcdef" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "base64_encoded_asset_data",
    "description": "Legendary Sword Texture",
    "metadata": {
      "type": "texture",
      "game": "RPG Adventure",
      "asset_id": "sword_001"
    }
  }'

响应包括您资产的内容标识符(CID)和访问 URL:

{
  "cid": "QmXyZ123...",
  "sizeMB": 2.5,
  "uris": {
    "ipfs": "ipfs://QmXyZ123...",
    "url": "https://ipfs.ninja/ipfs/QmXyZ123..."
  }
}

将 IPFS 与 Unity 集成

Unity 开发者可以使用 HTTP 请求在运行时检索资产来集成 IPFS。这是 Unity 的完整资产加载器:

using System.Collections;
using UnityEngine;
using UnityEngine.Networking;
using Newtonsoft.Json;

public class IPFSAssetLoader : MonoBehaviour
{
    [System.Serializable]
    public class AssetMetadata
    {
        public string name;
        public string description;
        public string image;
        public AssetAttributes attributes;
    }

    [System.Serializable]
    public class AssetAttributes
    {
        public int damage;
        public string rarity;
        public float weight;
    }

    private string gatewayUrl = "https://your-gateway.gw.ipfs.ninja";

    public IEnumerator LoadGameItem(string cid, System.Action<AssetMetadata> onComplete)
    {
        string url = $"{gatewayUrl}/ipfs/{cid}";
        
        using (UnityWebRequest request = UnityWebRequest.Get(url))
        {
            request.SetRequestHeader("Authorization", "Bearer your_gateway_token");
            
            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                AssetMetadata metadata = JsonConvert.DeserializeObject<AssetMetadata>(request.downloadHandler.text);
                onComplete?.Invoke(metadata);
            }
            else
            {
                Debug.LogError($"Failed to load asset: {request.error}");
            }
        }
    }

    public IEnumerator LoadTexture(string cid, System.Action<Texture2D> onComplete)
    {
        string url = $"{gatewayUrl}/ipfs/{cid}";
        
        using (UnityWebRequestTexture request = UnityWebRequestTexture.GetTexture(url))
        {
            request.SetRequestHeader("Authorization", "Bearer your_gateway_token");
            
            yield return request.SendWebRequest();

            if (request.result == UnityWebRequest.Result.Success)
            {
                Texture2D texture = DownloadHandlerTexture.GetContent(request);
                onComplete?.Invoke(texture);
            }
            else
            {
                Debug.LogError($"Failed to load texture: {request.error}");
            }
        }
    }
}

// 使用示例
public class GameItemManager : MonoBehaviour
{
    private IPFSAssetLoader assetLoader;

    void Start()
    {
        assetLoader = GetComponent<IPFSAssetLoader>();
        
        // 加载物品元数据
        StartCoroutine(assetLoader.LoadGameItem("QmItemMetadata123", OnItemLoaded));
        
        // 加载物品纹理
        StartCoroutine(assetLoader.LoadTexture("QmTexture456", OnTextureLoaded));
    }

    private void OnItemLoaded(IPFSAssetLoader.AssetMetadata metadata)
    {
        Debug.Log($"Loaded item: {metadata.name}");
        Debug.Log($"Damage: {metadata.attributes.damage}");
        Debug.Log($"Rarity: {metadata.attributes.rarity}");
    }

    private void OnTextureLoaded(Texture2D texture)
    {
        Debug.Log("Texture loaded successfully");
        // 将纹理应用到游戏对象
        GetComponent<Renderer>().material.mainTexture = texture;
    }
}

对于生产 Unity 游戏,实施缓存以避免重复下载:

public class IPFSCache : MonoBehaviour
{
    private Dictionary<string, Texture2D> textureCache = new Dictionary<string, Texture2D>();
    private Dictionary<string, string> metadataCache = new Dictionary<string, string>();

    public IEnumerator GetCachedTexture(string cid, System.Action<Texture2D> onComplete)
    {
        if (textureCache.ContainsKey(cid))
        {
            onComplete?.Invoke(textureCache[cid]);
            yield break;
        }

        // 从 IPFS 加载并缓存
        yield return StartCoroutine(LoadAndCacheTexture(cid, onComplete));
    }

    private IEnumerator LoadAndCacheTexture(string cid, System.Action<Texture2D> onComplete)
    {
        IPFSAssetLoader loader = GetComponent<IPFSAssetLoader>();
        
        yield return StartCoroutine(loader.LoadTexture(cid, (texture) =>
        {
            textureCache[cid] = texture;
            onComplete?.Invoke(texture);
        }));
    }
}

Unreal Engine 集成

Unreal Engine 开发者可以通过 Blueprint 或 C++ 使用 HTTP 请求来集成 IPFS。这是一个用于加载资产的 C++ 实现:

// IPFSAssetLoader.h
#pragma once

#include "CoreMinimal.h"
#include "Engine/GameInstanceSubsystem.h"
#include "Http.h"
#include "IPFSAssetLoader.generated.h"

USTRUCT(BlueprintType)
struct FAssetMetadata
{
    GENERATED_BODY()

    UPROPERTY(BlueprintReadOnly)
    FString Name;

    UPROPERTY(BlueprintReadOnly)
    FString Description;

    UPROPERTY(BlueprintReadOnly)
    FString ImageCID;

    UPROPERTY(BlueprintReadOnly)
    int32 Damage;

    UPROPERTY(BlueprintReadOnly)
    FString Rarity;
};

DECLARE_DYNAMIC_DELEGATE_OneParam(FOnAssetLoaded, const FAssetMetadata&, Metadata);
DECLARE_DYNAMIC_DELEGATE_OneParam(FOnTextureLoaded, UTexture2D*, Texture);

UCLASS()
class YOURGAME_API UIPFSAssetLoader : public UGameInstanceSubsystem
{
    GENERATED_BODY()

public:
    UFUNCTION(BlueprintCallable, Category = "IPFS")
    void LoadAssetMetadata(const FString& CID, const FOnAssetLoaded& OnComplete);

    UFUNCTION(BlueprintCallable, Category = "IPFS")
    void LoadTexture(const FString& CID, const FOnTextureLoaded& OnComplete);

private:
    void OnMetadataRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bSuccess);
    void OnTextureRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bSuccess);

    FString GatewayUrl = TEXT("https://your-gateway.gw.ipfs.ninja");
    FString AuthToken = TEXT("your_gateway_token");

    FOnAssetLoaded CurrentMetadataCallback;
    FOnTextureLoaded CurrentTextureCallback;
};
// IPFSAssetLoader.cpp
#include "IPFSAssetLoader.h"
#include "HttpModule.h"
#include "Engine/Texture2D.h"
#include "Dom/JsonObject.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonSerializer.h"

void UIPFSAssetLoader::LoadAssetMetadata(const FString& CID, const FOnAssetLoaded& OnComplete)
{
    CurrentMetadataCallback = OnComplete;

    FHttpRequestRef Request = FHttpModule::Get().CreateRequest();
    Request->SetURL(FString::Printf(TEXT("%s/ipfs/%s"), *GatewayUrl, *CID));
    Request->SetVerb(TEXT("GET"));
    Request->SetHeader(TEXT("Authorization"), FString::Printf(TEXT("Bearer %s"), *AuthToken));
    Request->OnProcessRequestComplete().BindUObject(this, &UIPFSAssetLoader::OnMetadataRequestComplete);
    Request->ProcessRequest();
}

void UIPFSAssetLoader::OnMetadataRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bSuccess)
{
    if (bSuccess && Response.IsValid())
    {
        TSharedPtr<FJsonObject> JsonObject;
        TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Response->GetContentAsString());

        if (FJsonSerializer::Deserialize(Reader, JsonObject))
        {
            FAssetMetadata Metadata;
            Metadata.Name = JsonObject->GetStringField(TEXT("name"));
            Metadata.Description = JsonObject->GetStringField(TEXT("description"));
            Metadata.ImageCID = JsonObject->GetStringField(TEXT("image"));

            // 解析属性
            TSharedPtr<FJsonObject> AttributesObj = JsonObject->GetObjectField(TEXT("attributes"));
            if (AttributesObj.IsValid())
            {
                Metadata.Damage = AttributesObj->GetIntegerField(TEXT("damage"));
                Metadata.Rarity = AttributesObj->GetStringField(TEXT("rarity"));
            }

            CurrentMetadataCallback.ExecuteIfBound(Metadata);
        }
    }
}

void UIPFSAssetLoader::LoadTexture(const FString& CID, const FOnTextureLoaded& OnComplete)
{
    CurrentTextureCallback = OnComplete;

    FHttpRequestRef Request = FHttpModule::Get().CreateRequest();
    Request->SetURL(FString::Printf(TEXT("%s/ipfs/%s"), *GatewayUrl, *CID));
    Request->SetVerb(TEXT("GET"));
    Request->SetHeader(TEXT("Authorization"), FString::Printf(TEXT("Bearer %s"), *AuthToken));
    Request->OnProcessRequestComplete().BindUObject(this, &UIPFSAssetLoader::OnTextureRequestComplete);
    Request->ProcessRequest();
}

void UIPFSAssetLoader::OnTextureRequestComplete(FHttpRequestPtr Request, FHttpResponsePtr Response, bool bSuccess)
{
    if (bSuccess && Response.IsValid())
    {
        const TArray<uint8>& ImageData = Response->GetContent();
        
        // 从二进制数据创建纹理
        UTexture2D* Texture = FImageUtils::ImportBufferAsTexture2D(ImageData);
        
        CurrentTextureCallback.ExecuteIfBound(Texture);
    }
}

使用 IPFS 构建游戏后端

游戏服务器可以使用 IPFS API 高效地管理资产。这是一个用于处理玩家物品上传的 Node.js 后端:

const express = require('express');
const multer = require('multer');
const FormData = require('form-data');
const fetch = require('node-fetch');
const app = express();

const IPFS_API_KEY = 'bws_1234567890abcdef1234567890abcdef';
const IPFS_API_URL = 'https://api.ipfs.ninja';

// 为文件上传配置 multer
const storage = multer.memoryStorage();
const upload = multer({ storage: storage });

// 上传玩家生成的内容
app.post('/upload-item', upload.single('asset'), async (req, res) => {
    try {
        const { playerId, itemName, itemType } = req.body;
        const fileBuffer = req.file.buffer;

        // 将文件转换为 base64 以进行 IPFS 上传
        const base64Content = fileBuffer.toString('base64');

        const uploadData = {
            content: base64Content,
            description: `${itemType} created by player ${playerId}`,
            metadata: {
                type: itemType,
                creator: playerId,
                name: itemName,
                timestamp: Date.now()
            }
        };

        const response = await fetch(`${IPFS_API_URL}/upload/new`, {
            method: 'POST',
            headers: {
                'X-Api-Key': IPFS_API_KEY,
                'Content-Type': 'application/json'
            },
            body: JSON.stringify(uploadData)
        });

        const result = await response.json();

        if (response.ok) {
            // 将物品存储在游戏数据库中
            await saveItemToDatabase({
                playerId,
                itemName,
                itemType,
                cid: result.cid,
                ipfsUrl: result.uris.url,
                size: result.sizeMB
            });

            res.json({
                success: true,
                itemId: result.cid,
                url: result.uris.url
            });
        } else {
            res.status(400).json({ error: result.message });
        }
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// 批量上传游戏资产
app.post('/upload-batch', async (req, res) => {
    try {
        const { assets } = req.body; // 资产数据数组
        const uploadPromises = assets.map(async (asset) => {
            const uploadData = {
                content: asset.content, // Base64 编码
                description: asset.description,
                metadata: {
                    type: asset.type,
                    game_version: asset.version,
                    category: asset.category
                }
            };

            const response = await fetch(`${IPFS_API_URL}/upload/new`, {
                method: 'POST',
                headers: {
                    'X-Api-Key': IPFS_API_KEY,
                    'Content-Type': 'application/json'
                },
                body: JSON.stringify(uploadData)
            });

            return response.json();
        });

        const results = await Promise.all(uploadPromises);
        res.json({
            success: true,
            uploads: results
        });
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

// 获取玩家物品
app.get('/player/:playerId/items', async (req, res) => {
    try {
        const { playerId } = req.params;
        const items = await getPlayerItemsFromDatabase(playerId);
        
        res.json({
            success: true,
            items: items.map(item => ({
                id: item.cid,
                name: item.name,
                type: item.type,
                url: item.ipfsUrl,
                created: item.timestamp
            }))
        });
    } catch (error) {
        res.status(500).json({ error: error.message });
    }
});

async function saveItemToDatabase(itemData) {
    // 在此处实施您的数据库逻辑
    console.log('Saving item to database:', itemData);
}

async function getPlayerItemsFromDatabase(playerId) {
    // 在此处实施您的数据库查询
    console.log('Getting items for player:', playerId);
    return []; // 返回玩家物品
}

app.listen(3000, () => {
    console.log('Game backend running on port 3000');
});

为游戏优化网关性能

游戏性能需要快速资产传送。配置专用网关以获得最佳加载时间:

class GameAssetManager {
    constructor() {
        this.gateways = [
            'https://primary.gw.ipfs.ninja',
            'https://backup.gw.ipfs.ninja'
        ];
        this.currentGateway = 0;
        this.cache = new Map();
    }

    async loadAsset(cid, type = 'json') {
        // 先检查缓存
        if (this.cache.has(cid)) {
            return this.cache.get(cid);
        }

        let lastError;
        
        // 尝试每个网关
        for (let i = 0; i < this.gateways.length; i++) {
            try {
                const gateway = this.gateways[this.currentGateway];
                const url = `${gateway}/ipfs/${cid}`;
                
                const response = await fetch(url, {
                    headers: {
                        'Authorization': 'Bearer your_token'
                    },
                    timeout: 5000 // 5 秒超时
                });

                if (response.ok) {
                    let data;
                    if (type === 'json') {
                        data = await response.json();
                    } else if (type === 'blob') {
                        data = await response.blob();
                    } else {
                        data = await response.text();
                    }
                    
                    // 缓存成功结果
                    this.cache.set(cid, data);
                    return data;
                }
            } catch (error) {
                lastError = error;
                this.currentGateway = (this.currentGateway + 1) % this.gateways.length;
            }
        }

        throw new Error(`Failed to load asset ${cid}: ${lastError.message}`);
    }

    async preloadAssets(cids) {
        const promises = cids.map(cid => this.loadAsset(cid));
        return Promise.allSettled(promises);
    }
}

// 在游戏中使用
const assetManager = new GameAssetManager();

// 预加载关卡资产
await assetManager.preloadAssets([
    'QmLevelData123',
    'QmTexture456',
    'QmAudioFile789'
]);

现实世界的游戏示例

几个成功的游戏展示了 IPFS 的潜力:

NFT 游戏:Axie Infinity 和 Gods Unchained 等游戏将卡片元数据存储在 IPFS 上,确保玩家即使在游戏易手或服务器下线时也能保留对其数字资产的访问。

模组社区:Minecraft 和 Garry’s Mod 社区使用 IPFS 来分发用户生成的内容,为流行模组和地图创建永久档案。

独立游戏分发:独立开发者使用 IPFS 来分发游戏补丁和 DLC,无需昂贵的 CDN 成本,使全球分发对小型工作室来说变得可行。

资产市场:游戏平台利用 IPFS 进行用户生成的内容市场,玩家可以信心十足地买卖和交易物品,相信永久可用性。

迁移策略

将现有游戏资产迁移到 IPFS 需要仔细规划:

渐进式迁移:从非关键资产如纹理和音频文件开始。在迁移基本游戏数据之前测试性能和用户体验。

双存储:在过渡期间维护传统和 IPFS 存储。这允许在出现问题时回滚,同时确保资产可用性。

批量处理:使用脚本批量转换和上传现有资产。学习如何将文件上传到 IPFS 了解详细的上传策略。

性能测试:使用您选择的 IPFS 网关测量来自不同全球地区的加载时间。与现有 CDN 性能进行比较以确保没有性能下降。

选择合适的 IPFS 提供商

游戏开发需要可靠的基础设施。在选择 IPFS 固定服务时,请考虑:

网关性能:专用网关确保一致的加载时间。比较 IPFS 固定服务 以找到提供针对游戏优化的基础设施的提供商。

API 功能:寻找提供上传令牌、批量操作和分析的服务。IPFS 上传 API 教程 涵盖了游戏开发者必备的 API 功能。

全球分布:选择具有全球网关分布的提供商以确保国际玩家的低延迟。

可靠性保证:游戏应用程序需要 99.9%+ 的正常运行时间。研究提供商的业绩记录和 SLA 提供。

成本结构:了解您预期的存储和带宽使用的定价模型。一些提供商为游戏工作负载提供更好的价值。

IPFS 代表着游戏资产存储的未来,提供持久性、性能和真正的数字所有权。通过将 IPFS 集成到您的开发工作流中,您可以减少托管成本,同时让玩家相信他们的数字投资将不受业务变化或技术故障的影响而保持可访问。

准备好开始固定了吗? 创建免费账户 — 50 个文件,1 GB 存储,2 GB 带宽/月。无需信用卡。

返回博客