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
A huge change of pyglet.shapes
#928
base: development
Are you sure you want to change the base?
Conversation
Here are my thoughts: There is quite a lot of duplicated code as a result of this design.
I imagine those using the Additionally, not all shapes which once had collision detection (by doing I suggest instead of having colliable shapes classes, you make them functions. For example, you can have a |
@Ross-TheBoss collision detection isn't mean
which are very difficult to implement and will be implemented later. Only considering circle, ellipse and rectangle, we will have 6 different ways to test whether they are collided or not.
So in the above condition, using function isn't a wise decision. A
|
I'll try to post full comments on the code later today. Re: Ross-TheBoss
To some degree, this is unavoidable for any performance-optimized Python code. Inlining helps avoid the high cost of function calls and
This could be a good choice, see my next response:
For the already-existing shapes or anything that's vertex-based, this is actually simple: we don't have to update anything because we can delegate Re: PR / zhengxyz123's commentSome of this PR is very good, but may need to be split up like #/927 was. I'll try to post more comments on that in the next few hours. In the meantime, here's a recap of what I posted on Discord: The fundamental question: Is it worth it to implement multiple collider types if we can get away with using polygons? Remember:
Composable colliders could address this: # This is like any(), but for colliders
sector_collider = CompositeCollider(
PolygonCollider(*triangle_params),
CircleCollider(*circle_params)
) However, this raises other questions:
The easiest answer is to make the implementation limited and only support polygons. |
@pushfoo I don't entirely argee with you. The most important reason is that using polygons is too inefficient. Here is my solution: We(pyglet) just implement collision between polygons, but leave users a way to allow them to access more efficient algorithms(the |
here is what I think.
So in a actual situation, if I need some simple UI collision detect(btw, why would you need that?), I do will use this code |
Well, there are some situations and solutions:
So, for situation (3) and (4), a built-in collision system is useful. And for situation (5), create inheritances of If you read DESIGN for how pyglet should be, a collision system was suggested about two decades ago. But if this collision system needs to be connected to a physics engine in order to work, instead of providing some simple function, a part of the users will not accept it. And those who want to use it in a more complex and efficient application through a physics engine, we can also make an example to show them how they should start. In summary, there is nothing wrong with adding a collision system, and one can choose to use it directly or rewrite it for better performance. |
I now understand why the drawable and collision shapes should be seperate. But now I'm wandering if they are so seperate that the collision detection shouldn't be included within the shapes2d module. Collision detection for a shape is not neccessarily exclusively used by the drawn shape. For example, the CollisionRectangle could used with UI elements (such as buttons) and Sprites or even different shapes as an approximation for efficiency. In the design, it is suggested that collision detection is part of the mathematical
|
It's also interesting that you think differently than I do. But essentially, they are both 2D shapes, aren't they? It's just that one(specialise in drawing) is visible and the other one(specialise in collision detection) is not. You should also be aware that Differ from you, I suggest having the following modules:
3D is entirely different from 2D, especially in rendering and collision detecting(it's more complex than the 2D one). We should use a unique way to treat it, or your This PR will be merged in the |
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 apologize for the delay in reviewing this. Large PRs like this one make are hard to review properly. It's easier to review and merge them when they arrive in smaller chunks.
In short:
- Thank you for adding docstrings! I've provided instructions for a converter script below to help them match the project's preferred format.
- Like other reviewers, I also have concerns about the collision & shapes design which are outlined in the review comments and Enhancement: Read-only vertices property for shapes & sprites #939
- There some more minor code quality and organization concerns which are easier to address
I suggest doing the following while the collision design continues to be discussed:
- Split the changes to
customtypes.py
into a new PR. These should be quick to merge. - To save you the trouble of converting from rst format to the project's preferred Google style, try the following:
pip install docconvert
- Put this in
doc_convert.json
:{ "input_style": "rest", "output_style": "google", "accepted_shebangs": [ "python" ], "output": { "first_line": true, "remove_type_backticks": "false", "use_types": false } }
- Run this for each
path/to/file_using_rst.py
:docconvert --config --in-place doc_convert.json path/to/file_using_rst.py
doc/modules/canvas.rst
Outdated
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 do not think this should be deleted. This module's contents are still in use around the codebase, and the doc page for it still builds: https://pyglet.readthedocs.io/en/latest/modules/canvas.html#pyglet.canvas.Canvas
pyglet/customtypes.py
Outdated
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.
The changes to this file should be a smaller, separate PR since they're useful in general. I will also have further suggestions for the type annotations in that PR.
pyglet/extlibs/poly_decomp.py
Outdated
if len(cutEdges) == 0: | ||
return [polygon] | ||
|
||
if isinstance(cutEdges, list) and len(cutEdges) != 0 and isinstance(cutEdges[0], list) and len(cutEdges[0]) == 2 and isinstance(cutEdges[0][0], list): |
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.
How often does this need to run? I haven't benchmarked this, but the repeated use of isinstance
is going to make it slow.
pyglet/extlibs/poly_decomp.py
Outdated
# Copy from https://github.com/wsilva32/poly_decomp.property | ||
# License under MIT. |
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 know keeping the entire text of the license annoying, but this is the easiest way to meet legal requirements for most of the world.
# Copy from https://github.com/wsilva32/poly_decomp.property | |
# License under MIT. | |
"""Polygon decomposition helpers | |
Copied from https://github.com/wsilva32/poly_decomp.py | |
MIT License | |
Copyright (c) 2016 Will Silva | |
Permission is hereby granted, free of charge, to any person obtaining a copy | |
of this software and associated documentation files (the "Software"), to deal | |
in the Software without restriction, including without limitation the rights | |
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
copies of the Software, and to permit persons to whom the Software is | |
furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all | |
copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
SOFTWARE. | |
""" |
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.
Although this particular file may work, I'm concerned about its efficiency.
Can we explore caching results and other optimizations in future revisions?
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.
how about make it like
Number = Union[int, float]
and
RGB = Tuple[int, int, int]
RGBA = Tuple[int, int, int, int]
Color = Union[RGB, RGBA]
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 this is in response to the wrong comment / file, but I generally agree. I have further changes to suggest as well, but the types changes should really be their own PR.
pyglet/shapes2d/util.py
Outdated
|
||
|
||
def sat(polygon1: List[Point2D], polygon2: List[Point2D]) -> bool: | ||
"""An algorithm to detect whether two polygons intersect or not..""" |
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.
"""An algorithm to detect whether two polygons intersect or not..""" | |
"""Return whether two polygons intersect or not. | |
Args: | |
polygon1: The first polygon to test | |
polygon2: The second polygon to test | |
Returns: | |
Whether the two polygons intersect | |
""" |
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.
As I mentioned in the comments on drawable.py
, I think drawables shouldn't be obligated to rely on colliders.
If we do so, we may be able to significantly simplify the design here.
pyglet/shapes2d/collidable.py
Outdated
method = f"collide_with_{other.__class__.__name__}" | ||
if hasattr(self, method) and callable(getattr(self, method)): | ||
return getattr(self, method)(other) | ||
method = f"collide_with_{self.__class__.__name__}" | ||
if hasattr(other, method) and callable(getattr(other, method)): | ||
return getattr(other, method)(self) | ||
if hasattr(self, "get_polygon") and hasattr(other, "get_polygon"): |
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.
My instinct is there's a better general approach, especially since we'll have ugly method names. For the immediate future, we should try to make full use of these:
- The
:=
operator (3.8+) - the default value argument for
getattr
I'm not sure if encapsulating this as a function is worth it, so maybe something like this could be cleaner?
method = f"collide_with_{other.__class__.__name__}" | |
if hasattr(self, method) and callable(getattr(self, method)): | |
return getattr(self, method)(other) | |
method = f"collide_with_{self.__class__.__name__}" | |
if hasattr(other, method) and callable(getattr(other, method)): | |
return getattr(other, method)(self) | |
if hasattr(self, "get_polygon") and hasattr(other, "get_polygon"): | |
self_method_name = f"collide_with_{other.__class__.__name__}" | |
if callable(self_method := getattr(self, self_method_name, None)): | |
return self_method(other) | |
other_method_name = f"collide_with_{self.__class__.__name__}" | |
if callable(other_method := getattr(other, other_method_name, None)): | |
return other_method(self) | |
try: |
pyglet/shapes2d/collidable.py
Outdated
raise TypeError( | ||
"No collision detection method found between " | ||
f"{self.__class__.__name__} and {other.__class__.__name__}" | ||
) |
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.
continuing from line 68
raise TypeError( | |
"No collision detection method found between " | |
f"{self.__class__.__name__} and {other.__class__.__name__}" | |
) | |
except AttributeError as e: | |
raise TypeError( | |
"No collision detection method found between " | |
f"{self.__class__.__name__} and {other.__class__.__name__}" | |
) from e |
pyglet/shapes2d/collidable.py
Outdated
polygon1 = CollisionPolygon(*self.get_polygon()) | ||
polygon2 = CollisionPolygon(*other.get_polygon()) | ||
return polygon1.is_collide(polygon2) |
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.
For each of these shapes, we seem to be iterating over the points 3 times:
- when the vertices are initially created for the drawable shape
- when we call
get_polygon
here - when we copy them to the polygon instance
It would be easier to calculate vertex positions once and reference them as outlined in #939. If nothing looks wrong with that PR, I'll try to implement it in the coming days.
I'm still not sure of the current design for
I could go either way on this. With the changes proposed in #939, putting them elsewhere could make sense since anything with a It also allows doing other neat things. For example, you could do the following in a prototype without needing shaders:
It wouldn't be performant, but it would let you validate gameplay ideas quickly. |
@pushfoo Thanks for your advice. First, I agree with spliting the changes to The algorithm implemented in
Here is a neat way to build collidable shapes from drawable shapes: CollisionPolygon.from_shape(...)
CollisionCircle.from_sprite(..., radius, anchor_position, rotation) If we want to update data when the sprite changes it's position, more code should be added. |
It seems that the rest of pyglet uses a different format and the
|
Conclusion before revision, |
Thank you! I'll try to take a closer look at the changes in the next 2 days.
The idea is to eliminate the calls to if hasattr(self, "get_polygon") and hasattr(other, "get_polygon"): An AttributeError will be raised if either method is missing anyway, so there's no point in checking if they're there beforehand. Catching the The To my understanding, the speed boost from eliminating function calls is greatest for versions earlier than 3.11. The current design of this makes it harder to measure the impact of removing the function calls due to the GL context involved, but the old versions still have years of support left ahead of them. It's probably worth the odd looking syntax here. |
In this PR, I made the following changes:
pyglet.shapes2d
pyglet.shapes
topyglet.shapes2d.drawable
pyglet.shapes2d.collidable
, a module focus on 2D collision detectioncollision.py
)Besides, type hints are added to
pyglet.shapes2d
, and #857 has done the same thing.In addition, I tried to use sphinx-recommended way to write the comments in
pyglet.shapes2d
::param int foo: bar