Proposal: Prefer Typed And Structured Names #20693
greeble-dev
started this conversation in
Ideas
Replies: 2 comments 1 reply
-
|
In case it's useful, here's some random notes I accumulated about names in other systems:
|
Beta Was this translation helpful? Give feedback.
1 reply
-
|
Prior art from Flecs, which is probably closer to Bevy than the other mentioned solutions:
|
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
This is a proposal to nudge Bevy towards typed and structured names, making it less reliant on the
Namecomponent. The goal is to keepNameas an opt-in system for users, while avoiding the problems ofNameas an ad-hoc API and debug tool.The gist of the proposal is:
Nameas it is.Nameas an API.Namecomponents (e.g. tags).Background
Most 2D/3D engines, file formats and DCCs have some concept of "entity" or "object" or "node". These things can often be assigned a name. Names can be represented by a human readable string, but beyond that each system has different rules:
namespace:nameconvention (e.g.player_character:head_joint).path/to/asset:object1.subobject2.foo, wherefoois its short name.Problems
Names can be a convenient and powerful tool for for identifying things. When an engine creates them by default they're a standard way to reference and debug anything. When created by the user they're a flexible way to connect all kinds of things together.
But there's a price - in some situations names can be unreliable and inefficient, and that's because they're convenient and powerful.
player_1andplayer_2and doinglet is_player = object.name.starts_with("player_").player_spawn_point.Names are not inherently bad. But they can become a problem when systems rely on or encourage names as the primary form of identification.
Alternatives
Many systems have more structured ways to identify things while still using human readable strings.
GltfMaterialName.["cat", "animal"]can be found by searching for"animal"or"cat OR dog".Animal.Mammal.Catcan be found by searching forAnimalorAnimal.Mammalor*.Cat.Names In Bevy
Bevy entities are not required to have a name. They're usually identified by the
Entitystruct, which is an opaque ID.Bevy does provide a generic
Namecomponent as an optional identifier. ANameis an arbitrary UTF-8 string stored in aCow<'static, str>, plus a hash for fast equality. Each entity can only have a singleNamecomponent, and names are not required to be unique.Namecomponents are often used as a debugging tool - most inspectors will default to displaying them as the main way to identify entities. But there's nothing to stop users from programmatically identifying entities withNamecomponents.Bevy engine code does not rely on
Namecomponents as a way of identifying entities (except for debugging and examples). But there are some cases where the engine automatically adds them:bevy_inputadds a name to gamepad entities (gamepad_connection_system).bevy_gltfadds a name to most of its entities.primitive_name).Name,bevy_gltfalso uses typed name components likeGltfMaterialName.The current
bevy_gltfuse ofNameas API has caused at least one bug with examples:GltfMeshNamecomponent that behaved like the old name (#19331).Names In
bevy_animationI've seen a few comments along the lines of "bevy_animation relies on
Name". This turns out to be untrue - skeleton joints from glTFs do get assigned aNamecomponent (as do all glTF nodes), butbevy_animationdoesn't use it directly.To give some background, this section will go into entirely too much detail on skeletal animation and names - it's skippable if you want to get to the proposal.
Skeletal animation systems often rely on names to identify joints. A branch of the scene that's (usually) skinned to a mesh and animated as a unit is defined as a "skeleton". A simple character skeleton might look like this:
torsoheadleft_upperarmleft_forearmleft_handright_upperarmThings within the skeleton are often required to have unique names, even when the system technically supports duplicate names. This is convenient for the user - a name will always resolve to a single joint within a skeleton. It's also common for names to be standardised across skeletons, so several different character skeletons might have different proportions but will always have a "head" bone. This allows sharing animations and other logic across multiple skeletons.
Bevy's animation system is unusual in that it doesn't define a skeleton - animations can be bound to any entity in the world. Animatable entities are identified by
AnimationTargetId, which is a semi-opaque UUID. For glTF skeletal animations, the UUID is a hash of the joint's name and the names of all its ancestors within the glTF scene. So in the example skeleton, the id ofheadwould be the hash ofheadandtorso. This method means theAnimationTargetIdwill be unique within the glTF scene as long as there are no duplicate names among siblings. It also means that a different glTF with the same hierarchy will result in the sameAnimationTargetId, so animations can be shared.The glTF
AnimationTargetIddoesn't technically depend on entities having aNamecomponent. The loader uses theNamestruct as temporary storage, but it could easily be replaced byString(proof of concept).AnimationTargetIdis powerful since it's less reliant on name uniqueness. But it can be inconvenient - the glTFAnimationTargetIdrequires looking at the hierarchy (example), where other animation systems would only need a single joint/attribute name. It could also be a problem when animations are shared but limited to a sub-branch of the skeleton hierarchy (e.g. a face animation's root might be the head rather than the body root, leading to a differentAnimationTargetId).Proposal
Keep
NameAs It IsEven though there's many ways to misuse it,
Namecan be the best choice for some cases. A game jam entry or a solo dev might not care about making their code scalable and reliable and performant - they can get away withif name.starts_with("player")just fine. So having a built-in and reasonably performant name component is a good thing.Add A Debug-Only Name Component
The engine should have a component that's similar to
Name, but for debugging only - it shouldn't be used as a programmatic identifier or API.Accidentally using a debug name as an identifier will be discouraged by a limited interface, and user will have the option to compile it out. Inspectors and other debug tools should be changed to prefer this component over
Name, or display them side by side.I've had a crack at a proof of concept here: main...greeble-dev:bevy:debug-name-poc-zst
EDIT: Now a PR: #21509.
DebugTag.DebugNameis taken by a similar struct inbevy_util.Cow<'static, str>.Namebut without the hash.Stringor other string internment options.debug_tagfeature is not enabled,DebugTagbecomes an empty struct.cfgs.commands.spawn((DebugTag::new("example"), Transform::IDENTITY))Query<(Entity, &DebugTag, Transform)>DebugTag::new("example")is efficient - the "example" string is compiled out.DebugTag::new(format!(...))are not compiled out.debug_tag!macro that behaves likeDebugTag::newbut is guaranteed to compile out anything inside the macro.let f = format!(...); let n = debug_name!(f).DebugTagonly implementsDebug.ToString, noDisplay, maybe not evenEq(controversial).Debugprints"[REDACTED]"(bike-sheddable)Don't Create
Names In Engine Code By DefaultEngine code should never create
Namecomponents by default. The goal is thatNameshould be opt-in - any users who don't like it shouldn't have to worry about misuse creeping in.The current uses of
Namewould be changed to typed names orDebugTag. This will be a bit annoying for users - they will have to track down uses ofNamein their code. The blow could be softened by adding an option for glTF to generateNames as before.Explore Alternatives
NameandDebugTagcan exist alongside various alternatives:struct MyString(String)component.Objection!
Wouldn't it be better to let users opt-out if they don't want
Namecomponents? Why make the simple case harder?If the engine encourages using
Nameeverywhere then in practice opt-out will become difficult. It'll be too tempting for the engine and third party crates to start relying on them. So that opt-out would become "well... you can try, but these crates you're using might randomly break".Isn't this bad for inspectors and editors? What will they show instead of a name?
An engine that assigns names to everything does simplify the job of editors and inspectors - the name is usually the best thing to show. Object oriented engines have an obvious solution - use the class name instead. But Bevy is too flexible for that.
Maybe the solution is to slap
DebugTags on everything? That could work... but it's annoying for users to do on their own entities, and might stress performance in development builds. Maybe it could be an opt-in.Another option is heuristics that try to find a sensible description based on an entity's components - e.g. an entity with a
Mesh3dand aSkinnedMeshcan be usefully described byformat!("SkinnedMesh:{:?}", mesh.handle). Plugins could register heuristics, and the engine expose a standard way for inspectors to use them. This could also tie into linting heuristics, like warning if an entity has aMesh3dbut no material component.Don't typed names like
GltfMeshMeshincrease Bevy's coupling to glTF?Yes. I don't have a great answer to this. Two options:
MeshNameandMaterialNameinbevy_meshandbevy_material. These can be reused by all loaders.Namelike before, defaulting to off.Summary
Nameshould be strictly an opt-in for the user. The engine can provide it, and BSN and the editor may encourage it through ergonomics - but the user decides, and in the long-term they should also have the option of more structured approaches (hierarchical/flat tags, typed names).Nameas an ad-hoc API.Beta Was this translation helpful? Give feedback.
All reactions