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.
- On map change, remove everything except the player castle.
- 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)…