Interplay between Assets, Formats and Prefabs for Terrain

#1

Hi there,

Loving Rust and the Amethyst ecosystem so far! I’ve been slowly working on an RPG hobby project, but am running into some issues with regards assets to and prefabs. Hopefully somebody can point me in the right direction.

I’ve got a structure that represents my game level, called Terrain (basically a 3d array of tile and collision data). It has a function that generates (Material, MeshData) pairs using MeshBuilder's, because my terrain can use multiple tilesets, so I’ll need to generate a mesh for each one.

Then I load these into Handle<Material> and Handle<Mesh> and manually add them to a list of sub-entities in my game state. So far so good.


Now I’d like to use Amethyst’s full range of power using assets, formats and prefabs, but their interplay in this case is unclear to me. I’ve read the book, but I’m a bit unclear where to apply which trait in my terrain system. Here are my considerations:

  • It seems like my Terrain is a perfect candidate for an Asset. Though I’m now generating my terrain in code, eventually I would want to load it from disk. It’s “data that’s loaded by a game when it is run”, which fits the description of assets in the book.
    • However, loading my terrain then would result in a Handle<Terrain>. I can attach one of these to an entity, but that of course doesn’t do anything.
    • Should I implement a System that asks every Handle<Terrain> to attach its meshes and materials to the same entity?
    • Should I implement a custom RenderPass that renders my terrain somehow?
  • Reading up on Prefab, this also seems to fit my Terrain. The meshes and materials are sub-assets which I then attach to an entity using the PrefabData trait.
    • If my Terrain is also an Asset, how would I go from loading the asset into a handle onto an entity to instantiating it as a prefab?
  • Ideally I’d like to be able to edit my Terrain in an editor. Whenever the underlying data changes, the meshes should update. This appears to clash with the instantiate-once-and-forget nature of prefabs.
    • How are people with in-game editors or destructive terrain dealing with this? Are you storing the new terrain data and using the asset hot-reloading system to take care of this?

Should my Terrain implement both? How does that work together?


Apologies for the rambling. This isn’t a concrete question like “why doesn’t trait x do y?”, but one about game structure and the interplay between several Amethyst concepts. I hope I’ve provided enough information for someone to shed some light on this.

1 Like
(Azriel Hoh) #2

Heya :wave:, very well written question, and deserves an answer.

If I understand correctly, each Terrain may have multiple "layer"s of (Material, MeshData)s, and each of the layers needs to be rendered. Your understanding of Assets seems good :+1:, shall try to clarify prefabs.

Prefabs are also assets, with the added functionality that, when attaching a Handle<PrefabType> to an entity, the PrefabLoaderSystem<PrefabType> will add other components to that entity, plus instantiate other entities in the prefab if necessary. This is better explained on the linked page.

Now, Amethyst can render each entity with (all of) these (&materials, &meshes, &transforms) components. That is, one entity with all 3 components will be rendered. This means instead of implementing Asset for Terrain (which has no built in logic to map that to multiple entities), you can implement PrefabData for Terrain instead (the impl<T> Asset for Prefab<T> is done for you, you don’t have to implement Prefab).

What you want for your PrefabData implementation is to have an enum with File and Handle variants, much like AssetPrefab, and in load_sub_assets turn the File variant into a Handle variant. Then, in add_to_entity, you want to instantiate an entity per (Material, MeshData) pair (and add on the Transform). This can be done by having Entities<'s> in the SystemData of the PrefabData. You also need to have the WriteStorage<'s, _>s for each of the material, transform, and mesh components (and any other components you want to add). You may want to track the child entities of the Terrain, so that they can all be deleted. The first entity in the entities: &[Entity] parameter is the entity with the Handle<Prefab<Terrain>> iirc.

Format should just be “deserialize these bytes into this type, and the bytes are in JSON/whatever else format”.

1 Like
#3

Thanks, @azriel91. This makes a lot of good sense. If I understand you correctly: “Need to map an asset to multiple components? -> use prefabs.”

I do have some follow-up questions if you don’t mind:

  • Why would I “want to track the child entities of the Terrain, so that they can all be deleted”? The prefab system doesn’t do that for you? Where would I store this state (I assume there’s just one Terrain prefab and multiple instantiations, so it doesn’t make sense to store this info in the Terrain prefab itself, right?)
  • What about updating the terrain run-time? I’m not that interested in continually updating procedurally generated meshes. It’s fine if my Terrain is static, I’m talking more about an in-game editor (since Amethyst is missing one for now).
    • Would it be ok to have my PrefabData be a struct with the Terrain state and Optional handles for the sub-assets? That way I keep my original Terrain data in-tact to use for editing purposes.
    • Adding/removing some tiles should result in mesh changes. Should I re-call load_sub_assets() and AssetStorage::replace() in the case that I’ve already got the sub-assets loaded? I don’t think PrefabLoaderSystem would pick up on those changes, right?
    • I’d prefer not to write the Terrain back to file to check for timestamp changes, because the user might want to discard changes in the editor or something.

