跳到主要内容
实时游戏

本页尚未翻译,当前显示英文版本。

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.
Unity C# is the same C# API here — 同样的特性可以在 Unity 中编译;FillRoom 需要主机权限,主客户端 build 具备该权限
[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 inputs
const 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 inputs
bot = 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.
Unity C# is the same C# API here — 在 Unity 中基于生成的类型可直接运行
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 一次网络调用不现实。
  • 单个机器人的指令频率:限流,并附带时间点。
  • 大脑响应的截止时间:超时后,执行声明好的不可用行为。