-
Notifications
You must be signed in to change notification settings - Fork 66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dedicated 2D Transform #82
base: main
Are you sure you want to change the base?
Changes from 5 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
# Dedicated 2D Transform | ||
|
||
## Summary | ||
|
||
The ergonomics of using Bevy's existing Transform for 2D games is poor. To counter this a 2D Transform component should be created that has improved ergonomics. | ||
|
||
## Motivation | ||
|
||
While it is readily possible to create 2D games in Bevy today, interacting with the existing Transform system is awkward, high in "ceremony", and prone to unintentional human error. There are two main issues: | ||
|
||
1. Translation is represented as a `Vec3` with the draw order set implicity based on the Z value | ||
2. Rotation being represented as a Quat even though all rotations are around the Z axis | ||
|
||
### The Problem with Translation | ||
|
||
Translation in Bevy is stored as a `Vec3` in the Transform component. This is fine for 3D games, and is usable for 2D games, but has several issues. | ||
|
||
#### Overloading the Concept | ||
|
||
In 2D games this `Vec3` represents two seperate concepts - the 2D position (X,Y) of the entity, and the draw order (Z) of the entity. Vector maths, thus, can end up changing the draw order value when done on the full `Vec3`, leading to graphical issues being caused by systems that seem unrelated to graphics. This can be easy to forget, and difficult to debug. Avoiding this requires writing a lot boilerplate, which leads to... | ||
|
||
#### Poor Ergonomics | ||
|
||
In order to avoid issues caused by the above overloading any system that interacts with an entities translation will often store the entities Z, copy a `Vec2` of the translation using either xy() or truncate(), perform any necessary operations, then re-extend the `Vec2` back to a `Vec3` with the stored Z before reassigning the value to the Transforms translation. The effect of this is that a 2D games code is littered with truncate() and extend(). This is then further exasperated when doing operations like Raycasting where maintaining the existing Z value will give incorrect results (including Z distance in ray calculations). All in all, this is a poor user experience. | ||
|
||
### The Problem with Rotation | ||
|
||
Rotation in bevy is stored as a `Quat` in the Transform component. Again, this is fine for 3D games, and is usable for 2D games, but has poor ergonomics. Rotation in a 2D game in Bevy is always counterclockwise around the Z axis. As with using `Vec3`'s for Translation this causes a lot of boilerplate, extracting the value of a quaternions rotation around Z in radians, performing calculations on that value, then creating a new `Quat` rotating around Z before reassigning. Much as with Translation, this is a poor user experience. | ||
|
||
## User-facing explanation | ||
|
||
Transform2D represents a 2D entities position, rotation, and scale in world space. It is defined as follows: | ||
|
||
```rust | ||
pub struct Transform2d { | ||
position: Vec2, | ||
rotation: f32 //RFC Note: We could use Rotation2d here | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I will 100% want this to be There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool. I'll make that change |
||
scale: Vec2 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is sidestepping the problems with nonuniform scale, and is a feature regression from the current. But I'm ok to leave that out for now and tackle 2d/3d scale at the same time in follow up, so long as it lands in the same release cycle. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ...I'm probably going to need to have someone talk to me live (discord?) about this so I can understand. I've read the threads, I've read a post about it....and I feel like I'm being gaslit when it comes to "the problems with nonuniform scale". So I'm going to leave this for now and come back when I don't feel like a crazy person and clearly understand the problem. |
||
} | ||
``` | ||
|
||
If you're making a 2D game and you want your entity to appear in the world at a given location it must have a `Transform2D` component. `Position` represents your entities location in the world (actually it's position on the 'xy-plane'). `Rotation` represents your entities anti-clockwise rotation in radians (around the Z-axis). `Scale` represents the entities scale in the world (like `Position` this is actually the entities scale on the 'xy-plane'). | ||
|
||
Many entities in a 2D game will have a sprite, a mesh, or some other graphics attached to them. By default, all these entities are drawn on the same 'layer'. When entities are drawn the last entity drawn appears on top of thosed drawn before it in the same location. Developers can control which layer an entity is on by changing that entities `DrawLayer`: | ||
|
||
```rust | ||
pub struct DrawLayer(pub f32); | ||
``` | ||
|
||
As you can see `DrawLayer` is just a wrapper around an `f32`. Entities with a higher value for their `DrawLayer` will be drawn on top of those with lower values. | ||
Comment on lines
+46
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So this is one of the approaches that's been tossed around for z ordering, and personally I'm not a huge fan. There are a few issues.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I thought about this after I wrote the RFC, but honestly, I would prefer this to be a u32 rather than an f32. Also yes, that's my suggestion, summing all the layers of the parents values and adding it the childs. |
||
|
||
## Implementation strategy | ||
|
||
### Components | ||
|
||
#### Transform | ||
|
||
A `Transform2d` component should be created as follows: | ||
|
||
```rust | ||
pub struct Transform2d { | ||
position: Vec2, | ||
rotation: f32 //Note: Optionally a Rotation2d | ||
scale: Vec2 | ||
} | ||
``` | ||
|
||
The existing `Transform` component should annotated as `[deprecated]` and a copy created named `Transform3d`. | ||
|
||
#### DrawOrder | ||
|
||
A `DrawOrder` component should be created as follows: | ||
|
||
```rust | ||
pub struct DrawLayer(pub f32); | ||
``` | ||
For entities with parent/child relationships, child entities should have their `DrawOrder` propogated to an offset from their parents `DrawOrder` as with the existing `Transform` component. We should not do anything more than this (ie. we should not attempt to automatically set child entities to some lower value of `DrawOrder` than their parent). | ||
|
||
### Bundles | ||
|
||
#### Spatial2dBundle | ||
|
||
A new spatial bundle should be created as per the existing `SpatialBundle`, but containing a `Transform2d` instead of a `Transform`. | ||
|
||
#### SpriteBundle/SpriteSheetBundle | ||
|
||
Two new bundles should be created `Sprite2dBundle` and `SpriteSheet2dBundle` matching the existing `SpriteBundle` and `SpriteSheetBundle` except replacing `Transform` with `Transform2d`. | ||
|
||
The existing `SpriteBundle` and `SpriteSheetBundle` should be annotated as `[Deprecated]` and copies created named `Sprite3dBundle` and `SpriteSheet3dBundle`. | ||
|
||
### Systems | ||
|
||
New versions of `sync_simple_transforms`, `propogate_transforms`, and `propogate_transforms_recursive` should be written that propogate the new `Transform2d` into the existing `GlobalTransform`. | ||
|
||
## Drawbacks | ||
|
||
### Maintenance Burden | ||
|
||
Beyond having three new systems and some new components to maintain Bevy as a whole would need to choose to either: | ||
|
||
* Offer `Vec2` versions of many of it's `Vec3` functions, potentially labelling as eg. `RayCast2d` vs `RayCast` | ||
* Require developers to call `extend()` and use the `Vec3` versions of these functions. Which may defy the point of doing this in the first place. | ||
|
||
This burden is unlikely to go away once this decision is made, for the lifetime of the engine. | ||
|
||
### Ecosystem Pressure | ||
|
||
As above, many ecosystem crates (`bevy_xpbd`, `bevy_rapier`, for example) would need to answer the same question. Either offering `Vec2` versions or requiring use of `extend()`. In some cases these crates already do this, but this would 'force the question'. | ||
|
||
### One Way Door? | ||
|
||
Once this door is passed through, it may be difficult to walk back, its likely that UI will want similar versions of `Transform` for their needs and ecosystem crates would likely start to support `Transform2d`. Once this change is made, it is likely made permanently. | ||
|
||
## Rationale and alternatives | ||
|
||
There are really only two options: | ||
|
||
* Have a `Tranform2d` component | ||
* Don't have a `Transform2d` component | ||
|
||
The existing system has poor ergonomics, not making this change will continue to favor the development of 3D games, make 2D games more difficult to develop and more prone to error. I believe this may, in the long term, harm uptake of the engine given the large proportion of indie games are 2D and Indie devs are more likely to risk using a new 'less proven' tech stack like Bevy and Rust. | ||
|
||
For `DrawOrder` we could have a specific layer system, where a user registers draw layers, and selects from a set of pre-registered draw layers, but this doesn't handle relative positioning. We could handle relative positioning in parent/child relationships by associating some float range and sharing the range between children but many developers would need to manually work around this in order to have children appear on the layers they want (see the existing demand for better control of transform propogation). | ||
|
||
In regards to support burden, this support burden must 'be paid' either way, either in the engine, or in the game. Right now we require that burden to be paid by the game developer. I believe it is more correct for that support burden to be paid by the engine and reduce the burden on the game developer. | ||
|
||
In regards to the ecosystem effect, right now functionality like this already exists in several crates. There is no standardized solution, and developers would need to know of these possible solutions before starting - something they're unlikely to know given they are unlikely to be embedded in the ecosystem. A standardized in engine solution would improve user experience, improve indie uptake, and - I believe - improve the crate ecosystem by allowing for better targetting of 2D specific crates. | ||
|
||
## Unresolved questions | ||
|
||
- Do we want to take this burden ourselves? | ||
- Are we happy about using a fairly simple/naive `DrawOrder` implementation? | ||
- What are the knock-on effects on UI and its desire for a potential `UiTransform`/`TransformUi` | ||
|
||
## Future possibilities | ||
|
||
I have my own crate `rantz_spatial2d` which I'm using in my own game. It's far from perfect and poorly documented, but in it, I have split `Transform2d` into its component parts of `Position2D`, `Rotation2D`, and `Scale2D`. It's out of the scope of this RFC, but often, in 2D games, you actually don't want the whole Transform, only a segment of it, and it may be beneficial to consider splitting `Transform2d` and `Transform` in the same way at some point. Again, outside the scope of this RFC, but an interesting thought. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would say that most but not all rotation is about Z, or possibly about the camera normal. There are reasons to want 2d rotation about a different axis, or affine rotations which mimic the effect of rotation beyond the plane. But I'm being pedantic.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it would be fine to give Transform3d to something you wanted the extra flexibility on. I think it should be trivial to mix and match given they all land in the same global transform.