Some of the questions above are the reason I discarded the idea of using the prefab system earlier. I was planning on creating a system myself to go over all Handle<Terrain>'s and instantiate meshes and materials myself (basically a hand-written PrefabLoaderSystem), but if you think I can make this work within the prefab system (or additions to it would make this possible) I’d be real happy to hear your suggstions.

(Azriel Hoh) #4

Yeaps, prefabs are one way. I don’t actually use prefabs in my game now – I did, but it got a bit complicated for my use case.

So the application doesn’t hold onto memory that isn’t needed, e.g. when a game is finished and you return to the menu, you want to delete all components from all entities (not just the main map entity). I don’t think the prefab system does that for you; the HideHierarchySystem might.

The questions you ask are very valid questions; and are quite unexplored territory. I’m not sure how the PrefabLoaderSystem behaves if the underlying data changes, so that part would have to be investigated.

Just a little warning that this part of Amethyst may be quite difficult to understand (I had to give up a number of times). If you can design (a set of) systems that make it easier for your use case, it may be worth going down that route instead of using Amethyst’s prefab system. For example, you can just load Terrains as regular assets, and when an entity has ComponentEvent::Insert or ComponentEvent::Modified with Handle<Terrain>, you can spawn the additional entities / update component values.

#5

Once again thanks for your thorough reply. I know we’ve been talking about this a bit over on Discord, but it’s way easier to understand this when communicating in a forum topic. :slight_smile:

The questions you ask are very valid questions; and are quite unexplored territory. I’m not sure how the PrefabLoaderSystem behaves if the underlying data changes, so that part would have to be investigated.

Looking at source code, it seems PrefabLoaderSystem only reacts to ComponentEvent::Inserted. I’m pretty sure it won’t update existing components if underlying data changes, and to be fair that’s not what I’d expect it to do either given the concept of prefabs coming from other engines (e.g. Unity).

Ok, gathering all information it seems I’m better off writing my own ReloadTerrainSystem, which I’ve just started doing (I’m already getting events in from the FlaggedStorage). I’ll report back when running into more problems, but for now the path ahead seems clear.

#6

Well, I’m nearly there but am running into one slight issue I can’t seem to solve. Do you see anything obviously wrong here?

  • I’ve got a system that creates child entities with material+mesh attached for every Terrain component insertion.
  • Whenever I get a Modified event I remove all old child entities and then recreate new ones.
  • Upon insertion I do see my terrain, but upon modification the child entities get destroyed and the new ones never show up!

These are the three steps my system goes through in run():

  1. I’m collecting incoming component events and using BitSet just like the PrefabLoaderSystem does:
for event in terrains.channel().read(&mut self.reader_id) {
    match event {
        ComponentEvent::Inserted(id) => {
            self.to_add.add(*id);
        }
        ComponentEvent::Modified(id) => {
            self.to_remove.add(*id);
            self.to_add.add(*id);
        }
        ComponentEvent::Removed(id) => {
            self.to_remove.add(*id);
        }
    }
}
  1. Remove any registered child entities
// Remove child entities for all removed/modified terrains
for (entity, _) in (&*entities, &self.to_remove).join() {
    // Retrieve a list of the children for this entity
    if let Some(children) = self.children.get(&entity) {
        // Remove every child
        for child in children {
            println!("Removing child {:?} from {:?}", child, entity);
            entities
                .delete(*child)
                .expect("Could not delete terrain child");
        }
        // And remove this mapping from the system
        self.children.remove(&entity);
    }
}
  1. Generate new child entities and attach the necessary components
for (entity, terrain, _) in (&*entities, &terrains, &self.to_add).join() {
    assert!(entities.is_alive(entity));

    for (material, mesh) in terrain.generate_meshes(true) {
        // Create a child for the terrain entity
        let child = entities.create();
        self.children.entry(entity).or_default().push(child);
        println!("Added child {:?} to {:?}", child, entity);

        // ... Add Named component
        // ... Add Parent component
        // ... Add Transform component
        // ... Add Material component
        // ... Add Mesh component
    }
}
  1. Clear the bitsets
self.to_remove.clear();
self.to_add.clear();

What’s confusing me is that Terrain component insertion and modification both use the same piece of code to instantiate child entities, but in the one case it works and in the other it doesn’t.