Skip to content
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

Refactor - Gravity, Twoway, DeltaTime based Movement, Attach to moving platforms, Abstract intersect, Crafty.defineField #708

Closed
wants to merge 4 commits into from

Conversation

mucaho
Copy link
Contributor

@mucaho mucaho commented Jan 28, 2014

Current state

next PR will have:

  • MultiWay should use Motion for movement, see Refactor MultiWay #876
  • Motion.js: move Multiway, Fourway, Twoway, Gravity into new src file

Motivation

In my opinion, the sharing and modification of 3-4 properties between Twoway and Gravity was hard to read & maintain and did not allow for easy extendability. Future components that would like to use velocities & acceleration just have to require("Motion") and apply the motion through it's methods.

Changes

I introduced a new component called GroundDetector which does what Gravity did before: It registers if the entity lands on the ground for the first time or if the entity lifts off from the ground for the first time. GroundDetector also snaps the entity to the ground upon landing.
Gravity manages start/stop of GroundDetector and specifies the function which GroundDetector calls to check if the entity is allowed to land (e.g. currently entity must be falling to be able to land). The functionality was split from Gravity so that the to be implemented component GroundAttacher (#601) can also listen for these events.

The next component is called Motion. Both Gravity and Twoway use that component to add acceleration (gravity constant) or to add velocity (jump speed) in the y direction. "Motion" has a method linearDelta() which returns the relative change that was introduced by all linear motion. The relative change in the y direction is used in order to determine if the entity is falling or rising (jumping) - this function is called in GroundDetector each time the entity wants to land. "Motion" also allows deltaTime based movement of entities as described in #649.

A recent forum post indicates that users want to configure the ability to jump/land to their preferences. I added two methods Crafty.e("GroundDetector").canLand() and Crafty.e("Twoway").canJump(). Users can override the default behaviour of these two methods to their liking.

The common intersect check (that was being manually duplicated all over the library) was extracted into the global method Crafty.intersect(rectA, rectB). Crafty.e("2D").intersect() was streamlined to operate on _x, _y, _w, _h properties rather than x, y, w, h - this is the default for similar methods (isAt, contains, ...). Is there a specific reason it deviated from the rest?

Crafty.defineField and Crafty.e.defineField were added. Crafty.e.setter was adapted to use these two methods. They add a get callback and disable configuration (= deletion & reconfiguration) of the property.

Backwards Compatibility

  • As Crafty.e("2D").intersect() was changed to operate on _x, _y, _w, _h. User code may break if it called intersect.

Any input would be greatly appreciated! I'm still vague about the component names. Do you have any ideas?

@mucaho
Copy link
Contributor Author

mucaho commented Jan 29, 2014

Does anyone know why we are using a seperate method for detecting ground collisision in the Gravity component, since we already have the method .hit(comp)?
The obvious difference is that Gravity searches 1px beneath the actual entity. Are there other discrepancies?

@mucaho
Copy link
Contributor Author

mucaho commented Jan 29, 2014

Motion component will be redone to have standard linearVelocity, angularVelocity, etc. methods. This way I can also include #649

@starwed
Copy link
Member

starwed commented Jan 29, 2014

Just a quick couple of notes until I have time to look at it later:

  • You seem to be calling the velocity "force" for some reason -- it's just the velocity! :)
  • Let's use the proper kinematic equations for motion: x += v_x * dt + 0.5 * a_x * dt^2
  • That also lets motion tie properly into dt instead of only being tick based

Does anyone know why we are using a seperate method for detecting ground collisision in the Gravity component, since we already have the method .hit(comp)?
The obvious difference is that Gravity searches 1px beneath the actual entity. Are there other discrepancies?

I think that's the main reason -- it might also be a little more performant, since it searches a smaller total area for collisions.

Knowing if two components are "touching" each other is probably common enough that we might want to abstract it.

@mucaho
Copy link
Contributor Author

mucaho commented Jan 29, 2014

Thank you for your input!

  • I added linear-, angular- velocity and acceleration. They can be calculated per dt as suggested.
  • I was unable to incorporate this equation x += v_x * dt + 0.5 * a_x * dt^2 directly. The problem is that the velocity itself must increase with each timestep. Given the above equation, the changes in x will be constant given a constant dt, but the changes in x have to increase over time (due to acceleration).
  • The current used equations are
    v = v + a*Δt where v = < vx, vy, vrotation>
    Δs = v * Δt where Δs = <Δx, Δy, Δrotation>
    (x, y, rotation) = (x, y, rotation) + Δs
  • The above equations cause numerical errors over time. There is a better (still not perfect) algorithm called RK4, but I think thats overkill. Can we leave it like this, since this is not a strict physics engine?

EDIT
Updated the above equations: I chose to abandon having seperate constant/accumulated velocities, as it just confuses things. Especially if Crafty will be used alongside a real physics engine.

@mucaho
Copy link
Contributor Author

mucaho commented Jan 30, 2014

I had no success implementing Crafty.math.Vector3D.angleTo(otherVec3D). Help me, please!

@starwed
Copy link
Member

starwed commented Jan 30, 2014

I was unable to incorporate this equation x += v_x * dt + 0.5 * a_x * dt^2 directly. The problem is that the velocity itself must increase with each timestep. Given the above equation, the changes in x will be constant given a constant dt, but the changes in x have to increase over time (due to acceleration).

Right, but at each timestep you first increase x using that equation, and then increase v. It can make a noticeable difference at low v. It's also exactly correct (well, up to rounding issues) for constant acceleration. Agreed we don't want to use anything more complicated than that, though.

I still haven't looked in depth, but I don't think the math.js stuff is necessarily optimized very well. I'd avoid using it as much as possible. You shouldn't treat <x, y, rotation> as a 3d vector in any case! :)

I'd not worry about handling active rotation right now. It's basically orthogonal to linear motion, and a lot of games wouldn't care about it in the first place.

@mucaho
Copy link
Contributor Author

mucaho commented Jan 30, 2014

So you propose this
x += vx * Δt + (0.5 * ax) * Δt2
vx += ax * Δt
instead of the current
vx += ax * Δt
x += vx * Δt
right? Isn't the first set of equations gonna introduce more rounding errors than the second (the number of float multiplications is higher) ?

About the Vectors:

  • Are you sure about the performance of math.js (especially Vector stuff)? I think the vector equations are pretty standard for game programming (they use atan2 instead of acos/asin, which is the preferred way of doing it according to various stackoverflow sources). The vector methods also do not instantiate new objects on the heap. I included a feature to use an argument vector for storing the result in, in case the methods did instantiate a new object for the result.
  • If I am not mistaken then the rotation velocity / acceleration is computed exactly the same as the linear velocity / acceleration
  • My point being is that's it convenient to place all in a single vector to reduce code duplication.

@mucaho
Copy link
Contributor Author

mucaho commented Feb 3, 2014

I think these are enough modifications for a single PR :) Check out the JSFiddle how the stuff works.
The next PR will modify movement components so that they use the stuff that's actually implemented in this one.

I think this PR is almost ready, what do you think? Be sure to read the updated, original (first) post again, I wrote a concise description of anything relevant in there

@mucaho
Copy link
Contributor Author

mucaho commented Feb 3, 2014

Javascript at its best -.-

@starwed
Copy link
Member

starwed commented Feb 3, 2014

I still have a bunch of concerns with this, to be honest. I'll leave them as separate comments.

If I am not mistaken then the rotation velocity / acceleration is computed exactly the same as the linear velocity / acceleration

In a fundamental way, a vector is defined by how it transforms under rotation. Using rotation as a third component of a 3D vector is a horrible, horrible abuse of the concept -- the only reason to do so would be if it offered a performance improvement, which doesn't seem to be the case here. :)

As your perf result indicates, we're probably better off not using them at all. The additional level of indirection (needing to access .obj.x instead of just .x) has a performance impact all by itself, as do the overheads of additional function calls. I remember reading about some benchmarks of various box2D ports -- the only one that achieved acceptable performance was a emscripten generated compiled version, which avoids using javascript objects completely.

There's also an overhead that comes by worrying about the rotation in the first place -- it places a cost on entities that are moving around but not rotating, which would actually be pretty common. Better to have them handled by separate components.

@starwed
Copy link
Member

starwed commented Feb 3, 2014

Isn't the first set of equations gonna introduce more rounding errors than the second (the number of float multiplications is higher) ?

I'm not sure, but it doesn't matter -- they produce different results. In the case of constant acceleration, using the exact equations will be limited only by floating point errors, while the second will introduce an additional set of errors, systematically overestimating the distance traveled each time step by an amount of 0.5 a dt^2. I'm pretty sure it wouldn't be any better in more complicate scenarios where the acceleration varies. :)

if(isNaN(this._jumpSpeed)) this._jumpSpeed = 0; //set to 0 if Twoway component is not present

