本页尚未翻译,当前显示英文版本。
Bots
机器人以普通玩家的身份加入,只有它的大脑运行在别处。 同样的会话、同样的准入校验、同样的规则、同样的 ACL。房间有意设计成分辨不出两者,所以机器人走的是真实的游戏规则,反作弊也永远不需要为机器人开例外。
何时使用
- 非高峰时段需要填满大厅:
FillRoom把对局补足到配额,真人玩家到来时机器人(bot)让出席位(seat)。 - 机器人必须遵守真实规则(准入校验、ACL、可见性),这样反作弊永远不需要为机器人开例外。
- 你接入外部的机器人大脑(brain),例如训练好的策略模型或一个服务,它像任何玩家一样通过
ConnectAsBot加入。 - 玩家掉线后,他的实体(entity)要交给机器人接管,重连后再交还,席位和预测都察觉不到。
- 无需使用:如果角色从不做决策。没有大脑的对话 NPC 属于世界物件。
本页讲的是如何接入机器人: 让它进入房间、把大厅补足到配额、在机器人和真人之间交接席位。编写负责决策的大脑,见编写机器人大脑。
分工
| 主体 | 在本页中的职责 |
|---|---|
bot-brain | 以玩家身份连接;接收感知;发送指令 |
room-owner | 声明机器人配置,把房间补足到配额,在机器人和真人之间交接 |
概览
filler 配置:难度数值、效用型大脑,以及用 FillRoom 补足配额[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)即将推出 —— Go
Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。
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);外部大脑(更重的 AI、训练好的策略模型、一个服务)像任何玩家一样连接:
ConnectAsBot 让外部大脑以玩家身份加入:收到同样的增量,发出同样的输入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 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 inputs即将推出 —— Go
Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。
// 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 inputs模型
机器人不引入任何自己的概念。 它只是一个主体(actor)凭据,套用的是房间、数据和移动已经声明好的东西。
机器人的声明包含什么。
| 声明项 | 含义 |
|---|---|
思考 tick | 多久询问一次大脑,它不是模拟 tick:大脑在外部运行,每个 tick 都发一次网络调用不现实 |
大脑的运行位置 | 大脑在哪里执行:云函数、工作室的 backend 或它的游戏服务器。具体是哪一个不属于契约,在它们之间迁移也不算破坏性变更 |
主体预设 | 机器人的权限,就是一个普通的主体预设(preset) |
机器人标记的可见性 | 是否告知参与者。标记本身始终存在,平台也始终能观察到;玩家能否看到由房间类型声明,因为在有些市场,公开 AI 对手是法定义务,在另一些市场则是产品选择 |
大脑不可用时的行为 | 三选一,没有默认值:do nothing · leave the room · fall back to built-in default behaviour |
名单填充 | 由房间类型声明:补多少、在什么条件下补、补到什么时候。Matchmaking 对机器人一无所知:它只匹配主体,不决定用谁来补位 |
对每个机器人都成立的规则。
| 始终成立 | 含义 |
|---|---|
是主体,不是玩家 | 它持有主体凭据,但没有登录提供方,没有关联账号,也没有会话 |
经济归属 | 没有:没有权益,没有购买记录,也没有排行榜条目 |
感知 | 与玩家相同:同样的可见范围、同样的谓词、同样的对象数量上限。处在同一位置的机器人和玩家收到同一组对象,所以机器人和玩家一样无法透视 |
更广的感知 | 属于主体预设,而不是机器人的属性:调试模式或“全知教练”模式声明为一个谓词范围更广的预设 |
两次思考之间 | 沿用最后一条指令,它如何处理取决于移动类型为过期输入声明的行为:大脑正在思考的机器人,与断网的玩家是同一种情况 |
房间容量 | 把机器人计算在内:它和其他人一样占一个席位 |
错误
- 房间不接受机器人属于冲突,重试也没用。
- 机器人数量已达上限属于冲突,而不是 forbidden:加入机器人的权限是有的,只是房间满了。等房间空出位置后值得重试。
- 没有权限的主体向机器人发送指令时返回 forbidden,重试没有意义。
- 大脑不可用不是错误,而是上面三种声明行为之一。
- 部署时声明,部署时拒绝:把机器人指定为排行榜条目的所有者、缺少思考 tick,都属于部署时的校验失败,不会在正在运行的房间里突然出现。
限制
具体数值会随平台限制一章给出;每个上限触发后的行为现在就已确定。
- 房间内的机器人数量:再加入会以冲突被拒绝;不会为了腾出空间而移除已有的机器人。
- 每个项目的机器人数量:同样返回冲突。
- 思考 tick 的下限:比下限更快的声明会在部署时拒绝,因为每个 tick 一次网络调用不现实。
- 单个机器人的指令频率:限流,并附带时间点。
- 大脑响应的截止时间:超时后,执行声明好的不可用行为。