Voltar ao Blog

Como Desenvolver uma Integração Segura de Banco de Dados para Sistemas de Inventário de Jogos para Itens Inspecionados

Publicado em 25 de junho de 2026
Como Desenvolver uma Integração Segura de Banco de Dados para Sistemas de Inventário de Jogos para Itens Inspecionados

Em resumo

Este artigo aborda como criar uma integração segura de banco de dados para sistemas de inventário de jogos, focando na transição de itens do estado de inspeção para o inventário de forma server-authoritative. Ele detalha o ciclo de vida desse processo na Unreal Engine usando C++ para evitar trapaças como a duplicação de itens via injeção de pacotes. Além disso, compara modelagens relacionais (PostgreSQL) e NoSQL (MongoDB) e discute como simplificar a persistência de dados em tempo real utilizando a plataforma horizOn.

Seus jogadores vão explorar qualquer brecha para duplicar itens raros, e as transições de estado client-authoritative são o seu alvo favorito. Em jogos de terror, aventura e RPGs, a ação de inspecionar um objeto — como pegar uma chave, girá-la em 3D para encontrar uma pista e adicioná-la ao inventário do jogador — é uma mecânica clássica. Se o client do seu jogo ditar diretamente quando um item é adicionado ao inventário sem verificação do servidor, uma ferramenta simples de injeção de pacotes como o Cheat Engine ou Fiddler pode enganar o client para enviar sinais de "adicionar item" para itens que o jogador nem sequer viu. Para evitar isso, os desenvolvedores devem implementar uma integração de banco de dados robusta para o sistema de inventário do jogo que conecte as interações client-side a uma lógica server-authoritative e persistência segura na nuvem.

O Ciclo de Vida de Inspeção para o Inventário

Para construir uma sincronização de inventário segura, devemos primeiro detalhar o ciclo de vida de inspeção de itens. Essa sequência coordena os actors do mundo físico, estados de inspeção client-side localizados, componentes de inventário server-authoritative e o armazenamento do banco de dados.

  1. Detecção de Interação: O jogador se aproxima de um actor físico (AInspectableActor) no mundo do jogo. Um line trace ou volume de colisão marca o objeto como interativo.
  2. Transição para o Modo de Inspeção: O jogador pressiona a tecla de interação. O client entra em um estado de inspeção localizado, bloqueando o movimento do personagem, rotacionando o objeto em um container dedicado de coordenadas screen-space e renderizando um overlay de descrição 2D.
  3. Estágio de Verificação: O jogador clica em "Pegar". Em vez de o client adicionar o item ao inventário, ele envia uma solicitação de token de interação para o servidor.
  4. Validação do Servidor: O servidor confirma se o jogador está dentro do raio de interação física (~250 unidades Unreal) do actor e se o actor está ativo.
  5. Integração com o Banco de Dados: O servidor adiciona o item ao array do inventário, grava a atualização no banco de dados persistente e transmite um evento de destruição para o actor no mundo.

Se você estiver desenvolvendo um título Multiplayer, gerenciar esse estado no servidor é crítico. Muitas equipes enfrentam pesadelos de inventário Multiplayer com proprietários de ActorComponent trocados na Unreal Engine quando não configuram corretamente a replicação e a autoridade de rede em componentes de inventário personalizados.

Escrevendo a Lógica de Inspeção em C++ na Unreal Engine

Para implementar esse fluxo, criaremos três componentes: uma interface (IInspectableInterface), um actor inspecionável (AInspectableActor) e um componente de inventário de jogador replicado (UInventoryComponent).

Aqui está o arquivo de cabeçalho da interface que descreve como os actors recebem comandos de inspeção:

// InspectableInterface.h
#pragma once

#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "InspectableInterface.generated.h"

UINTERFACE(MinimalAPI)
class UInspectableInterface : public UInterface
{
	GENERATED_BODY()
};

class HORIZON_GAME_API IInspectableInterface
{
	GENERATED_BODY()

public:
	virtual void OnInspectStarted(APlayerController* InspectingPlayer) = 0;
	virtual void OnInspectCompleted(APlayerController* InspectingPlayer, bool bWantsToTake) = 0;
};

Em seguida, vamos implementar a classe AInspectableActor. Essa classe manipula o objeto físico no mundo, armazenando seu identificador exclusivo, alcance máximo de interação e estado.

// InspectableActor.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "InspectableInterface.h"
#include "InspectableActor.generated.h"

UCLASS()
class HORIZON_GAME_API AInspectableActor : public AActor, public IInspectableInterface
{
	GENERATED_BODY()

public:
	AInspectableActor();

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inspection")
	FName ItemID;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inspection")
	FString ItemDisplayName;

	UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Inspection")
	float MaxInteractionDistance;

	virtual void OnInspectStarted(APlayerController* InspectingPlayer) override;
	virtual void OnInspectCompleted(APlayerController* InspectingPlayer, bool bWantsToTake) override;

protected:
	virtual void BeginPlay() override;

private:
	bool bIsBeingInspected;
	TWeakObjectPtr<APlayerController> CurrentInspectingPlayer;
};

Aqui está o arquivo de implementação do actor inspecionável:

// InspectableActor.cpp
#include "InspectableActor.h"
#include "GameFramework/PlayerController.h"

AInspectableActor::AInspectableActor()
{
	PrimaryActorTick.bCanEverTick = false;
	bIsBeingInspected = false;
	MaxInteractionDistance = 250.0f;
	ItemID = "item_default";
	ItemDisplayName = "Generic Item";
}

void AInspectableActor::BeginPlay()
{
	Super::BeginPlay();
}

void AInspectableActor::OnInspectStarted(APlayerController* InspectingPlayer)
{
	if (!InspectingPlayer || bIsBeingInspected) return;

	APawn* PlayerPawn = InspectingPlayer->GetPawn();
	if (!PlayerPawn) return;

	float Distance = FVector::Dist(PlayerPawn->GetActorLocation(), GetActorLocation());
	if (Distance > MaxInteractionDistance)
	{
		UE_LOG(LogTemp, Warning, TEXT("Player too far to inspect %s"), *GetName());
		return;
	}

	bIsBeingInspected = true;
	CurrentInspectingPlayer = InspectingPlayer;
}

void AInspectableActor::OnInspectCompleted(APlayerController* InspectingPlayer, bool bWantsToTake)
{
	if (InspectingPlayer != CurrentInspectingPlayer.Get()) return;

	if (bWantsToTake && HasAuthority())
	{
		UActorComponent* InvComp = InspectingPlayer->GetComponentByClass(UInventoryComponent::StaticClass());
		if (InvComp)
		{
			UInventoryComponent* Inventory = Cast<UInventoryComponent>(InvComp);
			if (Inventory)
			{
				Inventory->Server_TryAddInspectedItem(this);
			}
		}
	}
	
	bIsBeingInspected = false;
	CurrentInspectingPlayer.Reset();
}

Agora, vamos criar o UInventoryComponent, que gerencia a lista de inventário do jogador e replica esses dados pela rede para os jogadores clients.

// InventoryComponent.h
#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "InventoryComponent.generated.h"

class AInspectableActor;

USTRUCT(BlueprintType)
struct FInventoryItem
{
	GENERATED_BODY()

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory")
	FName ItemID;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory")
	int32 Quantity;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Inventory")
	FString InspectedTimestamp;
};

UCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))
class HORIZON_GAME_API UInventoryComponent : public UActorComponent
{
	GENERATED_BODY()

public:
	UInventoryComponent();

	UFUNCTION(Server, Reliable, WithValidation)
	void Server_TryAddInspectedItem(AInspectableActor* TargetActor);

	bool AddItemToLocalState(FName InItemID, int32 Quantity);
	void SaveInventoryToDatabase();

protected:
	UPROPERTY(Replicated, BlueprintReadOnly, Category = "Inventory")
	TArray<FInventoryItem> Items;

	virtual void GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const override;
};

E o arquivo de implementação onde definimos a lógica de replicação RPC e a validação de escrita:

// InventoryComponent.cpp
#include "InventoryComponent.cpp"
#include "InspectableActor.h"
#include "Net/UnrealNetwork.h"

UInventoryComponent::UInventoryComponent()
{
	SetIsReplicatedByDefault(true);
}

void UInventoryComponent::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME(UInventoryComponent, Items);
}

bool UInventoryComponent::Server_TryAddInspectedItem_Validate(AInspectableActor* TargetActor)
{
	if (!TargetActor) return false;

	APawn* OwnerPawn = Cast<APawn>(GetOwner());
	if (!OwnerPawn) return false;

	// Server-side distance check
	float Distance = FVector::Dist(OwnerPawn->GetActorLocation(), TargetActor->GetActorLocation());
	return Distance <= TargetActor->MaxInteractionDistance;
}

void UInventoryComponent::Server_TryAddInspectedItem_Implementation(AInspectableActor* TargetActor)
{
	if (!TargetActor) return;

	AddItemToLocalState(TargetActor->ItemID, 1);
	SaveInventoryToDatabase();

	TargetActor->Destroy();
}