this.bind("EnterFrame", this._enterFrame);
startGroundDetection: function(ground, canLandFn) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason this doesn't just hook into the landed/lifted events?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "GroundDetector" is a passive component, that does not do anything until Gravity calls startGroundDetection / stopGroundDetection. This also means that it is backward compatible, because the user does not know anything about "GroundDetector"; he uses "Gravity" just like before.

@mucaho
Copy link
Contributor Author

mucaho commented Feb 3, 2014

Using rotation as a third component of a 3D vector is a horrible, horrible abuse of the concept

Suggest me a nice & concise way to set & get 6 properties without cluttering the API :) Think vector as list, not vector in mathematical sense :)

The second, currently used equation systematically overestimates the distance

You are right. I will adapt the code accordingly.

@starwed
Copy link
Member

starwed commented Feb 3, 2014

Suggest me a nice & concise way to set & get 6 properties without cluttering the API :) Think vector as list, not vector in mathematical sense :)

Well, this is where we developers have to suffer so the users have better performance. :) Internally, the component should just use top level properties like ._vx and ._vy, and we set them individually. Users can call methods of the component to set both at once.

Totally apart from that, I really do think that the case of rotation should be handled by another component. Imagine the stereotypical platformer: In Mario or Braid you bounce up and down and run left and right, but there's never any rotation. If the Motion component has to worry about rotation, that adds a cost that the user might not want/need to pay.

@mucaho
Copy link
Contributor Author

mucaho commented Feb 4, 2014

I have updated the motion component in order to combine the best of all worlds:

  • Linear Velocity is returned as a Crafty.math.Vector2D. This allows the user to easily change/access the x and y velocities. The biggest advantage is that the user can do vector operations (which comes in very handy).
  • Internally, the calculations are done separately per property (x, y, rotation), so the performance is good. There is no additional level of indirection while operating on the properties, which would degradate the performance.
  • I disagree with removing the angular velocity. For top-down (bird view) games it is very common to do rotation of entities, so I have left the getter/setter for angular velocity.

Is there something else that should be changed? I am fairly confident about the maturity of this PR.

EDIT:
If you are really that worried about the angular velocity, I could split it up into 2 components (LinearMotion, AngularMotion), but if you need both the performance would suffer.

@mucaho
Copy link
Contributor Author

mucaho commented Feb 5, 2014

Splitting angular from linear motion just incured one more "EnterFrame" event.
Thanks starwed for the suggestion.

@mucaho mucaho added the Needs QA label Feb 6, 2014
@starwed
Copy link
Member

starwed commented Feb 6, 2014

So, I think this is something that probably needs lots of testing and design, but maybe the best way forward is just to merge it into develop and play with it. :)

I have a pretty similar system I used in a platformer game; once this is merged I can try to implement the additional capabilities on top of this system and see how that works. (Some things I found particularly tricky to get right: frictional forces, midair collisions.)

Could you squash the commits down a bit? Certainly all the "fixes" and "add test" ones. :) When doing an interactive rebase you can reorder commits if it makes sense to, squashing two together even if there's an unrelated commit between them. (This actually has a practical impact when using git bisect, which helped me track down that odd canvas lines problem.)

@mucaho
Copy link
Contributor Author

mucaho commented Feb 7, 2014

EDIT: ah ok rebased them somehow, I hope it's good now

@mucaho
Copy link
Contributor Author

mucaho commented Feb 13, 2014

@starwed does this PR look good to you?

@starwed
Copy link
Member

starwed commented Feb 13, 2014

@mucaho Honestly I haven't been able to take the time to go through it carefully -- there a lot of changes lumped together here. I think it would take building a small demo game on top of them for me to be sure i grokked everything.

Looking over it again, is the 3D vector stuff even used in the rest of the patch now? Could it be split out into a separate PR?

@mucaho
Copy link
Contributor Author

mucaho commented Feb 14, 2014

@starwed

I managed to split all commits with surgical precision :) :

  • Crafty.defineField
  • Crafty.intersect
  • Refactor Gravity

The other commits (which are not related to this PR) were split into seperate PRs

@starwed
Copy link
Member

starwed commented Feb 14, 2014

Thanks, that's much easier to read through now! I still need to build something with it, but here's a couple of technical notes:

  • You might want to update defineField, since we no longer support any browsers that don't support defineProperty. Also, adding an option to "hide" a field from enumeration would make sense, given the recent changes we landed. (I think you mentioned something about this in that PR.)
  • I'd be a little wary of using Crafty.intersect deep in the collision/etc loops -- the reason it was written like that was to avoid any potential overhead that comes from calling a function. Maybe you could check that with a benchmark? And you should just remove the rectmanager overlap function completely, in favor of directly calling intersect.

