1 /**
  2  * The Render Engine
  3  * Object2D
  4  *
  5  * @fileoverview An extension of the <tt>HostObject</tt> which is specifically geared
  6  *               towards 2d game development.
  7  *
  8  * @author: Brett Fattori (brettf@renderengine.com)
  9  * @author: $Author: bfattori $
 10  * @version: $Revision: 1555 $
 11  *
 12  * Copyright (c) 2011 Brett Fattori (brettf@renderengine.com)
 13  *
 14  * Permission is hereby granted, free of charge, to any person obtaining a copy
 15  * of this software and associated documentation files (the "Software"), to deal
 16  * in the Software without restriction, including without limitation the rights
 17  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 18  * copies of the Software, and to permit persons to whom the Software is
 19  * furnished to do so, subject to the following conditions:
 20  *
 21  * The above copyright notice and this permission notice shall be included in
 22  * all copies or substantial portions of the Software.
 23  *
 24  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 25  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 26  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 27  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 28  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 29  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 30  * THE SOFTWARE.
 31  *
 32  */
 33 
 34 // The class this file defines and its required classes
 35 R.Engine.define({
 36     "class":"R.objects.Object2D",
 37     "requires":[
 38         "R.engine.GameObject",
 39         "R.collision.OBBHull",
 40         "R.math.Circle2D",
 41         "R.math.Math2D",
 42         "R.components.Transform2D"
 43     ]
 44 });
 45 
 46 /**
 47  * @class An object for use in a 2d game environment.  If no <tt>transformComponent</tt> is provided,
 48  * the object will be assigned a {@link R.components.Transform2D Transform2D} component.  This class is the recommended
 49  * base class for objects used within a 2d game environment, instead of deriving from the base
 50  * {@link R.engine.GameObject} class.
 51  *
 52  * @param name {String} The name of the object
 53  * @param [transformComponent] {R.components.Transform2D} The transform component to use, or
 54  *        <code>null</code>.  If the value is <code>null</code>, the object will be assigned a default
 55  *        {@link R.components.Transform2D Transform2D} component.
 56  * @extends R.engine.GameObject
 57  * @constructor
 58  * @description Create a game object with methods for operating in a 2D context.
 59  */
 60 R.objects.Object2D = function () {
 61     return R.engine.GameObject.extend(/** @scope R.objects.Object2D.prototype */{
 62 
 63         /** @private */
 64         zIndex:1,
 65 
 66         /** @private */
 67         bBox:null,
 68         AABB:null,
 69         wBox:null,
 70         wCircle:null,
 71         lastPosition:null,
 72         origin:null,
 73         originNeg:null,
 74         originPos:null,
 75         collisionHull:null,
 76         genHull:null,
 77         defaultTxfmComponent:null,
 78 
 79         oldRenderPosition:null,
 80         oldBbox:null,
 81         oldScale:null,
 82 
 83         // Simple flag indicating object is descendant of Object2D
 84         __OBJECT2D:true,
 85 
 86         // Current origin/negative-origin matrices
 87         oMtx:null,
 88         oMtxN:null,
 89 
 90         /** @private */
 91         constructor:function (name, transformComponent) {
 92             this.base(name);
 93             this.lastPosition = R.math.Point2D.create(5, 5);
 94             this.originPos = R.math.Point2D.create(5, 5);
 95             this.oldRenderPosition = R.math.Point2D.create(5, 5);
 96             this.oldBbox = R.math.Rectangle2D.create(0, 0, 1, 1);
 97             this.oldScale = R.math.Vector2D.create(1, 1);
 98             this.bBox = R.math.Rectangle2D.create(0, 0, 1, 1);
 99             this.AABB = R.math.Rectangle2D.create(0, 0, 1, 1);
100             this.wBox = R.math.Rectangle2D.create(0, 0, 1, 1);
101             this.wCircle = R.math.Circle2D.create(0, 0, 1);
102             this.zIndex = 0;
103             this.origin = R.math.Point2D.create(0, 0);
104             this.originNeg = R.math.Point2D.create(0, 0);
105             this.collisionHull = null;
106             this.genHull = false;
107 
108             // Assign a default 2d transformation component to store position information
109             this.defaultTxfmComponent = transformComponent != null ? transformComponent : R.components.Transform2D.create("dTxfm__");
110             this.add(this.defaultTxfmComponent);
111 
112             // Initialize the matrices
113             this.oMtx = R.math.Math2D.identityMatrix();
114             this.oMtxN = R.math.Math2D.identityMatrix();
115             this.__OBJECT2D = true;
116         },
117 
118         /**
119          * Destroy the object.
120          */
121         destroy:function () {
122             this.bBox.destroy();
123             this.wBox.destroy();
124             this.wCircle.destroy();
125             this.lastPosition.destroy();
126             this.originPos.destroy();
127             this.oldRenderPosition.destroy();
128             this.oldBbox.destroy();
129             this.oldScale.destroy();
130             this.AABB.destroy();
131             this.wCircle.destroy();
132             this.origin.destroy();
133             this.originNeg.destroy();
134             if (this.collisionHull) {
135                 this.collisionHull.destroy();
136             }
137             this.base();
138         },
139 
140         /**
141          * Release the object back into the pool.
142          */
143         release:function () {
144             this.base();
145             this.zIndex = 1;
146             this.bBox = null;
147             this.wBox = null;
148             this.wCircle = null;
149             this.lastPosition = null;
150             this.originPos = null;
151             this.oldRenderPosition = null;
152             this.oldBbox = null;
153             this.oldScale = null;
154             this.AABB = null;
155             this.wCircle = null;
156             this.origin = null;
157             this.originNeg = null;
158             this.collisionHull = null;
159             this.genHull = null;
160 
161             // Free the matrices
162             this.oMtx = null;
163             this.oMtxN = null;
164         },
165 
166         /**
167          * Get the transformation matrix for this object
168          * @return {Matrix}
169          */
170         getTransformationMatrix:function () {
171             // Translation
172             var p = this.getRenderPosition();
173             var tMtx = $M([
174                 [1, 0, p.x],
175                 [0, 1, p.y],
176                 [0, 0, 1]
177             ]);
178             tMtx = tMtx.multiply(this.oMtxN);
179 
180             // Rotation
181             var a = this.getRotation();
182             var rMtx;
183             if (a != 0) {
184                 // Move the origin
185                 rMtx = this.oMtx.dup();
186                 // Rotate
187                 rMtx = rMtx.multiply(Matrix.Rotation(R.math.Math2D.degToRad(a), R.objects.Object2D.ROTATION_AXIS));
188                 // Move the origin back
189                 rMtx = rMtx.multiply(this.oMtxN);
190             }
191             else {
192                 // Set to identity
193                 rMtx = R.math.Math2D.identityMatrix();
194             }
195 
196             // Scale
197             var sX = this.getScaleX(), sY = this.getScaleY(), sMtx = $M([
198                     [sX, 0, 0],
199                     [0, sY, 0],
200                     [0, 0, 1]
201                 ]),
202                 txfmMtx = tMtx.multiply(rMtx).multiply(sMtx);
203 
204             rMtx = null;
205             sMtx = null;
206             return txfmMtx;
207         },
208 
209         /**
210          * Set the render origin of the object.  The render origin is where the object will be
211          * centered around when drawing position and rotation.
212          *
213          * @param x {Number|R.math.Point2D} The X coordinate or the render origin (default: 0,0 - top left corner)
214          * @param y {Number} The Y coordinate or <code>null</code> if X is a <code>Point2D</code>
215          */
216         setOrigin:function (x, y) {
217             this.origin.set(x, y);
218             this.originNeg.set(x, y).neg();
219 
220             var pX = x;
221             var pY = y;
222 
223             if (x.__POINT2D) {
224                 pX = x.x;
225                 pY = x.y;
226             }
227 
228             this.oMtx.setElements([
229                 [1, 0, pX],
230                 [0, 1, pY],
231                 [0, 0, 1]
232             ]);
233             this.oMtxN.setElements([
234                 [1, 0, -pX],
235                 [0, 1, -pY],
236                 [0, 0, 1]
237             ]);
238             this.markDirty();
239         },
240 
241         /**
242          * Get the render origin of the object.
243          * @return {R.math.Point2D}
244          */
245         getOrigin:function () {
246             return this.origin;
247         },
248 
249         /**
250          * Set the bounding box of this object
251          *
252          * @param width {Number|R.math.Rectangle2D} The width, or the rectangle that completely encompasses
253          *                                   this object.
254          * @param height {Number} If width is a number, this is the height
255          */
256         setBoundingBox:function (width, height) {
257             if (width.__RECTANGLE2D) {
258                 this.bBox.set(width);
259             }
260             else {
261                 this.bBox.set(0, 0, width, height);
262             }
263 
264             if (this.genHull) {
265                 // Do this so a new collision hull is generated
266                 this.collisionHull = null;
267                 this.genHull = false;
268             }
269 
270             this.markDirty();
271         },
272 
273         /**
274          * Get the object's local bounding box.
275          * @return {R.math.Rectangle2D} The object bounding rectangle
276          */
277         getBoundingBox:function () {
278             return this.bBox;
279         },
280 
281         /**
282          * [ABSTRACT] Get the object's local bounding circle.
283          * @return {R.math.Circle2D} The object bounding circle
284          */
285         getBoundingCircle:function () {
286             return null;
287         },
288 
289         /**
290          * Get the object's bounding box in world coordinates.
291          * @return {R.math.Rectangle2D} The world bounding rectangle
292          */
293         getWorldBox:function () {
294             // Only update if the object has moved, changed size, or has been scaled
295             if (this.getRenderPosition().equals(this.oldRenderPosition) &&
296                 this.bBox.equals(this.oldBbox) && this.getScale().equals(this.oldScale)) {
297                 return this.wBox;
298             }
299 
300             this.wBox.set(this.getBoundingBox());
301 
302             // Need to apply scaling
303             this.wBox.setWidth(this.wBox.w * this.getScaleX());
304             this.wBox.setHeight(this.wBox.h * this.getScaleY());
305 
306             var rPos = R.math.Point2D.create(this.getRenderPosition()).add(this.originNeg);
307             this.wBox.offset(rPos);
308             rPos.destroy();
309 
310             // Remember the changes
311             this.oldRenderPosition.set(this.getRenderPosition());
312             this.oldBbox.set(this.bBox);
313             this.oldScale.set(this.getScale());
314             return this.wBox;
315         },
316 
317         /**
318          * Get the object's bounding circle in world coordinates.  If {@link #getBoundingCircle} returns
319          * null, the bounding circle will be approximated using {@link #getBoundingBox}.
320          *
321          * @return {R.math.Circle2D} The world bounding circle
322          */
323         getWorldCircle:function () {
324             var c = this.getBoundingCircle();
325 
326             if (c === null) {
327                 c = R.math.Circle2D.approximateFromRectangle(this.getBoundingBox());
328                 this.wCircle.set(c);
329                 c.destroy();
330             } else {
331                 this.wCircle.set(c);
332             }
333 
334             var rPos = R.math.Point2D.create(this.getRenderPosition()).add(this.originNeg);
335             this.wCircle.offset(rPos);
336             rPos.destroy();
337             return this.wCircle;
338         },
339 
340         /**
341          * Get an axis aligned world bounding box for the object.  This bounding box
342          * is ensured to encompass the entire object.
343          * @return {R.math.Rectangle2D}
344          */
345         getAABB:function () {
346             // Start with the world bounding box and transform it
347             var bb = R.math.Rectangle2D.create(this.getBoundingBox());
348 
349             // Transform the world box
350             var txfm = this.getTransformationMatrix();
351 
352             var p1 = bb.getTopLeft();
353             var p2 = R.math.Point2D.create(bb.getTopLeft());
354             p2.x += bb.getDims().x;
355             var p3 = bb.getBottomRight();
356             var p4 = R.math.Point2D.create(bb.getTopLeft());
357             p4.y += bb.getDims().y;
358             var pts = [p1.transform(txfm), p2.transform(txfm), p3.transform(txfm), p4.transform(txfm)];
359 
360             // Now find the AABB of the points
361             R.math.Math2D.getBoundingBox(pts, this.AABB);
362 
363             bb.destroy();
364             p2.destroy();
365             p3.destroy();
366 
367             return this.AABB;
368         },
369 
370         /**
371          * Set the convex hull used for collision.  The {@link R.components.ConvexCollider ConvexCollider} component
372          * uses the collision hull to perform the collision testing.
373          * @param convexHull {R.collision.ConvexHull} The convex hull object
374          */
375         setCollisionHull:function (convexHull) {
376             Assert(convexHull instanceof R.collision.ConvexHull, "setCollisionHull() - not ConvexHull!");
377             this.collisionHull = convexHull;
378             this.collisionHull.setGameObject(this);
379             this.genHull = false;
380             this.markDirty();
381         },
382 
383         /**
384          * Get the convex hull used for collision testing with a {@link R.components.ConvexCollider ConvexCollider}
385          * component.  If no collision hull has been assigned, a {@link R.collision.OBBHull OBBHull} will
386          * be created and returned.
387          *
388          * @return {R.collision.ConvexHull}
389          */
390         getCollisionHull:function () {
391             if (this.collisionHull == null) {
392                 this.collisionHull = R.collision.OBBHull.create(this.getBoundingBox());
393 
394                 // A flag indicating the hull was auto-generated
395                 this.genHull = true;
396             }
397 
398             return this.collisionHull;
399         },
400 
401         /**
402          * Get the default transform component.
403          * @return {R.components.Transform2D}
404          */
405         getDefaultTransformComponent:function () {
406             return this.defaultTxfmComponent;
407         },
408 
409         /**
410          * Set, or override, the default transformation component.
411          * @param transformComponent {R.components.Transform2D}
412          */
413         setDefaultTransformComponent:function (transformComponent) {
414             Assert(transformComponent && transformComponent instanceof R.components.Transform2D, "Default transform component not R.components.Transform2D or subclass");
415 
416             // If this is the component created by the system, we can just destroy it
417             if (this.defaultTxfmComponent && this.defaultTxfmComponent.getName() === "DTXFM__") {
418                 this.remove(this.defaultTxfmComponent);
419                 this.defaultTxfmComponent.destroy();
420             }
421 
422             this.defaultTxfmComponent = transformComponent;
423         },
424 
425         /**
426          * Set the position of the object
427          * @param point {R.math.Point2D|Number} The position of the object, or a simple X coordinate
428          * @param [y] {Number} A Y coordinate if <tt>point</tt> is a number
429          */
430         setPosition:function (point, y) {
431             this.getDefaultTransformComponent().getPosition().set(point, y);
432             this.markDirty();
433         },
434 
435         /**
436          * Get the position of the object.
437          * @return {R.math.Point2D} The position
438          */
439         getPosition:function () {
440             return this.getDefaultTransformComponent().getPosition();
441         },
442 
443         /**
444          * Get the position of the object, at its origin.
445          * @return {R.math.Point2D} The position
446          */
447         getOriginPosition:function () {
448             return this.originPos.set(this.getPosition()).add(this.getOrigin());
449         },
450 
451         /**
452          * Get the render position of the object.
453          * @return {R.math.Point2D}
454          */
455         getRenderPosition:function () {
456             return this.getDefaultTransformComponent().getRenderPosition();
457         },
458 
459         /**
460          * Get the last position the object was rendered at.
461          * @return {R.math.Point2D}
462          */
463         getLastPosition:function () {
464             return this.getDefaultTransformComponent().getLastPosition();
465         },
466 
467         /**
468          * Set the rotation of the object
469          * @param angle {Number} The rotation angle
470          */
471         setRotation:function (angle) {
472             this.getDefaultTransformComponent().setRotation(angle);
473             this.markDirty();
474         },
475 
476         /**
477          * Get the rotation of the object
478          * @return {Number} Angle in degrees
479          */
480         getRotation:function () {
481             return this.getDefaultTransformComponent().getRotation();
482         },
483 
484         /**
485          * Get the world adjusted rotation of the object
486          * @return {Number} Angle in degrees
487          */
488         getRenderRotation:function () {
489             return this.getDefaultTransformComponent().getRenderRotation();
490         },
491 
492         /**
493          * Set the scale of the object along the X and Y axis in the scaling matrix
494          * @param scaleX {Number} The scale along the X axis
495          * @param [scaleY] {Number} Optional scale along the Y axis.  If no value is provided
496          *        <tt>scaleX</tt> will be used to perform a uniform scale.
497          */
498         setScale:function (scaleX, scaleY) {
499             this.getDefaultTransformComponent().setScale(scaleX, scaleY);
500             this.markDirty();
501         },
502 
503         /**
504          * Get the scale of the object along both the X and Y axis.
505          * @return {R.math.Vector2D}
506          */
507         getScale:function () {
508             return this.getDefaultTransformComponent().getScale();
509         },
510 
511         /**
512          * Get the scale of the object along the X axis
513          * @return {Number}
514          */
515         getScaleX:function () {
516             return this.getDefaultTransformComponent().getScaleX();
517         },
518 
519         /**
520          * Get the scale of the object along the Y axis.
521          * @return {Number}
522          */
523         getScaleY:function () {
524             return this.getDefaultTransformComponent().getScaleY();
525         },
526 
527         /**
528          * Set the depth at which this object will render to
529          * the context.  The lower the z-index, the further
530          * away from the front the object will draw.
531          *
532          * @param zIndex {Number} The z-index of this object
533          */
534         setZIndex:function (zIndex) {
535             if (this.getRenderContext() && this.getRenderContext().swapBins) {
536                 this.getRenderContext().swapBins(this, this.zIndex, zIndex);
537             }
538             this.zIndex = zIndex;
539             if (this.getRenderContext()) {
540                 this.getRenderContext().sort();
541             }
542             this.markDirty();
543         },
544 
545         /**
546          * Get the depth at which this object will render to
547          * the context.
548          *
549          * @return {Number}
550          */
551         getZIndex:function () {
552             return this.zIndex;
553         },
554 
555         /**
556          * Returns a bean which represents the read or read/write properties
557          * of the object.
558          *
559          * @return {Object} The properties object
560          */
561         getProperties:function () {
562             var self = this;
563             var prop = this.base(self);
564             return $.extend(prop, {
565                 "ZIndex":[function () {
566                     return self.getZIndex();
567                 }, function (i) {
568                     self.setZIndex(parseInt(i));
569                 }, true],
570                 "BoundingBox":[function () {
571                     return self.getBoundingBox().toString();
572                 }, null, false],
573                 "WorldBox":[function () {
574                     return self.getWorldBox().toString();
575                 }, null, false],
576                 "Position":[function () {
577                     return self.getPosition().toString();
578                 }, function (i) {
579                     var p = i.split(",");
580                     self.setPosition(R.math.Point2D.create(parseFloat(p[0]), parseFloat(p[1])));
581                 }, true],
582                 "Origin":[function () {
583                     return self.getOrigin().toString();
584                 }, function (i) {
585                     var p = i.split(",");
586                     self.setOrigin(parseFloat(p[0]), parseFloat(p[1]));
587                 }, true],
588                 "RenderPos":[function () {
589                     return self.getRenderPosition().toString()
590                 }, null, false],
591                 "Rotation":[function () {
592                     return self.getRotation();
593                 }, function (i) {
594                     self.setRotation(parseFloat(i));
595                 }, true],
596                 "ScaleX":[function () {
597                     return self.getScaleX();
598                 }, function (i) {
599                     self.setScale(parseFloat(i), self.getScaleY());
600                 }, true],
601                 "ScaleY":[function () {
602                     return self.getScaleY();
603                 }, function (i) {
604                     self.setScale(self.getScaleX(), parseFloat(i));
605                 }, true]
606             });
607         }
608 
609     }, /** @scope R.objects.Object2D.prototype */ {
610         /**
611          * Get the class name of this object
612          *
613          * @return {String} "R.objects.Object2D"
614          */
615         getClassName:function () {
616             return "R.objects.Object2D";
617         },
618 
619         /**
620          * Get a properties object with values for the given object.
621          * @param obj {R.objects.Object2D} The object to query
622          * @param [defaults] {Object} Default values that don't need to be serialized unless
623          *    they are different.
624          * @return {Object}
625          */
626         serialize:function (obj, defaults) {
627             // Defaults for object properties which can be skipped if no different
628             defaults = defaults || [];
629             $.extend(defaults, {
630                 "Position":"0.00,0.00",
631                 "Origin":"0.00,0.00",
632                 "Rotation":"0",
633                 "ScaleX":"1",
634                 "ScaleY":"1",
635                 "Action":""
636             });
637             var propObj = R.engine.PooledObject.serialize(obj, defaults);
638             if (obj.getDefaultTransformComponent().getName() !== "DTXFM__") {
639                 // They assigned a different transform component, make sure to export it
640                 propObj.DTXFM_COMP = obj.getDefaultTransformComponent().constructor.getClassName();
641                 propObj.DTXFM_NAME = obj.getDefaultTransformComponent().getName();
642             }
643             return propObj;
644         },
645 
646         /**
647          * Deserialize the object back into a 2d object.
648          * @param obj {Object} The object to deserialize
649          * @param [clazz] {Class} The object class to populate
650          * @return {R.objects.Object2D} The object which was deserialized
651          */
652         deserialize:function (obj, clazz) {
653             // Is there a special transform component assigned to this object?
654             var txfmComponent;
655             if (obj.DTXFM_COMP) {
656                 txfmComponent = R.getClassForName(obj.DTXFM_COMP).create(obj.DTXFM_NAME);
657                 delete obj.DTXFM_COMP;
658                 delete obj.DTXFM_NAME;
659             }
660 
661             // Now we can create the class
662             clazz = clazz || R.objects.Object2D.create(obj.name, txfmComponent);
663             R.engine.PooledObject.deserialize(obj, clazz);
664 
665             return clazz;
666         },
667 
668         /**
669          * The axis of rotation
670          * @private
671          */
672         ROTATION_AXIS:$V([0, 0, 1])
673     });
674 
675 };