bool UInventoryComponent::AddItemToLocalState(FName InItemID, int32 Quantity)
{
	for (FInventoryItem& Item : Items)
	{
		if (Item.ItemID == InItemID)
		{
			Item.Quantity += Quantity;
			return true;
		}
	}

	FInventoryItem NewItem;
	NewItem.ItemID = InItemID;
	NewItem.Quantity = Quantity;
	NewItem.InspectedTimestamp = FDateTime::UtcNow().ToString();
	Items.Add(NewItem);
	return true;
}

void UInventoryComponent::SaveInventoryToDatabase()
{
	// Database integration trigger goes here
}

Projetando o Esquema do Banco de Dados de Inventário

Assim que o servidor validar que o jogador de fato inspecionou o item, essa mudança de estado deve ser persistida. A escolha entre um banco de dados relacional (SQL) ou orientado a documentos (NoSQL) altera a forma como você estrutura seus esquemas de banco de dados.

Métrica de Avaliação Relacional (PostgreSQL) Document Store (NoSQL / MongoDB)
Estrutura de Dados Tabelas normalizadas, chaves estrangeiras estritas Documentos e arrays chave-valor aninhados
Segurança de Transações Total ACID-compliance out of the box Operações atômicas limitadas a documentos únicos
Complexidade de Queries Alta (Requer queries SQL JOIN para buscas de itens) Baixa (Lookup direto do perfil do jogador)
Escalabilidade Vertical (Requer sharding manual para escalar) Horizontal (Recursos nativos de sharding)

Esquema Relacional (PostgreSQL)

Em um banco de dados relacional, você deseja desacoplar jogadores, metadados globais de itens e listas de inventário ativas para evitar redundância de dados. Isso requer três tabelas:

