Teams
A team is a persistent, named group of players, scoped to your app, with membership, roles, and
permissions the app controls. A game creates, joins, and manages teams live from UCrowdyTeams; Crowdy
Studio's Teams pane edits the same rows ahead of time.
When you touch this
Any time players need a group that outlives one session, a guild, a faction, a village, with roles you can grant and revoke. In the example world a village is a team: joining one is how a player shows they belong to a place.
Runtime and Studio, one dataset
Creating, updating, and deleting teams is not a Studio-only workflow. The same create, update, delete,
role, and policy operations run from game code through UCrowdyTeams, subject to your app's team
creation policy, and Crowdy Studio's Teams pane reads and writes the identical server rows. There is one
store, not two.
Membership, roles, and permissions are server-authoritative truth, pulled through UCrowdyTeams rather
than trusted from the client, the same split every server-owned system in the SDK follows: see
The Two Planes for the general story. There is no container here, just the
Team API UCrowdyTeams wraps one call at a time. Joining a team does not put a player in a match; see
Sessions and presence.
The server owns team, role, and permission storage and every authorization check; a disallowed call comes back through the error delegate rather than being refused on the client. The exact GraphQL shape behind each call is on Teams and in the operations reference; a team can also be granted permissions over a grid region, covered on Grids and permissions.
Every call below is asynchronous and answers through exactly one of the two delegates it is given, even when the request never reaches the server.
Reading the cache first
GetCachedMyTeams and its neighbors read a local snapshot of the player's own memberships. The SDK keeps
it current for you:
- Signing in (log in, register, magic link, social, or a restored session) clears it and fills it again
with a
GetMyTeams. - Signing out clears it:
HasCachedTeams()is false, andOnMyTeamsCacheChangedfires with an empty list if the cache held anything. - A successful
CreateTeam,JoinTeam,LeaveTeam,DeleteTeam,UpdateTeam,RemoveTeamMember,SetTeamMemberRoles,UpdateTeamRoleorDeleteTeamRolerefreshes it.RequestToJoindoes not, since the membership is still pending. - An answer to a
GetMyTeamssent before a sign-out never fills the cache, and an older answer never replaces a newer one.
Read it for the value to show now; treat a query's result and OnMyTeamsCacheChanged as the update that
follows. In C++, ClearMyTeamsCache() empties it by hand and RefreshMyTeams() asks for the player's teams again, with the answer arriving through OnMyTeamsCacheChanged; neither is exposed to Blueprint.
| Call | Returns |
|---|---|
HasCachedTeams() | Whether the cache has been populated yet. |
GetCachedMyTeams() | The player's memberships (TArray<FCrowdyTeamMembership>) as of the last refresh. |
IsPlayerInTeam(TeamId) | Whether the player belongs to that team, from the cache. |
GetMyTeamById(TeamId, OutMembership) | The cached membership for one team, if any. |
IsInAnyTeam() | Whether the cache holds at least one membership. |
GetPrimaryMembership(OutMembership) | The first cached membership. |
HasPermissionInTeam(TeamId, Permission) | Whether the cached membership grants an ECrowdyTeamPermission. |
OnMyTeamsCacheChanged (FOnMyTeamsCacheChanged, Memberships) is a multicast, Blueprint-assignable
delegate that fires whenever the cache changes: after a GetMyTeams answers, after a mutation above
refreshes it, and when a sign-out empties it. Bind it once and re-read the cache; do not poll.
Queries
Each query takes an FOnTeamError alongside its success delegate; bind it to a UFUNCTION with the
signature (FCrowdyTeamError Error, FString Message), where Message repeats Error.Message.
| Call | Success delegate | Notes |
|---|---|---|
GetMyTeams(OnSuccess, OnError) | FOnMyTeamsSuccess | Repopulates the cache and fires OnMyTeamsCacheChanged. |
GetTeam(TeamId, OnSuccess, OnError) | FOnTeamSuccess | One team by id. |
GetTeams(OnSuccess, OnError) | FOnTeamsSuccess | Every team in the app, not just the player's. |
GetTeamMembers(TeamId, OnSuccess, OnError) | FOnTeamMembersSuccess | |
GetTeamRoles(TeamId, OnSuccess, OnError) | FOnTeamRolesSuccess | |
GetTeamPolicy(OnSuccess, OnError) | FOnTeamPolicySuccess | App-wide, not per-team. |
GetPendingJoinRequests(TeamId, OnSuccess, OnError) | FOnTeamMembersSuccess | See the warning below. |
It runs the same query as GetTeamMembers and then keeps only the members whose status is pending. That
is fine at team scale, a handful to a few dozen members, but do not call it in a hot loop expecting a
cheaper, dedicated request.
Creating and managing a team
Every mutation reports success through its own delegate and failure through the shared FOnTeamError.
Team lifecycle: CreateTeam(Name, Description, MembershipPolicy, OnSuccess, OnError) returns
FOnTeamSuccess with the new FCrowdyTeam; UpdateTeam(TeamId, Name, Description, OnSuccess, OnError)
also returns FOnTeamSuccess; DeleteTeam(TeamId, OnSuccess, OnError) returns FOnTeamVoidSuccess.
Membership: JoinTeam(TeamId, OnSuccess, OnError) honors the team's ECrowdyTeamMembershipPolicy,
seating the player immediately for an Open team; RequestToJoinTeam(TeamId, OnSuccess, OnError) is for
a Request policy team and returns a pending member. Both return FOnTeamMemberSuccess.
LeaveTeam(TeamId, OnSuccess, OnError) returns FOnTeamVoidSuccess. AddTeamMember(TeamId, UserId, OnSuccess, OnError) is the admin add that bypasses the membership policy; RemoveTeamMember(TeamId, UserId, OnSuccess, OnError) takes them off. AddTeamMember answers with FOnTeamMemberSuccess,
RemoveTeamMember with FOnTeamVoidSuccess.
Roles and permissions: CreateTeamRole(TeamId, RoleName, Permissions, Rank, OnSuccess, OnError) and
UpdateTeamRole(TeamRoleId, RoleName, Permissions, OnSuccess, OnError) return FOnTeamRoleSuccess;
DeleteTeamRole(TeamRoleId, OnSuccess, OnError) returns FOnTeamVoidSuccess.
SetTeamMemberRoles(TeamId, UserId, RoleIds, OnSuccess, OnError) returns FOnTeamMemberSuccess.
Permissions is an FCrowdyTeamPermissions your code builds before the call.
Pass every role the member should end up holding, including ones they already have, or the call drops the roles you left out.
App-wide policy: SetTeamPolicy(CreationPolicy, DefaultMembershipPolicy, OnSuccess, OnError) returns
FOnTeamPolicySuccess and is an app-admin operation.
Ban a member
A manager removes a member for good with BanTeamMember(TeamId, UserId, Reason, OnSuccess, OnError). It takes
the user off the team and refuses every later join, request to join and add of that user until a manager lifts
the ban with UnbanTeamMember(TeamId, UserId, OnSuccess, OnError). The caller needs the manage-members
permission, and the team's owner and the caller themselves cannot be banned. A user who is not on the team 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 (FOnTeamMemberSuccess), whose Status is banned; unban
answers bWasBanned (FOnTeamUnbanSuccess), which is false when the user had no ban.
A banned user's JoinTeam, RequestToJoinTeam, or a manager's AddTeamMember for them, fails with the error
code Banned in ECrowdyTeamErrorCode. It 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.
The village's elder bans a member from the village team. VillageTeamId is the id the game instance kept when it
founded the village.
- C++
- Blueprint
A Ban button on the member list calls BanFromVillage with the member picked. HandleVillageBanned receives the
member record, and HandleVillageBanFailed the error.
// LanternGameInstance.h
#include "Subsystem/CrowdyTeams.h"
public:
void BanFromVillage(int64 UserId, const FString& Reason);
UFUNCTION()
void HandleVillageBanned(FCrowdyTeamMember Member);
UFUNCTION()
void HandleVillageBanFailed(FCrowdyTeamError Error, FString Message);
// LanternGameInstance.cpp
void ULanternGameInstance::BanFromVillage(int64 UserId, const FString& Reason)
{
UCrowdyTeams* Teams = GetSubsystem<UCrowdyTeams>();
FOnTeamMemberSuccess Banned;
Banned.BindDynamic(this, &ULanternGameInstance::HandleVillageBanned);
FOnTeamError Failed;
Failed.BindDynamic(this, &ULanternGameInstance::HandleVillageBanFailed);
Teams->BanTeamMember(VillageTeamId, UserId, Reason, Banned, Failed);
}
void ULanternGameInstance::HandleVillageBanned(FCrowdyTeamMember Member)
{
UE_LOG(LogTemp, Log, TEXT("User %lld is banned from the village"), Member.UserId);
}
void ULanternGameInstance::HandleVillageBanFailed(FCrowdyTeamError Error, FString Message)
{
UE_LOG(LogTemp, Warning, TEXT("The ban failed: %s"), *Message);
}
Ban Team Member is a latent node with Team Id, User Id and Reason inputs, and Unban Team Member one
with Team Id and User Id. They are wired the way Ban Channel Member is in
Ban a member: On Success carries the member record (Was Banned for an
unban), and On Error carries Error and Message. Break Error with Break Crowdy Team Error and
compare its Code with Banned in a Branch to handle a ban on Join Team, as
Handle a ban on join does for a channel.
Data types
| Struct | Holds |
|---|---|
FCrowdyTeam | TeamId, AppId, Name, Description, OwnerUserId, MembershipPolicy, Status, CreatedAt. |
FCrowdyTeamMember | TeamMemberId, TeamId, UserId, Status (active, pending, or banned in the answer to a ban), Roles sorted by rank, CreatedAt. |
FCrowdyTeamMembership | Team, Roles, the effective Permissions (a server-computed union across roles), JoinedAt, and HasPermission(ECrowdyTeamPermission). |
FCrowdyTeamPermissions | Flags bManageTeam, bManageMembers, bManageRoles, bInviteMembers, all BlueprintReadWrite, and Has(). Build one to pass into CreateTeamRole or UpdateTeamRole. |
FCrowdyTeamPolicy | AppId, CreationPolicy, DefaultMembershipPolicy, MaxMembers (0 is uncapped), MaxTeamsPerUser (0 is uncapped). |
FCrowdyTeamRole | TeamRoleId, TeamId, RoleName, Rank, bIsSystem (every team has an undeletable system leader role), Permissions, CreatedAt. |
FCrowdyTeamError | Code (ECrowdyTeamErrorCode) and Message. |
| Enum | Values |
|---|---|
ECrowdyTeamCreationPolicy | Admin, Member, Anyone |
ECrowdyTeamMembershipPolicy | Open, Request, Invite, Admin |
ECrowdyTeamPermission | ManageTeam, ManageMembers, ManageRoles, InviteMembers |
ECrowdyTeamErrorCode | Unknown, NotFound, Forbidden, PolicyViolation, AlreadyMember, NotMember, NetworkError, ServerError, Banned |
FCrowdyTeamError is built by pattern-matching the server's message text, so every Code but one is a
guess. Banned is the exception: it comes from the server's own error code. Read Message for the detail
and branch on Code only for the broad cases.
Blueprint nodes
The subsystem's per-call delegates need a Create Event node per call. For a graph that just drops in a
node, use the async-action latent nodes instead: one static factory and Activate() per node, with
multicast OnSuccess/OnError, wrapping the same server calls.
| Node | Class | Returns |
|---|---|---|
| Get My Teams | UCrowdyTeams_GetMyTeams | FCrowdyMyTeamsResult |
| Get Team | UCrowdyTeams_GetTeam | FCrowdyTeam |
| Get All Teams | UCrowdyTeams_GetTeams | FCrowdyTeamsResult |
| Get Team Members | UCrowdyTeams_GetTeamMembers | FCrowdyTeamMembersResult |
| Get Team Roles | UCrowdyTeams_GetTeamRoles | FCrowdyTeamRolesResult |
| Get Team Policy | UCrowdyTeams_GetTeamPolicy | FCrowdyTeamPolicy |
| Get Pending Join Requests | UCrowdyTeams_GetPendingJoinRequests | FCrowdyTeamMembersResult |
| Create Team | UCrowdyTeams_CreateTeam | FCrowdyTeam |
| Update Team | UCrowdyTeams_UpdateTeam | FCrowdyTeam |
| Delete Team | UCrowdyTeams_DeleteTeam | void |
| Join Team | UCrowdyTeams_JoinTeam | FCrowdyTeamMember |
| Request to Join Team | UCrowdyTeams_RequestToJoinTeam | FCrowdyTeamMember |
| Leave Team | UCrowdyTeams_LeaveTeam | void |
| Add Team Member | UCrowdyTeams_AddTeamMember | FCrowdyTeamMember |
| Remove Team Member | UCrowdyTeams_RemoveTeamMember | void |
| Ban Team Member | UCrowdyTeams_BanTeamMember | FCrowdyTeamMember |
| Unban Team Member | UCrowdyTeams_UnbanTeamMember | Was Banned (bool) |
| Set Team Member Roles | UCrowdyTeams_SetTeamMemberRoles | FCrowdyTeamMember |
| Create Team Role | UCrowdyTeams_CreateTeamRole | FCrowdyTeamRole |
| Update Team Role | UCrowdyTeams_UpdateTeamRole | FCrowdyTeamRole |
| Delete Team Role | UCrowdyTeams_DeleteTeamRole | void |
| Set Team Policy | UCrowdyTeams_SetTeamPolicy | FCrowdyTeamPolicy |
FCrowdyTeamsResult, FCrowdyMyTeamsResult, FCrowdyTeamMembersResult, and FCrowdyTeamRolesResult
each wrap one multi-item result (Teams, Memberships, Members, Roles); a single-item call like
GetTeam or CreateTeam returns its struct directly.
On Success and On Error of a node share one set of data pins: the result, then Error and
Message. On On Success, Error and Message are empty. On On Error the result is empty, Error
is an FCrowdyTeamError (break it to read its Code) and Message is the text to show; a node that
returns nothing has only Error and Message.
The pins are a multicast delegate named after the result: FTeamAsyncResult (Get Team, Create Team,
Update Team), FTeamsAsyncResult (Get All Teams), FMyTeamsAsyncResult (Get My Teams),
FMemberAsyncResult (Join Team, Request to Join Team, Add Team Member, Ban Team Member, Set Team Member
Roles), FMembersAsyncResult (Get Team Members, Get Pending Join Requests), FRoleAsyncResult (Create Team
Role, Update Team Role), FRolesAsyncResult (Get Team Roles), FPolicyAsyncResult (Get Team Policy, Set
Team Policy), FTeamUnbanAsyncResult (Unban Team Member), and FTeamsAsyncOnError (Delete Team, Leave
Team, Remove Team Member, Delete Team Role). These are separate from the per-call FOnTeam*Success
delegates above; the async-action nodes never use those.
The response arrives asynchronously; if the object is gone by then, the callback has nowhere to land.
const TArray<T>&.FOnTeamsSuccess, FOnTeamMembersSuccess, FOnTeamRolesSuccess, FOnMyTeamsSuccess, and the cache event
FOnMyTeamsCacheChanged pass their array as const TArray<T>&, which is what the C++ handlers on this page
take. A handler must match or it fails to bind, and a project coming from 2.14.0 or earlier, where these
passed by value, changes its handlers once; see
What's Changed. In Blueprint prefer the async-action
nodes above, whose result pins are structs; a Blueprint pin has no by-value distinction.
Example: founding and joining a village
A village is a team. The player asks to join "the Village," and a successful join lights their Torch,
the light component the example cast already carries, to show membership at a glance.
- C++
- Blueprint
A "Found the Village" button calls FoundVillage(), which calls CreateTeam("The Village", "Everyone who shares one fire.", Open, HandleVillageFounded, FOnTeamError()); HandleVillageFounded(FCrowdyTeam)
stores the new id in a private VillageTeamId, read back through the BlueprintPure GetVillageTeamId():
// LanternGameInstance.h
#include "Subsystem/CrowdyTeams.h"
public:
// A Found the Village button calls this; the id the handler keeps is the one every player joins.
UFUNCTION(BlueprintCallable, Category = "Lantern")
void FoundVillage();
UFUNCTION(BlueprintPure, Category = "Lantern")
int64 GetVillageTeamId() const { return VillageTeamId; }
private:
int64 VillageTeamId = 0;
UFUNCTION()
void HandleVillageFounded(FCrowdyTeam Team);
// LanternGameInstance.cpp
void ULanternGameInstance::FoundVillage()
{
UCrowdyTeams* Teams = GetSubsystem<UCrowdyTeams>();
FOnTeamSuccess Founded;
Founded.BindDynamic(this, &ULanternGameInstance::HandleVillageFounded);
Teams->CreateTeam(TEXT("The Village"), TEXT("Everyone who shares one fire."),
ECrowdyTeamMembershipPolicy::Open, Founded, FOnTeamError());
}
void ULanternGameInstance::HandleVillageFounded(FCrowdyTeam Team)
{
VillageTeamId = Team.TeamId;
}
Overlapping the village post calls JoinVillage(), which calls JoinTeam(GetVillageTeamId(), ...);
on success HandleJoinedVillage(FCrowdyTeamMember) sets Torch->SetVisibility(true):
// LanternPlayer.h
#include "Subsystem/CrowdyTeams.h"
virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;
void JoinVillage();
UFUNCTION()
void HandleJoinedVillage(FCrowdyTeamMember Member);
// LanternPlayer.cpp
#include "LanternGameInstance.h"
#include "LanternPost.h"
void ALanternPlayer::NotifyActorBeginOverlap(AActor* OtherActor)
{
Super::NotifyActorBeginOverlap(OtherActor);
if (Cast<ALanternPost>(OtherActor))
{
JoinVillage();
}
}
void ALanternPlayer::JoinVillage()
{
const ULanternGameInstance* Game = GetGameInstance<ULanternGameInstance>();
UCrowdyTeams* Teams = Game ? Game->GetSubsystem<UCrowdyTeams>() : nullptr;
if (!Teams || Game->GetVillageTeamId() == 0)
{
return;
}
// Open membership seats the player at once; a Request team would answer with a pending member instead.
FOnTeamMemberSuccess Joined;
Joined.BindDynamic(this, &ALanternPlayer::HandleJoinedVillage);
Teams->JoinTeam(Game->GetVillageTeamId(), Joined, FOnTeamError());
}
void ALanternPlayer::HandleJoinedVillage(FCrowdyTeamMember Member)
{
Torch->SetVisibility(true);
}
ShowVillageMembership(), called from BeginPlay, guards on HasCachedTeams() and sets
Torch->SetVisibility(Teams->IsPlayerInTeam(GetVillageTeamId())) from the cache alone, before any round
trip completes:
// LanternPlayer.h
void ShowVillageMembership();
// LanternPlayer.cpp
#include "LanternGameInstance.h"
#include "Subsystem/CrowdyTeams.h"
void ALanternPlayer::ShowVillageMembership()
{
const ULanternGameInstance* Game = GetGameInstance<ULanternGameInstance>();
UCrowdyTeams* Teams = Game ? Game->GetSubsystem<UCrowdyTeams>() : nullptr;
// The cache holds this player's own memberships only, and nothing until the first Get My Teams has answered.
if (!Teams || !Teams->HasCachedTeams())
{
return;
}
Torch->SetVisibility(Teams->IsPlayerInTeam(Game->GetVillageTeamId()));
}
WatchVillage(), bound at BeginPlay, calls OnMyTeamsCacheChanged.AddDynamic and GetMyTeams();
HandleVillageCacheChanged(const TArray<FCrowdyTeamMembership>&) re-runs ShowVillageMembership()
whenever the cache changes, from anywhere:
// LanternPlayer.h
#include "Subsystem/CrowdyTeams.h"
void WatchVillage();
UFUNCTION()
void HandleVillageCacheChanged(const TArray<FCrowdyTeamMembership>& Memberships);
// LanternPlayer.cpp
void ALanternPlayer::WatchVillage()
{
UCrowdyTeams* Teams = GetGameInstance()->GetSubsystem<UCrowdyTeams>();
Teams->OnMyTeamsCacheChanged.AddDynamic(this, &ALanternPlayer::HandleVillageCacheChanged);
// Only Get My Teams fills the cache; a join or a leave does not, so pull again after one.
Teams->GetMyTeams(FOnMyTeamsSuccess(), FOnTeamError());
}
void ALanternPlayer::HandleVillageCacheChanged(const TArray<FCrowdyTeamMembership>& Memberships)
{
ShowVillageMembership();
}
The Blueprint needs an int64 variable VillageTeamId and the Torch light component the cast already
has.
Creating the village: the Create Team async node takes Name and Description literals and a
MembershipPolicy of Open; its On Success pin fires when the server has the team, breaks the Team
output, and sets TeamId into VillageTeamId.
Joining it: overlapping the village post fires Event ActorBeginOverlap, which reads VillageTeamId
and calls Join Team; its On Success pin gets the Torch and sets its visibility.
Reading the cache on spawn: Event BeginPlay gets the Torch, gets the Crowdy Teams subsystem and
VillageTeamId, and feeds them into Is Player in Team (pure), whose return value drives the torch's
visibility directly, no network call. The cache is empty until the sign-in refresh or a Get My Teams has answered, so on
its own this graph hides the torch on spawn; the sync step below is what shows it. A Has Cached Teams
(pure) check into Branch guards the read, so the visibility set only runs once the cache has answered.
Staying in sync: add a custom event with one input, Memberships (array of Crowdy Team Membership),
connect it with Bind Event to On My Teams Cache Changed at Begin Play, then call Get My Teams; the
event's body re-runs the membership check above. No figure is shown for this step; the C++ tab shows the
same one.
Gotchas
- The cache holds only the player's own memberships.
GetTeamslists every team in the app; the cache never does. - A successful join, leave, create, delete or role change refreshes the cache, but a pending
RequestToJoindoes not, since the player is not a member yet. Between a change and its refresh,IsPlayerInTeamstill answers from before it. GetPendingJoinRequestsfilters the full member list on the client; it is not a dedicated server call.SetTeamMemberRolesreplaces the member's role set. Pass the full set every time.FCrowdyTeamError::Codeis pattern-matched from the server's message text, exceptBanned, which is the server's own code; treat the rest as a coarse bucket andMessageas the detail.- A per-call delegate needs a
UFUNCTIONon aUObjectthat outlives the request, including when the request never reaches the server. - The array delegates take
const TArray<T>&handlers; a mismatch fails to bind. The async-action nodes in Blueprint have no such split.
Related
- Authentication: a signed-in player is what every team call needs.
- Avatars
- Voice chat
- Host election
- Channels: a sibling group-like system that shares the same permission table.
- Teams and Channels in Crowdy Studio: the same data, authored ahead of time.
- Play together: parties and guilds, a team and its chat channel as the Game Kit's other clients make them.