Skip to main content
Examples

Not translated yet — showing English.

Health crates in Tanks

Before
a tank that takes damage stays damaged until it dies. There is no way back, so every fight is a countdown and the arena has no reason to move through.
After
health crates appear around the arena, spaced apart and away from whoever is fighting. Driving over one heals you. Nothing else about the game changes, and the room still has no code of its own.

This is the second Tanks lesson. It takes three steps and no new module: a declaration for the crate, a declaration for where crates appear, and one hook for what picking one up does. Take it after Getting Started, in either order with the leaderboard lesson.

Declare the crate

A crate is an entity with the world-objects preset applied and a body that reports contact without stopping anyone. Response.Pass on the pickups layer is what makes it a pickup rather than an obstacle: the contact is reported, the motion passes straight through.

A runtime-only crate on the pickups layer — contact reported, motion unaffected
[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)])
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

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
Runs off the engine

Declarations are authored in the server project and pushed with playserv push; the Unity binding consumes the generated typed API (HealthCrate) on the client surface.

Two things you did not have to write: where the crate is drawn (the client already renders declared world objects) and how its position reaches the clients: [Sync] is the network call.

Owned by Entity Presets and Collision.

Declare where crates appear

Placement is a declaration too, and this is the step that decides whether the feature feels fair. Spacing keeps crates from clumping, distance from players keeps them from spawning into a duel, and the no-repeat rule keeps the same spot from being the answer every time.

Crates on the ground layer — spaced, away from fighting, and never the same spot twice in a row
[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)
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

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
Runs off the engine

Declarations are authored in the server project and pushed with playserv push; the Unity client sees the results as spawned world items and pickup events.

Valid positions come from Map — the table asks for a spot on the ground layer and the map answers with one that is actually reachable, so a crate never lands inside a wall.

Owned by Entity Presets and Map.

Heal on pickup

One hook, and it is the only code in the lesson. It runs on the platform as a cloud function, which is why it appears on neither engine tab.

The pickup hook heals the tank, and refuses politely when there is nothing to heal
[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)
Coming soon — Go

Attribute-style declaration in Go is still open (O-1) — the samples land when the carrier is decided.

Runs off the engine

A hook is a cloud function: it executes on the platform, not in the engine. Write it in C#, TypeScript or Python — Unreal subscribes to the resulting stat-changed and pickup events. Engine-authored functions are planned: Blueprint graphs, and C++ (translated to a platform-validated script target). Both are analyzed and pushed like any declaration; execution stays on the platform.

Runs off the engine

A hook is a cloud function: it executes on the platform, not in the engine. Write it in C#, TypeScript or Python — Unity subscribes to the resulting stat-changed and pickup events.

What the hook doesn't have to do:

  • The refusal is typed. A tank at full health gets already at full health with a reason a client can show, and the crate is still there for someone who needs it.
  • Adjusting a stat is server-authoritative. It is not something a client can ask for, so there is no anti-cheat exception to write for pickups.
  • The HUD updates without being told to. Adjust emits changed; the client is already subscribed to the tank's declared stats. You did not write a network message.

Owned by Extensibility and Entity Presets.

Where to go next

  • Entity Presets — the drop generator, world objects and the stat model this lesson leaned on, all three of them presets of entity rather than modules.
  • Collision — layers, responses, and the difference between a reported contact and a blocking one.
  • Map — how a valid position is chosen, and what "reachable" means.
  • Extensibility — every hook point in order, with the veto contract.
  • A leaderboard in Tanks — the other Tanks example.