CREATE TABLE players (
    player_id VARCHAR(64) PRIMARY KEY,
    username VARCHAR(100) NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE game_items (
    item_id VARCHAR(64) PRIMARY KEY,
    display_name VARCHAR(100) NOT NULL,
    item_type VARCHAR(50) DEFAULT 'QuestItem'
);

CREATE TABLE inventory_items (
    id SERIAL PRIMARY KEY,
    player_id VARCHAR(64) REFERENCES players(player_id) ON DELETE CASCADE,
    item_id VARCHAR(64) REFERENCES game_items(item_id),
    quantity INTEGER CHECK (quantity > 0),
    inspected_timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    UNIQUE(player_id, item_id)
);

Esquema de Documento (NoSQL)

Em um banco de dados NoSQL, você armazena o inventário do jogador como um array aninhado dentro do documento principal do jogador. Isso permite que seu Backend recupere todo o estado do jogador em uma única query:

{
  "_id": "usr_90a82b3d11ef",
  "username": "SurvivorX",
  "inventory": [
    {
      "item_id": "key_rusted_01",
      "quantity": 1,
      "inspected_timestamp": "2026-06-25T00:02:20Z"
    },
    {
      "item_id": "document_diary_03",
      "quantity": 1,
      "inspected_timestamp": "2026-06-25T00:05:12Z"
    }
  ],
  "last_updated": "2026-06-25T00:05:12Z"
}

O Dilema de Sincronização: Gravações Client-Side vs Server-Authoritative

A sincronização desse estado altera a forma como os jogadores vivenciam o jogo e o quão robusto ele é contra tentativas de trapaça. Se o código client-side salvar diretamente no banco de dados, um jogador poderá modificar endereços de memória para gravar valores arbitrários no banco de dados.

Para evitar isso, você deve implementar validações server-authoritative:

  1. Tokens de Interação: Quando um jogador começa a inspecionar um item, o servidor gera um token de interação efêmero (válido por 60 segundos) e o registra na instância ativa do actor no mundo.
  2. Proteção contra Double-Spend: Quando o jogador pega o item, o servidor consome o token. Se um pacote de coleta duplicado for recebido, o servidor o rejeita.
  3. Replicação Síncrona: Garanta que as alterações de inventário sejam enviadas imediatamente para as telas dos clients.

Além disso, para enviar atualizações visuais para as UIs de inventário em clients de jogos conectados em tempo real, depender de mecanismos pesados de polling vai sobrecarregar o tick rate do seu servidor. Em vez disso, você deve abandonar o polling HTTP e implementar uma conexão Websockets dedicada para sincronizações de inventário em tempo real.

Implementando o Código de Verificação do Backend

Se você optar por escrever seu próprio servidor customizado, precisará de um endpoint de API que trate as atualizações do banco de dados com segurança. Abaixo está um snippet de Backend Node.js Express usando PostgreSQL. Este script gerencia connection pooling, isolamento de transações e consumo de tokens para gravar a coleta de itens:

const express = require('express');
const { Pool } = require('pg');
const app = express();
app.use(express.json());

const dbPool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

app.post('/api/v1/inventory/add', async (req, res) => {
  const { playerId, itemId, interactionToken } = req.body;
  
  if (!playerId || !itemId || !interactionToken) {
    return res.status(400).json({ error: 'Missing required parameters' });
  }
  
  try {
    // 1. Verify interaction token exists and is active
    const tokenResult = await dbPool.query(
      'SELECT status FROM interactions WHERE token = $1 AND player_id = $2',
      [interactionToken, playerId]
    );
    
    if (tokenResult.rows.length === 0 || tokenResult.rows[0].status !== 'active') {
      return res.status(400).json({ error: 'Invalid or expired interaction token' });
    }

    // 2. Begin transaction
    await dbPool.query('BEGIN');
    
    // Consume the token to prevent double-spending
    await dbPool.query(
      "UPDATE interactions SET status = 'consumed' WHERE token = $1",
      [interactionToken]
    );

    // Insert or increment inventory record
    await dbPool.query(
      `INSERT INTO inventory_items (player_id, item_id, quantity, inspected_timestamp) 
       VALUES ($1, $2, 1, NOW()) 
       ON CONFLICT (player_id, item_id) 
       DO UPDATE SET quantity = inventory_items.quantity + 1`,
      [playerId, itemId]
    );

    await dbPool.query('COMMIT');
    return res.status(200).json({ success: true });
  } catch (error) {
    await dbPool.query('ROLLBACK');
    console.error('Database transaction error:', error);
    return res.status(500).json({ error: 'Internal Server Error' });
  }
});

Construir essa infraestrutura por conta própria gera um alto overhead de desenvolvimento. Você precisa configurar load balancers, provisionar clusters de banco de dados, escrever gerenciadores de retry client-side customizados e criar protocolos de autenticação seguros. Isso representa facilmente de 4 a 6 semanas de trabalho de infraestrutura antes de você escrever uma única linha de código do game loop.

Simplificando a Persistência de Estado com a horizOn

Em vez de escrever middleware Express customizado, gerenciar pools de conexão PostgreSQL e lutar contra instabilidades de rede, você pode delegar essa complexidade para a horizOn. Com a horizOn, você obtém uma solução de banco de dados totalmente gerenciada, projetada especificamente para o desenvolvimento de jogos.

Usando os recursos de banco de dados na nuvem, você não precisa escrever nenhum script de Backend para lidar com a validação de estado. Você pode disparar gravações persistentes no banco de dados diretamente de sua instância de servidor Unreal Engine server-authoritative usando a biblioteca client:

void UInventoryComponent::SaveInventoryToCloud(FName InItemID, int32 InQuantity)
{
	TSharedPtr<FJsonObject> RequestData = MakeShareable(new FJsonObject());
	RequestData->SetStringField("player_id", PlayerID);
	RequestData->SetStringField("item_id", InItemID.ToString());
	RequestData->SetNumberField("quantity", InQuantity);

	// Single call to [horizOn](https://horizon.pm)'s secure database client
	FHorizonClient::Get()->Database("inventories")
		->Upsert(RequestData)
		->OnSuccess(this, &UInventoryComponent::OnSaveSuccess)
		->OnFailure(this, &UInventoryComponent::OnSaveFailure)
		->Execute();
}

Isso atualiza o banco de dados de forma transacional, garante que os jogadores não possam forjar os registros em seus clients e armazena as atualizações em cache localmente de forma automática caso o jogador sofra quedas temporárias de internet.

Melhores Práticas para Inventários de Jogos Persistentes

Para construir um sistema de inventário de alta performance, implemente estas melhores práticas:

  1. Imponha Validação Baseada em Distância no Servidor: Verifique a distância (FVector::Dist) entre o pawn e o actor inspecionável antes de adicionar os itens. Se a distância for fisicamente impossível, registre isso como atividade suspeita do client.
  2. Utilize Tokens de Transação Idempotentes: Gere UUIDs de transação exclusivos no servidor quando a inspeção for iniciada. Isso evita bugs de duplo incremento quando pacotes de rede sofrem retry devido a picos de latência.
  3. Use Atualizações Locais Otimistas com Correção do Servidor: Torne a UI mais responsiva atualizando a tela de inventário do jogador imediatamente no lado do client, mas mantenha-a em um estado de "Confirmação Pendente" até que o servidor retorne uma confirmação de gravação bem-sucedida.
  4. Registre Anomalias de Estado: Monitore com que frequência os clients tentam pegar itens que não estão marcados como inspecionados em sua sessão activa, pois esse é um indicador primário do uso de ferramentas de injeção de cheats.

Pronto para escalar seu Backend Multiplayer? Experimente a horizOn gratuitamente ou confira nossos documentos de desenvolvedor para começar hoje mesmo.


Fonte: How do you put an item into the inventory after inspecting it?