1 /** 2 * The Render Engine 3 * 4 * SpriteActor object 5 * 6 * @author: Brett Fattori (brettf@renderengine.com) 7 * 8 * @author: $Author: bfattori@gmail.com $ 9 * @version: $Revision: 1562 $ 10 * 11 * Copyright (c) 2011 Brett Fattori (brettf@renderengine.com) 12 * 13 * Permission is hereby granted, free of charge, to any person obtaining a copy 14 * of this software and associated documentation files (the "Software"), to deal 15 * in the Software without restriction, including without limitation the rights 16 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 17 * copies of the Software, and to permit persons to whom the Software is 18 * furnished to do so, subject to the following conditions: 19 * 20 * The above copyright notice and this permission notice shall be included in 21 * all copies or substantial portions of the Software. 22 * 23 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 24 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 25 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 26 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 27 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 28 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 29 * THE SOFTWARE. 30 * 31 */ 32 33 // The class this file defines and its required classes 34 R.Engine.define({ 35 "class":"R.objects.SpriteActor", 36 "requires":[ 37 "R.objects.Object2D", 38 "R.components.render.Sprite", 39 "R.components.render.DOM", 40 "R.components.input.Keyboard", 41 "R.components.collision.Convex", 42 "R.components.transform.PlatformMover2D", 43 "R.collision.OBBHull" 44 ] 45 }); 46 47 /** 48 * @class A <tt>SpriteActor</tt> is an actor object within a game, whose renderer 49 * is a 2D sprite. It can have actions assigned to it, which are triggered 50 * by either a controlling player or generated by the program. 51 * @param name {String} The name of the object 52 * @extends R.objects.Object2D 53 * @constructor 54 * @description Create a sprite actor 55 */ 56 R.objects.SpriteActor = function () { 57 return R.objects.Object2D.extend(/** @scope R.objects.SpriteActor.prototype */{ 58 59 editing:false, 60 sprite:null, 61 actorId:null, 62 collisionMask:null, 63 collidable:null, 64 scriptedActions:null, 65 scriptedVars:null, 66 67 /** @private */ 68 constructor:function (name, tileMap) { 69 this.base(name || "Actor", R.components.transform.PlatformMover2D.create("move", tileMap)); 70 71 this.editing = false; 72 73 this.actorId = ""; 74 this.collisionMask = "0"; 75 this.scriptedActions = {}; 76 this.scriptedVars = {}; 77 78 this.collidable = false; 79 80 // Add sprite component to draw the player 81 this.add(R.components.render.Sprite.create("draw")); 82 83 // We need an element to render to when using a DOM context 84 this.setElement($("<div>").css({ 85 position:"absolute", 86 left:0, 87 top:0 88 })); 89 90 // We also need the DOM render component. This is what 91 // causes the transformations to be updated each frame 92 // for a DOM object. 93 this.add(R.components.render.DOM.create("domdraw")); 94 }, 95 96 /** 97 * After the actor is added to the context, allow it a chance to initialize. 98 * @private 99 */ 100 afterAdd:function (parent) { 101 this.base(parent); 102 this.callScriptedEvent("onInit", []); 103 }, 104 105 /** 106 * Destroy the object 107 */ 108 destroy:function () { 109 this.callScriptedEvent("onDestroy", []); 110 this.base(); 111 }, 112 113 /** 114 * Release the object back into the pool. 115 */ 116 release:function () { 117 this.base(); 118 this.scriptedActions = null; 119 this.scriptedVars = null; 120 }, 121 122 setTileMap:function (tileMap) { 123 this.getComponent("move").setTileMap(tileMap); 124 }, 125 126 getTileMap:function () { 127 return this.getComponent("move").getTileMap(); 128 }, 129 130 /** 131 * Get a properties object for this sprite actor 132 * @return {Object} 133 */ 134 getProperties:function () { 135 var self = this; 136 var prop = this.base(self); 137 return $.extend(prop, { 138 "Sprite":[function () { 139 return self.sprite.getSpriteResource().resourceName + ":" + self.sprite.getName(); 140 }, 141 typeof LevelEditor !== "undefined" ? { "multi":true, 142 "opts":LevelEditor.getSpriteOptions, 143 "fn":function (s) { 144 self.setSprite(LevelEditor.getSpriteForName(s)); 145 }} : null, 146 !!(typeof LevelEditor !== "undefined") ], 147 "Collidable":[ function () { 148 return self.isCollidable(); 149 }, 150 typeof LevelEditor !== "undefined" ? { "toggle":true, 151 "fn":function (s) { 152 self.setCollidable(s); 153 }} : null, 154 !!(typeof LevelEditor !== "undefined") ], 155 "Gravity":[ function () { 156 return self.getGravityFlag(); 157 }, 158 { "toggle":true, 159 "fn":function (s) { 160 self.setGravityFlag(s); 161 } 162 }, true] 163 }); 164 }, 165 166 /** 167 * Check to see if this actor is affected by gravity. 168 * @return {Boolean} 169 */ 170 getGravityFlag:function () { 171 return !this.getComponent("move").getGravity().isZero(); 172 }, 173 174 /** 175 * Set a flag indicating that this actor is affected by gravity. 176 * @param state {Boolean} <code>true</code> to enable gravity on this actor 177 */ 178 setGravityFlag:function (state) { 179 if (state) { 180 // TODO: Make this a "level property" 181 this.getComponent("move").setGravity(0.0, 0.2); 182 } else { 183 this.getComponent("move").setGravity(R.math.Vector2D.ZERO); 184 } 185 }, 186 187 /** 188 * Set the actor's Id which can be looked up with {@link R.objects.SpriteActor#findActor} 189 * @param actorId {String} A unique Id to reference this object 190 */ 191 setActorId:function (actorId) { 192 this.actorId = actorId; 193 }, 194 195 /** 196 * Get the actor's unique Id which references this object 197 * @return {String} 198 */ 199 getActorId:function () { 200 return this.actorId; 201 }, 202 203 /** 204 * Set the collision bitmask for this object 205 * @param collisionMask {String} A binary string of ones and zeros 206 */ 207 setCollisionMask:function (collisionMask) { 208 this.collisionMask = collisionMask; 209 }, 210 211 /** 212 * Get the collision bitmask for this object 213 * @return {String} 214 */ 215 getCollisionMask:function () { 216 return this.collisionMask; 217 }, 218 219 /** 220 * Get the event associated with the action name. 221 * @param {Object} actionName 222 * @private 223 */ 224 getActorEvent:function (actionName) { 225 return this.scriptedActions[actionName]; 226 }, 227 228 /** 229 * Set the event handler for the action name. 230 * @param {Object} actionName 231 * @param {Object} script 232 * @private 233 */ 234 setActorEvent:function (actionName, script) { 235 this.scriptedActions[actionName] = { "script":script }; 236 }, 237 238 /** 239 * Calls a scripted event. If the event handler hasn't been compiled yet, it 240 * will be compiled and then called in the scope of this actor. 241 * @param eventName {String} The name of the event to call 242 * @param argNames {Array} An array of argument names to map the arguments array to (1:1) 243 * @param args {Array} The array of arguments to pass to the event handler 244 * @private 245 */ 246 callScriptedEvent:function (eventName, argNames, args) { 247 var eScript = evtScript = this.getActorEvent(eventName); 248 if (R.isEmpty(evtScript)) { 249 return; 250 } 251 252 // Is it compiled already? 253 if (eScript.compiled) { 254 evtScript = eScript.compiled; 255 } else { 256 // Compile the script, inject the variables 257 var varScript = ""; 258 for (var a in argNames) { 259 varScript += "var " + argNames[a] + "=arguments[" + a + "]; "; 260 } 261 evtScript = this.scriptedActions[eventName].compiled = new Function(varScript + eScript.script); 262 } 263 264 return evtScript.apply(this, args); 265 }, 266 267 /** 268 * Get the value of the specified variable. 269 * @param varName {String} 270 * @return {Object} The value of the variable 271 */ 272 getVariable:function (varName) { 273 return this.scriptedVars[varName]; 274 }, 275 276 /** 277 * Set the value of the specified variable. 278 * @param varName {String} The name of the variable 279 * @param value {Object} The value to assign to the variable 280 */ 281 setVariable:function (varName, value) { 282 this.scriptedVars[varName] = value; 283 }, 284 285 /** 286 * Get the events object for this actor. The configuration is a 287 * collection of variables and scripts which are used to run the actor. When 288 * scripts are called, the scope of the callback is the actor. The following are 289 * included: 290 * <ul> 291 * <li>onInit() - Called when the actor is added to the level</li> 292 * <li>onDestroy() - Called when the actor is removed from the level</li> 293 * <li>onCollide(collisionData, targetMask, worldTime) - Called when the actor collides with another object. The data 294 * contains information about the collision. See: {@link R.struct.CollisionData} The mask is the target's 295 * collision bitmask, and the time is the world time when the collision occurred.</li> 296 * <li>onVisibility(state) - Called when the actor enters or leaves the frame. The state 297 * will be <tt>true</tt> when visible (rendered).</li> 298 * <li>onBeforeUpdate(worldTime) - Called before the actor is updated, providing the world time.</li> 299 * <li>onAfterUpdate(worldTime) - Called after the actor is updated, providing the world time.</li> 300 * </ul> 301 * 302 * @return {Object} 303 */ 304 getConfig:function () { 305 // name : type (script|var) 306 var self = this; 307 var cfg = {}; 308 return $.extend(cfg, { 309 "onInit":"script", 310 "onDestroy":"script", 311 "onCollide":"script", 312 "onCollideWorld":"script", 313 "onVisibility":"script", 314 "onBeforeUpdate":"script", 315 "onAfterUpdate":"script" 316 }); 317 }, 318 319 320 /** 321 * Update the player within the rendering context. This draws 322 * the shape to the context, after updating the transform of the 323 * object. If the player is thrusting, draw the thrust flame 324 * under the ship. 325 * 326 * @param renderContext {R.rendercontexts.AbstractRenderContext} The rendering context 327 * @param time {Number} The engine time in milliseconds 328 * @param dt {Number} The delta between the world time and the last time the world was updated 329 * in milliseconds. 330 */ 331 update:function (renderContext, time, dt) { 332 renderContext.pushTransform(); 333 334 this.callScriptedEvent("onBeforeUpdate", ["worldTime", "delta"], [time, dt]); 335 this.base(renderContext, time, dt); 336 this.callScriptedEvent("onAfterUpdate", ["worldTime", "delta"], [time, dt]); 337 338 var bBox = this.getBoundingBox(), pos = R.clone(this.getPosition()), moveVec = this.getComponent("move").getMoveVector(), 339 testPt = R.clone(bBox.getCenter()).add(pos), mNormal = R.clone(moveVec).normalize(), rayInfo, 340 dir = R.math.Vector2D.create(0, 0); 341 342 // If movement along the X coordinate isn't zero, we want to test for collisions along the axis. 343 // We'll cast a ray in the direction of movement, one tile width long, from the center of the 344 // bounding box (yes, this looks familiar for a reason...) 345 if (moveVec.x != 0) { 346 // We want to cast a ray along the X axis of movement 347 testPt.setX(testPt.x + (bBox.getHalfWidth() * mNormal.x)); 348 dir.set(moveVec.x, 0).normalize().mul(this.getComponent("move").getTileSize()); 349 rayInfo = R.struct.RayInfo.create(testPt, dir); 350 351 R.resources.types.TileMap.castRay(this.getTileMap(), rayInfo, renderContext); 352 353 // There's something in the direction of horizontal movement, call the scripted action for "onCollideWorld" 354 if (rayInfo.shape) { 355 var dist = R.math.Vector2D.create(rayInfo.impactPoint).sub(testPt).len(), 356 cResult = this.callScriptedEvent("onCollideWorld", ["tile", "impactPoint", "distance", "worldTime", "delta"], [rayInfo.shape, rayInfo.impactPoint, dist, time, dt]); 357 } 358 359 rayInfo.destroy(); 360 } 361 362 if (this.editing) { 363 renderContext.setLineStyle("white"); 364 renderContext.setLineWidth(2); 365 var bbox = R.math.Rectangle2D.create(this.getSprite().getBoundingBox()); 366 var o = R.math.Point2D.create(this.getOrigin()); 367 o.neg(); 368 bbox.offset(o); 369 renderContext.drawRectangle(bbox); 370 bbox.destroy(); 371 o.destroy(); 372 } 373 374 renderContext.popTransform(); 375 }, 376 377 /** 378 * Set a flag which will determine if the actor will collide with anything 379 * @param state {Boolean} <tt>true</tt> to collide with other objects 380 */ 381 setCollidable:function (state) { 382 this.collidable = state; 383 if (state) { 384 // Add the collision component 385 this.add(R.components.ConvexCollider.create("collide"), null); 386 } else if (this.getComponent("collide") != null) { 387 // Remove the collision component 388 this.remove("collide").destroy(); 389 } 390 }, 391 392 /** 393 * Returns <tt>true</tt> if the actor can be collided with 394 * @return {Boolean} 395 */ 396 isCollidable:function () { 397 return this.collidable; 398 }, 399 400 /** 401 * Set the sprite which represents this actor 402 * @param sprite {R.resources.types.Sprite} The sprite 403 */ 404 setSprite:function (sprite) { 405 this.sprite = sprite; 406 this.setBoundingBox(sprite.getBoundingBox()); 407 this.getComponent("draw").setSprite(sprite); 408 409 // Set the collision hull 410 this.setCollisionHull(R.collision.OBBHull.create(sprite.getBoundingBox())); 411 }, 412 413 /** 414 * Get the sprite which represents this actor 415 * @return {R.resources.types.Sprite} 416 */ 417 getSprite:function () { 418 return this.sprite; 419 }, 420 421 /** 422 * Set the editing mode of the actor, used by the LevelEditor 423 * @private 424 */ 425 setEditing:function (state) { 426 this.editing = state; 427 }, 428 429 /** 430 * Queried by the LevelEditor to determine if an object is editable 431 * @private 432 */ 433 isEditable:function () { 434 return true; 435 }, 436 437 /** 438 * Host callback which is triggered when collision occurs between this object and 439 * another object. This will typically trigger an event callback for scripted events. 440 * @param collisionObj {R.objects.Object2D} The object that this object collided with 441 * @param time {Number} The time at which the collision occurred 442 * @param dt {Number} The delta between the world time and the last time the world was updated 443 * in milliseconds. 444 * @param targetMask {Number} The collision mask for the object this collided with 445 * @return {Number} Returns a flag which tells the collision system what to do 446 */ 447 onCollide:function (collisionObj, time, dt, targetMask) { 448 var cData = this.getComponent("collide").getCollisionData(); 449 var cResult = this.callScriptedEvent("onCollide", ["collisionData", "targetMask", "worldTime", "delta"], [cData, targetMask, time, dt]); 450 451 // We may want to do something here... 452 453 return cResult; 454 }, 455 456 // --------------------------------------------------------------------- 457 // Methods intended to be called from scripted actions 458 459 moveLeft:function (speed) { 460 this.setMoveVector(-speed, 0); 461 }, 462 463 moveRight:function (speed) { 464 this.setMoveVector(speed, 0); 465 }, 466 467 getSpeedX:function () { 468 return this.getMoveVector().x; 469 }, 470 471 getSpeedY:function () { 472 return this.getMoveVector().y; 473 }, 474 475 changeDirection:function () { 476 this.getMoveVector().neg(); 477 }, 478 479 flipRenderX:function () { 480 this.setScale(this.getScaleX() * -1, this.getScaleY() * 1); 481 }, 482 483 flipRenderY:function () { 484 this.setScale(this.getScaleX() * 1, this.getScaleY() * -1); 485 }, 486 487 lastMVec:null, 488 489 stop:function () { 490 this.lastMVec = R.clone(this.getMoveVector()); 491 this.setMoveVector(0, 0); 492 }, 493 494 go:function () { 495 if (this.lastMVec) { 496 this.setMoveVector(this.lastMVec); 497 this.lastMVec.destroy(); 498 } 499 }, 500 501 getMoveVector:function () { 502 return this.getComponent("move").getMoveVector(); 503 }, 504 505 setMoveVector:function (ptOrX, y) { 506 this.getComponent("move").setMoveVector(ptOrX, y); 507 } 508 509 }, /** @scope R.objects.SpriteActor.prototype */{ 510 /** 511 * Get the class name of this object 512 * @return The string <tt>R.objects.SpriteActor</tt> 513 * @type String 514 */ 515 getClassName:function () { 516 return "R.objects.SpriteActor"; 517 }, 518 519 /** 520 * Serialize the sprite actor into an object. 521 * @param actor {R.engine.SpriteActor} The object to serialize 522 * @param [defaults] {Object} Default values that don't need to be serialized unless 523 * they are different. 524 * @return {Object} 525 */ 526 serialize:function (actor, defaults) { 527 defaults = defaults || []; 528 var propObj = R.objects.Object2D.serialize(actor, defaults); 529 530 // Get the actor config 531 var aCfg = { 532 "actorId":actor.getActorId(), 533 "bitMask":actor.getCollisionMask() 534 }; 535 536 for (var c in actor.getConfig()) { 537 var val = actor.getConfig()[c] == "var" ? actor.getVariable(c) : 538 (actor.getActorEvent(c) && actor.getActorEvent(c).script ? actor.getActorEvent(c).script : ""); 539 if (val) { 540 aCfg[c] = val; 541 } 542 } 543 544 // Add in the actor config 545 propObj.ACTOR_CONFIG = aCfg; 546 return propObj; 547 }, 548 549 /** 550 * Deserialize the object back into a sprite actor. 551 * @param obj {Object} The object to deserialize 552 * @param spriteLoaders {Array} An array of sprite loaders 553 * @param [clazz] {Class} The object class to populate 554 * @return {R.objects.SpriteActor} The object which was deserialized 555 */ 556 deserialize:function (obj, spriteLoaders, clazz) { 557 // Extract the actor config from the object 558 var aCfg = obj.ACTOR_CONFIG; 559 delete obj.ACTOR_CONFIG; 560 561 // Create the class 562 clazz = clazz || R.objects.SpriteActor.create(obj.name); 563 R.objects.Object2D.deserialize(obj, clazz); 564 565 // If the sprite wasn't already found, use the sprite loaders passed to us 566 if (!clazz.getSprite()) { 567 var resourceName = obj.Sprite.split(":")[0], spriteName = obj.Sprite.split(":")[1]; 568 for (var sl = 0; sl < spriteLoaders.length; sl++) { 569 if (spriteLoaders[sl].get(resourceName)) { 570 clazz.setSprite(spriteLoaders[sl].getSprite(resourceName, spriteName)); 571 break; 572 } 573 } 574 } 575 576 if (!clazz.getSprite()) { 577 throw new ReferenceError("The resource '" + resourceName + "' for sprite '" + spriteName + "' could not be found."); 578 } 579 580 // Repopulate the actor config 581 clazz.setActorId(aCfg.actorId); 582 clazz.setCollisionMask(aCfg.bitMask); 583 delete aCfg.actorId; 584 delete aCfg.bitMask; 585 586 // Events and variables 587 for (var c in aCfg) { 588 var val = clazz.getConfig()[c] == "var" ? clazz.setVariable(c, aCfg[c]) : 589 clazz.setActorEvent(c, aCfg[c]); 590 } 591 592 return clazz; 593 } 594 }); 595 }; 596