1 /*{# Copyright (c) 2012 Turbulenz Limited #}*/ 2 /* 3 * @title: 2D Physics callbacks 4 * @description: 5 * This sample shows how to use 2D physics contact callbacks 6 * (preSolve, begin and progress) to implement one-way platforms and 7 * destruction based on collision strength. 8 * Left click to pick up and move the rigid bodies and right click to add new ones. 9 */ 10 /*{{ javascript("jslib/observer.js") }}*/ 11 /*{{ javascript("jslib/requesthandler.js") }}*/ 12 /*{{ javascript("jslib/utilities.js") }}*/ 13 /*{{ javascript("jslib/services/turbulenzservices.js") }}*/ 14 /*{{ javascript("jslib/services/turbulenzbridge.js") }}*/ 15 /*{{ javascript("jslib/services/gamesession.js") }}*/ 16 /*{{ javascript("jslib/services/mappingtable.js") }}*/ 17 /*{{ javascript("jslib/shadermanager.js") }}*/ 18 /*{{ javascript("jslib/physics2ddevice.js") }}*/ 19 /*{{ javascript("jslib/draw2d.js") }}*/ 20 /*{{ javascript("jslib/boxtree.js") }}*/ 21 /*{{ javascript("jslib/physics2ddebugdraw.js") }}*/ 22 /*{{ javascript("jslib/textureeffects.js") }}*/ 23 /*global TurbulenzEngine: true */ 24 /*global TurbulenzServices: false */ 25 /*global RequestHandler: false */ 26 /*global Physics2DDevice: false */ 27 /*global Physics2DDebugDraw: false */ 28 /*global Draw2D: false */ 29 TurbulenzEngine.onload = function onloadFn() { 30 //========================================================================== 31 // Turbulenz Initialization 32 //========================================================================== 33 var graphicsDevice = TurbulenzEngine.createGraphicsDevice({}); 34 var mathDevice = TurbulenzEngine.createMathDevice({}); 35 var requestHandler = RequestHandler.create({}); 36 37 var gameSession; 38 function sessionCreated() { 39 TurbulenzServices.createMappingTable(requestHandler, gameSession, function (/* table */ ) { 40 }); 41 } 42 gameSession = TurbulenzServices.createGameSession(requestHandler, sessionCreated); 43 44 //========================================================================== 45 // Phys2D/Draw2D 46 //========================================================================== 47 // set up. 48 var phys2D = Physics2DDevice.create(); 49 50 // size of physics stage. 51 var stageWidth = 30; 52 var stageHeight = 22; 53 54 var draw2D = Draw2D.create({ 55 graphicsDevice: graphicsDevice 56 }); 57 var debug = Physics2DDebugDraw.create({ 58 graphicsDevice: graphicsDevice 59 }); 60 61 // Configure draw2D viewport to the physics stage. 62 // As well as the physics2D debug-draw viewport. 63 draw2D.configure({ 64 viewportRectangle: [0, 0, stageWidth, stageHeight], 65 scaleMode: 'scale' 66 }); 67 debug.setPhysics2DViewport([0, 0, stageWidth, stageHeight]); 68 69 var world = phys2D.createWorld({ 70 gravity: [0, 20] 71 }); 72 73 // group that objects breakable by spikes are given 74 // for filtering event listeners. 75 var breakGroup = 4; 76 77 var shapeSize = 2; 78 var boxShape = phys2D.createPolygonShape({ 79 vertices: phys2D.createBoxVertices(shapeSize, shapeSize), 80 userData: { 81 breakCount: 3, 82 shapeSize: shapeSize 83 }, 84 group: breakGroup 85 }); 86 87 // Create a static body at (0, 0) with no rotation 88 // Which we add to the world to use as the first body 89 // In hand constraint. We set anchor for this body 90 // as the cursor position in physics coordinates. 91 var handReferenceBody = phys2D.createRigidBody({ 92 type: 'static' 93 }); 94 world.addRigidBody(handReferenceBody); 95 var handConstraint = null; 96 97 // Generate event listener for one-way platforms 98 // given a direction of permitted movement through body. 99 function oneWayListener(direction) { 100 return function (arbiter/*, otherShape */ ) { 101 var normal = arbiter.getNormal(); 102 103 // May need to flip directional logic if shapeA is not 'this' 104 // as normals always point from shapeA to shapeB. 105 var flip = (arbiter.shapeA !== this); 106 if ((mathDevice.v2Dot(normal, direction) < 0) === flip) { 107 // Ignore interaction, and make action persistent 'till 108 // objects seperate. 109 arbiter.setAcceptedState(false); 110 arbiter.setPersistentState(true); 111 } 112 }; 113 } 114 115 // Event listener used for teleporter at bottom of stage. 116 function teleportListener(arbiter, otherShape) { 117 var otherBody = otherShape.body; 118 119 // Teleport to top of screen. 120 var pos = otherBody.getPosition(); 121 pos[1] = -1; 122 otherBody.setPosition(pos); 123 } 124 125 // Event listener used for spikes to break apart objects. 126 function spikeListener(arbiter, otherShape) { 127 // Shape may have hit more than one spike, ensure we don't break it again 128 // once already broken. 129 var otherBody = otherShape.body; 130 if (!otherBody.world) { 131 return; 132 } 133 134 // Assert that the impact was strong enough for spikes to break the box. 135 var impulse = arbiter.getImpulseForBody(otherBody); 136 if (mathDevice.v2Length(impulse) < (5 * otherBody.getMass())) { 137 return; 138 } 139 140 world.removeRigidBody(otherBody); 141 142 var shapeSize = (otherShape.userData.shapeSize / 2); 143 var breakCount = (otherShape.userData.breakCount - 1); 144 if (breakCount === 0) { 145 // Object simply destroyed instead. 146 return; 147 } 148 149 // Break shape into a 2x2 grid of smaller parts. 150 // We only created square shapes, so this is easy 151 var x, y; 152 for (x = 0; x < 2; x += 1) { 153 for (y = 0; y < 2; y += 1) { 154 var localPosition = [ 155 (shapeSize / 2) * ((2 * x) - 1), 156 (shapeSize / 2) * ((2 * y) - 1) 157 ]; 158 159 var burstVelocity = otherBody.transformLocalVectorToWorld(localPosition); 160 var newBody = phys2D.createRigidBody({ 161 position: otherBody.transformLocalPointToWorld(localPosition), 162 rotation: otherBody.getRotation(), 163 velocity: mathDevice.v2AddScalarMul(otherBody.getVelocity(), burstVelocity, 10), 164 angularVelocity: otherBody.getAngularVelocity() 165 }); 166 167 var newShape = otherShape.clone(); 168 newShape.scale(1 / 2); 169 newBody.addShape(newShape); 170 171 // Set up for next level of breaking! 172 newShape.userData = { 173 breakCount: breakCount, 174 shapeSize: shapeSize 175 }; 176 newShape.setGroup(breakGroup); 177 178 world.addRigidBody(newBody); 179 } 180 } 181 } 182 183 function lightTunnelListener(position, direction) { 184 return function (arbiter, otherShape) { 185 var otherBody = otherShape.body; 186 187 // Counteract gravity force on world. 188 // 60 because our time step is (1 / 60) and gravity is a force. 189 var mass = otherBody.getMass(); 190 var gravityImpulse = mathDevice.v2ScalarMul(world.getGravity(), -mass / 60); 191 192 // Tunnel direction impulses. 193 var tunnelImpulse = mathDevice.v2ScalarMul(direction, mass); 194 195 // Impulse to guide body to centre of tunnel. 196 var amount = mathDevice.v2PerpDot(mathDevice.v2Sub(otherBody.getPosition(), position), direction) * mass; 197 var directionX = direction[0]; 198 var directionY = direction[1]; 199 var guideImpulse = mathDevice.v2Build(-directionY * amount, directionX * amount); 200 201 // Add a velocity dampener. 202 mathDevice.v2AddScalarMul(guideImpulse, otherBody.getVelocity(), -mass * 0.1, guideImpulse); 203 204 otherBody.applyImpulse(gravityImpulse); 205 otherBody.applyImpulse(tunnelImpulse); 206 otherBody.applyImpulse(guideImpulse); 207 208 // Dampen angular velocity too. 209 otherBody.setAngularVelocity(otherBody.getAngularVelocity() * 0.95); 210 }; 211 } 212 213 function reset() { 214 // Remove all bodies and constraints from world. 215 world.clear(); 216 handConstraint = null; 217 218 // Create border body. 219 var thickness = 0.01; 220 var border = phys2D.createRigidBody({ 221 type: 'static', 222 shapes: [ 223 phys2D.createPolygonShape({ 224 vertices: phys2D.createRectangleVertices(0, 0, thickness, stageHeight) 225 }), 226 phys2D.createPolygonShape({ 227 vertices: phys2D.createRectangleVertices((stageWidth - thickness), 0, stageWidth, stageHeight) 228 }), 229 phys2D.createPolygonShape({ 230 vertices: phys2D.createRectangleVertices(stageWidth - 2, (stageHeight - thickness - 1), stageWidth, stageHeight) 231 }), 232 phys2D.createPolygonShape({ 233 vertices: phys2D.createRectangleVertices(0, (stageHeight - thickness - 1), stageWidth - 8, stageHeight) 234 }) 235 ] 236 }); 237 238 // Set up top bar of border with preSolve handler allowing one-way motion 239 // from the top down. 240 var topBar = phys2D.createPolygonShape({ 241 vertices: phys2D.createRectangleVertices(0, 1, stageWidth, thickness + 1) 242 }); 243 border.addShape(topBar); 244 245 // Set as a deterministic handler operating on all objects (undefined mask). 246 topBar.addEventListener('preSolve', oneWayListener([0, 1]), undefined, true); 247 248 // Set up sensor bar at bottom of border to teleport bodies. 249 var sensorBar = phys2D.createPolygonShape({ 250 vertices: phys2D.createRectangleVertices(stageWidth - 8, (stageHeight - 0.5), stageWidth - 2, stageHeight), 251 sensor: true 252 }); 253 border.addShape(sensorBar); 254 255 // Unspecified mask means operate on all objects. 256 sensorBar.addEventListener('begin', teleportListener); 257 258 world.addRigidBody(border); 259 260 var spikes = function spikesFn(x, y, directionX, directionY, count, startIndex) { 261 var spikeBody = phys2D.createRigidBody({ 262 type: 'static' 263 }); 264 265 // Create set of spikes. 266 var spikeWidth = 0.5; 267 var spikeHeight = 0.5; 268 var i; 269 startIndex = (startIndex || 0); 270 for (i = startIndex; i < (startIndex + count); i += 1) { 271 var posX = x - (directionY * spikeWidth * i); 272 var posY = y + (directionX * spikeWidth * i); 273 var spike = phys2D.createPolygonShape({ 274 vertices: [ 275 [ 276 posX, 277 posY 278 ], 279 [ 280 posX - (spikeWidth * directionY / 2) + (spikeHeight * directionX), 281 posY + (spikeWidth * directionX / 2) + (spikeHeight * directionY) 282 ], 283 [ 284 posX - (spikeWidth * directionY), 285 posY + (spikeWidth * directionX) 286 ] 287 ] 288 }); 289 290 // Don't invoke listener on shapes we don't want to break. 291 // Set event listener for 'begin' and 'progress' so we can also 292 // cause boxes to break by pushing them into the spikes. 293 spike.addEventListener('begin', spikeListener, breakGroup); 294 spike.addEventListener('progress', spikeListener, breakGroup); 295 spikeBody.addShape(spike); 296 } 297 298 world.addRigidBody(spikeBody); 299 }; 300 301 // Horizontal spikes (facing up [0, -1]) at bottom-left of border. 302 spikes(2, stageHeight - 1, 0, -1, 8); 303 304 // Vertical spikes (facing right [1, 0]) at mid-point of left of border. 305 spikes(0, stageHeight / 4, 1, 0, 8, -4); 306 307 // Light tunnel. 308 var lightShape = phys2D.createPolygonShape({ 309 vertices: phys2D.createRectangleVertices(0, stageHeight / 4 - 1, stageWidth, stageHeight / 4 + 1), 310 sensor: true 311 }); 312 313 // light tunnel in left direction [-1, 0] 314 var listener = lightTunnelListener([0, stageHeight / 4], [-1, 0]); 315 lightShape.addEventListener('begin', listener); 316 lightShape.addEventListener('progress', listener); 317 318 var lightBody = phys2D.createRigidBody({ 319 shapes: [lightShape], 320 type: 'static' 321 }); 322 world.addRigidBody(lightBody); 323 324 // Middle platform with one-way tunnel 325 var platform = phys2D.createRigidBody({ 326 type: 'static', 327 shapes: [ 328 phys2D.createPolygonShape({ 329 vertices: [ 330 [9, 11], 331 [11, 11], 332 [11, 15] 333 ] 334 }), 335 phys2D.createPolygonShape({ 336 vertices: [ 337 [17, 11], 338 [19, 11], 339 [17, 15] 340 ] 341 }), 342 phys2D.createPolygonShape({ 343 vertices: [ 344 [14, 12.5], 345 [14.4, 13], 346 [13.6, 13] 347 ], 348 sensor: true 349 }), 350 phys2D.createPolygonShape({ 351 vertices: [ 352 [14.1, 13], 353 [14.1, 13.5], 354 [13.9, 13.5], 355 [13.9, 13] 356 ], 357 sensor: true 358 }) 359 ] 360 }); 361 362 thickness = 0.1; 363 topBar = phys2D.createPolygonShape({ 364 vertices: phys2D.createRectangleVertices(11, 11, 17, 11 + thickness) 365 }); 366 platform.addShape(topBar); 367 368 // Set as a deterministic handler operating on all objects (undefined mask). 369 topBar.addEventListener('preSolve', oneWayListener([0, -1]), undefined, true); 370 var bottomBar = phys2D.createPolygonShape({ 371 vertices: phys2D.createRectangleVertices(11, 15 - thickness, 17, 15) 372 }); 373 platform.addShape(bottomBar); 374 375 // Set as a deterministic handler operating on all objects (undefined mask). 376 bottomBar.addEventListener('preSolve', oneWayListener([0, -1]), undefined, true); 377 378 world.addRigidBody(platform); 379 380 // Set up some initial objects. 381 var generate = function generateFn(x, y, scale, breakCount) { 382 var shape = boxShape.clone(); 383 shape.scale(scale); 384 shape.userData = { 385 shapeSize: (shapeSize * scale), 386 breakCount: breakCount 387 }; 388 389 var body = phys2D.createRigidBody({ 390 shapes: [shape], 391 position: [x, y] 392 }); 393 world.addRigidBody(body); 394 }; 395 396 generate(15, 14, 1, 3); 397 generate(13.5, 14.5, 0.5, 2); 398 generate(12.75, 14.75, 0.25, 1); 399 400 generate(4, 15, 1, 3); 401 generate(4, 13, 1, 3); 402 generate(4, 11, 1, 3); 403 404 generate(stageWidth - 5, 15, 1, 3); 405 } 406 407 reset(); 408 409 //========================================================================== 410 // Mouse/Keyboard controls 411 //========================================================================== 412 var inputDevice = TurbulenzEngine.createInputDevice({}); 413 var keyCodes = inputDevice.keyCodes; 414 var mouseCodes = inputDevice.mouseCodes; 415 416 var mouseX = 0; 417 var mouseY = 0; 418 var onMouseOver = function mouseOverFn(x, y) { 419 mouseX = x; 420 mouseY = y; 421 }; 422 inputDevice.addEventListener('mouseover', onMouseOver); 423 424 var onKeyUp = function onKeyUpFn(keynum) { 425 if (keynum === keyCodes.R) { 426 reset(); 427 } 428 }; 429 inputDevice.addEventListener('keyup', onKeyUp); 430 431 var onMouseDown = function onMouseDownFn(code, x, y) { 432 mouseX = x; 433 mouseY = y; 434 435 if (handConstraint) { 436 return; 437 } 438 439 var point = draw2D.viewportMap(x, y); 440 var body; 441 if (code === mouseCodes.BUTTON_0) { 442 if (handConstraint) { 443 world.removeConstraint(handConstraint); 444 handConstraint = null; 445 } 446 447 var bodies = []; 448 var numBodies = world.bodyPointQuery(point, bodies); 449 var i; 450 for (i = 0; i < numBodies; i += 1) { 451 body = bodies[i]; 452 if (body.isDynamic()) { 453 handConstraint = phys2D.createPointConstraint({ 454 bodyA: handReferenceBody, 455 bodyB: body, 456 anchorA: point, 457 anchorB: body.transformWorldPointToLocal(point), 458 stiff: false, 459 maxForce: 1e5 460 }); 461 world.addConstraint(handConstraint); 462 break; 463 } 464 } 465 } else if (code === mouseCodes.BUTTON_1) { 466 body = phys2D.createRigidBody({ 467 shapes: [boxShape.clone()], 468 position: point 469 }); 470 world.addRigidBody(body); 471 } 472 }; 473 inputDevice.addEventListener('mousedown', onMouseDown); 474 475 var onMouseLeaveUp = function onMouseLeaveUpFn() { 476 if (handConstraint) { 477 world.removeConstraint(handConstraint); 478 handConstraint = null; 479 } 480 }; 481 inputDevice.addEventListener('mouseleave', onMouseLeaveUp); 482 inputDevice.addEventListener('mouseup', onMouseLeaveUp); 483 484 //========================================================================== 485 // Main loop. 486 //========================================================================== 487 var realTime = 0; 488 var prevTime = TurbulenzEngine.time; 489 490 function mainLoop() { 491 if (!graphicsDevice.beginFrame()) { 492 return; 493 } 494 495 inputDevice.update(); 496 graphicsDevice.clear([0.3, 0.3, 0.3, 1.0]); 497 498 if (handConstraint) { 499 handConstraint.setAnchorA(draw2D.viewportMap(mouseX, mouseY)); 500 var body = handConstraint.bodyB; 501 502 // Additional angular dampening of body being dragged. 503 // Helps it to settle quicker instead of spinning around 504 // the cursor. 505 body.setAngularVelocity(body.getAngularVelocity() * 0.9); 506 } 507 508 var curTime = TurbulenzEngine.time; 509 var timeDelta = (curTime - prevTime); 510 511 if (timeDelta > (1 / 20)) { 512 timeDelta = (1 / 20); 513 } 514 realTime += timeDelta; 515 prevTime = curTime; 516 517 while (world.simulatedTime < realTime) { 518 world.step(1 / 60); 519 } 520 521 // physics2D debug drawing. 522 debug.setScreenViewport(draw2D.getScreenSpaceViewport()); 523 debug.begin(); 524 debug.drawWorld(world); 525 debug.end(); 526 527 graphicsDevice.endFrame(); 528 } 529 530 var intervalID = TurbulenzEngine.setInterval(mainLoop, (1000 / 60)); 531 532 // Create a scene destroy callback to run when the window is closed 533 TurbulenzEngine.onunload = function destroyScene() { 534 if (mainLoop) { 535 TurbulenzEngine.clearInterval(intervalID); 536 } 537 538 if (gameSession) { 539 gameSession.destroy(); 540 gameSession = null; 541 } 542 }; 543 };