Open tbrosman opened 8 years ago
I'm not sure I follow the part about not being able to inline getter calls. Other than the fact that you're using interfaces, which are obviously going to prevent it. Do you think users will need to switch between Frame2Default
and FlxSpriteFrame2
on the fly? Because I'm pretty sure if you go the Frame2Type
route, you could inline everything just fine.
I've never been one to try to tie mathematical constructs to a game engine's objects. I'd rather keep them in sync by hand. Though part of that might be that I don't use Flixel or similar. I write all the code to move, rotate, and so on, then once per frame send it to the engine for rendering. If I was relying on the engine to handle more of the logic, I might actually care about integration.
Anyway, how about a couple more design options?
Vector2
. This differs from Design A because here the offset is canonical - changes you make to the vector are saved.orthoNormalize()
to calculate the basis from incomplete data, or using Vector2
arithmetic to convert from inside to outside coordinates. Downside is, it's harder to add an angle. (Basically the pros and cons are exactly the same as when considering Cartesian vs. polar coordinates.)It has been a while since I thought about this. Coming back to it, I think I was trying to do too many things with a single type. Frames were supposed to accomplish two things:
6+ years later, I think these two concerns should be decoupled. The "Frame" types should only solve representation, and some static adapter class should solve the "sync with existing engines" problem. @player-03 I agree that storing the offset as a Vector2 makes the most sense. All the convenience methods live there already. I also agree that storing it as an explicit member rather than a getter (with slightly weird semantics: see cons of design A) is more straightforward. The performance tradeoff is probably moot in Haxe 4.
As for the rotation part, 2x2 matrices don't convert nicely into angles for all cases. FlxSpriteFrame2 is a (admittedly heavy-handed) attempt to have both and keep them in sync with the downside that you can never set the matrix directly. Exposing the matrix means non-uniform scales, reflections, shears, etc. may be specified. Extracting the angle by using atan2 on one of the basis vectors would give a useless result. One of the principles behind hxmath was to design the APIs so that the client code author doesn't need to know about the edge cases to get the math right.
There's another option for rotations: rotors, which are generalized quaternions. In 2D they look like one of the basis vectors from the rotation matrix. See http://geocalc.clas.asu.edu/GA_Primer/GA_Primer/introduction-to-geometric/rotors-and-rotations-in-the.html for a short derivation. There are fewer degrees of freedom to deal with, though it is still possible to build a rotor that isn't normalized. Building a rotation matrix of a rotor is trivial as well. The downside is that most engines don't work with rotors, so it might just result in more allocations than it is worth. Summary of options:
Wow, that was a rabbit hole. Geometric algebra certainly opens up options.
I'd recommend linking to this introductory article as documentation, in addition to or instead of the one you linked above.
(with slightly weird semantics: see cons of design A)
But those weird semantics resulted from using a getter, right? If the vector is a normal variable (or (default, null)
, or a final
variable), then you aren't incurring extra allocations, and frame.offset.x += 30
works just fine.
Exposing the matrix means non-uniform scales, reflections, shears, etc. may be specified.
That's why I suggested storing multiple Vector2
s instead of one matrix. Then the user can call orthoNormalize()
after making changes. Similar to how you'd normalize a Quaternion
if you performed an operation that modified the length.
I definitely see where you're coming from - don't give users the option to shoot themselves in the foot if you can avoid it.
Building a rotation matrix of a rotor is trivial as well.
More so in 2D than in 3D, but yeah.
The downside is that most engines don't work with rotors
From what I've read so far, it sounds like you can use the same underlying type for Rotor3
and Quaternion
. You're right for 2D rotors, though.
Changing the angle is very slow (requires use of atan2, sin, and cos).
I don't think that's true.
sin()
and cos()
to make a brand new matrix or rotor.sin()
and cos()
to construct the "delta" matrix/rotor, and from there it's much more efficient arithmetic.As for the big question, I think we should consider use cases. Why go to the trouble of constructing a frame at all? Maybe it's just to store an offset plus rotation and retrieve them later. But more likely, it's for the transformation functionality: you want to convert to/from local coordinates. This functionality either requires a matrix or something equivalent to it (a rotor, a vector's rotate()
function, it's all pretty much the same).
All of the games I've made thus far fall under use case 3. And even if I made a game where the angle was constantly changing, it'd actually change exactly once per frame, and there would always be at least one coordinate conversion in between, resulting in use case 2. I can't think of an example of use case 1 at the moment (and even if I could, it's easy to store your own float and work around the problem).
Conclusion: Option 2C gets my vote.
I'm weighing the following possible approaches. Design A is what is implemented now. Tangentially related to issue #23 :)
Design A: Complex objects instantiated when getting translation/rotation
Design B: Get/set function adapters for (x, y, rotation) components
Design C: Frame is not an interface but instead contains the (x, y, rotation) components directly