physics2d_callbacks.js
  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 };