Appearance
Client-Server Requests
Send typed request-response messages between the game client and server using MetaRequest and MetaResponse.
Appearance
Send typed request-response messages between the game client and server using MetaRequest and MetaResponse.
The MetaRequest/MetaResponse system provides typed, async request-response communication between the game client and server. Instead of manually managing separate fire-and-forget messages, listener registration, and response correlation, you define a request-response pair and use a single await call to send a request and receive the response.
This is ideal for operations where the client sends a message and needs a server response before continuing, such as matchmaking queries, authentication challenges, or server-side validation.
When to use MetaRequest vs. other patterns
Use MetaRequest/MetaResponse when your client needs a direct response from the server for a specific request. For fire-and-forget messages that don't need a response, use regular MetaMessage. For synchronized game state changes, use Actions. For server-to-server communication, use EntityAsk.
The system has three layers:
MetaRequest and MetaResponse classes carry the data.SessionMetaRequestMessage/SessionMetaResponseMessage with an auto-generated ID for correlation.SendRequestAsync<T>() returns a Task<T> that completes when the server sends the matching response.You only interact with layer 1 (defining the payload types) and the SendRequestAsync call. The SDK handles ID generation, correlation, and transport.
Create two classes: one extending MetaRequest for the request, and one extending MetaResponse for the response. Both need unique type codes via [MetaSerializableDerived].
using Metaplay.Core.Message;
using Metaplay.Core.Model;
[MetaSerializableDerived(MessageCodes.IdleMatchingRequest)]
public class IdleMatchingRequest : MetaRequest
{
public IdleMatchingRequest() { }
}
[MetaSerializableDerived(MessageCodes.IdleMatchingResponse)]
public class IdleMatchingResponse : MetaResponse
{
public bool IsSuccess { get; set; }
public bool DidWinBattle { get; set; }
IdleMatchingResponse() { }
public IdleMatchingResponse(bool isSuccess, bool didWinBattle)
{
IsSuccess = isSuccess;
DidWinBattle = didWinBattle;
}
}Public properties on MetaRequest and MetaResponse subclasses are serialized automatically — you don't need explicit [MetaMember] attributes.
Use IMessageDispatcher.SendRequestAsync<T>() to send a request and await the typed response. The method returns a Task<T> where T is your MetaResponse type.
public async Task<IdleMatchingResponse> RequestMatchmakingAsync()
{
return await _messageDispatcher.SendRequestAsync<IdleMatchingResponse>(
new IdleMatchingRequest());
}SendRequestAsync has no built-in timeout — if the server never calls SendResponse, the Task stays pending until the session ends (at which point it's cancelled). To add a timeout, pass a CancellationToken:
CancellationTokenSource cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
IdleMatchingResponse response = await _messageDispatcher.SendRequestAsync<IdleMatchingResponse>(
new IdleMatchingRequest(), cts.Token);On the server side, handle incoming requests in your SessionActor by overriding GameTryHandleIncomingPayloadMessage. Match on SessionMetaRequestMessage and use SendResponse() to send the response back with the correlated request ID.
using Metaplay.Core.Message;
public class SessionActor : SessionActorBase
{
protected override async Task<bool> GameTryHandleIncomingPayloadMessage(
MessageRoutingRule routingRule, MetaMessage message)
{
if (routingRule is MessageRoutingRuleSession)
{
switch (message)
{
case SessionMetaRequestMessage request
when request.Payload is IdleMatchingRequest:
await HandleMatchingRequest(request.Id);
return true;
}
}
return false;
}
async Task HandleMatchingRequest(int requestId)
{
// ... your server-side logic ...
SendResponse(requestId,
new IdleMatchingResponse(isSuccess: true, didWinBattle: true));
}
}Every code path in your handler must call SendResponse() with the requestId. If you don't send a response, the client's Task will remain pending until the session ends.
Since RequestMatchmakingAsync() returns a Task<IdleMatchingResponse>, you can consume it like any async RPC call. The response is available directly through await or through a continuation.
In game code where you can use await:
IdleMatchingResponse response = await matchmakingClient.RequestMatchmakingAsync();
if (response.IsSuccess)
{
// Handle success
}In synchronous contexts like Unity Editor UI callbacks, use ContinueWithCtx to handle the response:
matchmakingClient.RequestMatchmakingAsync().ContinueWithCtx(
task => _latestResponse = task.Result);All MetaRequest messages are routed to the SessionActor, which acts as the entry point for client requests on the server. From there, you can forward work to any other entity — the player's own PlayerActor, other players, matchmakers, or any game-specific entity — using server-side patterns like EntityAskAsync.
For example, a "send gift to a friend" request might validate with both the sender's and recipient's PlayerActor before responding:
async Task HandleSendGiftRequest(int requestId, SendGiftRequest request)
{
// Ask the sender's PlayerActor to validate they own the item
InternalValidateGiftResponse senderCheck = await EntityAskAsync<InternalValidateGiftResponse>(
PlayerId, new InternalValidateGiftRequest(request.ItemId));
if (!senderCheck.IsValid)
{
SendResponse(requestId, SendGiftResponse.Failure("You don't own this item."));
return;
}
// Ask the recipient's PlayerActor to check they can receive it
InternalCanReceiveGiftResponse recipientCheck = await EntityAskAsync<InternalCanReceiveGiftResponse>(
request.RecipientId, new InternalCanReceiveGiftRequest(request.ItemId));
if (!recipientCheck.CanReceive)
{
SendResponse(requestId, SendGiftResponse.Failure("Recipient can't receive this gift."));
return;
}
// Both checks passed — execute the transfer
...
SendResponse(requestId, SendGiftResponse.Success());
}The SDK's DefaultOfflineServer handles the built-in SDK requests (social authentication and ImmutableX login) automatically. Custom MetaRequest types fall through to HandleCustomMessage, which you can override in your game's offline server to provide mock responses during offline development:
protected override void HandleCustomMessage(MetaMessage msg)
{
switch (msg)
{
case SessionMetaRequestMessage request
when request.Payload is IdleMatchingRequest:
{
// Return a dummy success response for offline testing
SendToClient(new SessionMetaResponseMessage()
{
RequestId = request.Id,
Payload = new IdleMatchingResponse(
isSuccess: true, didWinBattle: true)
});
break;
}
default:
base.HandleCustomMessage(msg);
break;
}
}The Metaplay SDK uses MetaRequest/MetaResponse internally for:
SocialAuthenticateRequest/SocialAuthenticateResult validates social platform claims.ImmutableXLoginChallengeRequest/ImmutableXLoginChallengeResponse implements a Web3 challenge-response flow.DevOverwritePlayerStateRequest/DevOverwritePlayerStateFailure overwrites player state for testing.