Worlds
A World in Altruist is a spatial container for game objects. It manages physics, partitioning, object spawning, and spatial queries. You can have one large world with multiple map regions, or multiple separate worlds for different game areas.
Configuring Worlds
Worlds are defined in config.yml:
altruist:
environment:
mode: "3D" # or "2D"
game:
engine:
framerateHz: 30
unit: "hz"
gravity: { x: 0, y: -9.81, z: 0 }
worlds:
partitioner: { width: 256, height: 256, depth: 256 }
items:
- index: 0
id: "main-world"
size: { x: 1000, y: 1000, z: 1000 }
gravity: { x: 0, y: -9.81, z: 0 }
data-path: "Resources/world.json" # Optional: load from JSON
position: { x: 0, y: 0, z: 0 }
| Field | Description |
|---|---|
partitioner | Cell size for spatial partitioning (used for efficient queries) |
index | Unique integer index to reference this world |
id | Human-readable name |
size | World dimensions |
gravity | Physics gravity vector |
data-path | Optional JSON file to load world geometry from |
position | World origin offset |
Single Large World with Multiple Maps
A common pattern is using one world with a large size and placing multiple map regions at different coordinates within it. Each region is managed via zones (see Zone Management):
altruist:
game:
worlds:
partitioner: { width: 25600, height: 25600, depth: 25600 }
items:
- index: 0
id: "main"
size: { x: 3300000, y: 1900000, z: 50000 }
gravity: { x: 0, y: 0, z: 0 }
Each map region lives at specific coordinates within this world. Players and entities are placed at their map's coordinate offset. This avoids the overhead of multiple physics worlds while supporting many distinct areas.
How Many Maps Fit?
Each map occupies a rectangular region within the world. The total world size determines how many maps you can place.
Example calculation: If each map is 100,000 x 100,000 units and your world is 3,300,000 x 1,900,000 units:
Maps along X: 3,300,000 / 100,000 = 33 maps
Maps along Y: 1,900,000 / 100,000 = 19 maps
Total maps: 33 x 19 = 627 maps
Each map starts at a base offset. For example, with 100,000 unit maps:
| Map | Base X | Base Y | Coordinate Range |
|---|---|---|---|
town_01 | 0 | 0 | (0–100000, 0–100000) |
forest_01 | 100000 | 0 | (100000–200000, 0–100000) |
desert_01 | 200000 | 0 | (200000–300000, 0–100000) |
mountains_01 | 0 | 100000 | (0–100000, 100000–200000) |
A player saved at position (152300, 48700) is in forest_01 because their X falls in the 100000–200000 range and Y in the 0–100000 range.
What defines a map?:
A "map" is a zone — a named region with a position and size, registered through the world's zone manager (world.Zones). Zones have a maximum size constraint: they cannot be larger than the world's partition size (configured via partitioner in config.yml). Multiple zones can exist within a single partition, and zones can be smaller than partitions. See Zone Management for the full API.
How Players Spawn in a Specific Map:
In a single-world setup, there is no "map ID" to route players to. A player's persisted X/Y/Z coordinates place them in the correct map region implicitly. For example, a town might occupy coordinates (50000–60000, 80000–90000) while a forest is at (150000–160000, 200000–210000). When you load a player from the database, their saved position already falls within the right region. The ZoneManager separately tracks which zone the player is in for entity activation/deactivation, but it does not affect where the player physically is.
Spawning a Player in a Specific Map
// Player's vault stores their last position (already in world-space coordinates)
var prefab = await _prefabs.Query<PlayerPrefab>()
.Where(p => p.Character.StorageId == characterId)
.IncludeAll()
.FirstOrDefaultAsync();
prefab.ClientId = clientId;
// All maps share world 0 — the saved coordinates place the player in the correct zone
var world = _worldOrganizer.GetWorld(0);
await prefab.InitializeRuntime(world);
// The zone manager resolves which zone contains the player's position
var zone = world.Zones.FindZoneAt(
(int)prefab.Character.X,
(int)prefab.Character.Y,
(int)prefab.Character.Z);
if (zone != null)
OnPlayerEnteredZone(clientId, zone.Name);
// Refresh visibility so the player sees nearby entities
_visibilityTracker.RefreshObserver(clientId);
Note:
The key insight: coordinates ARE the map. There is no indirection — the zone manager's FindZoneAt resolves the player's position to a zone automatically. If town_01 occupies (0, 0, 0) to (50000, 50000, 50000) and the player's saved position is (32000, 18000, 0), FindZoneAt returns town_01.
Multiple Separate Worlds
For completely isolated game areas (e.g., overworld, dungeon, arena), use separate world entries:
altruist:
game:
worlds:
items:
- index: 0
id: "overworld"
size: { x: 5000, y: 5000, z: 5000 }
- index: 1
id: "dungeon"
size: { x: 500, y: 500, z: 500 }
- index: 2
id: "arena"
size: { x: 200, y: 200, z: 200 }
Access worlds programmatically:
var overworld = _worldOrganizer.GetWorld(0);
var dungeon = _worldOrganizer.GetWorld("dungeon");
Note:
Use multiple worlds when areas need completely separate physics simulations (e.g., instanced dungeons). For open-world games with seamless transitions, prefer a single world with zones.
Loading Worlds from JSON
Altruist can load world geometry (objects, colliders) from a JSON file exported from a game engine like Unity. Set data-path in the config to point to the file.
altruist:
game:
worlds:
items:
- index: 0
id: "main-world"
data-path: "Resources/world.json"
Note:
The data-path is relative to the application's working directory. Make sure the file is copied to output in your .csproj:
<None Update="Resources\world.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
Example world.json
This is a complete, working world file. It defines a landscape with buildings, trees, and a gate — each with different collider types and archetype mappings:
{
"transform": {
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"size": { "x": 1000, "y": 100, "z": 1000 }
},
"objects": [
{
"id": "ground_plane",
"type": "Static",
"position": { "x": 0, "y": 0, "z": 0 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"size": { "x": 1000, "y": 1, "z": 1000 },
"colliders": [
{
"shape": "box",
"size": { "x": 1000, "y": 1, "z": 1000 },
"center": { "x": 0, "y": -0.5, "z": 0 }
}
]
},
{
"id": "building_01",
"type": "Static",
"archetype": "Building",
"position": { "x": 50, "y": 0, "z": 30 },
"rotation": { "x": 0, "y": 45, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"size": { "x": 10, "y": 8, "z": 12 },
"colliders": [
{
"shape": "box",
"size": { "x": 10, "y": 8, "z": 12 },
"center": { "x": 0, "y": 4, "z": 0 }
}
],
"children": [
{
"id": "building_01_door",
"type": "Static",
"position": { "x": 0, "y": 0, "z": 6 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"size": { "x": 2, "y": 3, "z": 0.5 },
"colliders": [
{
"shape": "box",
"size": { "x": 2, "y": 3, "z": 0.5 },
"center": { "x": 0, "y": 1.5, "z": 0 }
}
]
}
]
},
{
"id": "tree_01",
"type": "Static",
"archetype": "Tree",
"position": { "x": -20, "y": 0, "z": 15 },
"rotation": { "x": 0, "y": 90, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"size": { "x": 2, "y": 6, "z": 2 },
"colliders": [
{
"shape": "capsule",
"radius": 0.5,
"height": 4,
"direction": 1,
"center": { "x": 0, "y": 2, "z": 0 }
}
]
},
{
"id": "tree_02",
"type": "Static",
"archetype": "Tree",
"position": { "x": -25, "y": 0, "z": 22 },
"rotation": { "x": 0, "y": 30, "z": 0 },
"scale": { "x": 1.2, "y": 1.5, "z": 1.2 },
"size": { "x": 2.4, "y": 9, "z": 2.4 },
"colliders": [
{
"shape": "capsule",
"radius": 0.6,
"height": 5,
"direction": 1,
"center": { "x": 0, "y": 2.5, "z": 0 }
}
]
},
{
"id": "boulder_01",
"type": "Static",
"position": { "x": 10, "y": 0, "z": -15 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"size": { "x": 3, "y": 2, "z": 3 },
"colliders": [
{
"shape": "sphere",
"radius": 1.5,
"center": { "x": 0, "y": 1.5, "z": 0 }
}
]
},
{
"id": "gate_01",
"type": "Static",
"archetype": "Gate",
"position": { "x": 0, "y": 0, "z": 50 },
"rotation": { "x": 0, "y": 0, "z": 0 },
"scale": { "x": 1, "y": 1, "z": 1 },
"size": { "x": 6, "y": 5, "z": 1 },
"colliders": [
{
"shape": "box",
"size": { "x": 1, "y": 5, "z": 1 },
"center": { "x": -2.5, "y": 2.5, "z": 0 }
},
{
"shape": "box",
"size": { "x": 1, "y": 5, "z": 1 },
"center": { "x": 2.5, "y": 2.5, "z": 0 }
},
{
"shape": "box",
"size": { "x": 6, "y": 1, "z": 1 },
"center": { "x": 0, "y": 4.5, "z": 0 }
}
]
}
]
}
This example demonstrates:
| Object | Archetype | Collider | Notes |
|---|---|---|---|
ground_plane | (none) | Box | No archetype — becomes AnonymousWorldObject3D |
building_01 | Building | Box | Has a child object (door) |
tree_01, tree_02 | Tree | Capsule | Different scales, capsule with direction: 1 (Y-axis) |
boulder_01 | (none) | Sphere | Pure physics geometry, no archetype |
gate_01 | Gate | 3 Boxes | Multiple colliders: two pillars + crossbar |
Note:
Objects without an archetype (like ground_plane and boulder_01) become AnonymousWorldObject3D — they exist as physics colliders that characters can walk on or bump into, but have no custom C# behavior. Objects with an archetype (like Tree or Gate) are instantiated as your [WorldObject] classes with full Step() support.
Schema Reference
Root: WorldSchema
| Field | Type | Description |
|---|---|---|
transform | WorldTransformSchema | Overall world/landscape transform |
objects | WorldObjectSchema[] | Root-level objects (with nested children) |
Object: WorldObjectSchema
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier |
type | string | "Static" or "Dynamic" |
archetype | string? | Maps to [WorldObject("archetype")] classes |
position | Vector3 | Local position (relative to parent) |
rotation | Vector3 | Euler angles in degrees |
scale | Vector3 | Local scale |
size | Vector3 | World-space AABB size |
colliders | ColliderSchema[] | Physics colliders |
children | ObjectSchema[] | Child objects (hierarchy) |
Collider: WorldColliderSchema
| Field | Type | Description |
|---|---|---|
shape | string | "box", "sphere", "capsule", or "mesh" |
size | Vector3? | Box dimensions (full size, not half-extents) |
center | Vector3? | Local center offset |
radius | float? | Sphere/capsule radius |
height | float? | Capsule height |
direction | int? | Capsule axis: 0=X, 1=Y, 2=Z |
Note:
Mesh colliders ("mesh") are approximated as boxes using their bounding volume. For precise mesh collision, export your mesh as a navmesh instead.
Archetypes: Mapping JSON to Runtime Entities
The [WorldObject("archetype")] attribute connects your world JSON data to your C# classes. When the "archetype" field in the JSON matches the name in [WorldObject("name")], the framework creates an instance of your class with the position, rotation, scale, and colliders from the JSON.
Objects without an archetype field (or without a matching class) become static geometry — they exist as physics colliders but have no custom behavior.
Example: Connecting JSON to Code
Given this JSON object in your world file:
{
"id": "tree_042",
"type": "Static",
"archetype": "Tree",
"position": { "x": 100, "y": 0, "z": 200 },
"colliders": [{ "shape": "capsule", "radius": 0.5, "height": 4 }]
}
The loader will look for a class annotated with [WorldObject("Tree")]:
[WorldObject("Tree")]
public class TreeObject : WorldObject3D
{
public TreeObject() : base(default) { }
public override void Step(float dt, IGameWorldManager3D world)
{
// Custom per-tick logic for trees (e.g., growth, resource gathering)
}
}
Because the JSON has "archetype": "Tree" and the class has [WorldObject("Tree")], the framework creates a TreeObject at position (100, 0, 200) with the capsule collider from the JSON.
Note:
Classes loaded from world JSON must have a parameterless constructor.
Objects Without Archetypes
Objects without an archetype field become static physics geometry — colliders that characters can bump into, but with no custom behavior:
{
"id": "wall_section_17",
"type": "Static",
"position": { "x": 50, "y": 0, "z": 80 },
"colliders": [{ "shape": "box", "size": { "x": 10, "y": 3, "z": 0.5 } }]
}
Note:
Use archetypes for objects that need custom behavior (AI, interaction, destructibility). Leave the archetype empty for walls, floors, and obstacles.
Custom World Loader
If your world data uses a different format, implement IWorldLoader3D. See Custom World Loader for the full guide.
Spawning Objects at Runtime
Once a world is loaded, spawn objects dynamically:
var world = _worldOrganizer.GetWorld(0);
// Spawn with physics body (affected by forces, collisions)
await world.SpawnDynamicObject(myEntity);
// Spawn as static geometry (immovable collider)
await world.SpawnStaticObject(myStaticObject);
// Remove an object
world.DestroyObject(entity.InstanceId);
Note:
SpawnDynamicObject creates a physics body and colliders from the object's BodyDescriptor and ColliderDescriptors. Make sure these are set before spawning, or the object will have no physics presence.
Querying Objects
// Find a specific object
var obj = world.FindObject("instance-id");
// Find all objects of a type
var players = world.FindAllObjects<PlayerEntity>();
// Find nearby objects
var nearby = world.GetNearbyObjectsInRoom("monster", x, y, z, radius: 500, roomId: "");
// Find partitions near a position
var partitions = world.FindPartitionsForPosition(x, y, z, radius: 1000);
World Object Lifecycle
Every world object implements IWorldObject3D with a Step method called each engine tick:
public override void Step(float dt, IGameWorldManager3D world)
{
// dt = delta time since last tick
// Update AI, movement, state transitions, etc.
}
Set Expired = true on a world object to have the framework automatically destroy it on the next tick.
Note:
The Step() method is your main hook into the game loop. Keep it lightweight — it runs for every object every tick. Offload heavy work (pathfinding, database queries) to async services.