3 min read
Worldmap Travel

Back in 2022, I added the ability to move around on the worldmap. However, the player’s castle stayed behind and maps were completely static.

Moving the castle is difficult because I haven’t actually defined what a castle is.

Well, let’s start there.

Defining a castle

A castle is an Entity with a Position, Health, Area, and SigilPool.

A castle is also an Array<Entity> that contains independent sigils, static walls, doors, and a floor.

And when the castle teleports, it should bring along anything inside of its Area, like mages.

Finding a castle

I need to gather the castle pieces at runtime. Some entities are static, others are dynamic based on position.

I added a PlayerPersistTag component to add to any entity that must be persisted between maps. Since I generate a WizardTower prefab at startup, I can tag all the walls/doors/floor.

The rest can be determined at runtime based on position.

Array<Entity> extractPlayerCastle() {
    Array<Entity> castleData = new Array<>();
    // fetch the castle's area (singleton)
    Entity playerCastle = engine.getSingleton(Family.all(PlayerCastleTag).get());
    // add anything explicitly persisted
    castleData.addAll(
        engine.getEntitiesFor(Family.all(PlayerPersistTag.class).get())
    );
    // check candidates
    for (Entity e : Family.all(Position.class).exclude(PlayerPersistTag.class)) {
        // check if entity is inside the castle
        if (AreaUtils.ContainsPoint(playerCastle, e)) {
            castleData.add(e);
        }
    }
    return castleData;
}

With the castle identified, I had two approaches.

  1. On map change, remove everything except the player castle.
  2. On map change, remove everything, add player castle.

I determined (1) was an optimization on (2) since initial load always had to do (2), so I should start with (2).

Moving the castle

Pooled entities return to pools when they are deleted, so I can’t just clear the engine and add them back.

Instead, I chose to serialize and deserialize the castle to copy it. In the future, I’ll need to save the castle state between maps anyway.

void changeMap(UUID toMapId) {
    // TODO generate maps if they don't exist
    MapData map = getMapById(toMapId);

    // extract the player's castle from the game, serialize it
    Array<Entity> playerCastle = gameScreen.extractPlayerCastle();
    String castleJson = entityMapper.serialize(playerCastle);
    // clear entire game state, pooled objects will be used to clone
    gameScreen.clear();
    // deserialize to create a copy castle
    Array<Entity> playerCastleRealloc = entityMapper.deserialize(castleJson);
    // shift entire castle to spawn point and load the map
    PositionUtils.ShiftPositionAroundRoot(playerCastleRealloc, lastSpawn, map.spawn);
    gamescreen.loadMap(map, playerCastleRealloc);
}

Generating maps

I should write an entire post on the new map generation, but I’ll outline the frame here.

World generation creates MapNode headers that describe the type of map (CastleBattle, Adventure, Shop, etc) and a seed.

I wrote a MapGenerator that creates MapData for each MapNode containing the full map.

void changeMap(UUID toMapId) {
    // Generates a new map if it doesn't already exist
    MapData map;
    if (!worldState.hasMapData(toMapId)) {
        MapNode mapNode = worldState.getMapNode(toMapId);
        MapData map = mapGenerator.generateMap(mapNode, mapNode.seed);
    }
    // ...
}

And after a few hours of fixing serialization bugs (and a day questioning using this approach)…

Mass Teleport Demo