Trouv / bevy_ecs_ldtk

ECS-friendly ldtk plugin for bevy, leveraging bevy_ecs_tilemap
Other
641 stars 73 forks source link

feat: add LdtkFields trait with convenience methods for accessing field instances #180

Closed Trouv closed 1 year ago

Trouv commented 1 year ago

Closes #175.

Summary

Users have predictable LDtk entities and know the exact nature of any ldtk field they've created: its identifier, its type, and whether or not it's nullable. However, finding an instance and coercing it to the type the user knows it should be involves a lot of boilerplate with several layers of .iter().find(...)s, matches, if lets and .unwrap()s.

This PR adds many extension methods to Level and EntityInstance to take that burden from the user. For every variant of FieldValue, this adds a method (or two) for accessing a field instance at a given identifier with that type - unwrapping the variant to its internal type. For example, there is a .get_maybe_int_field("MyIdentifier") which, if the field instance exists and is an Int, will return an &Option<i32> referencing its value. There's also a .get_int_field("MyIdentifier") which unwraps it further to a &i32, if possible. If any of these assumptions doesn't hold, an Err(LdtkFieldsError) is returned instead.

Implementation

Most of these methods, and their tests, have been generated by macros. This is a good use case for macros since the code for each of the methods is very similar, though there are some hiccups:

All of these methods access the data and return it by reference. This includes those whose types are cheap to copy. This makes the methods less opinionated about what is and isn't "cheap" to copy, and makes them more consistent. This consistency made the macro writing a little bit easier.

The identifier itself is passed into the methods as &str, despite the fact that each of them contains a .to_string(). This .to_string() only ever occurs in the error case, so its prevalence here doesn't affect the "happy path".

The methods for accessing Vec<Option<T>> variants in a non-optional way has some special considerations. One design question is whether to only return all elements if they are all Some, erroring otherwise, or to return all Some elements even if some of the elements are None. I went with the first option as the intention of these particular methods is to allow users to easily access the data if they've specified it as non-nullable in LDtk. All of these methods act as type coercion, and they should be homomorphic with LDtk's (very limited) field type system.

Furthermore, unlike the normal optional accessors, we cannot simply return a slice to this Vec<Option<T>> data as &[T]. This is because it is not contiguous in memory - the Option wrapping "gets in the way". Another option is to return it as a Vec<&T>, since this preserves the reference to the original data and unwraps the options. However, this involves iterating over the original collection and re-collecting it, even though the user may just want to iterate over it again. The solution chosen here is to instead return an iterator instead of a collection. Specifically, it returns an AllSomeIter<'a, T>, which is a new type added in this PR to provide some type safety and helpful naming to the return type. AllSomeIter just wraps around a Flatten<Iter<'a, Option<T>>, but guarantees that every element of the original collection is Some on construction. This preserves the original references, unwraps the options in the original data, and provides type guarantees while returning as early an intermediate result as possible.

Since it's kind of hard to see what methods have been added here due to most of them being generated by macros, here's a list: