godotengine / godot-proposals

Godot Improvement Proposals (GIPs)
MIT License
1.12k stars 69 forks source link

Replace ParallaxBackground and ParallaxLayer with different workflow #8779

Closed markdibarry closed 6 months ago

markdibarry commented 8 months ago

Describe the project you are working on

A faux lighting system using dithering and masking sprites.

Describe the problem or limitation you are having in your project

While I was making sure my lighting system supported camera rotation, I noticed the parallax nodes in Godot doesn't, which would prevent a lot of useful scenarios for my system. I noticed ParallaxBackground inherits from CanvasLayer but doesn't behave as a CanvasLayer is intended. It also has to jump through some hoops to adhere to the camera's transforms, but even then doesn't support rotation, and has tons various bugs and stale issues that date back to early 3.x. Even if these bugs were fixed, it inherently requires a strict hierarchy of ParallaxBackground -> ParallaxLayer, making it impossible to modify background layers with non-parallax backgrounds, or even by themselves as a group.

Describe the feature / enhancement and how it helps to overcome the problem or limitation

Instead of inheriting from CanvasLayer, it should be either a Node2D or Sprite2D, and be a standalone node. This would allow for automatic transforms adhering to the current camera, and more flexibility (you can always just put it inside a CanvasLayer if you really want it to be). This also means faster and less confusing setup, the ability to group the layers (in a CanvasGroup or otherwise), and possibly better performance on mobile (someone in rendering, please correct me if I'm wrong). Parallax is a common point of confusion for the community, so it'd be great to have an easier to use system.

Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams

There are two avenues. One being Node2D as the base, and the other as a Sprite2D.

On the side of Node2D, it would be a bit more work behind the scenes, and require some more manual setup on the user's side, but you could use each as a layer where you could put multiple sprites. A noticeable drawback it would have is issues with nesting (like ParallaxLayer currently has), with unpredictable results.

Sprite2D, however, can allow for a lot of quality of life shortcuts already taken care of, like defaulting its region rect to be large enough for the current current camera to handle scrolling and rotation without texture cut-offs. The drawback is that you'd have to setup a separate one for every texture in the same "layer", but it would be dead simple and fast to setup. At least much more so than the current system. Plus, with Parallax you'd have a lot more backgrounds with different settings than the same.

Personally, I think it'd be better for the Sprite2D "it just works" approach.

Here's a video of an example scene using a ParallaxSprite2D node while rotating the camera:

https://github.com/godotengine/godot-proposals/assets/21325943/015b9552-011b-43bf-a748-f4b476ca0905

Here's an example of multiple ParallaxSprite2Ds being used in a CanvasGroup:

https://github.com/godotengine/godot-proposals/assets/21325943/579b4972-36fe-455e-b155-2835c2406609

The options currently are: image

Repeat horizontal/vertical repeats the image as you scroll, but you can override the RegionRect if you need it to be a different size/repeat point.

With the scroll at 0 for either x or y, it follows the camera. Between 0 and 1 it moves slower than the camera. At 1, it moves as if it were a normal Sprite2D. At >1 it moves faster, as you'd expect.

Limits behave similar to ParallaxBackground, where it'll stop scrolling when the edge of the camera reaches the specified limit.

If there's any features or cases I haven't considered, please let me know. If there's interest in this, I'd be happy to make a PR for this to replace the current parallax nodes.

If this enhancement will not be used often, can it be worked around with a few lines of script?

It cannot, unfortunately. I don't mind making a node for my own use, but while I'm at it, I'd be glad to share and fix the current system.

Is there a reason why this should be core and not an add-on in the asset library?

ParallaxBackground and ParallaxLayer are core nodes.

KoBeWi commented 8 months ago

I also had problems with ParallaxLayer and opted for a custom solution, which made me realize how difficult it is to properly setup a parallax. My main problem is that scenes in my game are closed rooms (it's a metroidvania). The background has to work in different configurations of room sizes and I also added offsets between maps (so e.g. the same background can be slightly moved from the origin on different scenes). It was generally pain to work with, because I basically had to check if there is no gaps on every scene. I eventually made a custom tool to make editing easier:

https://twitter.com/KoBeWi_/status/1377324816118673408

With the ParallaxLayer it's basically impossible to see if your background will fit properly in game, without running the game.

If there's any features or cases I haven't considered, please let me know.

Not sure how commonly useful is that, but in my system I have what I call "positional repeat". It allows repeating small sprite by wrapping them when they go out of screen. In your case, you could separate the clouds into a single cloud sprite and they'd repeat in the formation they are placed.

markdibarry commented 8 months ago

@KoBeWi These are some good ideas! Making it not inherit from CanvasLayer makes it a better base-system to start with, but I think it'd be great to try to get it as feature rich without over-bloating it as possible. Two things I want to make sure is supported is animated sprites, and non-repeating backgrounds.

In my current setup, Repeat Vertical being disabled shouldn't show any positional difference, but it would not repeat (like in the case of the tree-top layer). Instead of modifying the sprite's Region offset, it leaves that at 0 and modifies the Sprite2D's Offset. In both cases, setting the position of the sprite will be respected as offset as well, but you've got me thinking that it'd be super easy to use the Region in both cases, but to just disable the texture repeat, so in the case of your clouds, as long as it's wider than the screen (preferably even after rotating), it should repeat off screen without the need for a large texture.

You got me thinking about your closed room scenario. I'd love to see an example or two where this has been a real problem so I can visualize it better. Something that'd be cool is to have a bit of a guide for is where you want it to scroll from a specific start to specific end, where maybe at the start of your level (0,0) a pyramid in the background is all the way on the right, but by the end of three rooms (10000,0) it's all the way on the left. If there isn't a way to visualize that in the editor, making it clear what some offsets do would make for a better workflow for many.

Even cooler (and this is where I worry about bloat, though seems like a fun experiment), would be to figure a way to support only repeating for a certain stretch. Currently, mirroring how the current Parallax system works, the limits mean when you reach a specified global position with the camera it'll start following the camera like it's stuck. I'm not sure how I'd make the edges of the textures work, but maybe we could make it work as a regular sprite, but between a region work as parallax. That may be to complex, but if I could figure a good workflow, it'd be neat.

Keep the suggestions (and criticisms) coming!

KoBeWi commented 8 months ago

Something that'd be cool is to have a bit of a guide for is where you want it to scroll from a specific start to specific end, where maybe at the start of your level (0,0) a pyramid in the background is all the way on the right, but by the end of three rooms (10000,0) it's all the way on the left.

In my previous project I had a similar approach. My parallaxes were basically all single layer and they were either infinitely looping or constrained to the room. With the latter the idea is that your image is bigger than camera limits and it interpolates from left side to right side as the camera moves. However, since the interpolation moves across the whole room, the speed will differ depending on room size. That, plus it won't look that good with multiple layers as fitting them perfectly would be problematic.

However, maybe I could revise this approach and just assign positions at top-left and bottom-right corners of the room for each sprite 🤔

If there isn't a way to visualize that in the editor, making it clear what some offsets do would make for a better workflow for many.

I think the main problem with visualizing in the editor is that the camera is not constrained. In game there is a constant zoom and limits are applied

I'd love to see an example or two where this has been a real problem so I can visualize it better.

Example of a broken background (notice the gap at the bottom):

https://github.com/godotengine/godot-proposals/assets/2223172/3ab2c94a-4038-4c0e-805b-ebc5a37bb387

Same background in another. Less verticality and higher ground level, so no gaps appear:

https://github.com/godotengine/godot-proposals/assets/2223172/846cd043-466e-4173-8bdf-a299faf25333

markdibarry commented 8 months ago

Hm. I'll need to spend a bit more time to fully flesh out my thoughts, but for now, my first idea is being able to specify a start and end position for textures with a non-repeating axis, and it would auto handle the scroll scale. For example with your texture, you'd specify the limit end for the y axis at the point you want it to stop scrolling (Like you'd already normally do. The bottom of your level or wherever in this case), and then you'd set the y offset you want your texture to be at at that point. Same for the top limit. Now it'll scroll horizontally repeating at the speed you set, but the vertical will only scroll the length you specify for the height of your room.

Maybe there's a cleaner way to have the export options, but I think that'd do the trick.

markdibarry commented 8 months ago

@KoBeWi I had some more time to work on this and playing around with some ideas for guides. I think it might be beneficial to see where the start and ending point (scroll range) will be in-game for a non-repeating texture when you adjust the scroll speed:

https://github.com/godotengine/godot-proposals/assets/21325943/1a226a43-5e8b-4f12-9e1f-db5fca370244

This is nice, but usually you want to make sure that it scrolls from point A to point B. Say I want the image's scroll range to be from the first set of blocks to the second set:

https://github.com/godotengine/godot-proposals/assets/21325943/9b848753-3bbb-406c-9b91-62a330c86526

Thinking of more stuff and how to visualize repeating textures. Please provide any feedback for I could improve this or simplify it to convey the idea better.

KoBeWi commented 8 months ago

That looks interesting 🤔 Though if you wanted to preview multiple sprites, it can be unreadable I guess. Also, the starting and ending points are in relation to what? Normally I'd use camera limits, or something like "expected camera limits" (in case it needs to extrapolate).

markdibarry commented 8 months ago

Though if you wanted to preview multiple sprites, it can be unreadable I guess.

Well the guides currently only show when that item is selected, and only if you have guides enabled, so it shouldn't be an issue.

Also, the starting and ending points are in relation to what?

I tried to show how it relates in-game in the video, but I didn't do a good job. 😓 The leftmost line is when the texture will first appear onscreen when scrolling using the current camera, and the rightmost line will be when the texture will go offscreen. Keep in mind that these guides are just for when you don't have repeat mode on, so this would be for a non-repeating texture you just want to scroll at a different speed. I also have added a feature so that when you set the limits you can decide if it freezes (like in godot's current implementation), or returns to normal scrolling speed. I wasn't sure about also displaying transparent textures, but it seems like it would help with the visualization. Not sure if others think it's better with or without.

KoBeWi commented 8 months ago

I did a quick attempt at implementing something. A background node which moves its children within boundaries. Each child can define beginning and ending position used for interpolation and you can preview it normally in the editor (yellow line is the boundary).

https://github.com/godotengine/godot-proposals/assets/2223172/fa974504-1964-4d12-a905-e6159fce4bb8

(it would be better with a quality sprites, but eh)

If you look closely, the position property of Sprite2D is hidden, because it's fully determined by the background. Though maybe a dedicated editor would be useful make adjusting position easier. Another feature that I could add is making the sprites repeatable per axis, including a spaced repeat (positional repeat in my earlier comment). Though Sprite2D is insufficient for that and the node requires custom drawing.

markdibarry commented 8 months ago

I read over and watched the video a few times and it looks interesting, but I'm having trouble understanding what's going on. How do you set things to scroll at different speeds, and how is the editor visualizing this? Maybe you're right I just need to see a different example texture.

You brought up a few of the cases I wanted to handle. Here's what the current godot system supports:

Things the current godot system doesn't support but would be ideal:

Making it into a Sprite2D should fix most of the issues, but it removes the ability to have multiple on the same layer, but that was never a big benefit, and it did have performance issues. I think parallax is going to be hard to visualize no matter what, but at least some guides would remove some of the confusion around it. However I do want to support spaced repeat like you said. I don't mind rigging up some custom drawing, but I want to avoid having a custom shader if I can help it. Those are always a headache in core nodes. Though, it might be better to have the guides be part of the editor than the node itself, if it requires a lot of editor hint checking.

I'm going to keep working on it and report back.

KoBeWi commented 8 months ago

How do you set things to scroll at different speeds, and how is the editor visualizing this?

The speed is set indirectly. You define the position at the beginning and at the end of the boundary and the images are interpolated. Here's some visualisation: image Yellow is the boundary, green is game's viewport (screen size). The beginning is the screen at top-left corner, the end is the screen at the bottom-right corner. The red line visualizes interpolation (though X and Y are separate).

Normally you'd want the boundary to be equal to camera limits, but if the camera goes outside, it can extrapolate. When in editor, the green rectangle is the editor's viewport. The background interpolates based on the view position. The problem with the editor is that the camera can easily go outside the boundary, but it still gives some insight on how the parallax will behave in game.

Here's the scene from my video: BackgroundExperiments.zip (the script can be better optimized, I just threw together a prototype)

markdibarry commented 8 months ago

I decided to go at it from a different angle, since I realized that I'd have to make a ParallaxAnimatedSprite2D and so-on, which seems horrid. Instead I decided to try to fix the system, where I just removed ParallaxBackground and reworked ParallaxLayer and the renderer to be able to handle the mirroring without the need for ParallaxBackground or it to be a first child of a CanvasLayer. I haven't added any guides yet, but as of right now it looks like it has feature parity with the current system, plus a bunch of extra stuff just from it not being a CanvasLayer. Seems to be working well with CanvasGroup. I'll post more updates when I have them.

https://github.com/godotengine/godot-proposals/assets/21325943/896f7938-2be7-400e-95ec-3bb685fdf203

KoBeWi commented 8 months ago

Ok I finished my custom parallax. It supports both constrained and repeated mode, with lots of customization available. Here's how it appears in the editor. The red rectangle simulates the camera view:

https://github.com/godotengine/godot-proposals/assets/2223172/f1ecea91-f2cc-4711-82e0-ed4ffe00cadf

I didn't test it with zoom and rotation though.

The scripts: CustomParallax.gd.txt ParallaxSprite.gd.txt

I might publish it as addon once I test it in my project 🤔

EDIT: It's useless. Tying scrolling speed with map size is a bad idea, at least for multilayered backgrounds.

markdibarry commented 8 months ago

@KoBeWi Yeah, I don't think there's a reasonable way to visualize it all within the editor. I think if there are at least small reliable guides, that's better than nothing. I made a draft to at least start from where I remove the need for the two node combo, so I'm going to explore some ideas with guides on that. I also want to think about the zoom-out mirroring, but not sure how terrible it'd be.

KoBeWi commented 8 months ago

No, my system is super accurate and what you see in the editor is what you get in game, it's just the constrained view in this form does not work well in practice.

I made a video of how it looks in my project:

https://github.com/godotengine/godot-proposals/assets/2223172/da7ec514-8b29-4d47-b5d6-b66a3d71321b

Look e.g. at the clouds. They move very fast vertically with the camera and it looks unnatural. I'll try making it anchored to the screen instead of the global bounds, maybe it will improve things.

The looped scrolling works quite well though (horizontal movement in the video above).

markdibarry commented 8 months ago

Oh you got it working! Looks actually really cool just in the editor itself. I imagine you have to reset the positions? Or how does that work?

KoBeWi commented 8 months ago

I have a separate set of properties that control the position. The position of Node2D is unexposed via _validate_property().

hsandt commented 5 months ago

Just to confirm: I'm having troubles with the current ParallaxBackground/Layer system which either places all the sprites behind or above my object scene objects (even at same Layer value, changing Z Index cannot make certain layers/objects go to the other side).

From what I see, the new Parallax2D node is just a Node2D and therefore would use the same Z Index system as other sprites, allowing me to freely put parallax objects in front of / behind other scene objects at will. Is this correct?

If so, you can add this as an extra benefit in the first post of the PR!

I've recently compiled Godot on my other machine so I must already have this feature without even knowing it. I'll check it next time and see if it can solve my problem! It's just for an extra visual effect, so I can wait for Godot 4.3 to be released to use it for the actual game.