跳到主要内容
示例

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

Tanks 中的医疗箱

之前
坦克受伤后会一直带伤,直到阵亡。受损无法恢复,每场战斗都是倒计时,玩家也没有理由在竞技场里四处移动。
之后
竞技场各处会出现医疗箱,彼此相隔一段距离,也远离正在交战的玩家。开过医疗箱就能回血。游戏的其他部分都不变,房间(room)本身依然没有一行代码。

这是第二节 Tanks 课程,共三步,不引入新模块:一份声明定义医疗箱(health crate),一份声明定义医疗箱在哪里出现,再用一个 hook 定义拾取后的效果。请在 快速入门 之后学习,与 排行榜课程 的先后顺序不限。

声明医疗箱

医疗箱是一个实体(entity),套用了世界物件预设,并带有一个只报告接触、不阻挡任何人的碰撞体。正是 pickups 层上的 Response.Pass 让它成为拾取物(pickup),而不是障碍物:照常报告接触,运动则直接穿过。

pickups 层上仅存在于运行时的医疗箱:报告接触,不影响运动
[Entity("health-crate", Persistence = Persistence.Runtime, Presets = new[] { Preset.WorldObjects })]
public class HealthCrate
{
    [Sync] public Vector3 Position;

    [Body(Shape.Sphere, Radius = 0.5f, Layer = "pickups")]
    [CollidesWith("vehicles", Response.Pass)]      // reported, motion passes through
    public Body Body;
}
@Entity('health-crate', { persistence: Persistence.Runtime, presets: [Preset.WorldObjects] })
export class HealthCrate {
  @Sync position: Vector3;

  @Body({ shape: 'sphere', radius: 0.5, layer: 'pickups' })
  @CollidesWith('vehicles', Response.Pass)         // reported, motion passes through
  body: Body;
}
@entity("health-crate", persistence=Persistence.RUNTIME, presets=[Preset.WORLD_OBJECTS])
class HealthCrate:
    position: Vector3 = sync()

    body: Body = body(shape="sphere", radius=0.5, layer="pickups",
                      collides_with=[("vehicles", Response.PASS)])
即将推出 —— Go

Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。

UCLASS(PSEntity = (Name = "health-crate", Persistence = "Runtime", Presets = "world-objects"))
class UHealthCrate : public UObject
{
    GENERATED_BODY()

    UPROPERTY(PSSync) FVector3f Position;

    UPROPERTY(PSBody = (Shape = "Sphere", Radius = "0.5", Layer = "pickups"),
              PSCollidesWith = "vehicles:Pass")                             // reported, motion passes through
    FPSBody Body;
};
// declarations compile into the same pushed model — playserv push from the UE project or CI
在引擎之外运行

声明在服务器项目中编写,用 playserv push 推送;Unity 绑定在客户端使用生成的类型化 API(HealthCrate)。

有两件事你不必写:医疗箱画在哪里(客户端已经会渲染声明的世界物件),以及它的位置怎样到达各个客户端:[Sync] 本身就是网络调用。

相关模块:Entity Presets 和 Collision。

声明医疗箱出现的位置

放置位置同样是声明,这一步决定了这个功能玩起来是否公平。间距让医疗箱不会扎堆,与玩家保持距离让它们不会在对决现场生成,不重复规则则避免每次都落在同一个地点。

地面层上的医疗箱:彼此间隔、远离战斗,且不会连续两次出现在同一地点
[DropTable("health-crates", Layer = "ground", MinSpacing = 8, AwayFromPlayers = 10, NoRepeat = 3)]
public static partial class HealthCrates
{
    public static readonly Drop Crate = Drop.Of<HealthCrate>(weight: 1);
}
@DropTable('health-crates', { layer: 'ground', minSpacing: 8, awayFromPlayers: 10, noRepeat: 3 })
export class HealthCrates {
  static crate = Drop.of(HealthCrate, { weight: 1 });
}
@drop_table("health-crates", layer="ground", min_spacing=8, away_from_players=10, no_repeat=3)
class HealthCrates:
    crate = drop_of(HealthCrate, weight=1)
