-
Notifications
You must be signed in to change notification settings - Fork 10
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
[RFC] Amethyst UI #10
Comments
Re: layout algorithms, I'm quite partial to the way that IOS apps control layout. Their system is based on the Cassowary algorithm, which has been implemented in Rust. For an idea of how that kind of layout works, check out this tutorial. |
I analysed the layout of one of the pictures. While most of it can be represented as non-overlapping boxes, some parts involve arbitrary shapes and object placements (triangle, circle). There's also the lines that link different elements. I'm not sure how those would work here. I was thinking about using either cassowary or flexbox, but I'm still trying to understand how some of the ui layouts work and if those layouting algorithms would restrict what it is possible to do. In the past, I had a hard time using cassowary, so I'm biased against it. I started using flexbox recently, but I can't remember all the css classes for it yet, so its not going to well either ;) Let me know what you think about the comments I left on the picture. |
In my experience, UI like that circular tech tree don't use a conventional layout system, there's either a minimal custom layout system, or everything is manually positioned and there's no layout system at all. The menu in the box on the lest side of the image could be done with a standard layout system, but the circular UI stuff is likely completely custom. Similarly, the lines linking different elements are likely done manually using some line drawing primitive. This sort of thing is pretty common in game development, and I don't think it necessarily makes sense to try to cover that case with amethyst's built-in UI system. Neither cassowary nor flexbox would be able to make this UI, and I doubt there's anything that would handle this kind of thing easily. Rather than trying to find one UI layout system that can handle every game's UI, we should make it easy for developers to manually position elements on the screen so that they can create any exotic layout they want. |
Random tidbit before I forget:
|
Perhaps adding couple of methods to Not sure if you will keep the current |
This isn't quite reasonable, as draw order matters a lot for these. First you need to draw the overlay on the lowest layer, then the text, then the cursor. We could make each of these render passes dependent on each other, but then it'd probably be easier to simplify this into a single render pass. Maybe we can have the pass call three separate functions instead.
Hold on, we separated these for a reason.
Here's my first attempt. Have an pub struct ImageArcRender {
pub start: f32,
pub radian_distance: f32,
}
and over time increase |
That still makes the
|
Also furthermore the current Z order rendering works because it's all in the same |
That's a noble goal and I agree, which is why I think we should stop calling it
Objective: Render a PNG with some pixels where alpha channel is not equal to 1.0 and have it blend with the pixels below it correctly. Allows us to render non-rectangular elements. Approach 1: Render this using GLSL depth buffer Benefit: Massively parallel, takes full advantage of CPU and GPU parallel power. Drawback: If multiple UI elements are layered on top of each other, as tends to happen with more complex UI such as an inventory, the elements beneath the top most image appear to be invisible because their pixels were discarded by the depth buffer culling. Basically, we can't optimize out the rendering of the lower elements because their pixels are still an important part of the final image, even if they are partially covered. Approach 2: Render all elements one after another, starting with the elements furthest from the screen. Benefit: Renders correctly, we now can see all UI elements without weird holes in them. Drawback: Extremely single threaded. Rendering an element can't proceed until the operation before it is complete because the output of rendering the further elements is an input into the rendering of nearer elements. |
I'm not sure which solution is the best concerning the rendering When I was refactoring the selection logic (click to select an element, tab goes to next, etc), I put CachedSelectionOrder as a Resource. (previously called CacheTabOrder) UiTransform approachesApproach 1: Approach 2: Edit: re: UiText, in case I didn't explain it properly, I intend to have edit text be 3 separated entities
Sprites if I manage to separate Sprites from Spritesheet on a data-layout level. Otherwise UiImage. |
Cool idea, I like it! Assuming we can make sure this happens after all The biggest reason I'd prefer approach 2 is because I'm not completely convinced there's actually that much overlap between screen space handling and world space handling. Let's investigate what aspects of each transform would need differing implementations:
Basically I'm of the opinion 3D math is an unnecessary burden on the 2D ecosystem. If I want to lay things out in 2D the last thing I want is to be thinking about quaternions, because my rotation can be expressed as a single f32. |
I'm currently working on refactoring the The basic idea is to make a component for each type of interaction. This way, any Ui Widget that has a system supporting this can react to it's different kinds of interactions: world.create_entity()
.with(button_transform)
.with(button_texture)
.with(OnHover(...))
.with(OnClick...))
.build() This would make handling these in the respective systems very easy. The question is what is passed into those. The most basic solution would be to have a simple action enum, and for every event you want you can add an additional reaction component: .with(OnHover(PlaySound(sound_handle))
.with(OnHover(ChangeTexture(tex_handle))
// and so on Implementing it like this could cause more complex actions to result in a lot of boilerplate though, and there is no way to control the order in which these happen or add any delay between them. My idea is to have some kind of action chain, which would enable all of the above. A builder pattern can be used to make the syntax very speaking: .with(OnHover::(Chain::new()
.then(PlaySound(sound_handle))
.then(Delay(100.0))
.then(ChangeTexture(tex_handle))
.build()
))
// and so on The chain would just be pushed into a vec behind the scenes, and a system would keep track of running actions. You would also be able to create reusable actions and attach them to all your buttons, for example. Here is some things I'm still unsure about:
Would love to hear some comments on this, I'll probably implement a simple example and report back. Edit: As for naming, I'm feeling |
Alright, so I've scrapped the idea for actions chains for now, as they would require the system handling them to keep around a large amount of state, which is probably not desirable for now and way out of the scope of the button refactor. The current idea is that you just pass an array of There can be a central system receives the events, plays sounds, adds the necessary components to the entities for which events have been triggers, and keeps track of them. This would actually be very similar to the |
For positioning elements CSS has some (maybe) related solutions: |
What you can do is provide support for 2d vector graphics in your layout - so you get a scalable interface. So those lines, circles etc. are described as paths, and then something like |
I think we can make 2 really powerful demonstrations of the UI framework once its done |
1189: Implement EventRetriggers and refactor UiButton r=jojolepro a=happenslol I'm opening this up for preliminary review, since this introduces a lot of changes and I'd like some feedback on it before I actually implement the new patterns for all use cases. Here's what this PR basically does: * Introduce a new generic `System` called `EventRetriggerSystem`. It works very similarly to the one proposed in #1072, allowing you to basically trigger follow-up events for any events. * Refactor `UiButton` and its builder to use the new system to trigger its click and hover actions (This is only implemented for `SetTextColor` so far) * Refactor the UI prefab builder to also use these (Haven't started on this, shouldn't be too much work though). Here's things I'm still unsure about/not satisfied with: * How to keep track of the state the buttons are in/what has changed due to which event. At the moment, I'm basically keeping a stack of changes, where the first element is the original state. This feels a little hacky, and might be nicer to read with an additional struct that contains the original state as a field, plus a `Vec` of changes. Let me know if you see a better way to do this. * How to structure the `UiButtonRetrigger` component. Currently it's a struct containing `Vec`s for all event types (`on_hover`, `on_click`, etc.) and I've been told it's not a good idea to keep `Vec`s in components. It might be a good idea here to use `SmallVec` to keep the memory contiguous, but I'm not sure. We definitely need some dynamically sized array here, since with a builder or at any point at runtime, the user can add/remove events from this. Since there's no variant typing and multiple events of the same type might occur for one action, it's also not feasible to add a separate component for every one. If you want to see what's working so far in action, please take a look at the `simple_ui` example. I'm still not sure if I want to flesh out this example a bit while I develop this, but I think it might be a good idea to have an example around that programmatically constructs a UI with some simple effects. <!-- Reviewable:start --> --- This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/amethyst/amethyst/1189) <!-- Reviewable:end --> Co-authored-by: Hilmar Wiegand <me@hwgnd.de>
1189: Implement EventRetriggers and refactor UiButton r=happenslol a=happenslol I'm opening this up for preliminary review, since this introduces a lot of changes and I'd like some feedback on it before I actually implement the new patterns for all use cases. Here's what this PR basically does: * Introduce a new generic `System` called `EventRetriggerSystem`. It works very similarly to the one proposed in #1072, allowing you to basically trigger follow-up events for any events. * Refactor `UiButton` and its builder to use the new system to trigger its click and hover actions (This is only implemented for `SetTextColor` so far) * Refactor the UI prefab builder to also use these (Haven't started on this, shouldn't be too much work though). Here's things I'm still unsure about/not satisfied with: * How to keep track of the state the buttons are in/what has changed due to which event. At the moment, I'm basically keeping a stack of changes, where the first element is the original state. This feels a little hacky, and might be nicer to read with an additional struct that contains the original state as a field, plus a `Vec` of changes. Let me know if you see a better way to do this. * How to structure the `UiButtonRetrigger` component. Currently it's a struct containing `Vec`s for all event types (`on_hover`, `on_click`, etc.) and I've been told it's not a good idea to keep `Vec`s in components. It might be a good idea here to use `SmallVec` to keep the memory contiguous, but I'm not sure. We definitely need some dynamically sized array here, since with a builder or at any point at runtime, the user can add/remove events from this. Since there's no variant typing and multiple events of the same type might occur for one action, it's also not feasible to add a separate component for every one. If you want to see what's working so far in action, please take a look at the `simple_ui` example. I'm still not sure if I want to flesh out this example a bit while I develop this, but I think it might be a good idea to have an example around that programmatically constructs a UI with some simple effects. <!-- Reviewable:start --> --- This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/amethyst/amethyst/1189) <!-- Reviewable:end --> Co-authored-by: Hilmar Wiegand <me@hwgnd.de>
I am moving this to the RFC repo with this nifty transfer beta feature! I totally have a backup! |
Ui RFC
Here we go...
Warning: Things might be out of order or otherwise hard to understand. Don't be afraid to jump between sections to get a better view.
So here, I will be describing the requirements, the concepts, the choices offered and the tradeoffs as well as the list of tasks.
Surprisingly enough, most games actually have more work in their ui than in the actual gameplay. Even for really small projects (anything that isn't a prototype), games will have a ui for the menu, in game information display, settings screen, etc...
Let's start by finding use cases, as they will give us an objective to reach.
I will be using huge games, to be sure to not miss out on anything.
Since the use cases are taken from actual games and I will be listing only the re-usable/common components in them, nobody can complain that the scope is too big. :)
Use cases: Images
Use cases: Text
So now, let's extract the use cases from those pictures.
Trans
)Use cases: Conclusion
There are a lot of use cases and a lot of them are really complex. It would be easy to do like any other engine and just provide the basic elements, and let the game devs do their own custom elements. If you think about it however, if we are not able to provide those while we are the ones creating the engine, do you honestly expect game developers to be able to make them from the outside?
Also, if we can implement those using reusable components and systems, and make all of that data oriented, I think we will be able to cover 99.9% of all the use cases of the ui.
Big Categories
Let's create some categories to know which parts will need to be implemented and what can be done when.
I'll be listing some uses cases on each to act as a "description". The lists are non-exhaustives.
Eventing
Layouting
Rendering
Partial Solutions / Implementation Details
Here is a list of design solution for some of the use cases. Some are pretty much ready, some require some thinking and others are just pieces of solutions that need more work.
Note: A lot are missing, so feel free to write on the discord server or reply on github with more designs. Contributions are greatly appreciated!
Here we go!
Drag
Add events to the UiEvent enum. The UiEvent enum already exists and is responsible of notifying the engine about what user-events (inputs) happened on which ui elements.
Only entities having the "Draggable" component can be dragged.
Dragging an entity can cause a ghost entity to appear (semi transparent clone of the original entity moving with the mouse, using element_offset)
When hovering over draggable elements, your mouse optionally changes to a grab icon.
The Dragged ghost can have a DragGhost component to identify it.
Event chains/re-triggers/re-emitters
The point of this is to generate either more events, or side effects from previously emitted events.
Here's an example of a event chain:
UiEvent
for that entity with event_type: ClickEventRetriggerSystem
catches that event (as well as State::handle_event and custom user-defined systems!), and checks if there was aEventRetrigger
Component on that entity. It does find one. This particularEventRetrigger
was configured to create aTrans
event that gets added into theTransQueue
Trans
event and applies the changes to theStateMachine
. (PR currently opened for this.)This can basically be re-used for everything that makes more sense to be event-driven instead of data-driven (user-input, network
Future
calls, etc).The implementation for this is still unfinished. Here's a gist of what I had in mind:
Note: You can have multiple
EventRetrigger
components on your entity, provided they have uniqueIn, Out
types.Edit text
Currently, the edit text behaviour is
All the event handling, the rendering and the selection have dedicated code only for the text.
The plan here is to decompose all of this into various re-usable parts.
The edit text could either be composed of multiple sub-entities (one per letter), or just be one single text entity with extra components.
Depending on the choice made, there are different paths we can take for the event handling.
The selection should be managed by a SelectionSystem, which would be the same for all ui elements (tab moves to the next element, shift-tab moves back, UiEventType::ClickStart on an element selects it, etc...)
The rendering should also be divided into multiple parts.
There is:
Each of those should be managed by a specific system.
For example, the
CursorSystem
should move a child entity of the editable text according to the current position.The blinking of the cursor would happen by using a
Blinking
component with a rate: f32 field in conjunction with aBlinkSystem
that would be adding and removing aHiddenComponent
over time.Selection
I already wrote quite a bit on selection in previous sections, and I didn't fully think about all the ways you can select something, so I will skip the algorithm here and just show the data.
Element re-use
A lot of what is currently in amethyst_ui looks a lot like other components that are already defined.
UiTransform::local + global positions should be decomposed to use Transform+GlobalTransform instead and
GlobalTransform should have its matrix4 decomposed into translation, rotation, scale, cached_matrix.
UiTranform::id should go in Named
UiTransform::width + height should go into a Dimension component (or other name), if they are deemed necessary.
UiTransform::tab_order should go into the Selectable component.
UiTransform::scale_mode should go into whatever component is used with the new layouting logic.
UiTransform::opaque should probably be implicitly indicated by the Interactable component.
I'm also trying to think of a way of having the ui elements be sprites and use the DrawSprite pass.
Defining complex/composed ui elements
Once we are able to define recursive prefabs with child overrides, we will be able to define the most complex elements (the entire scene) as a composition of simpler elements.
Let's take a button for example.
It is composed of: A background image and a foreground text.
It is possible to interact with it in multiple ways: Selecting (tab key, or mouse), clicking, holding, hovering, etc.
Here is an example of what the base prefab could look like for a button:
And its usage:
Ui Editor
Since we have such a focus on being data-oriented and data-driven, it only makes sense to have the ui be the same way. As such, making a ui editor is as simple as making the prefab editor, with a bit of extra work on the front-end.
The bulk of the work will be making the prefab editor. I'm not sure how this will be done yet.
A temporary solution was proposed by @randomPoison until a clean design is found: Run a dummy game with the prefab types getting serialized and sent to the editor, edit the data in the editor and export that into json.
Basically, we create json templates that we fill in using a pretty interface.
Long-Term Requirements
Crate Separation
A lot of things we make here could be re-usable for other rust projects.
It could be a good idea to make some crates for everyone to use.
One for the layouting, this is quite obvious.
Probably one describing the different ui event and elements from a data standpoint (with a dependency to specs).
And then the one in amethyst_ui to integrate the other two and make it compatible with the prefabs.
Remaining Questions
If you are not good with code, you can still help with the design of the api and the data layouts.
If you are good with code, you can implement said designs into the engine.
As a rule of thumb for the designs, try to make the
System
s the smallest possible, and the components as re-usable as possible, while staying self contained (and small).Imgur Image Collection
Tags explanation:
The text was updated successfully, but these errors were encountered: