Documentation menu

Ports & portability

The two handles you use in every game, the native escape hatch for everything else, and (later) what makes a game cross-engine.

Inside a lifecycle method you act on the game through two handles, PlayerHandle and GameWorld. They cover the orchestration every game needs. For anything engine-specific, there is one explicit escape hatch. That is the whole page for most games.

Building for one engine? You can stop after the escape hatch below. The rest of this page (capabilities, value-type purity, the core/adapter split) is only relevant the day you ship the same game on a second engine. Until then, ignore it. A single-engine game is one module, full stop.

The two handles you use

PlayerHandle is a player with no engine type attached. GameWorld is the instance’s arena. You get both off the context in every verb.

Verb.java
public void onStart(MatchContext ctx) {
  // PlayerHandle: a player, no engine type attached.
  ctx.players().forEach(p -> {
    p.teleport(ctx.spawn(p));            // ctx.spawn reads a marker
    p.sendMessage("Good luck!");
  });

  // GameWorld: arena geometry, read by marker type.
  ctx.world().markers("Chest").forEach(this::fillChest);
}
PlayerHandleDoes
uuid() / name()identity
teleport(location)move the player
sendMessage(text)chat line
notify(title, subtitle)on-screen title
setSpectator(bool)toggle spectator
inventory()give / clear / set items, by item id
GameWorldDoes
markers(type)read arena points by marker type ("Player_Spawnpoint", "Chest", …)
spawnProp(spec)place a prop declaratively
resetArena()restore the arena between rounds
players()players currently in the world

Markers are how a game stays map-agnostic: the genre reads arena.spawns from the manifest, and your code reads markers("Chest"). Neither hard-codes a coordinate, so the same game runs on any map an artist builds.

The context also hands you a Scheduler (delayed and repeating tasks, already thread-correct), a Hud (the scoreboard), a StatsSink (player data) and, in the party genre, an Economy (coins). You rarely reach for these directly; the genre drives them.

The native escape hatch

The handles are deliberately narrow, they cover orchestration. Native engine APIs are wide: particles, sounds, custom cameras, bespoke mechanics live in that long tail. No portable surface should try to cover it. When you need it, reach the native API explicitly with .as(...) or .native(...):

Elimination.java
// Portable path: works on any engine, no native types.
ctx.eliminate(p);

// Native flourish, Hytale only. The .as(...) is the exact line
// where portability stops, and grep finds every one instantly.
PlayerRef ref = p.as(PlayerRef.class);
World world = ctx.world().native(World.class);
world.execute(() ->                            // hop to the world thread
  SoundUtil.playSoundEvent2dToPlayer(ref, Sounds.ELIMINATED, SoundCategory.SFX));

This is real Hytale: world.execute(...), PlayerRef, SoundUtil are exactly what a hand-written Hytale game calls. The escape hatch is a feature, not a leak. That .as(PlayerRef.class) line is where your game stopped being portable, and it is greppable, so the cost is always visible rather than hidden.

A Hytale-only game uses the hatch freely. If a game is mostly native calls, that is the honest signal it is a mechanics-heavy game (think custom collision or physics) better written straight against the engine than dressed up as portable.

Mechanics live in your own engine code. A round that needs, say, fall detection writes a normal Hytale ECS system for it (Byte Crashers ships a MinigameFallDetectionSystem that disqualifies a player when their y drops below a threshold). The SDK does not try to abstract that. It gives you the game shape and the player/world handles; the engine mechanics stay yours.


Advanced: going cross-engine

Everything below earns its keep only when you ship one game on two engines (Hytale and Minecraft). A single-engine game never needs it.

Value types stay pure

So the same game logic compiles against either adapter, the handles never pass an engine type:

  • Inventory works on abstract item ids (give(itemId, count)), never a native ItemStack. The adapter resolves an id to a real item at grant time.
  • PropSpec is a declarative “what to place” (template id + location + rotation), not an engine prefab handle.
  • Location / Vec3 are plain records, no JOML, no Bukkit Location.

Capabilities

Some engine features don’t exist everywhere. Rather than branch on the engine, a game declares the capability it needs, and the SDK offers it only where the adapter satisfies it (or degrades gracefully):

CapabilityExample
CameraCapabilitycinematic server cameras (Hytale)
NpcCapabilityspawn/steer NPCs (Hytale)
ConcurrentGamesmany instances in one process, in parallel

ConcurrentGames is why “how many games fit on one server” isn’t your problem: Hytale (per-world threads) and Folia (regions) run many instances in one process; single-thread servers don’t, and the network scales those out as more servers instead. Your game code never asks.

One engine, one module

Portability is opt-in, something the handles buy you, not a tax paid up front. For one engine you write one module against the adapter (plus the escape hatch) and never touch a core/adapter split. You extract a shared, engine-agnostic core from working code the day a second engine becomes real, never before. A solo dev shipping one game should never juggle three modules.

Next