即将推出 —— Go

Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。

USTRUCT(PSDropTable = (Name = "health-crates", Layer = "ground", MinSpacing = 8,
                       AwayFromPlayers = 10, NoRepeat = 3))
struct FHealthCrates
{
    GENERATED_BODY()

    UPROPERTY(PSEntry = (Entity = "health-crate", Weight = 1)) FPSDrop Crate;
};
// declarations compile into the same pushed model — playserv push from the UE project or CI
在引擎之外运行

声明在服务器项目中编写,用 playserv push 推送;Unity 客户端看到的结果是生成的世界物品和拾取事件。

有效位置由 Map 提供:掉落表(drop table)在 ground 层上请求一个地点,地图返回一个确实可到达的地点,所以医疗箱永远不会落进墙里。

相关模块:Entity Presets 和 Map。

拾取时回血

只有一个 hook,也是本课唯一的代码。它作为云函数(cloud function)在平台上运行,所以两个引擎标签页里都没有它。

拾取 hook 为坦克回血,满血时礼貌地拒绝
[Before(Drops.Pickup)]
public static Verdict HealOnPickup(PickupIntent p)
{
    if (p.WorldItem.Kind != "health-crate") return Hook.Continue(p);
    if (p.Player.Tank.Hp.IsFull) return Hook.Reject("already at full health");

    p.Player.Tank.Hp.Adjust(+40, by: p.Player);
    return Hook.Continue(p);
}
export const healOnPickup = before(Drops.pickup, (p: PickupIntent) => {
  if (p.worldItem.kind !== 'health-crate') return Hook.continue(p);
  if (p.player.tank.hp.isFull) return Hook.reject('already at full health');

  p.player.tank.hp.adjust(+40, { by: p.player });
  return Hook.continue(p);
});
@before(drops.pickup)
def heal_on_pickup(p: PickupIntent) -> Verdict:
    if p.world_item.kind != "health-crate":
        return Hook.continue_(p)
    if p.player.tank.hp.is_full:
        return Hook.reject("already at full health")

    p.player.tank.hp.adjust(+40, by=p.player)
    return Hook.continue_(p)
即将推出 —— Go

Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。

在引擎之外运行

hook 是云函数,在平台上执行,不在引擎里执行。用 C#、TypeScript 或 Python 编写,Unreal 订阅由此产生的属性变更和拾取事件。在引擎中编写函数的方式已在计划中:Blueprint 图,以及 C++(转换为经平台校验的脚本目标)。两者都像其他声明一样经过分析并推送,执行仍在平台上。

在引擎之外运行

hook 是云函数,在平台上执行,不在引擎里执行。用 C#、TypeScript 或 Python 编写,Unity 订阅由此产生的属性变更和拾取事件。

这个 hook 不必做的事:

  • 拒绝是有类型的。 满血的坦克会收到 already at full health,附带客户端可以直接显示的原因,医疗箱则留给需要它的人。
  • 调整属性由服务器权威决定。 客户端无法请求修改属性(stat),所以不需要为拾取编写反作弊例外。
  • HUD 自动更新。 Adjust 发出 changed,而客户端早已订阅了坦克声明的属性。你没有写任何网络消息。

相关模块:Extensibility 和 Entity Presets。

后续阅读

  • Entity Presets:本课依赖的掉落生成器、世界物件和属性模型,三者都是 entity 的预设,而不是模块。
  • Collision:层、响应,以及只报告的接触与阻挡型接触的区别。
  • Map:有效位置如何选出,以及“可到达”的含义。
  • Extensibility:按顺序列出所有 hook 挂载点及否决约定。
  • Tanks 中的排行榜:另一个 Tanks 示例。