Pixel perfect games in Godot #9256
Replies: 162 comments 10 replies
-
Little update for anyone stumbling upon this via search engines: Reddit user /u/golddotasksquestions provided an excellent step by step write-up on how to combine low res UI with low-res games using viewports in 4.0. As you can see, the process is pretty janky and unintuitive by Godot standards- especially for such a common practice in indie games. It's also worth mentioning that this probably won't help with smooth camera movement or alleviating jitter unless you're willing to implement a cocktail of features on top of it such as physics interpolation, custom camera logic, as well as a pixel buffer. I've tried everything I could find so far and haven't had much luck, but your mileage may vary. My biggest concern about this whole process is the amount of new users that are going to inevitably google something as basic as "how to set up pixel perfect smooth camera in godot 4" and be faced with 3 or 4 results consisting of wildly different workarounds that don't even work anymore. For that, I think this is a pretty crucial usability issue that needs to be addressed in some shape or form. |
Beta Was this translation helpful? Give feedback.
-
Can you upload the project that was used to create the video somewhere? This could be useful to try to make it work correctly. Also, was the video recorded with integer window scaling (or a sharp bilinear shader applied on a SubViewport)? |
Beta Was this translation helpful? Give feedback.
-
Working on cleaning up the project now. The video was recorded with manual integer window scaling; aka setting the project resolution to 1920x1080, setting the subviewport size to 640x360, and setting the subviewport scale to 3x. Bypassing the subviewport scale entirely and using the built in editor scaling in the settings achieves the same effect. |
Beta Was this translation helpful? Give feedback.
-
The current workaround with sub viewport is not optimal, as the editor only displays content in the bounding box of the sub viewport and it is not possible to drag & drop nodes into the scene. Having a large scene (e.g. TileMap) is very difficult to edit/deal with on a pixel perfect setup. The workaround is to navigate to the scene inside the SubViewport directly and edit the level from there (needs to be its own scene file). Especially for larger games this process is painful. PixelPerfectCamera2D hopefully addresses that issue, just like Camera2D from an editor perspective, so the scene itself is still editable/fully viewable. |
Beta Was this translation helpful? Give feedback.
-
This issue encompasses far more individual issues than what the title suggests. The viewport-upscaled-to-screen technique still works just fine in Godot 4.0, for both 3D and 2D, so this convention is untouched. For @bitbrain's problem, this is easily solved by a runtime Autoload script which arranges the SceneTree to handle your viewport setup, so that it's not part of your scenes at all. You'd want autoloads in a larger project that needs to manage game state in a consistent way anyway (think scene transitions, persistent state across scenes, networking, etc). I usually set up a global autoloaded state machine that bootstraps the selected play-in-editor scene into itself to manage the game while still being able to start from a specific scene, 2D or otherwise. The "smooth 2D camera" technique is actually fairly specific to very small viewport resolutions and the specifics of how it's implemented could practically vary between games (custom camera solutions?). I'm not sure if I would want such an effect for something that is mimicking a real retro console. This specific method only applies to a particular subcategory of a category of 2D games and thus I don't think it makes sense to have it as a core feature. For coordinate snapping, I use this script in addition to the vertex snapping option, and it mostly works fine. In combination with Nearest filtering, you will get about 90% of the way there. Counterintuitively, you do not want to snap the Camera2D's coordinates when using this approach, otherwise you will get the jittering seen in those issues because the rounding directions across different items will not always match. extends Node2D
class_name PixelSnapped
# usage: make this node the parent of a CanvasItem you want snapped to worldspace integer boundaries
# only apply this to purely visual node hierarchies
func _process(delta: float) -> void:
var parent = get_parent()
if parent == null:
return
position.x = round(parent.global_position.x) - parent.global_position.x
position.y = round(parent.global_position.y) - parent.global_position.y The editor failing to snap to integer coordinates in some scenarios is definitely a bug. If you need to, you can write tests in your game's test framework to verify that scenes have no nodes on subpixel coordinates, which will catch any project member's mistakes too. I can't imagine how having collision shapes snap to pixels would work. Not even retro console games did this; they often used fixed point arithmetic ("subpixels") when integrating physics, but didn't allow static objects to exist at subpixel coordinates. Changing this would require a completely different physics model. A given project might want that, but I don't think this makes sense as a core feature. The problem with sprite texture filtering exists regardless of engine. In Godot 4 you can set the default filtering to Nearest No-Repeat to alleviate this, but you're still ultimately on the hook to ensure your materials have the proper filtering settings. Once again, since Godot is general purpose, there are things you are ultimately responsible for deciding how to accomplish. Overall I feel the story here is that Godot is a flexible engine serving a lot of interests and there is no one-size-fits-all option when it comes to rendering in 2D (IMO there never has been, but Godot comes closer than any other option I've seen). This is a problem that existed before 4 and will continue to exist forever. Asking it to serve a specific niche like this introduces a lot of unnecessary complexity to the core. |
Beta Was this translation helpful? Give feedback.
-
@HybridEidolon a GDExtension providing a PixelPerfectCamera2D node can always be an alternative, in case it is a too specific use case for Godot itself. However, I am not sure if the Godot C++ API allows for that (yet).
I would not call this problem niche, considering how many pixel perfect games are out there. I have no quantifiable data to back this up, though. EDIT @HybridEidolon a bit offtopic but could you share an example of how you implemented this?
|
Beta Was this translation helpful? Give feedback.
-
@HybridEidolon I would like to clarify that the list I included in the post isn't a collection "changes the core engine needs" or "things that need to be fixed", it was just a list of factors that the end user has to battle with when approaching a common use case like this. Some of them may or may not be impacted by open issues, which is why I linked them as they only add to the complexity. This list has only grown since 3.x, which complicates things considerably.
I mentioned this specific implementation because it covered a wide variety of use cases in 3.x, but is no longer as effective under identical conditions in 4.0. Like I said, it isn't perfect and is in no way a comprehensive solution. However, I did find success in tweaking it for a wide variety of different resolutions for what it's worth.
I'm not suggesting that it specifically should be implemented into the engine as a core feature, it's just an example of one of many common techniques that previously worked. Whether or not it's a common use case ultimately comes down to opinion, but that still doesn't impact the issue of complexity that many will have to deal with. In terms of the other suggestions you made such as using an autoloaded state machine to dynamically arrange and handle viewports or using a script to handle vertex snapping- I would personally argue that these are cumbersome workarounds that shouldn't be required in the first place. These aren't just practices for very specific and niche scenarios, these are practices that apply to pixel perfect games as a whole- which highlights the whole usability problem even more.
This isn't a matter of asking "I want Godot to implement a core engine feature that that fits the exact needs of my game". It's a specific example of usability that wasn't great to begin with that was worsened in 4.0. Adding complexity to the core is the opposite of what I'm suggesting- this is more about improving usability in general. Additionally, it's not just the camera smoothing, it's that the Camera2D and viewport nodes are just flat out frustrating to work with in pixel perfect games. There's too many points of failures and long-standing "unsolvable" issues that have only worsened in 4.0. If these usability issues are inherent to the engine and will continue to exist forever, then a dedicated solution makes sense in my opinion. That goes beyond just having a smooth camera. At the end of the day, the workflow for working with pixel perfect games is significantly more complicated in 4.0, and borderline unfeasible for more specific use cases like the one I initially described. The line between user error and unintended behavior is far too blurry at the moment, as issues with jitter seem to affect low-res games across the board to some degree. I now realize that this problem likely goes beyond what I initially created this issue for. If I could guess, it's probably a cacophony of bugs, user error, and lack of documentation that all contribute to the same exceedingly complicated problem. However, I still think it's a crucial issue that needs to be addressed in some way. |
Beta Was this translation helpful? Give feedback.
-
Niche in the sense that it has very specific rendering requirements that don't gel well with any other "style", not necessarily in popularity. This has always been a difficult problem with modern rendering and will continue to be. Even moreso for super low-res art.
I don't have an example on hand, but the pieces you need are described in the documentation. The SceneTree's root node has 1 child node which is the "current scene" and then all of your configured Autoload child nodes when the game starts. You can then move the nodes around however you need them arranged at runtime and implement your own "change scene" functions to accommodate. In Godot 4, the SceneTree root node is a Window. It may help to open the remote scene tree debugger while the game is running to visualize how the root hierarchy is arranged.
The former is what makes Godot uniquely powerful compared to contemporaries and I disagree that it is a "workaround". No other engine I've seen gives you as much control over the runtime scene tree as Godot. Learning how these tools work makes Godot significantly more powerful than it initially appears. The current documentation is a little sparse, but it does point out this flexibility. The latter is just one method of implementing what you need; you could perform the same logic by putting all your snapped visual nodes into a SceneTree group and iterating over them in an Autoload so you don't have to litter your scene configurations with these nodes. I would favor solutions that don't require specialized node setups if I was starting a project today, and Godot absolutely grants you the power to do that.
While these tools require a deep understanding of Godot's scene model to work well for 2D pixel art, it is also a massive boon to the engine that they are individually simple and flexible and have relatively unsurprising behavior.
They're inherent to any engine that uses real numbers, triangle rasterization and a scene graph to represent the rendered world. The linear algebra becomes less intuitive when you start needing to round numbers at specific points. There is inherent complexity introduced by being able to "attach" sprites to other sprites and apply linear transformations to compose the scene. You would experience these same sorts of issues in a hand-rolled engine if you were using hardware rendering too. In some ways 2D pixel art games are easier done with simple masked blitting against a framebuffer and a flat array of "objects" with update and draw callbacks (which I'll add, you absolutely can do in GDScript if you really wanted to). I think the problems outlined here are ultimately solved by better learning material and a template to demonstrate how to do it correctly. It is absolutely possible to get Godot 4 to do 2D pixel art well without significant hurdles; I have not really experienced regressions relative to Godot 3 in this respect, even porting Godot 3 projects into 4. Maybe I could do a write-up on what is specifically needed and why certain issues occur when implementing it. |
Beta Was this translation helpful? Give feedback.
-
While I do completely agree with this, I still think the elephant in the room is the lost functionality from 4.0. It's not that making pixel perfect games are too hard, it's downright infeasible after a certain point as far as I can tell. There's just too many new issues that go far beyond this feature suggestion unfortunately. |
Beta Was this translation helpful? Give feedback.
-
Here is a project to serve as a test case: PixelPerfect.zip
Notably, no scripts are needed to snap transforms on pixel alignment. This is a departure from approaches I've used for Godot 3. Everything became a lot simpler when I stopped trying to hack in my own solution! For the most part, the behavior of the snapping is exactly as desired. The only thing that isn't correct is when the player begins moving for the first time, there are noticeable rounding issues between the camera and the player sprite, but as soon as the player's Y changes, this disappears entirely. I think that is probably a rounding issue in the canvas renderer. As far as I'm aware, this is basically identical to Godot 3.5, except that Godot 4 also grants us the ability to control the snapping settings on the SubViewport node rather than the entire project, which is a pretty significant improvement. Hopefully this can serve as a useful basis to implement the smooth scrolling behavior described above; the actual Camera transforms aren't affected by scripts, so their global positions can be used in a shader on top of the viewport texture. |
Beta Was this translation helpful? Give feedback.
-
@HybridEidolon great solution right there! However, I was wondering how we could achieve "smooth" camera movement with this (e.g. camera following the player smoothly, with everything still pixel-perfect but the camera itself is not stuttering). A great example is Celeste: https://youtu.be/qyOapJgLcEI?t=997 they have pixel-perfect viewport (all the pixels always align on the screen) but the camera is smooth. I tried to do it with the example you attached but the camera stutters once I enable position smoothing. |
Beta Was this translation helpful? Give feedback.
-
You need to make sure your camera is updating at the same frequency as the physics (i.e. switch the camera process callback to Physics) |
Beta Was this translation helpful? Give feedback.
-
Thank you for the suggestion. However, this doesn't really achieve the desired result. However, although not the perfect solution, this was a lot easier to achieve in the earlier version, and I agree with everyone else here that this is something that would be really useful if made easier and more straight forward, as most of 2D indie games are pixel-art games. |
Beta Was this translation helpful? Give feedback.
-
The camera is not on subpixel alignment in Celeste. The camera appears smooth because the resolution is relatively high and the camera is much more complex than the built-in smoothing option and doesn't directly lag behind the player in most situations.
This is still possible and there are no regressions preventing this. All you need to do is apply a shader to a quad using the SubViewportTexture and displace it by its UVs, with the SubViewport being a few pixels larger than the target resolution. Same as Godot 3. |
Beta Was this translation helpful? Give feedback.
-
Did anyone manage to get this working? |
Beta Was this translation helpful? Give feedback.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
This comment has been minimized.
-
@c0d3r9 Your comments are marked as off-topic because they don't respect the Godot Code of Conduct. To cite the code of conduct:
|
Beta Was this translation helpful? Give feedback.
-
Hi all! I know I may be late to the party on this but I came up with a simple integer lerp solution for jitter-free position smoothing in Godot 4. This isn't perfect but it works well enough (needs to be called from static func lerpi(origin: float, target: float, weight: float) -> float:
target = floorf(target)
origin = floorf(origin)
var distance: float = ceilf(absf(target - origin) * weight)
return move_toward(origin, target, distance) |
Beta Was this translation helpful? Give feedback.
This comment has been hidden.
This comment has been hidden.
-
As per @bitbrain suggestion on Twitter, I converted the issue to a discussion, so people that used to follow it will be able to continue to comment and discuss without it being tied to a proposal. |
Beta Was this translation helpful? Give feedback.
-
Wow, I never thought that one of the most requested feature could be closed as "not planned" by the team... Many people came up with very different needs, but at one point, the request remained simple to me : allowing the camera to be smooth like in the video and being able to move the camera at the level of "real pixels of the screen" like it was possible to do it with a complex mix of codes and nodes, but still not stable through the different realeases of godot. I find very harsh to close it. Even if I understand the decision because of the conversation that was going nowere, I find it very controversial to close something so requested by the community, and to gather votes and then say to those many that have voted that they won't be heard. I respect the choice, but this is hard to admit that maybe it will never be possible to have a really smooth camera in pixel perfect games due to this decision, although many people wants to make pixel art games with godot. |
Beta Was this translation helpful? Give feedback.
-
If smooth jitter-free scrolling for pixel art games is indeed solvable with an extension of the Camera2D node or a script that does some magic with My understanding is that this proposal existed exactly because a GDScript solution was not possible, so I am quite curious of how it would look like. People just want their stuff to work - it doesn't have to be built-in, so if someone can show such a script I am sure all complaints would be quenched. :) |
Beta Was this translation helpful? Give feedback.
-
@jordanlis The proposal wasn't closed because it wasn't popular enough or the feature wasn't desired. It was because the proposal didn't propose a viable way to achieve the request, and most of the conversation was off-topic and not leading us any closer to it being solved. The suggestion was to create an all-in-one node, but the consensus was that it isn't currently feasible. The effect they're creating is very complex and the only example provided requires:
I'm not sure how all of that would be possible in one node. Personally, I actually would love to see this as a possible feature or plugin, but no one has provided one or proposed a way to make one yet. If there is a viable proposal, I'd be happy to help work on it. In the meantime, until a solution is devised, we've been talking about including documentation and examples for different pixel art setups, since this is widely misunderstood subject. I'd love to champion anyone who'd like to step up and make a tutorial or template for making this effect in 4.x. |
Beta Was this translation helpful? Give feedback.
-
@jordanlis I feel that pixel perfect graphics in Godot is a solvable problem and I'd like to personally see it through, as this impacts me as well. At the same time, we need actionable items in order to make progress. If you can think of any specific issues, please feel free to mention them here so that we can track these efforts. |
Beta Was this translation helpful? Give feedback.
-
When I first started following this topic, I was mostly interested in two improvements:
As far as I can tell, with minimal work this can now be achieved in 4.3dev4 with results like so: 2024-03-07.16-16-49.mp4Project from #84380 Note: The camera sprite looks like it jitters around only because the actual camera is following the sub-pixel position of the sprite and the sprite is snapping to the closest pixel for the render. Good camera smoothing example in the next video. 2024-03-07.16-21-39.mp4Game project here. Note: The box movement while it's being pushed here looks a little jittery because it moves on different frames than the player does, which can be resolved with proper pixel-perfect physics coding(I think). The steps I took to make these changes in 4.3dev4:
And that's it. There are some caveats, like how lines and other primitive draw shapes will not be pixelated anymore, but I wasn't using those anyway, and you could probably give them a pixelated effect using shaders too. I'm not sure if this will help anyone else or if it's what people want from the engine for their pixel-perfect games, but it works for what I want to do, so I thought I'd share. |
Beta Was this translation helpful? Give feedback.
-
I'm perfectly fine with this, as the point of the proposal was to spark a discussion about what could be done and to get more attention on an issue that was, admittedly, very difficult to effectively communicate or replicate. The only thing I (now) disagree with is the idea that we're not any closer to it being solved. If #86837 is any indication to go by, there has been a great deal of progress on tackling this problem and pixel perfect workflows are as viable as they've ever been in 4.x. Awesome! Despite all the bellyaching I've done in this thread, I can't say thank you enough to the core engine developers and contributors working on tackling this. For anyone with specific & ongoing issues that are finding themselves here via Google, take a look at #86837 (not a place for discussion though) |
Beta Was this translation helpful? Give feedback.
-
Been following this thread for a while. Really appreciate the work that you've all done. Unfortunately I'm still coming up short when trying to get pixel perfect movement. As far as I can tell, I have it down to 2 configurations which each produce the opposite problem: Both Scenarios:
Config 1: Low Res + Stretch
When moving, this produces a smooth character, but a jittery background: low_res.mp4Config 2: High Res + Zoom
When moving, this produces a jittery character, but a smooth background: high_res.mp4I see some people seem to have gotten it working with, I think, either one of these 2 configurations. So I'm wondering what I could be missing. Hope this contribution is helpful. |
Beta Was this translation helpful? Give feedback.
-
There a two major issues in Godot that causes these issues. First one has been mentioned, which is the Position smoothing of Camera2D. This is because by default the smoothing happens outside of the Physics loop. You can resolve this issue by changing the Process Callback to "Physics" instead of "Idle" on the Camera2D node Inspector settings. The second and biggest issue is pixel skewing that is caused by Transforms not being perfectly pixel snapped. So neither the Camera2D zoom, nor the var saved_size := Vector2(0, 0)
var desired_height :=1440.0
@onready var root_node := get_parent()
func _process(_delta: float) -> void:
var size = DisplayServer.window_get_size()
if saved_size != size:
saved_size = size
root_node.scale = Vector2(size.y / desired_height, size.y / desired_height)
|
Beta Was this translation helpful? Give feedback.
-
I would avoid doing anything that would modify the physics simulation in any way. Having to change velocity scaling based on zoom level would mean that players would get different results based on the camera zoom (or even the size of the window), meaning that some players' machines might run into hard-to-find bugs (not to mention players could intentionally change the scale value in order to cheese sections of the game). It also means having to decorate too much of the code with scaling values which violates DRY (same problem with doing physics code in variable step and putting delta everywhere). I think the best solution here would be to render the low-resolution "view" of the game world via custom viewport, scale this viewport to zoom the scene, and combine this with either |
Beta Was this translation helpful? Give feedback.
-
Describe the project you are working on
A low-res 2D platformer with a high-res UI, smooth camera movement, and zoom.
Describe the problem or limitation you are having in your project
In 3.0x+, the common method for making pixel perfect games with a high resolution UI was fairly straightforward. You would toggle the 2D stretch mode, set your project resolution to something like 1920 x 1080, and throw your game inside a viewport node at a desired base resolution (such as 640 x 360). If you wanted take it a step further and implement a smooth camera, a common solution was to use a framebuffer shader with custom camera logic. Although a bit cumbersome, it did the trick well enough.
In 4.0, it's no longer this simple. Implementing a high-res UI with a low-res game is doable, but every known method for smooth camera movement I could find no longer works as effectively in 4.0. Additionally, 4.0 introduces even more factors that impact how pixels are displayed in 4.0, each creating dozens of different combinations that produce vastly different, loosely documented, and often undesirable effects. While not all specific to 4.0, here are some factors I can think of:
Usage of integer scaling (not a feature in the engine, here's a community made option.)To me, this is a painful amount of variables to deal with for such a common use case. I have tried every combination of these options alongside upscaling shaders, pixel buffers, custom camera smoothing, viewport textures, and more. The resulting process is a maddening game of cat-and-mouse in which you're constantly balancing jitter, blur, and sprite distortion while never quite eliminating one or the other.
This is the closest I was able to get before throwing in the towel. I achieved smooth camera movement on the sides of the screen, but it introduces pixel distortion. Enabling pixel/vertices snap gets rid of the pixel distortion, but introduces blur on everything. I'm not sure which is worse, so I just avoided using it.
2023-03-01.20-40-03.mp4
Describe the feature / enhancement and how it helps to overcome the problem or limitation
The exact solution to this problem is complicated, as it is most likely a complex combination of intentional engine design, user error, and lack of documentation. However, I think a few things would help in this regard.
Before mentioning any in-engine solutions, it's worth mentioning that a detailed and precise collection in the docs outlining the best practices for pixel perfect games, especially in regards to smooth camera movement or zoom, would help a great deal without needing to touch the engine itself. It wouldn't fix the inherent issue of complexity here, but combined with some sort of "hybrid pixel perfect" starting template, it would be a decent remedy.
In terms of in-engine solutions, a few preexisting proposals such as integrated integer scaling would help alleviate this problem to some degree. However, I think a "PixelCamera2D" node or something similar designed specifically for this common use case would be wildly beneficial.
Describe how your proposal will work, with code, pseudo-code, mock-ups, and/or diagrams
An example would be a camera node called "PixelCamera2D" that would allow functionality for sub-pixel perfect smooth scrolling in 2D games. This concept isn't new or particularly complicated, and this write up by Daniel Ludwig is a perfect starting point for how it could be implemented. In essence, it would just be a standard camera with extended framebuffer capabilities that would be used for low-res subviewports. The issue with this approach is that it isn't perfect (especially with parallax) and doesn't sufficiently mitigate jitter or distortion in 4.0+ for some reason.
An alternate approach may be necessary that avoids using a pixel buffer entirely that utilizes the core engine to achieve smooth camera movement in some other way. This goes well beyond my scope, but definitely worth looking into.
If this enhancement will not be used often, can it be worked around with a few lines of script?
I imagine it would be used very frequently, at least by indie developers who work with pixel art a lot. It could very easily optionally just not be used.
Is there a reason why this should be core and not an add-on in the asset library?
Normally just implementing that method on a per-project basis or as an addon would be more than sufficient, but as previously mentioned, things have changed quite a bit. Here's a few reasons why I believe this should be a core feature:
Beta Was this translation helpful? Give feedback.
All reactions