Channels
A channel is a named message group within one app: create it, join it, publish to it, and every member receives the message wherever they stand. It is also the transport a Multicast CrowdyEvent and a replicated subsystem's state deltas ride, and every client joins one channel, the session channel, whether or not it ever creates another.
UCrowdyChannels is a game-instance subsystem. Every server call on it is asynchronous and answers through exactly one of two delegates, success or FOnChannelError, including when the request never reaches the server. Publishing is not one of those calls: it goes out over UDP and is not acknowledged.
When to use one
Guild chat, a party's coordination, a village-wide announcement, trade broadcasts: anything that must reach a group regardless of distance. For a moment that only nearby players need, a SpatialMulticast event is cheaper; for a value, Crowdy State, or your server code if it is truth.
The session channel
Every app has one channel every client joins on connect, named __crowdy_session_<appId>, created on demand when it does not exist and the app's creation policy lets a member create one (otherwise create it once in Crowdy Studio, or the log warns that default-channel events will drop). It is an SDK-owned transport, not a place for your own messages. It carries every Multicast CrowdyEvent with no CrowdyChannel of its own and every replicated subsystem's Crowdy State delta.
Every channel the connect-time bootstrap joins (the session channel and each channel a Multicast event names) and every channel you hand to RegisterReliableRpcChannel is an RPC transport: a payload arriving on it goes to the RPC decoder and is never broadcast to the game. Publish your own notices on a channel of your own, one channel per purpose.
GetSessionChannelId() is its resolved id, 0 until joined or created, and AreReliableChannelsReady() turns true once the connect-time bootstrap has joined every channel the app's Multicast events name plus the session channel. Both are pure nodes you can poll.
The bootstrap joins named channels; it never invents one, because a missing name is usually a typo. It logs [CrowdyChannels] Multicast channel 'X' was not found for this app - RPCs targeting it will drop. Create it (or fix the name) in Crowdy Studio. A channel you create at runtime after the bootstrap has run needs RegisterReliableRpcChannel(ChannelId, Name) once you have joined it, or events naming it still drop. Registering it makes it an RPC transport: raw PublishChannelMessage payloads on it are decoded as RPC frames and are not delivered to OnChannelMessageReceived, so keep one channel per purpose.
Create and join
CreateChannel(Name, Description, MembershipPolicy, bMembersCanSend, OnSuccess, OnError) creates one; the creator becomes its owner with the system leader role, which holds every permission including send_messages. ECrowdyChannelMembershipPolicy decides how others get in: Open (join at once), Request (a join request awaits approval), Invite (managers add members), Admin (app admins only). bMembersCanSend true creates a default member role granting send_messages to everyone who joins; false makes an announce-only channel where joiners receive but cannot post until given a role. The app's FCrowdyChannelPolicy (read with GetChannelPolicy) holds who may create channels at all (ECrowdyChannelCreationPolicy: Admin, Member, Anyone), the default membership policy, and the caps MaxMembers and MaxChannelsPerUser (0 means none); SetChannelPolicy sets the two policies, and the caps are set in Crowdy Studio.
The lantern village creates a village channel for lit notices once the connection is up: CreateVillageChannel, called from the game instance's connected handler (the create needs the game token), and HandleVillageChannelCreated stores the id behind GetVillageChannelId, the one place the id lives.
- C++
- Blueprint
// LanternGameInstance.h
#include "Queries/Data/Channels/Types/FCrowdyChannel.h"
UFUNCTION(BlueprintPure, Category = "Lantern")
int64 GetVillageChannelId() const { return VillageChannelId; }
private:
int64 VillageChannelId = 0;
void CreateVillageChannel();
UFUNCTION()
void HandleVillageChannelCreated(FCrowdyChannel Channel);
// LanternGameInstance.cpp
#include "Subsystem/CrowdyChannels.h"
void ULanternGameInstance::CreateVillageChannel()
{
// Called once connected (the create needs the game token); one client creates, the rest join.
UCrowdyChannels* Channels = GetSubsystem<UCrowdyChannels>();
FOnChannelSuccess Created;
Created.BindDynamic(this, &ULanternGameInstance::HandleVillageChannelCreated);
Channels->CreateChannel(TEXT("village"), TEXT("Lantern lit notices"),
ECrowdyChannelMembershipPolicy::Open, true, Created, FOnChannelError());
}
void ULanternGameInstance::HandleVillageChannelCreated(FCrowdyChannel Channel)
{
VillageChannelId = Channel.ChannelId;
}
In the Game Mode Blueprint, at Event BeginPlay a Crowdy SDK Subsystem getter feeds Bind Event to On UDP Connection Success, whose custom event OnConnected calls Create Channel, a latent node with Name, Description, Membership Policy, and Members Can Send inputs and On Success (a Channel struct) and On Error pins. Binding to the connection is what keeps the create after sign-in; at BeginPlay there may be no token yet and the create would answer on On Error. The Game Mode belongs to one map, so keep the id in a variable on the Game Instance if the next map needs it. (Do not start from the Game Instance's Event Init: it runs before the SDK's subsystems exist, see Authentication.) To keep the id, drag a Break Crowdy Channel off the Channel pin on the success branch and set an Integer64 variable VillageChannelId from Channel Id.
Every connecting client running this asks the server to create the channel. A real game has one client create it, the host for instance, or calls GetChannels first and creates only on a miss; the server's creation policy is the real gate on who may.
JoinChannel(ChannelId, ...) joins an open channel and RequestToJoinChannel files a request on a Request channel; LeaveChannel leaves. The creator is already a member and needs neither, though the local cache (HasCachedChannels, IsPlayerInChannel) does not show the new channel until GetMyChannels has run.
Ban a member
A manager removes a member for good with BanChannelMember(ChannelId, UserId, Reason, OnSuccess, OnError). It takes the user out of the channel and refuses every later join, request to join and add of that user until a manager lifts the ban with UnbanChannelMember(ChannelId, UserId, OnSuccess, OnError). The caller needs the manage-members permission, and the channel's owner and the caller themselves cannot be banned. A user who is not a member yet can be banned too, and leaving or being removed does not lift a ban.
Reason is optional and at most 500 characters. A longer one fails at once with a PolicyViolation error, and nothing is sent. Ban answers the member record, whose Status is banned; unban answers bWasBanned, which is false when the user had no ban.
The village's elder bans a member from the village channel. VillageChannelId is the id the game instance kept when it created the channel.
- C++
- Blueprint
A Ban button on the member list calls BanFromVillageChannel with the member picked. HandleChannelBanned receives the member record, and HandleChannelBanFailed the error.
// LanternGameInstance.h
#include "Queries/Data/Channels/Types/FCrowdyChannelMember.h"
#include "Queries/Data/Channels/Types/FCrowdyChannelError.h"
public:
// A Ban button on the member list calls this for the member picked.
void BanFromVillageChannel(int64 UserId, const FString& Reason);
UFUNCTION()
void HandleChannelBanned(FCrowdyChannelMember Member);
UFUNCTION()
void HandleChannelBanFailed(FCrowdyChannelError Error, FString Message);
// LanternGameInstance.cpp
#include "Subsystem/CrowdyChannels.h"
void ULanternGameInstance::BanFromVillageChannel(int64 UserId, const FString& Reason)
{
UCrowdyChannels* Channels = GetSubsystem<UCrowdyChannels>();
FOnChannelMemberSuccess Banned;
Banned.BindDynamic(this, &ULanternGameInstance::HandleChannelBanned);
FOnChannelError Failed;
Failed.BindDynamic(this, &ULanternGameInstance::HandleChannelBanFailed);
// Needs the manage-members permission; the owner and yourself cannot be banned. A Reason over 500 characters fails before it is sent.
Channels->BanChannelMember(VillageChannelId, UserId, Reason, Banned, Failed);
}
void ULanternGameInstance::HandleChannelBanned(FCrowdyChannelMember Member)
{
UE_LOG(LogTemp, Log, TEXT("User %lld is banned from the village channel"), Member.UserId);
}
void ULanternGameInstance::HandleChannelBanFailed(FCrowdyChannelError Error, FString Message)
{
UE_LOG(LogTemp, Warning, TEXT("The ban failed: %s"), *Message);
}
Ban Channel Member is a latent node with Channel Id, User Id and Reason inputs. The figure bans the user that the OnBanPressed event passes in, for a fixed reason. On Success carries the member record; On Error carries Error and Message, and the figure opens Error with Break Crowdy Channel Error to print its Code and the Message.
Handle a ban on join
A banned user's JoinChannel, RequestToJoinChannel, or a manager's AddChannelMember for them, fails with the error code Banned in ECrowdyChannelErrorCode. The code comes from the server's own error code, not from the message text, so it is safe to branch on. A ban lasts until a manager lifts it, so do not retry: tell the player instead.
- C++
- Blueprint
A Join the Village button calls JoinVillageChannel. Its failure handler checks the code first.
// LanternGameInstance.h
#include "Queries/Data/Channels/Types/FCrowdyChannelError.h"
public:
// A Join the Village button calls this.
void JoinVillageChannel();
UFUNCTION()
void HandleVillageJoinFailed(FCrowdyChannelError Error, FString Message);
// LanternGameInstance.cpp
#include "Subsystem/CrowdyChannels.h"
void ULanternGameInstance::JoinVillageChannel()
{
FOnChannelError Failed;
Failed.BindDynamic(this, &ULanternGameInstance::HandleVillageJoinFailed);
GetSubsystem<UCrowdyChannels>()->JoinChannel(VillageChannelId, FOnChannelMemberSuccess(), Failed);
}
void ULanternGameInstance::HandleVillageJoinFailed(FCrowdyChannelError Error, FString Message)
{
// A ban stays until a manager lifts it, so trying again is pointless; tell the player instead.
if (Error.Code == ECrowdyChannelErrorCode::Banned)
{
UE_LOG(LogTemp, Warning, TEXT("You are banned from the village channel: %s"), *Message);
return;
}
UE_LOG(LogTemp, Warning, TEXT("Could not join the village channel: %s"), *Message);
}
On the Join Channel node's On Error pin, open Error with Break Crowdy Channel Error and compare Code with Banned in a Branch; the figure prints the Message on the banned branch.
Publish and receive
PublishChannelMessage(ChannelId, Payload) sends raw bytes to every active member except the sender. The caller must already be a member holding send_messages; an open channel's default member role grants it. The server caps a channel message payload at 1024 bytes. PublishChannelMessageAs(ChannelId, SenderUuid, Payload) sends under a sender id you choose, as the Game Kit's chat does, since the session's own id is empty until the player's entity resolves; a receiver sees that id, which proves nothing. CanPublishChannelMessage() is true once the game session exists and the realtime connection is routing.
It is the right tool for a notice, not for a value. Put anything that must be right on every client in a state property, or in your server code if it is truth.
The lantern's PublishLit, called from the owner-gated overlap, publishes a one-byte notice, its lit flag, on the village channel, reading the id from the game instance's GetVillageChannelId:
- C++
- Blueprint
// Lantern.cpp
#include "LanternGameInstance.h"
#include "Subsystem/CrowdyChannels.h"
void ALantern::PublishLit()
{
// Tell every village member the lantern changed, over the channel the game instance created.
const ULanternGameInstance* Game = GetGameInstance<ULanternGameInstance>();
UCrowdyChannels* Channels = Game ? Game->GetSubsystem<UCrowdyChannels>() : nullptr;
if (Channels)
{
Channels->PublishChannelMessage(Game->GetVillageChannelId(), { uint8(bLit) });
}
}
From Event ActorBeginOverlap, behind an Is Crowdy Entity Locally Controlled branch (the owner gate the C++ has): a Crowdy Channels subsystem getter feeds the Target, Get VillageChannelId (the Integer64 variable the create graph set) the Channel Id, and a one-element Make Array the Payload byte array of Publish Channel Message. The array's one element is the lit byte, 0 in the figure; wire your lit flag into it. It is a plain call with no result pins.
OnChannelMessageReceived fires on the game thread for every delivery, with the channel id, the sender's SenderUUID, and the payload. The village's HandleVillageNotice, bound by WatchVillageChannel from Init, reads the one byte and sets every lantern's visibility from it; the check on the payload's length before the read is the validation the caution below asks for.
- C++
- Blueprint
// LanternGameInstance.h
void WatchVillageChannel();
UFUNCTION()
void HandleVillageNotice(int64 ChannelId, FString SenderUUID, const TArray<uint8>& Payload);
// LanternGameInstance.cpp
#include "EngineUtils.h"
#include "Lantern.h"
#include "Subsystem/CrowdyChannels.h"
void ULanternGameInstance::WatchVillageChannel()
{
GetSubsystem<UCrowdyChannels>()->OnChannelMessageReceived.AddDynamic(this, &ULanternGameInstance::HandleVillageNotice);
}
void ULanternGameInstance::HandleVillageNotice(int64 ChannelId, FString SenderUUID, const TArray<uint8>& Payload)
{
// A channel message is not server-signed; validate it before acting. Here the one byte is the lit flag.
const bool bLit = Payload.Num() > 0 && Payload[0] != 0;
for (TActorIterator<ALantern> It(GetWorld()); It; ++It)
{
It->Light->SetVisibility(bLit);
}
}
There is no Blueprint figure for the receive side: OnChannelMessageReceived carries a byte-array Payload, and the generated graphs on this site cannot express a bound event with an array parameter. In your own Blueprint, drag off a Crowdy Channels subsystem getter, choose Bind Event to On Channel Message Received, and create the matching custom event from the Event pin; it receives Channel Id, Sender UUID, and Payload as an array of bytes.
The server does not sign the delivery, and any member with send_messages could have sent it. Treat the bytes as untrusted input: bound every length, clamp every value, and never let one decide an outcome that must hold against a modified client. The SDK does not filter payloads on a channel: every payload you are sent reaches your handler, whatever text it begins with.
The full surface
Cache and queries, on UCrowdyChannels: HasCachedChannels (has GetMyChannels ever completed), GetCachedMyChannels (the memberships, no round trip), IsPlayerInChannel, GetMyChannelById, IsInAnyChannel, HasPermissionInChannel (an ECrowdyChannelPermission: SendMessages, ManageChannel, ManageMembers, ManageRoles, InviteMembers), GetMyChannels, GetChannel, GetChannels, GetChannelMembers, GetChannelRoles, GetChannelPolicy, GetPendingJoinRequests. OnMyChannelsCacheChanged fires when the cached membership list changes.
Mutations: CreateChannel, UpdateChannel, DeleteChannel, JoinChannel, RequestToJoinChannel, LeaveChannel, AddChannelMember, RemoveChannelMember, BanChannelMember, UnbanChannelMember, CreateChannelRole, UpdateChannelRole, DeleteChannelRole, SetChannelMemberRoles, SetChannelPolicy.
Every query and mutation also exists as a latent Blueprint node with On Success and On Error pins, one class each: UCrowdyChannels_GetMyChannels (Get My Channels), UCrowdyChannels_GetChannel, UCrowdyChannels_GetChannels (Get All Channels), UCrowdyChannels_GetChannelMembers, UCrowdyChannels_GetChannelRoles, UCrowdyChannels_GetChannelPolicy, UCrowdyChannels_GetPendingJoinRequests, UCrowdyChannels_CreateChannel, UCrowdyChannels_UpdateChannel, UCrowdyChannels_DeleteChannel, UCrowdyChannels_JoinChannel, UCrowdyChannels_RequestToJoinChannel (Request to Join Channel), UCrowdyChannels_LeaveChannel, UCrowdyChannels_AddChannelMember, UCrowdyChannels_RemoveChannelMember, UCrowdyChannels_BanChannelMember (Ban Channel Member), UCrowdyChannels_UnbanChannelMember (Unban Channel Member), UCrowdyChannels_SetChannelMemberRoles, UCrowdyChannels_CreateChannelRole, UCrowdyChannels_UpdateChannelRole, UCrowdyChannels_DeleteChannelRole, UCrowdyChannels_SetChannelPolicy. A list result arrives wrapped (FCrowdyChannelsResult, FCrowdyMyChannelsResult, FCrowdyChannelMembersResult, FCrowdyChannelRolesResult) so a Break node unwraps it. PublishChannelMessage has no latent twin; it is a plain call on the subsystem.
On Success and On Error of a node share one set of data pins: the result (Channel, Member, Channels, and so on; a node that returns nothing has none), then Error and Message. On On Success, Error and Message are empty. On On Error the result is empty, Error is an FCrowdyChannelError (break it to read its Code) and Message is the text to show.
The types:
| Type | Holds |
|---|---|
FCrowdyChannel | ChannelId, AppId, Name, Description, OwnerUserId, MembershipPolicy, Status, CreatedAt. |
FCrowdyChannelMember | ChannelMemberId, ChannelId, UserId, Status (active; pending while a request waits; banned in the answer to a ban), Roles sorted by rank, CreatedAt. |
FCrowdyChannelMembership | Your own membership of one channel: Channel, Roles, the effective Permissions across them, JoinedAt. |
FCrowdyChannelRole | ChannelRoleId, ChannelId, RoleName, Rank, bIsSystem, Permissions, CreatedAt. Every channel has a non-deletable leader role. |
FCrowdyChannelPermissions | The five flags: bSendMessages, bManageChannel, bManageMembers, bManageRoles, bInviteMembers. |
FCrowdyChannelPolicy | AppId, CreationPolicy, DefaultMembershipPolicy, MaxMembers, MaxChannelsPerUser. |
FCrowdyChannelError | An ECrowdyChannelErrorCode (Unknown, NotFound, Forbidden, PolicyViolation, AlreadyMember, NotMember, NetworkError, ServerError, Banned) and the Message to show. All but Banned are a best-effort classification of the message text; Banned comes from the server's own error code. |
Every id is an int64; the server's ids are 64-bit.
In C++ each call takes a pair of dynamic delegates: a success type shaped for its result (FOnChannelSuccess with an FCrowdyChannel, FOnChannelsSuccess with the array, FOnChannelMemberSuccess, FOnChannelMembersSuccess, FOnChannelRoleSuccess, FOnChannelRolesSuccess, FOnMyChannelsSuccess with the memberships, FOnChannelPolicySuccess, FOnChannelVoidSuccess for the calls that return nothing, FOnChannelUnbanSuccess with a bWasBanned for an unban) and FOnChannelError. The two events are FOnMyChannelsCacheChanged and FOnChannelMessageReceived. The latent nodes expose the same shapes as multicast pins that also carry Error and Message: FChannelAsyncResult, FChannelsAsyncResult, FMyChannelsAsyncResult, FChannelMemberAsyncResult, FChannelMembersAsyncResult, FChannelRoleAsyncResult, FChannelRolesAsyncResult, FChannelPolicyAsyncResult, FChannelUnbanAsyncResult, and FChannelAsyncOnError for the nodes that return nothing.
What rides a channel from the SDK itself
A Multicast CrowdyEvent is encoded into a channel payload and published over the session channel, or the channel its CrowdyChannel names once this client has joined it. A replicated subsystem's Crowdy State delta travels the same way. Both are capped at 1024 bytes by the SDK's own codec and dropped loudly, never truncated, when they would exceed it; a Multicast event whose parameters can never fit is refused at registration. "Reliable" on this path means every member is a recipient, not that delivery is guaranteed; see Recipients and routing.
crowdy.rpc.reliable.trace 1 logs the reliable sends and receives on the channel transport.
Gotchas
- The session channel is always joined, even in a project with no Multicast event.
- A named channel is join-only from the SDK's side. Create it in Crowdy Studio or at runtime, then
RegisterReliableRpcChannelif you created it after connecting. send_messagesgates publishing, not membership. An announce-only channel is a channel whose joiners lack it.- The sender never receives an echo of its own message.
- Channel management is a Game API call and needs the app-scoped token the sign-in gives you; see Authentication. The wire protocol and the GraphQL calls are on the Game API channels page.
Related
- Recipients and routing: the Multicast recipient.
- RPC events in C++: the
CrowdyChannelkey and the payload cap. - Replicated subsystems: deltas over the session channel.
- Connection and reconnect: when the channel bootstrap runs.
- Teams and channels in Studio: authoring channels without code.
- Play together: chat rooms and party chat as text, with Send Chat and On Chat Message.