Not translated yet — showing English.
Bots
A bot joins as an ordinary player. Only the brain lives elsewhere. Same session, same entry validation, same rules, same ACL. The room cannot tell the difference, by design, so bots exercise your real game rules and anti-cheat never needs a bot exception.
When to use it
- Your lobbies need filling at off-peak hours:
FillRoomtops matches to a quota, and bots yield seats as humans arrive. - Bots must play by the real rules (entry validation, ACL, visibility), so anti-cheat never needs a bot exception.
- You bring an external brain, such as a learned policy or a service, that joins through
ConnectAsBotlike any player. - A disconnected player's entity must hand to a bot and back on reconnect, without the seat or prediction noticing.
- Skip it when the character never decides. A dialogue NPC with no brain lives in World Objects.
This page covers connecting a bot: getting it into a room, filling a lobby to quota, handing a seat between a bot and a human. Writing the brain that decides is Writing a Brain.
Who does what
| Actor | On this page |
|---|---|
bot-brain | connects as a player; receives perception; sends commands |
room-owner | declares profiles, fills rooms to quota, hands over bot/human |
At a glance
filler profile: difficulty numbers, a utility brain, and FillRoom to a quota[BotProfile("filler")]
[Brain(Kind.Utility)]
public static class Filler
{
public static Difficulty Difficulty = Difficulty.Of(reactionMs: 250, aimJitter: 0.08f);
[Consider(Targeting.NearestEnemy)] public static Behaviour Target;
[Steer(Steering.SeekAndStrafe)] public static Behaviour Move;
[UseAbilities(When.Ready)] public static Behaviour Fire;
}
PlayServ.Bots.FillRoom("battle", toQuota: 8, profile: "filler", minHumans: 1);@BotProfile('filler')
@Brain({ kind: 'utility' })
export class Filler {
static difficulty = Difficulty.of({ reactionMs: 250, aimJitter: 0.08 });
@Consider(Targeting.nearestEnemy) target: Behaviour;
@Steer(Steering.seekAndStrafe) move: Behaviour;
@UseAbilities(When.ready) fire: Behaviour;
}
PlayServ.bots.fillRoom('battle', { toQuota: 8, profile: 'filler', minHumans: 1 });@bot_profile("filler")
@brain(kind="utility")
class Filler:
difficulty = Difficulty.of(reaction_ms=250, aim_jitter=0.08)
target = consider(Targeting.NEAREST_ENEMY)
move = steer(Steering.SEEK_AND_STRAFE)
fire = use_abilities(When.READY)
playserv.bots.fill_room("battle", to_quota=8, profile="filler", min_humans=1)Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.
USTRUCT(PSBotProfile = "filler", PSBrain = (Kind = "Utility"))
struct FFiller
{
GENERATED_BODY()
UPROPERTY(PSDifficulty = (ReactionMs = 250, AimJitter = "0.08"))
FPSDifficulty Difficulty;
UPROPERTY(PSConsider = (Targeting = "NearestEnemy")) FPSBehaviour Target;
UPROPERTY(PSSteer = (Steering = "SeekAndStrafe")) FPSBehaviour Move;
UPROPERTY(PSUseAbilities = (When = "Ready")) FPSBehaviour Fire;
};
// declarations compile into the same pushed model — playserv push from the UE project or CI
// a host tops up the room it serves
Client->Bots->FillRoom(PSKeys::Rooms::Battle,
FPSFillRoomParams{ .ToQuota = 8, .Profile = TEXT("filler"), .MinHumans = 1 });
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.[BotProfile("filler")]
[Brain(Kind.Utility)]
public static class Filler
{
public static Difficulty Difficulty = Difficulty.Of(reactionMs: 250, aimJitter: 0.08f);
[Consider(Targeting.NearestEnemy)] public static Behaviour Target;
[Steer(Steering.SeekAndStrafe)] public static Behaviour Move;
[UseAbilities(When.Ready)] public static Behaviour Fire;
}
PlayServ.Bots.FillRoom("battle", toQuota: 8, profile: "filler", minHumans: 1);An external brain (heavier AI, a learned policy, a service) connects like any player:
ConnectAsBot joins an external brain as a player: same deltas in, same inputs outvar bot = await PlayServ.ConnectAsBot(projectKey, botId: "trainer-07");
var seat = await bot.Matchmaking.Find("battle");
var room = await bot.Rooms.Join(seat);
// perception in ← the same deltas a player receives; commands out ← the same inputsconst bot = await PlayServ.connectAsBot(projectKey, { botId: 'trainer-07' });
const seat = await bot.matchmaking.find('battle');
const room = await bot.rooms.join(seat);
// perception in ← the same deltas a player receives; commands out ← the same inputsbot = await PlayServ.connect_as_bot(project_key, bot_id="trainer-07")
seat = await bot.matchmaking.find("battle")
room = await bot.rooms.join(seat)
# perception in ← the same deltas a player receives; commands out ← the same inputsAttribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.
// An Unreal-based trainer client is a legitimate brain — it connects as a player.
FPlayServClient::ConnectAsBot(ProjectKey, TEXT("trainer-07"),
TPSOnResult<FPlayServClient*>::CreateLambda([](const TPSResult<FPlayServClient*>& Result)
{
if (!Result.HasValue()) { return; }
FPlayServClient* Bot = Result.Value();
Bot->Matchmaking->Of<FBattleQueue>()->Tickets->Create(FPSTicketClaim{ .Mode = TEXT("battle") },
TPSOnResult<FPSTicket*>::CreateLambda([Bot](const TPSResult<FPSTicket*>& TicketResult)
{
if (!TicketResult.HasValue()) { return; }
TPSSubscription Placement = TicketResult.Value()->Subscribe(
[Bot](const FPSSeat& Seat) { Bot->Rooms->Join(Seat); });
}));
}));
// perception in ← the same deltas a player receives; commands out ← the same inputs
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.var bot = await PlayServ.ConnectAsBot(projectKey, botId: "trainer-07");
var seat = await bot.Matchmaking.Find("battle");
var room = await bot.Rooms.Join(seat);
// perception in ← the same deltas a player receives; commands out ← the same inputsThe model
A bot introduces no notion of its own. It is an actor credential wearing what rooms, data and locomotion already declare.
What a bot's declaration carries.
| Declares | What it is |
|---|---|
thinking tick | how often the brains are asked, and it is not the simulation tick: the brains run outside, and a network call every tick is unfeasible |
direction of the brains | where they execute — a cloud function, the studio's backend, its game server. Which one is not part of the contract, and moving between them is not a breaking change |
actor preset | the bot's rights, as an ordinary actor preset |
visibility of the bot marker | whether participants are told. The marker itself always exists and the platform always observes it; whether players see it is the room type's declaration, because in some markets disclosing an AI opponent is an obligation and in others it is a product choice |
behaviour when the brains are unavailable | one of three, with no default: do nothing · leave the room · fall back to built-in default behaviour |
roster filling | declared by the room type — how many, under what condition, until what moment. Matchmaking knows nothing about bots: it matches actors, and does not decide whom to top up with |
What is true of every bot.
| Always | What it is |
|---|---|
an actor, not a player | it holds an actor credential but has no login provider, no links and no sessions |
economic ownership | is none: no entitlements, no purchases, no leaderboard entries |
perception | is a player's: the same visibility zone, the same predicate, the same object-count limit. A bot and a player in the same position receive the same set of objects, so a bot cannot wallhack any more than a player can |
wider perception | is an actor preset, not a bot property: a debug or "omniscient coach" mode is declared as a preset with a wider predicate |
between thoughts | the last command applies, and its fate is whatever the movement type already declares for stale input — a bot whose brain is thinking is the same case as a player whose network dropped |
room capacity | counts a bot: it occupies a seat like anyone else |
Errors
- The room does not accept bots is a conflict, and repeating it will not help.
- The bot limit is exhausted is a conflict, not forbidden — the permission to introduce one is held; the room is full. Worth repeating once the room frees up.
- A command for a bot from an actor without the permission answers forbidden, and repeating is pointless.
- The brains being unavailable is not an error — it is one of the three declared behaviours above.
- Declared at deploy, refused at deploy: a bot named as the owner of a leaderboard entry, and a missing thinking tick, are both validation failures at deploy time rather than surprises in a live room.
Limits
The numbers arrive with the platform limits chapter; what happens at each ceiling is fixed now.
- Bots in a room: introduction is refused as a conflict; existing bots are never removed to make room.
- Bots per project: the same conflict.
- The thinking tick from below: a declaration faster than the floor is refused at deploy time, because a network call per tick is impractical.
- The rate of commands for one bot: a rate limit with a deadline.
- The deadline for a response from the brains: once it expires, the declared unavailability behaviour applies.