@mucaho
Copy link
Contributor Author

mucaho commented Feb 14, 2014

The performance of a function call is horrible on Chrome compared to hard-coding it. There is no difference on FF. Should I revert it to like it was before?

@starwed
Copy link
Member

starwed commented Feb 14, 2014

Don't revert it just yet -- what matters most is if it takes up a measurable fraction of the total game loop time. Chrome and Firefox can report exactly how much time is spent in a given function, so that'll give us a better "real world" indication of whether it makes a difference. (And certainly using a function is way more readable.)

(But we do that comparison often enough that it might actually matter.)

@mucaho
Copy link
Contributor Author

mucaho commented Feb 14, 2014

So, how would we go about measuring the fraction of the total game loop time it takes? Or do we just "assume" it is irrelevant for now?

@starwed
Copy link
Member

starwed commented Feb 14, 2014

Basically, run a few Crafty games with that change, and use Chrome/Firefox's dev tools to measure the time spent in that particular function. I have a couple simple "benchmark" apps that just bounce a bunch of blocks around, with and without collision detection.

In Firefox it's the "Profiler" option -- you run it while the game is running, and it reports a lot of detailed information from the js engine. Here's an example -- we can see that while the manager was running, 10% of the time was spent inside renderCanvas, and a negligible fraction of that time (4/1791 = 0.2%) was spent inside overlap.

@mucaho
Copy link
Contributor Author

mucaho commented Mar 6, 2014

Thanks for your input! I agree with your suggestions.
Couple of notes for the "Gravity & GroundDetector" post though:

Doesn't this act wonky if you hit a thick platform from beneath or from the side? That was something that was recently fixed.

That's why canLand checks if the entity is moving downwards: if (entity.velocity.y > 0) return true else return false

Use "CheckLand" events to allow listeners to alter a "canLand" flag

Interesting and great approach. Each code block can decide for itself if entity can land -> decouples code!

@starwed
Copy link
Member

starwed commented Mar 6, 2014

That's why canLand checks if the entity is moving downwards:

Aha, missed that somehow. There'd be a crazy corner case still where, if you hit a platform at the very top of your arc, you could still get teleported above it. (Since vy is altered after position.) But using the smaller hitbox fixes that anyway.

A meta point: it might be worthwhile to split the gravity/ground detector patch off from the linear/angular motion patch? Then we could focus on landing the latter. (Up to you!)

@mucaho
Copy link
Contributor Author

mucaho commented Mar 6, 2014

There'd be a crazy corner case still where, if you hit a platform at the very top of your arc, you could still get teleported above it.

That is the same behaviour as before and is actually the desired behaviour in my opinion. (Keep in mind the hibox is shifted +1 px in y direction, so top pixel of the player will not be considered for collision detection). pawurb's quote from #592:

If character is in contact with the platform during the descending phase of the jump then he will be teleported on top of it.


But using the smaller hitbox (a single pixel high) fixes that anyway

I have had bad experiences when I tried to alter the hitbox's height to 1px. The ground collision was detected sometimes and sometimes not. We can always change that later after we have tested that properly.
I think the problem is when the downward velocity is too great, the vertical distance travelled in each "EnterFrame" is far greater than the height of the platform hitbox and the 1px player hitbox combined - sometimes no collision gets registered! Having a larger player hitbox helps counter that. Perhaps we should limit vertical velocity? (although I never had any problems with the way it currently works, but even with the large player hitbox it could happen theoretically)

@mucaho
Copy link
Contributor Author

mucaho commented Mar 6, 2014

these properties could use setters to trigger events, much like .x and .y do

Yeah, but how would we achieve this? Right now, in each motionTick the underlying properties _vx, _vy, _ax, _ay get modified directly. Now in order to achieve firing events the setters would have to be called explicitly (vx, vy, ax, ay). Wouldn't this hurt performance? Do you remember we dropped an additional level of indirection (vector algebra) for performance sake? Is there another alternative of firing these events?
EDIT: only 1 setter is used (vx,vy,vrotation) in each frame, so it's not that big of a performance hit, great!

@mucaho
Copy link
Contributor Author

mucaho commented Mar 7, 2014

Btw you can always submit a PR against my branch. I have to admit that rewriting the same code over and over again is very boring :)

@mucaho
Copy link
Contributor Author

mucaho commented Mar 8, 2014

