diff --git a/src/controls/controls.js b/src/controls/controls.js index d5958fd6..1ba7e183 100644 --- a/src/controls/controls.js +++ b/src/controls/controls.js @@ -1222,16 +1222,36 @@ Crafty.c("Fourway", { * #Twoway * @category Input * @trigger NewDirection - When direction changes a NewDirection event is triggered with an object detailing the new direction: {x: x_movement, y: y_movement}. This is consistent with Fourway and Multiway components. - * @trigger Moved - When entity has moved on x-axis a Moved event is triggered with an object specifying the old position {x: old_x, y: old_y} - * + * @trigger Moved - triggered on movement on either x or y axis. If the entity has moved on both axes for diagonal movement the event is triggered twice - { x:Number, y:Number } - Old position + * @trigger CheckJumping - When entity is about to jump. This event is triggered with the object the entity is about to jump from (if it exists). Third parties can respond to this event and enable the entity to jump. + * * Move an entity left or right using the arrow keys or `D` and `A` and jump using up arrow or `W`. */ Crafty.c("Twoway", { _speed: 3, - _up: false, + /**@ + * #.canJump + * @comp Twoway + * + * The canJump function determines if the entity is allowed to jump or not (e.g. perhaps the entity should not jump if it's already falling). + * The Twoway component will trigger a "CheckJumping" event. + * Interested parties can listen to this event and enable the entity to jump by setting `canJump` to true. + * + * @example + * ~~~ + * var player = Crafty.e("2D, Twoway"); + * player.bind("CheckJumping", function(ground) { + * if (!ground && player.hasDoubleJumpPowerUp) { // custom behaviour + * player.hasDoubleJumpPowerUp = false; + * player.canJump = true; + * } + * }); + * ~~~ + */ + canJump: true, init: function () { - this.requires("Fourway, Keyboard, Gravity"); + this.requires("Fourway, Motion, Supportable"); }, /**@ @@ -1249,7 +1269,7 @@ Crafty.c("Twoway", { * The key presses will move the entity in that direction by the speed passed in * the argument. Pressing the `Up Arrow` or `W` will cause the entity to jump. * - * @see Gravity, Fourway + * @see Gravity, Fourway, Motion */ twoway: function (speed, jump) { @@ -1262,22 +1282,23 @@ Crafty.c("Twoway", { }); if (speed) this._speed = speed; - if (arguments.length < 2){ - this._jumpSpeed = this._speed * 2; - } else{ - this._jumpSpeed = jump; + if (arguments.length < 2) { + this._jumpSpeed = this.__convertPixelsToMeters(this._speed * 2); + } else { + this._jumpSpeed = this.__convertPixelsToMeters(jump); } - this.bind("EnterFrame", function () { + var ground; + this.bind("KeyDown", function (e) { if (this.disableControls) return; - if (this._up) { - this.y -= this._jumpSpeed; - this._falling = true; - this.trigger('Moved', { x: this._x, y: this._y + this._jumpSpeed }); + if (e.key === Crafty.keys.UP_ARROW || e.key === Crafty.keys.W || e.key === Crafty.keys.Z) { + ground = this.ground(); + this.canJump = !!ground; + this.trigger("CheckJumping", ground); + if (this.canJump) { + this.vy -= this._jumpSpeed; // Motion component will trigger Move events + } } - }).bind("KeyDown", function (e) { - if (!this._falling && (e.key === Crafty.keys.UP_ARROW || e.key === Crafty.keys.W || e.key === Crafty.keys.Z)) - this._up = true; }); return this; diff --git a/src/core/core.js b/src/core/core.js index c7563cdc..bb64890b 100644 --- a/src/core/core.js +++ b/src/core/core.js @@ -878,6 +878,7 @@ Crafty.fn = Crafty.prototype = { return clone; }, + /**@ * #.setter * @comp Crafty Core @@ -887,16 +888,43 @@ Crafty.fn = Crafty.prototype = { * Will watch a property waiting for modification and will then invoke the * given callback when attempting to modify. * + * This feature is deprecated; use .defineField() instead. + * @see .defineField */ setter: function (prop, callback) { - if (Crafty.support.setter) { - this.__defineSetter__(prop, callback); - } else if (Crafty.support.defineProperty) { - Object.defineProperty(this, prop, { - set: callback, - configurable: true - }); - } + return this.defineField(prop, function(){}, callback); + }, + + /**@ + * #.defineField + * @comp Crafty Core + * @sign public this .defineField(String property, Function getCallback, Function setCallback) + * @param property - Property name to assign getter & setter to + * @param getCallback - Method to execute if the property is accessed + * @param setCallback - Method to execute if the property is mutated + * + * Assigns getters and setters to the property. + * A getter will watch a property waiting for access and will then invoke the + * given getCallback when attempting to retrieve. + * A setter will watch a property waiting for mutation and will then invoke the + * given setCallback when attempting to modify. + * + * @example + * ~~~ + * var ent = Crafty.e("2D"); + * ent.defineField("customData", function() { + * return this._customData; + * }, function(newValue) { + * this._customData = newValue; + * }); + * + * ent.customData = "2" // set customData to 2 + * console.log(ent.customData) // prints 2 + * ~~~ + * @see Crafty.defineField + */ + defineField: function (prop, getCallback, setCallback) { + Crafty.defineField(this, prop, getCallback, setCallback); return this; }, @@ -1745,6 +1773,44 @@ Crafty.extend({ }; })(), + /**@ + * #Crafty.defineField + * @category Core + * @sign public void Crafty.defineField(Object object, String property, Function getCallback, Function setCallback) + * @param object - Object to define property on + * @param property - Property name to assign getter & setter to + * @param getCallback - Method to execute if the property is accessed + * @param setCallback - Method to execute if the property is mutated + * + * Assigns getters and setters to the property in the given object. + * A getter will watch a property waiting for access and will then invoke the + * given getCallback when attempting to retrieve. + * A setter will watch a property waiting for mutation and will then invoke the + * given setCallback when attempting to modify. + * + * @example + * ~~~ + * var ent = Crafty.e("2D"); + * Crafty.defineField(ent, "customData", function() { + * return this._customData; + * }, function(newValue) { + * this._customData = newValue; + * }); + * + * ent.customData = "2" // set customData to 2 + * console.log(ent.customData) // prints 2 + * ~~~ + * @see .defineField + */ + defineField: function(obj, prop, getCallback, setCallback) { + Object.defineProperty(obj, prop, { + get: getCallback, + set: setCallback, + configurable: false, + enumerable: true, + }); + }, + clone: clone }); diff --git a/src/graphics/canvas-layer.js b/src/graphics/canvas-layer.js index f284b4ab..a4a5fdbc 100644 --- a/src/graphics/canvas-layer.js +++ b/src/graphics/canvas-layer.js @@ -146,6 +146,7 @@ Crafty.extend({ l = changed.length, dirty = this._dirtyRects, rectManager = Crafty.rectManager, + overlap = rectManager.overlap, ctx = this.context, dupes = [], objs = []; @@ -205,7 +206,7 @@ Crafty.extend({ for (j = 0, len = objs.length; j < len; ++j) { obj = objs[j]; var area = obj._mbr || obj; - if (rectManager.overlap(area, rect)) + if (overlap(area, rect)) obj.draw(); obj._changed = false; } diff --git a/src/spatial/2d.js b/src/spatial/2d.js index 12157e88..d43e1c84 100644 --- a/src/spatial/2d.js +++ b/src/spatial/2d.js @@ -467,7 +467,7 @@ Crafty.c("2D", { * @param w - Width of the rect * @param h - Height of the rect * @sign public Boolean .intersect(Object rect) - * @param rect - An object that must have the `x, y, w, h` values as properties + * @param rect - An object that must have the `_x, _y, _w, _h` values as properties * Determines if this entity intersects a rectangle. If the entity is rotated, its MBR is used for the test. */ intersect: function (x, y, w, h) { @@ -476,15 +476,14 @@ Crafty.c("2D", { rect = x; } else { rect = { - x: x, - y: y, - w: w, - h: h + _x: x, + _y: y, + _w: w, + _h: h }; } - return mbr._x < rect.x + rect.w && mbr._x + mbr._w > rect.x && - mbr._y < rect.y + rect.h && mbr._h + mbr._y > rect.y; + return Crafty.rectManager.overlap(mbr, rect); }, /**@ @@ -920,21 +919,202 @@ Crafty.c("2D", { } }); +/**@ + * #Supportable + * @category 2D + * @trigger LandedOnGround - When entity has landed. This event is triggered with the object the entity landed on. + * @trigger LiftedOffGround - When entity has lifted off. This event is triggered with the object the entity stood on before lift-off. + * @trigger CheckLanding - When entity is about to land. This event is triggered with the object the entity is about to land on. Third parties can respond to this event and prevent the entity from being able to land. + * + * Component that detects if the entity collides with the ground. This component is automatically added and managed by the Gravity component. + * The appropriate events are fired when the entity state changes (lands on ground / lifts off ground). The current state can also be accessed with .ground(). + */ +Crafty.c("Supportable", { + _ground: false, + _groundComp: null, + + /**@ + * #.canLand + * @comp Supportable + * + * The canLand boolean determines if the entity is allowed to land or not (e.g. perhaps the entity should not land if it's not falling). + * The Supportable component will trigger a "CheckLanding" event. + * Interested parties can listen to this event and prevent the entity from landing by setting `canLand` to false. + * + * @example + * ~~~ + * var player = Crafty.e("2D, Gravity"); + * player.bind("CheckLanding", function(ground) { + * if (player.isAirplane) // custom behaviour + * player.canLand = false; + * }); + * ~~~ + */ + canLand: true, + + init: function () { + this.requires("2D"); + this.__pos = {_x: 0, _y: 0, _w: 0, _h: 0}; + }, + remove: function(destroyed) { + this.unbind("EnterFrame", this._detectGroundTick); + }, + + /*@ + * #.startGroundDetection + * @comp Supportable + * @sign private this .startGroundDetection([comp]) + * @param comp - The name of a component that will be treated as ground + * + * This method is automatically called by the Gravity component and should not be called by the user. + * + * Enable ground detection for this entity no matter whether comp parameter is specified or not. + * If comp parameter is specified all entities with that component will stop this entity from falling. + * For a player entity in a platform game this would be a component that is added to all entities + * that the player should be able to walk on. + * + * @example + * ~~~ + * Crafty.e("2D, DOM, Color, Gravity") + * .color("red") + * .attr({ w: 100, h: 100 }) + * .gravity("platform"); + * ~~~ + * + * @see Gravity + */ + startGroundDetection: function(ground) { + if (ground) this._groundComp = ground; + this.uniqueBind("EnterFrame", this._detectGroundTick); + + return this; + }, + /*@ + * #.stopGroundDetection + * @comp Supportable + * @sign private this .stopGroundDetection() + * + * This method is automatically called by the Gravity component and should not be called by the user. + * + * Disable ground detection for this component. It can be reenabled by calling .startGroundDetection() + */ + stopGroundDetection: function() { + this.unbind("EnterFrame", this._detectGroundTick); + + return this; + }, + /**@ + * #.ground + * @comp Supportable + * @sign public Object|false .ground() + * @return the ground entity if this entity is currently on the ground or false if this entity is not currently on the ground + * + * Determine the ground entity and thus whether this entity is currently on the ground or not. + * The information is also available through the events, when the state changes. + */ + ground: function() { + return this._ground; + }, + _detectGroundTick: function() { + var obj, hit = false, + q, i = 0, l; + + var pos = this.__pos; + pos._x = this._x; + pos._y = this._y + 1; //Increase by 1 to make sure map.search() finds the floor + pos._w = this._w; + pos._h = this._h; + // Decrease width by 1px from left and 1px from right, to fall more gracefully + // pos._x++; pos._w--; + + + q = Crafty.map.search(pos); + l = q.length; + for (; i < l; ++i) { + obj = q[i]; + //check for an intersection directly below the player + if (obj !== this && obj.has(this._groundComp) && obj.intersect(pos)) { + hit = obj; + break; + } + } + + + if (hit && !this._ground) { // collision with ground was detected for first time + this.canLand = true; + this.trigger("CheckLanding", hit); // is entity allowed to land? + if (this.canLand) { + this._ground = hit; + this.y = hit._y - this._h; // snap entity to ground object + this.trigger("LandedOnGround", this._ground); + } + } else if (!hit && this._ground) { // no collision with ground was detected for first time + var ground = this._ground; + this._ground = false; + this.trigger("LiftedOffGround", ground); + } + } +}); + +/**@ + * #GroundAttacher + * @category 2D + * + * Component that attaches the entity to the ground when it lands. Useful for platformers with moving platforms. + * Remove the component to disable the functionality. + * + * @example + * ~~~ + * Crafty.e("2D, Gravity, GroundAttacher") + * .gravity("Platform"); // entity will land on and move with entites that have the "Platform" component + * ~~~ + */ +Crafty.c("GroundAttacher", { + _groundAttach: function(ground) { + ground.attach(this); + }, + _groundDetach: function(ground) { + ground.detach(this); + }, + + init: function () { + this.requires("Supportable"); + + this.bind("LandedOnGround", this._groundAttach); + this.bind("LiftedOffGround", this._groundDetach); + }, + remove: function(destroyed) { + this.unbind("LandedOnGround", this._groundAttach); + this.unbind("LiftedOffGround", this._groundDetach); + } +}); + + /**@ * #Gravity * @category 2D - * @trigger Moved - When entity has moved on y-axis a Moved event is triggered with an object specifying the old position {x: old_x, y: old_y} + * @trigger Moved - triggered on movement on either x or y axis. If the entity has moved on both axes for diagonal movement the event is triggered twice - { x:Number, y:Number } - Old position * * Adds gravitational pull to the entity. + * + * @see Motion */ Crafty.c("Gravity", { - _gravityConst: 0.2, - _gy: 0, - _falling: true, - _anti: null, - init: function () { - this.requires("2D"); + this.requires("2D, Supportable, Motion"); + this._gravityConst = this.__convertPixelsToMeters(9.81); + + this.bind("LiftedOffGround", this._startGravity); // start gravity if we are off ground + this.bind("LandedOnGround", this._stopGravity); // stop gravity once landed + }, + remove: function(removed) { + this.unbind("LiftedOffGround", this._startGravity); + this.unbind("LandedOnGround", this._stopGravity); + }, + + _gravityCheckLanding: function(ground) { + if (this._dy < 0) + this.canLand = false; }, /**@ @@ -947,6 +1127,7 @@ Crafty.c("Gravity", { * If comp parameter is specified all entities with that component will stop this entity from falling. * For a player entity in a platform game this would be a component that is added to all entities * that the player should be able to walk on. + * See the Supportable component documentation for additional methods & events that are available. * * @example * ~~~ @@ -955,12 +1136,25 @@ Crafty.c("Gravity", { * .attr({ w: 100, h: 100 }) * .gravity("platform"); * ~~~ + * @see Supportable */ gravity: function (comp) { - if (comp) this._anti = comp; - if(isNaN(this._jumpSpeed)) this._jumpSpeed = 0; //set to 0 if Twoway component is not present + this.bind("CheckLanding", this._gravityCheckLanding); + this.startGroundDetection(comp); + this._startGravity(); - this.bind("EnterFrame", this._enterFrame); + return this; + }, + /**@ + * #.antigravity + * @comp Gravity + * @sign public this .antigravity() + * Disable gravity for this component. It can be reenabled by calling .gravity() + */ + antigravity: function () { + this._stopGravity(); + this.stopGroundDetection(); + this.unbind("CheckLanding", this._gravityCheckLanding); return this; }, @@ -971,84 +1165,446 @@ Crafty.c("Gravity", { * @sign public this .gravityConst(g) * @param g - gravitational constant * - * Set the gravitational constant to g. The default is .2. The greater g, the faster the object falls. + * Set the gravitational constant to g. The default is 9.81 . The greater g, the faster the object falls. * * @example * ~~~ * Crafty.e("2D, DOM, Color, Gravity") * .color("red") * .attr({ w: 100, h: 100 }) - * .gravity("platform") - * .gravityConst(2) + * .gravityConst(5) + * .gravity("platform"); * ~~~ */ gravityConst: function (g) { - this._gravityConst = g; + var newGravityConst = this.__convertPixelsToMeters(g); + if (!this.ground()) { // gravity active, change acceleration + this.ay -= this._gravityConst; + this.ay += newGravityConst; + } + this._gravityConst = newGravityConst; + return this; }, + _startGravity: function() { + this.ay += this._gravityConst; + }, + _stopGravity: function() { + this.ay = 0; + this.vy = 0; + } +}); - _enterFrame: function () { - if (this._falling) { - //if falling, move the players Y - this._gy += this._gravityConst; - this.y += this._gy; - this.trigger('Moved', { x: this._x, y: this._y - this._gy }); - } else { - this._gy = 0; //reset change in y - } +var __motionProp = function(self, prefix, prop, setter) { + var publicProp = prefix + prop; + var privateProp = "_" + publicProp; + + var motionEvent = { key: "", value: 0}; + // getters & setters for public property + if (setter) { + Crafty.defineField(self, publicProp, function() { return this[privateProp]; }, function(newValue) { + var oldValue = this[privateProp]; + if (newValue !== oldValue) { + this[privateProp] = newValue; + + motionEvent.key = publicProp; + motionEvent.value = oldValue; + this.trigger("MotionChange", motionEvent); + } + }); + } else { + Crafty.defineField(self, publicProp, function() { return this[privateProp]; }, function(newValue) {}); + } - var obj, hit = false, - pos = this.pos(), - q, i = 0, - l; + // hide private property + Object.defineProperty(self, privateProp, { + value : 0, + writable : true, + enumerable : false, + configurable : false + }); +}; - //Increase by 1 to make sure map.search() finds the floor - pos._y++; +var __motionVector = function(self, prefix, setter, vector) { + var publicX = prefix + "x", + publicY = prefix + "y", + privateX = "_" + publicX, + privateY = "_" + publicY; + + if (setter) { + Crafty.defineField(vector, "x", function() { return self[privateX]; }, function(v) { self[publicX] = v; }); + Crafty.defineField(vector, "y", function() { return self[privateY]; }, function(v) { self[publicY] = v; }); + } else { + Crafty.defineField(vector, "x", function() { return self[privateX]; }, function(v) {}); + Crafty.defineField(vector, "y", function() { return self[privateY]; }, function(v) {}); + } + if (Object.seal) { Object.seal(vector); } - //map.search wants _x and intersect wants x... - pos.x = pos._x; - pos.y = pos._y; - pos.w = pos._w; - pos.h = pos._h; + return vector; +}; - q = Crafty.map.search(pos); - l = q.length; +/**@ + * #AngularMotion + * @category 2D + * @trigger Rotated - When entity has rotated due to angular velocity/acceleration a Rotated event is triggered. - Number - Old rotation + * @trigger MotionChange - when a motion property has changed - { key: String propertyName, value: Number oldPropertyValue } + * + * Component that allows rotating an entity by applying angular velocity and acceleration. + */ +Crafty.c("AngularMotion", { + /**@ + * #.vrotation + * @comp AngularMotion + * + * A number for accessing/modifying the angular(rotational) velocity. + * The velocity remains constant over time, unless the acceleration increases the velocity. + * + * @example + * ~~~ + * var ent = Crafty.e("2D, AngularMotion"); + * + * var vrotation = ent.vrotation; // retrieve the angular velocity + * ent.vrotation += 1; // increase the angular velocity + * ent.vrotation = 0; // reset the angular velocity + * ~~~ + */ + _vrotation: 0, - for (; i < l; ++i) { - obj = q[i]; - //check for an intersection directly below the player - if (obj !== this && obj.has(this._anti) && obj.intersect(pos)) { - hit = obj; - break; - } - } + /**@ + * #.arotation + * @comp AngularMotion + * + * A number for accessing/modifying the angular(rotational) acceleration. + * The acceleration increases the velocity over time, resulting in ever increasing speed. + * + * @example + * ~~~ + * var ent = Crafty.e("2D, AngularMotion"); + * + * var arotation = ent.arotation; // retrieve the angular acceleration + * ent.arotation += 1; // increase the angular acceleration + * ent.arotation = 0; // reset the angular acceleration + * ~~~ + */ + _arotation: 0, - if (hit) { //stop falling if found and player is moving down - if (this._falling && ((this._gy > this._jumpSpeed) || !this._up)){ - this.stopFalling(hit); - } - } else { - this._falling = true; //keep falling otherwise + /**@ + * #.drotation + * @comp AngularMotion + * + * A number that reflects the change in rotation (difference between the old & new rotation) that was applied in the last frame. + * + * @example + * ~~~ + * var ent = Crafty.e("2D, AngularMotion"); + * + * var drotation = ent.drotation; // the change of rotation in the last frame + * ~~~ + */ + _drotation: 0, + + init: function () { + this.requires("2D"); + + __motionProp(this, "v", "rotation", true); + __motionProp(this, "a", "rotation", true); + __motionProp(this, "d", "rotation", false); + + this.bind("EnterFrame", this._angularMotionTick); + }, + remove: function(destroyed) { + this.unbind("EnterFrame", this._angularMotionTick); + }, + + /**@ + * #.resetAngularMotion + * @comp AngularMotion + * @sign public this .resetAngularMotion() + * @return this + * + * Reset all motion (resets velocity, acceleration, motionDelta). + */ + resetAngularMotion: function() { + this._drotation = 0; + this.vrotation = 0; + this.arotation = 0; + + return this; + }, + + /* + * s += v * Δt + (0.5 * a) * Δt * Δt + * v += a * Δt + */ + _angularMotionTick: function(frameData) { + var dt = frameData.dt/1000; + + var oldRotation = this._rotation; + // s += v * Δt + (0.5 * a) * Δt * Δt + var newRotation = oldRotation + this._vrotation * dt + 0.5 * this._arotation * dt * dt; + // v += a * Δt + this.vrotation = this._vrotation + this._arotation * dt; + // Δs = s[t] - s[t-1] + this._drotation = newRotation - oldRotation; + + if (this._drotation !== 0) { + this.rotation = newRotation; + this.trigger('Rotated', oldRotation); } + } +}); + +/**@ + * #Motion + * @category 2D + * @trigger Moved - When entity has moved due to velocity/acceleration on either x or y axis a Moved event is triggered. If the entity has moved on both axes for diagonal movement the event is triggered twice. - { x:Number, y:Number } - Old position + * @trigger MotionChange - when a motion property has changed - { key: String propertyName, value: Number oldPropertyValue } + * + * Component that allows moving an entity by applying linear velocity and acceleration. + */ +Crafty.c("Motion", { + /* + * Utility function which converts the input argument `pixels` into `meters`. + */ + __convertPixelsToMeters: function(pixels) { + return pixels * 100; }, - stopFalling: function (e) { - if (e) this.y = e._y - this._h; //move object + /**@ + * #.vx + * @comp Motion + * + * A number for accessing/modifying the linear velocity in the x axis. + * The velocity remains constant over time, unless the acceleration increases the velocity. + * + * @example + * ~~~ + * var ent = Crafty.e("2D, Motion"); + * + * var vx = ent.vx; // retrieve the linear velocity in the x axis + * ent.vx += 1; // increase the linear velocity in the x axis + * ent.vx = 0; // reset the linear velocity in the x axis + * ~~~ + */ + _vx: 0, - //this._gy = -1 * this._bounce; - this._falling = false; - if (this._up) this._up = false; - this.trigger("hit"); + /**@ + * #.vy + * @comp Motion + * + * A number for accessing/modifying the linear velocity in the y axis. + * The velocity remains constant over time, unless the acceleration increases the velocity. + * + * @example + * ~~~ + * var ent = Crafty.e("2D, Motion"); + * + * var vy = ent.vy; // retrieve the linear velocity in the y axis + * ent.vy += 1; // increase the linear velocity in the y axis + * ent.vy = 0; // reset the linear velocity in the y axis + * ~~~ + */ + _vy: 0, + + /**@ + * #.ax + * @comp Motion + * + * A number for accessing/modifying the linear acceleration in the x axis. + * The acceleration increases the velocity over time, resulting in ever increasing speed. + * + * @example + * ~~~ + * var ent = Crafty.e("2D, Motion"); + * + * var ax = ent.ax; // retrieve the linear acceleration in the x axis + * ent.ax += 1; // increase the linear acceleration in the x axis + * ent.ax = 0; // reset the linear acceleration in the x axis + * ~~~ + */ + _ax: 0, + + /**@ + * #.ay + * @comp Motion + * + * A number for accessing/modifying the linear acceleration in the y axis. + * The acceleration increases the velocity over time, resulting in ever increasing speed. + * + * @example + * ~~~ + * var ent = Crafty.e("2D, Motion"); + * + * var ay = ent.ay; // retrieve the linear acceleration in the y axis + * ent.ay += 1; // increase the linear acceleration in the y axis + * ent.ay = 0; // reset the linear acceleration in the y axis + * ~~~ + */ + _ay: 0, + + /**@ + * #.dx + * @comp Motion + * + * A number that reflects the change in x (difference between the old & new x) that was applied in the last frame. + * + * @example + * ~~~ + * var ent = Crafty.e("2D, Motion"); + * + * var dx = ent.dx; // the change of x in the last frame + * ~~~ + */ + _dx: 0, + + /**@ + * #.dy + * @comp Motion + * + * A number that reflects the change in y (difference between the old & new y) that was applied in the last frame. + * + * @example + * ~~~ + * var ent = Crafty.e("2D, Motion"); + * + * var dy = ent.dy; // the change of y in the last frame + * ~~~ + */ + _dy: 0, + + init: function () { + this.requires("2D"); + + __motionProp(this, "v", "x", true); + __motionProp(this, "v", "y", true); + this._velocity = __motionVector(this, "v", true, new Crafty.math.Vector2D()); + __motionProp(this, "a", "x", true); + __motionProp(this, "a", "y", true); + this._acceleration = __motionVector(this, "a", true, new Crafty.math.Vector2D()); + __motionProp(this, "d", "x", false); + __motionProp(this, "d", "y", false); + this._motionDelta = __motionVector(this, "d", false, new Crafty.math.Vector2D()); + + this.bind("EnterFrame", this._linearMotionTick); + }, + remove: function(destroyed) { + this.unbind("EnterFrame", this._linearMotionTick); }, /**@ - * #.antigravity - * @comp Gravity - * @sign public this .antigravity() - * Disable gravity for this component. It can be reenabled by calling .gravity() + * #.resetMotion + * @comp Motion + * @sign public this .resetMotion() + * @return this + * + * Reset all linear motion (resets velocity, acceleration, motionDelta). */ - antigravity: function () { - this.unbind("EnterFrame", this._enterFrame); + resetMotion: function() { + this.vx = 0; this.vy = 0; + this.ax = 0; this.ay = 0; + this._dx = 0; this._dy = 0; + + return this; + }, + + /**@ + * #.motionDelta + * @comp Motion + * @sign public Vector2D .motionDelta() + * @return A Vector2D with the properties {x, y} that reflect the change in x & y. + * + * Returns the difference between the old & new position that was applied in the last frame. + * + * @example + * ~~~ + * var ent = Crafty.e("2D, Motion"); + * + * var deltaY = ent.motionDelta().y; // the change of y in the last frame + * ~~~ + * @see Crafty.math.Vector2D + */ + motionDelta: function() { + return this._motionDelta; + }, + + /**@ + * #.velocity + * @comp Motion + * Method for accessing/modifying the linear(x,y) velocity. + * The velocity remains constant over time, unless the acceleration increases the velocity. + * + * @sign public Vector2D .velocity() + * @return The velocity Vector2D with the properties {x, y} that reflect the velocities in the direction of the entity. + * Returns the current velocity. You can access/modify the properties in order to retrieve/change the velocity. + + * @example + * ~~~ + * var ent = Crafty.e("2D, Motion"); + * + * var vel = ent.velocity(); //returns the velocity vector + * vel.x; // retrieve the velocity in the x direction + * vel.x = 0; // set the velocity in the x direction + * vel.x += 4 // add to the velocity in the x direction + * ~~~ + * @see Crafty.math.Vector2D + */ + velocity: function() { + return this._velocity; + }, + + + /**@ + * #.acceleration + * @comp Motion + * Method for accessing/modifying the linear(x,y) acceleration. + * The acceleration increases the velocity over time, resulting in ever increasing speed. + * + * @sign public Vector2D .acceleration() + * @return The acceleration Vector2D with the properties {x, y} that reflects the acceleration in the direction of the entity. + * Returns the current acceleration. You can access/modify the properties in order to retrieve/change the acceleration. + * + * @example + * ~~~ + * var ent = Crafty.e("2D, Motion"); + * + * var acc = ent.acceleration(); //returns the acceleration object + * acc.x; // retrieve the acceleration in the x direction + * acc.x = 0; // set the acceleration in the x direction + * acc.x += 4 // add to the acceleration in the x direction + * ~~~ + * @see Crafty.math.Vector2D + */ + acceleration: function() { + return this._acceleration; + }, + + /* + * s += v * Δt + (0.5 * a) * Δt * Δt + * v += a * Δt + */ + _linearMotionTick: function(frameData) { + var dt = frameData.dt/1000; + + var oldX = this._x; + var oldY = this._y; + // s += v * Δt + (0.5 * a) * Δt * Δt + var newX = oldX + this._vx * dt + 0.5 * this._ax * dt * dt; + var newY = oldY + this._vy * dt + 0.5 * this._ay * dt * dt; + // v += a * Δt + this.vx = this._vx + this._ax * dt; + this.vy = this._vy + this._ay * dt; + // Δs = s[t] - s[t-1] + this._dx = newX - oldX; + this._dy = newY - oldY; + + if (this._dx !== 0) { + this.x = newX; + this.trigger('Moved', {x: oldX, y: newY}); + } + if (this._dy !== 0) { + this.y = newY; + this.trigger('Moved', {x: newX, y: oldY}); + } } }); diff --git a/src/spatial/collision.js b/src/spatial/collision.js index 1f0e71d3..93f093b4 100644 --- a/src/spatial/collision.js +++ b/src/spatial/collision.js @@ -265,6 +265,7 @@ Crafty.c("Collision", { l = results.length, dupes = {}, id, obj, oarea, key, + overlap = Crafty.rectManager.overlap, hasMap = ('map' in this && 'containsPoint' in this.map), finalresult = []; @@ -280,9 +281,7 @@ Crafty.c("Collision", { id = obj[0]; //check if not added to hash and that actually intersects - if (!dupes[id] && this[0] !== id && obj.__c[comp] && - oarea._x < area._x + area._w && oarea._x + oarea._w > area._x && - oarea._y < area._y + area._h && oarea._h + oarea._y > area._y) + if (!dupes[id] && this[0] !== id && obj.__c[comp] && overlap(oarea, area)) dupes[id] = obj; } diff --git a/src/spatial/rect-manager.js b/src/spatial/rect-manager.js index 752489ed..e9cb8593 100644 --- a/src/spatial/rect-manager.js +++ b/src/spatial/rect-manager.js @@ -27,12 +27,20 @@ Crafty.extend({ return target; }, - - - /** Checks whether two rectangles overlap */ - overlap: function (a, b) { - return (a._x < b._x + b._w && a._y < b._y + b._h && a._x + a._w > b._x && a._y + a._h > b._y); - }, + /**@ + * #Crafty.rectManager.overlap + * @comp Crafty.rectManager + * @sign public Boolean Crafty.rectManager.overlap(Object rectA, Object rectA) + * @param rectA - An object that must have the `_x, _y, _w, _h` values as properties + * @param rectB - An object that must have the `_x, _y, _w, _h` values as properties + * @return true if the rectangles overlap; false otherwise + * + * Checks whether two rectangles overlap. + */ + overlap: function (rectA, rectB) { + return (rectA._x < rectB._x + rectB._w && rectA._x + rectA._w > rectB._x && + rectA._y < rectB._y + rectB._h && rectA._h + rectA._y > rectB._y); + }, /**@ * #Crafty.rectManager.mergeSet diff --git a/src/spatial/spatial-grid.js b/src/spatial/spatial-grid.js index 01ffce39..a7cae249 100644 --- a/src/spatial/spatial-grid.js +++ b/src/spatial/spatial-grid.js @@ -75,6 +75,7 @@ var Crafty = require('../core/core.js'), search: function (rect, filter) { var keys = HashMap.key(rect, keyHolder), i, j, k, l, cell, + overlap = Crafty.rectManager.overlap, results = []; if (filter === undefined) filter = true; //default filter to true @@ -101,8 +102,7 @@ var Crafty = require('../core/core.js'), id = obj[0]; //unique ID obj = obj._mbr || obj; //check if not added to hash and that actually intersects - if (!found[id] && obj._x < rect._x + rect._w && obj._x + obj._w > rect._x && - obj._y < rect._y + rect._h && obj._h + obj._y > rect._y) + if (!found[id] && overlap(obj, rect)) found[id] = results[i]; } diff --git a/tests/2d.js b/tests/2d.js index 76da8e43..4264d0f4 100644 --- a/tests/2d.js +++ b/tests/2d.js @@ -46,10 +46,10 @@ strictEqual(player.intersect(0, 0, 100, 50), true, "Intersected"); strictEqual(player.intersect({ - x: 0, - y: 0, - w: 100, - h: 50 + _x: 0, + _y: 0, + _w: 100, + _h: 50 }), true, "Intersected Again"); strictEqual(player.intersect(100, 100, 100, 50), false, "Didn't intersect"); @@ -317,38 +317,38 @@ Crafty.trigger('KeyDown', { key: Crafty.keys.D }); - Crafty.trigger('EnterFrame'); + Crafty.trigger('EnterFrame', {dt: 1000}); equal(e._movement.x, 1); equal(e._x, 1); e.disableControl(); - Crafty.trigger('EnterFrame'); + Crafty.trigger('EnterFrame', {dt: 1000}); equal(e._movement.x, 0); equal(e._x, 1); Crafty.trigger('KeyUp', { key: Crafty.keys.D }); - Crafty.trigger('EnterFrame'); + Crafty.trigger('EnterFrame', {dt: 1000}); equal(e._movement.x, 0); equal(e._x, 1); e.enableControl(); - Crafty.trigger('EnterFrame'); + Crafty.trigger('EnterFrame', {dt: 1000}); equal(e._movement.x, 0); equal(e._x, 1); Crafty.trigger('KeyDown', { key: Crafty.keys.D }); - Crafty.trigger('EnterFrame'); + Crafty.trigger('EnterFrame', {dt: 1000}); equal(e._movement.x, 1); equal(e._x, 2); Crafty.trigger('KeyUp', { key: Crafty.keys.D }); - Crafty.trigger('EnterFrame'); + Crafty.trigger('EnterFrame', {dt: 1000}); equal(e._movement.x, 0); equal(e._x, 2); @@ -493,4 +493,263 @@ ok(e._cbr === null, "_cbr should now be removed along with Collision"); }); + + test("Motion", function() { + var Vector2D = Crafty.math.Vector2D; + var zero = new Vector2D(); + var ent = Crafty.e("2D, Motion, AngularMotion") + .attr({x: 0, y:0}); + + ok(ent.velocity().equals(zero), "linear velocity should be zero"); + strictEqual(ent.vrotation, 0, "angular velocity should be zero"); + ok(ent.acceleration().equals(zero), "linear acceleration should be zero"); + strictEqual(ent.arotation, 0, "angular acceleration should be zero"); + ok(ent.motionDelta().equals(zero), "linear delta should be zero"); + strictEqual(ent.drotation, 0, "angular delta should be zero"); + + ent.motionDelta().x = 20; + ok(ent.motionDelta().equals(zero), "linear delta should not have changed"); + ent.drotation = 10; + strictEqual(ent.drotation, 0, "angular delta should not have changed"); + + + var v0 = new Vector2D(2,5); var v0_r = 10; + ent.velocity().setValues(v0); + ent.vrotation = v0_r; + ok(ent.velocity().equals(v0), "linear velocity should be <2,5>"); + strictEqual(ent.vrotation, v0_r, "angular velocity should be 10"); + + var a = new Vector2D(4,2); var a_r = -15; + ent.acceleration().setValues(a); + ent.arotation = a_r; + ok(ent.acceleration().equals(a), "linear acceleration should be <4,2>"); + strictEqual(ent.arotation, a_r, "angular acceleration should be -15"); + + ent.velocity().x += 1; + ent.velocity().y *= 2; + ent.velocity().y -= 1; + ok(ent.velocity().equals(new Vector2D(v0.x+1, v0.y*2-1)), "linear velocity should be <3,9>"); + ent.arotation += 5; + strictEqual(ent.arotation, a_r + 5, "angular acceleration should be -10"); + + + ent.resetMotion(); + ent.resetAngularMotion(); + ok(ent.velocity().equals(zero), "linear velocity should be zero"); + strictEqual(ent.vrotation, 0, "angular velocity should be zero"); + ok(ent.acceleration().equals(zero), "linear acceleration should be zero"); + strictEqual(ent.arotation, 0, "angular acceleration should be zero"); + ok(ent.motionDelta().equals(zero), "linear delta should be zero"); + strictEqual(ent.drotation, 0, "angular delta should be zero"); + + + + + ent.velocity().setValues(v0); + ent.vrotation = v0_r; + Crafty.trigger('EnterFrame', {dt: 1000}); + ok(ent.velocity().equals(v0), "velocity should be <2,5>"); + strictEqual(ent.vrotation, v0_r, "angular velocity should be 10"); + ok(ent.motionDelta().equals(v0), "delta should be <2,5>"); + strictEqual(ent.drotation, v0_r, "angular delta should be 10"); + equal(ent.x, v0.x, "entity x should be 2"); + equal(ent.y, v0.y, "entity y should be 5"); + equal(ent.rotation, v0_r, "entity rotation should be 10"); + + var dPos = new Vector2D(a).scale(0.5).add(v0), dPos_r = v0_r + 0.5*a_r; + ent.acceleration().setValues(a); + ent.arotation = a_r; + Crafty.trigger('EnterFrame', {dt: 1000}); + ok(dPos.equals(new Vector2D(4,6)), "should be <4,6>"); + strictEqual(dPos_r, 2.5, "should be 2.5"); + ok(ent.motionDelta().equals(dPos), "delta should be <4,6>"); + strictEqual(ent.drotation, dPos_r, "should be 2.5"); + equal(ent.x, v0.x + dPos.x, "entity x should be 6"); + equal(ent.y, v0.y + dPos.y, "entity y should be 11"); + equal(ent.rotation, v0_r + dPos_r, "entity rotation should be 12.5"); + var v1 = new Vector2D(v0).add(a), v1_r = v0_r + a_r; + ok(ent.velocity().equals(v1), "linear velocity should be <6,7>"); + strictEqual(ent.vrotation, v1_r, "angular velocity should be -5"); + + + + ent.attr({x: 0, y: 0}) + .resetMotion() + .resetAngularMotion(); + + ent.velocity().x = 10; + ent.acceleration().x = 5; + Crafty.trigger('EnterFrame', {dt: 500}); + equal(ent.velocity().x, 10+5*0.5, "velocity x should be 12.5"); + equal(ent.x, 10*0.5+0.5*5*0.5*0.5, "entity x should be 5.625"); + + ent.destroy(); + }); + + test("Supportable", function() { + var ground = Crafty.e("2D, Ground").attr({x: 0, y: 10, w:10, h:10}); // [0,10] to [0,20] + + var landedCount = 0, liftedCount = 0; + var ent = Crafty.e("2D, Supportable") + .attr({x: 0, y:0, w:5, h:5}) + .bind("LandedOnGround", function(obj) { + ok(ent.ground(), "entity should be on ground"); + equal(obj, ground, "ground object should be equal"); + landedCount++; + }) + .bind("LiftedOffGround", function(obj) { + ok(!ent.ground(), "entitiy should not be on ground"); + equal(obj, ground, "ground object should be equal"); + liftedCount++; + }) + .startGroundDetection("Ground"); + + + ok(!ent.ground(), "entity should not be on ground"); + Crafty.trigger("EnterFrame"); + ok(!ent.ground(), "entity should not be on ground"); + + ent.y = 5; + Crafty.trigger("EnterFrame"); // 1 landed event should have occured + equal(ent.y, 5, "ent y should not have changed"); + ok(ent.ground(), "entity should be on ground"); + + ent.y = 0; + Crafty.trigger("EnterFrame"); // 1 lifted event should have occured + equal(ent.y, 0, "ent y should not have changed"); + ok(!ent.ground(), "entity should not be on ground"); + + ent.y = 7; + Crafty.trigger("EnterFrame"); // 1 landed event should have occured + equal(ent.y, 5, "ent y should have been snapped to ground"); + ok(ent.ground(), "entity should be on ground"); + + ent.y = 0; + Crafty.trigger("EnterFrame"); // 1 lifted event should have occured + equal(ent.y, 0, "ent y should not have changed"); + ok(!ent.ground(), "entity should not be on ground"); + + ent.bind("CheckLanding", function(ground) { + this.canLand = false; + }); + ent.y = 7; + Crafty.trigger("EnterFrame"); // no event should have occured + equal(ent.y, 7, "ent y should not have changed"); + ok(!ent.ground(), "entity should not be on ground"); + + + equal(landedCount, 2, "landed count mismatch"); + equal(liftedCount, 2, "lifted count mismatch"); + + ground.destroy(); + ent.destroy(); + }); + + test("GroundAttacher", function() { + var ground = Crafty.e("2D, Ground"); + var player = Crafty.e("2D, GroundAttacher"); + + player.trigger("LandedOnGround", ground); + ground.x = 10; + strictEqual(player.x, 10, "player moved with ground"); + + player.trigger("LiftedOffGround", ground); + ground.x = 20; + strictEqual(player.x, 10, "player did not move with ground"); + }); + + test("Gravity", function() { + var ground = Crafty.e("2D, platform") + .attr({ x: 0, y: 280, w: 600, h: 20 }); + + var player = Crafty.e("2D, Gravity") + .attr({ x: 0, y: 100, w: 32, h: 16 }) + .gravity("platform"); + + strictEqual(player.acceleration().y, player._gravityConst, "acceleration should match gravity constant"); + + var vel = -1; + player.bind("EnterFrame", function() { + if (!this.ground()) { + ok(this.velocity().y > vel, "velocity should increase"); + vel = this.velocity().y; + } else { + vel = -1; + } + }); + + var landCount = 0, liftCount = 0; + player.bind("LandedOnGround", function() { + landCount++; + strictEqual(this.acceleration().y, 0, "acceleration should be zero"); + strictEqual(this.velocity().y, 0, "velocity should be zero"); + + if (landCount === 1) { + this.bind("LiftedOffGround", function() { + liftCount++; + + Crafty.trigger("EnterFrame", {dt: 50}); + Crafty.trigger("EnterFrame", {dt: 50}); + Crafty.trigger("EnterFrame", {dt: 50}); + vel = -1; + + var oldVel = this.velocity().y; + this.gravityConst(5); + strictEqual(this._gravityConst, this.__convertPixelsToMeters(5), "gravity constant should have changed"); + strictEqual(this.acceleration().y, this._gravityConst, "acceleration should match gravity constant"); + strictEqual(this.velocity().y, oldVel, "velocity shouldn't have been resetted"); + }); + this.attr({y: 100}); + } else { + strictEqual(landCount, 2, "two land on ground events should have been registered"); + strictEqual(liftCount, 1, "one lift off ground event should have been registered"); + + ground.destroy(); + player.destroy(); + + start(); + } + }); + + stop(); + }); + + test("Twoway", function() { + var ground = Crafty.e("2D, platform") + .attr({ x: 0, y: 200, w: 10, h: 20 }); + + var player = Crafty.e("2D, Gravity, Twoway") + .attr({ x: 0, y: 150, w: 32, h: 10 }) + .gravity("platform") + .twoway(2, 4); + + var landCount = 0, liftCount = 0; + player.bind("LandedOnGround", function() { + landCount++; + + if (landCount === 1) { + this.bind("LiftedOffGround", function() { + liftCount++; + this.bind("EnterFrame", function() { + this.trigger("KeyDown", {key: Crafty.keys.UP_ARROW}); + if (this.velocity().y < -this._jumpSpeed) + ok(false, "Twoway should not modify velocity"); + }); + }); + + this.trigger("KeyDown", {key: Crafty.keys.UP_ARROW}); + } else { + strictEqual(landCount, 2, "two land on ground events should have been registered"); + strictEqual(liftCount, 1, "one lift off ground event should have been registered"); + + ground.destroy(); + player.destroy(); + + start(); + } + }); + + stop(); + }); + })(); diff --git a/tests/core.js b/tests/core.js index 7699861a..59bb1957 100644 --- a/tests/core.js +++ b/tests/core.js @@ -88,28 +88,49 @@ }); - test("setter", function() { + test("defineField", function() { if (!(Crafty.support.setter || Crafty.support.defineProperty)) { // IE8 has a setter() function but it behaves differently. No test is currently written for IE8. expect(0); return; } + var first = Crafty.e("test"); - first.setter('p1', function(v) { + + + first.setter('p0', function(v) { + this._p0 = v * 5; + }); + first.p0 = 2; + strictEqual(first._p0, 10, "single property setter"); + strictEqual(first.p0, undefined, "single property getter"); + + + first.defineField('p1', function() { + return this._p1; + }, function(v) { this._p1 = v * 2; }); first.p1 = 2; - strictEqual(first._p1, 4, "single property setter"); + strictEqual(first.p1, 4, "single property getter & setter"); - first.setter('p2', function(v) { + first.defineField('p2', function() { + return this._p2; + }, function(v) { this._p2 = v * 2; - }).setter('p3', function(v) { + }).defineField('p3', function() { + return this._p3; + }, function(v) { this._p3 = v * 2; }); first.p2 = 2; first.p3 = 3; - strictEqual(first._p2 + first._p3, 10, "two property setters"); + strictEqual(first.p2 + first.p3, 10, "two property getters & setters"); + if (Crafty.support.defineProperty) { + delete first.p1; + strictEqual(first.p1, 4, "property survived deletion"); + } }); test("bind", function() { @@ -636,7 +657,6 @@ deepEqual(fox.attr('contact'), {email: 'foxxy@example.com', phone: '555-555-4545'}); }); - module("Timer"); test('Timer.simulateFrames', function() {