Re: Game world and implementation

(This whole post is a suggestion.)

Now that I’m somewhat versed in Amethyst and ECS, I think it’s clear that the octree would be part of an “OctreeModel” component, or some other component.

But I’m also wondering whether each large octree should have its own specs::world::World.

In all the examples and documentations there is only one World object for the whole game, which is created in main(). The World object is the foundation of the entity-component system. You register entities and components with the World object, and it keeps track of both internally. (Types of components are not restricted to a single world object, but component storage is instantiated by world objects) Systems borrow the world object to do anything useful.

If you look at a game like Minecraft, there is a distinction between top-level entities and entities that are specific to the voxel or block grid (the “overworld” or the “planet”, etc). You have things like mobs which have orientation expressed as a floating point, then you have things like trees or machines which aren’t true entities, which have orientation expressed as a side of the unit cube. (The latter would be “block entities” in Minecraft.)

So when you remove all the abstraction from ECS, and look at the way things are being represented in memory, with only one world object you have only one storage per component. That means there’s one table dedicated to FloatingPointOrientation and another table dedicated to SideOrientation. You’ll have FloatingPointPosition for “true” entities, and IntegerPosition for “block” entities. With one world object all of the component storage tables share the same indexing scheme, which is inefficient because there are really two classes of entities with little to no component overlap. If you’re using a VecStorage, for example, you’re going to waste all that memory because a significant number of entities won’t be associated with the component. With a DenseVecStorage you’re wasting a usize (eight bytes on a 64-bit machine) per entity without the component, which may be more or less than the inefficiency from VecStorage.

And it’s the same with systems. It is only natural for a planet to have its own systems, like weather, pollution, local gravity, etcetera. All you’re going to do is tell each celestial body entity to handle their own internal systems by calling some method on each of them. And to do that you need to know which bodies have weather components, which have pollution, which have internal local gravity, and so on. Each time you’re iterating over a table and skipping the null entries for entities like mobs. Then you finally reach a planet, and the next step is to iterate over a big table again this time only grabbing the mobs so you can make them wet from rain, cough from pollution, fall from gravity, etc. Inefficient!

I think it makes more sense to have two tiers of worlds. So, worlds within worlds.

  • The top-tier world is set on the universe-level. The entire universe, or at least what is currently loaded, is contained in one global specs::world::World variable. Entities and systems on this tier include celestial bodies and their interactions, as well as smaller free-floating objects in space. For example, OrbitComponent might be a component consisting of some representation of an orbital path and an immutable reference to an anchor entity. An orbit system might run every so many ticks to move entities along their orbital paths. Celestial bodies might have a CelestialBody component, with a model field (either polygon or voxel). You’ll have components like shape, position, velocity, mass, temperature, luminosity, etc.

  • Worlds in the lower-tier can coexist, and there are two types.

    • The first type of lower-tier world is for “true” entities that “belong” to a top-tier entity. “True entity” means something you would call an entity even if we weren’t talking about an entity-component-system, for example a mob object or a projectile object. So a mob walking on the surface of planet X would be “true” entity for planet X. There’s no reason to iterate over a thousand mobs on planet X when we’re looking at orbital paths in deep space. Likewise there’s no reason to iterate over planets Y or Z when we’re trying to determine whether mobs on planet X are tired or not. Entities on a planet use a different coordinate system than entities in space, so there would be little to no component overlap between the top layer and the second layer. That’s why each planet - each celestial body - gets its own Option<specs::world::World> field.

    • The second type of lower-tier world is for “block” entities and their components. Planets and the like will have a very large model component such as a voxel octree that represents the entire planet. Some entities would operate entirely within the voxel octree - if we were remaking Minecraft, that means chests, furnaces, signs, banners, furnaces, beacons, and so on. If we were making a normal polygon game, this would be all of the objects that are considered “part of the map itself”, like buildings and trees. These would be registered with a specs::world::World unique to that model and are, for all intents and purposes, considered part of the model.

How about an example? Let’s say you have a large spaceship. This ship has two specs::world::World fields in its various components, one in the (voxel) model component, and one to hold entities that are “on” the ship, like your player character or the crew. Right now, the spaceship is in space so it is technically a top-tier entity with the FreeFloater component.

What happens when you land your ship on a planet? The planet is another top-tier entity, and at some point your ship entity needs to move from the top-tier to the second-tier. The top-tier FreeFloater component will have a method to move all the relevant data over, which means a new entity is created in the planet’s World and all the necessary data (and pointers to data) is moved over. All of the big datasets are behind pointers so it shouldn’t be that computationally expensive.

The end result is that you end up with nested Worlds, there’s the global world for the universe (tier-1), then two Worlds for the planet (tier-2), then two Worlds for your ship which is on the planet (tier-3). In case your ship is a carrier, there may be vehicles on it which have their own World(s) (tier-4). I don’t think there will be more than four tiers, however.

As another example, the debug formatter might produce something like this:

Debug formatting for the top-level World
universe: World
{
	resources:
	[
		entities: EntitiesRes
		&[
			star_1: Entity,
			planet_1A: Entity,
			planet_1B: Entity,
			freefloater_1: Entity,
		],
		component_storage: MaskedStorage
		&[
			position: VecStorage<Position { f32, f32, f32 }>,
			orientation: VecStorage<Orientation { f32 }>,
			shape: VecStorage<ShapeComponent { Box<dyn Shape> }>,
			mass: VecStorage<Mass { f32 }>,
			velocity: VecStorage<Velocity { f32 }>,
			orbit: VecDenseStorage<OrbitComponent { Box<dyn Orbit> }>,
			temperature: VecStorage<Temperature { f32 }>,
			luminosity: VecStorage<Luminosity { f32 }>,
			celestial_body: BTreeStorage<CelestialBody { Option<World>, Box<dyn Model> }>,
			...
		]
	]
}

If we were to then fetch the CelestialBody component of planet_1A, the debug formatter might produce something like this:

Debug formatting for planet_1A's CelestialBody component
{
	world: Option::<World>::Some
	{
		resources:
		[
			entities: EntitiesRes
			&[
				mob_1: Entity,
				mob_2: Entity,
				vehicle_1: Entity,
				projectile_1: Entity
			],
			component_storage: MaskedStorage
			&[
				position: VecStorage<Position { f32, f32, f32 }>,
				orientation: VecStorage<Orientation { f32 }>,
				shape: VecStorage<ShapeComponent { Box<dyn Shape> }>,
				mass: VecStorage<Mass { f32 }>,
				velocity: VecStorage<Velocity { f32 }>,
				health: VecStorage<Health { f32, f32 }>,
				exhaustion: VecStorage<Exhaustion { f32 }>,
				...
			]
		]
	},
	model: VoxelOctreeModel
	{
		world: Option::<World>::Some
		{
			resources:
			[
				entities: EntitiesRes
				&[
					blockstate_1,
					blockstate_2,
					blockstate_3,
					...
				],
				component_storage: MaskedStorage
				&[
					shape: VecStorage<BlockShape>, // enum BlockShape
					color: VecStorage<Color { u32 }>,
					hardness: VecStorage<Hardness { f32 }>,
					orientation: HashMapStorage<Side>, // enum Side
					...
				]
			]
		},
		tree: Octree
		{
			max_depth: u8,
			data: Box<Vec<Node>> // enum Node
		}
	}
}
2 Likes