Ok, I implemented the suggestions.

  • The top-level properties trigger a "MotionChange" event with e.g. {key: "vx", value: 123} as the event object. Is that ok?
  • Do you like vrotation & arotation or vrot & arot more?
  • The current gravity constant is 981 and the recommended jump speed is about 400. That are large numbers compared to the other numbers that float around. What to do about it? Perhaps change dt from frameData.dt / 1000 to frameData.dt / 10 -> new gravity constant would be 9.81 and recommended jump speed would be 4

@mucaho mucaho mentioned this pull request Mar 8, 2014
@mucaho mucaho force-pushed the refactor_twoway_gravity branch 2 times, most recently from c00210e to 0f8d861 Compare February 22, 2015 22:03
@mucaho
Copy link
Contributor Author

mucaho commented Feb 22, 2015

Ok I rebased this PR onto the new changes from develop branch.

The only remaining "issue" is that users should set the jumpSpeed of Twoway to 400 instead of 4 (but is automatically calculated correctly if user just specifies moveSpeed (== the first speed parameter of Twoway).
This will of course also influence the new MultiWay which will use (either optionally or replaced entirely by) Motion, as the speed values there will also need to be adjusted.
Would love to hear your input on this ;)

Other than that, I hope this bad boy is ready for deployment, what do you think?
@starwed @kevinsimper

@starwed
Copy link
Member

starwed commented Feb 23, 2015

Cool, I'll try to take a look at this again tomorrow. (It's been a while since I've reviewed the code!)

I think that even if there turn out to be issues, we should go ahead and land it soon, just so people benefit from all the work you've done! Then we can fix odd corner cases or revamp APIs as necessary.

@mucaho
Copy link
Contributor Author

mucaho commented Feb 23, 2015

Nice!

In the meantime I added a __convertPixelsToMeters utility method to 2D which converts pixels to meters so that both Gravity._gravityConst and Twoway._jumpspeed can use it. For now I put it to be 100 px per meter (which can be changed, but plays nicely with Twoway & Gravity in my demo). This means old code using Twoway will not break, but users that used their own gravityConst will need to adapt their code.

Also changed Moved event to be triggered for both axis x & y seperately (similar to how it works in MultiWay).

* @param rectB - An object that must have the `_x, _y, _w, _h` values as properties
* @return true if the rectangles intersect; false otherwise
*/
intersect: function (rectA, rectB) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there actually a reason for this right now? (i.e., is there any reason to have both this and rectManager.overlap?)

@starwed
Copy link
Member

starwed commented Feb 25, 2015

Ok, looks basically fine to me. Once it lands we can get a better feel for how the various new components will play in different custom scenarios, and maybe modify them down the road based on that feedback. (I'll probably try to write a tiny platformer as a test.)

I left a couple of comments; I really would prefer everything related to be attached to rectManager rather than the Crafty global; if you feel otherwise we can land this anyway, because it's not something I feel super strongly about. At some point I'll do some perf tests to see if we're spending any significant time in Crafty.intersect, though! :)

@mucaho
Copy link
Contributor Author

mucaho commented Feb 25, 2015

Thanks for taking a look at it!

I really would prefer everything related to be attached to rectManager rather than the Crafty global

Sounds good, will adapt.

I guess I should also point out that the reason a lot of places use the check explicitly, rather than calling out to a common function, was because theoretically it's a bit faster

Yep, I also thought about it, I will try to assign the function to a local variable where applicable. That should give us some improvement in loops. If overhead of function call will be too large after profiling, we can revert.

I'll probably try to write a tiny platformer as a test

I actually have a small demo and will upload it to the craftyjs/demos repository (once PR lands). However, I would like to add it as a playground also, but haven't had much experience with these playgrounds yet.

@mucaho
Copy link
Contributor Author

mucaho commented Feb 26, 2015

Added suggested changes and merged manually into develop.

@mucaho mucaho closed this Feb 26, 2015
@mucaho mucaho deleted the refactor_twoway_gravity branch February 26, 2015 09:25
@mucaho
Copy link
Contributor Author

mucaho commented Feb 26, 2015

The demo is in the craftyjs\demos\example_gravity.
One thing I noticed is that MotionChange event get's triggered on every change rather than on once in every frame. Still can't decide if it's better to allow multiple MotionChanges per frame, or just collect and trigger them at end of the frame.
EDIT:
on second thought triggering MotionChange events on every change makes sense: "Move" events are also triggered on every change to properties x, y. If user does not care about every change to vx and vy, he can listen to Moved and NewDirection(since #876) events which are triggered max once per frame.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants