index.html images/ ... js/ require.js physicsjs/ ...
html, body { padding: 0; margin: 0; width: 100%; height: 100%; overflow: hidden; } body:after { font-size: 30px; line-height: 140px; text-align: center; color: rgb(60, 16, 11); background: rgba(255, 255, 255, 0.6); position: absolute; top: 50%; left: 50%; width: 400px; height: 140px; margin-left: -200px; margin-top: -70px; border-radius: 5px; } body.before-game:after { content: 'press "z" to start'; } body.lose-game:after { content: 'press "z" to try again'; } body.win-game:after { content: 'Win! press "z" to play again'; } body { background: url(images/starfield.png) 0 0 repeat; }在body标签结束的地方添加下面代码用来载入RequireJS和初始化我们的应用:
<script src="js/require.js"></script> <script src="js/main.js"></script>下一步我们需要创建main.js文件,这个文件将在本教程中不断被更改。请注意,我上传的代码是在CodePen上可以执行的示例,所以你可能需要更改你本地示例中的RequireJS的文件路径。下面的代码可能对你有些帮助:
// 开始 main.js require( { // use top level so we can access images baseUrl: './', packages: [{ name: 'physicsjs', location: 'js/physicsjs', main: 'physicsjs' }] }, ...
planet.view = new Image(); planet.view.src = require.toUrl('images/planet.png');
// add things to the world world.add([ ship, planet, Physics.behavior('newtonian', { strength: 1e-4 }), Physics.behavior('sweep-prune'), Physics.behavior('body-collision-detection'), Physics.behavior('body-impulse-response'), renderer ]);
define( [ 'require', 'physicsjs', 'physicsjs/bodies/circle', 'physicsjs/bodies/convex-polygon' ], function( require, Physics ){ // code here... });现在我们获得了一个圆形物体和一些被摧毁的多边形碎片(爆炸效果嘛,之前承诺过的不是)。准备就绪,动起来~
// extend the circle body Physics.body('player', 'circle', function( parent ){ // private helpers // ... return { // extension definition }; });
// private helpers var deg = Math.PI/180; var shipImg = new Image(); var shipThrustImg = new Image(); shipImg.src = require.toUrl('images/ship.png'); shipThrustImg.src = require.toUrl('images/ship-thrust.png'); var Pi2 = 2 * Math.PI; // VERY crude approximation to a gaussian random number.. but fast var gauss = function gauss( mean, stddev ){ var r = 2 * (Math.random() + Math.random() + Math.random()) - 3; return r * stddev + mean; }; // will give a random polygon that, for small jitter, will likely be convex var rndPolygon = function rndPolygon( size, n, jitter ){ var points = [{ x: 0, y: 0 }] ,ang = 0 ,invN = 1 / n ,mean = Pi2 * invN ,stddev = jitter * (invN - 1/(n+1)) * Pi2 ,i = 1 ,last = points[ 0 ] ; while ( i < n ){ ang += gauss( mean, stddev ); points.push({ x: size * Math.cos( ang ) + last.x, y: size * Math.sin( ang ) + last.y }); last = points[ i++ ]; } return points; };这是我们对圆形物体的扩展(具体细节请看注释):
return { // we want to do some setup when the body is created // so we need to call the parent's init method // on "this" init: function( options ){ parent.init.call( this, options ); // set the rendering image // because of the image i've chosen, the nose of the ship // will point in the same angle as the body's rotational position this.view = shipImg; }, // this will turn the ship by changing the // body's angular velocity to + or - some amount turn: function( amount ){ // set the ship's rotational velocity this.state.angular.vel = 0.2 * amount * deg; return this; }, // this will accelerate the ship along the direction // of the ship's nose thrust: function( amount ){ var self = this; var world = this._world; if (!world){ return self; } var angle = this.state.angular.pos; var scratch = Physics.scratchpad(); // scale the amount to something not so crazy amount *= 0.00001; // point the acceleration in the direction of the ship's nose var v = scratch.vector().set( amount * Math.cos( angle ), amount * Math.sin( angle ) ); // accelerate self this.accelerate( v ); scratch.done(); // if we're accelerating set the image to the one with the thrusters on if ( amount ){ this.view = shipThrustImg; } else { this.view = shipImg; } return self; }, // this will create a projectile (little circle) // that travels away from the ship's front. // It will get removed after a timeout shoot: function(){ var self = this; var world = this._world; if (!world){ return self; } var angle = this.state.angular.pos; var cos = Math.cos( angle ); var sin = Math.sin( angle ); var r = this.geometry.radius + 5; // create a little circle at the nose of the ship // that is traveling at a velocity of 0.5 in the nose direction // relative to the ship's current velocity var laser = Physics.body('circle', { x: this.state.pos.get(0) + r * cos, y: this.state.pos.get(1) + r * sin, vx: (0.5 + this.state.vel.get(0)) * cos, vy: (0.5 + this.state.vel.get(1)) * sin, radius: 2 }); // set a custom property for collision purposes laser.gameType = 'laser'; // remove the laser pulse in 600ms setTimeout(function(){ world.removeBody( laser ); laser = undefined; }, 600); world.add( laser ); return self; }, // 'splode! This will remove the ship // and replace it with a bunch of random // triangles for an explosive effect! blowUp: function(){ var self = this; var world = this._world; if (!world){ return self; } var scratch = Physics.scratchpad(); var rnd = scratch.vector(); var pos = this.state.pos; var n = 40; // create 40 pieces of debris var r = 2 * this.geometry.radius; // circumference var size = 8 * r / n; // rough size of debris edges var mass = this.mass / n; // mass of debris var verts; var d; var debris = []; // create debris while ( n-- ){ verts = rndPolygon( size, 3, 1.5 ); // get a random polygon if ( Physics.geometry.isPolygonConvex( verts ) ){ // set a random position for the debris (relative to player) rnd.set( Math.random() - 0.5, Math.random() - 0.5 ).mult( r ); d = Physics.body('convex-polygon', { x: pos.get(0) + rnd.get(0), y: pos.get(1) + rnd.get(1), // velocity of debris is same as player vx: this.state.vel.get(0), vy: this.state.vel.get(1), // set a random angular velocity for dramatic effect angularVelocity: (Math.random()-0.5) * 0.06, mass: mass, vertices: verts, // not tooo bouncy restitution: 0.8 }); d.gameType = 'debris'; debris.push( d ); } } // add debris world.add( debris ); // remove player world.removeBody( this ); scratch.done(); return self; } };你可能注意到我们正在使用一些叫做Phisics.scratchpad的东西。这是你的好朋友。一个scratchpad是一个通过回收临时对象(数组)来减少创建对象和回收 垃圾 时间的helper。点击 这里 ,你可以读到更多关于scratchpads的信息。
那么现在我们有了一个玩家,但是它还没有和任何用户输入相关联起来。我们要做的就是创建一个玩家动作以响应用户的输入。所以我们用相似的方式创建另一个文件,叫做player-behavior.js(具体细节请看注释):
define( [ 'physicsjs' ], function( Physics ){ return Physics.behavior('player-behavior', function( parent ){ return { init: function( options ){ var self = this; parent.init.call(this, options); // the player will be passed in via the config options // so we need to store the player var player = self.player = options.player; self.gameover = false; // events document.addEventListener('keydown', function( e ){ if (self.gameover){ return; } switch ( e.keyCode ){ case 38: // up self.movePlayer(); break; case 40: // down break; case 37: // left player.turn( -1 ); break; case 39: // right player.turn( 1 ); break; case 90: // z player.shoot(); break; } return false; }); document.addEventListener('keyup', function( e ){ if (self.gameover){ return; } switch ( e.keyCode ){ case 38: // up self.movePlayer( false ); break; case 40: // down break; case 37: // left player.turn( 0 ); break; case 39: // right player.turn( 0 ); break; case 32: // space break; } return false; }); }, // this is automatically called by the world // when this behavior is added to the world connect: function( world ){ // we want to subscribe to world events world.subscribe('collisions:detected', this.checkPlayerCollision, this); world.subscribe('integrate:positions', this.behave, this); }, // this is automatically called by the world // when this behavior is removed from the world disconnect: function( world ){ // we want to unsubscribe from world events world.unsubscribe('collisions:detected', this.checkPlayerCollision); world.unsubscribe('integrate:positions', this.behave); }, // check to see if the player has collided checkPlayerCollision: function( data ){ var self = this ,world = self._world ,collisions = data.collisions ,col ,player = this.player ; for ( var i = 0, l = collisions.length; i < l; ++i ){ col = collisions[ i ]; // if we aren't looking at debris // and one of these bodies is the player... if ( col.bodyA.gameType !== 'debris' && col.bodyB.gameType !== 'debris' && (col.bodyA === player || col.bodyB === player) ){ player.blowUp(); world.removeBehavior( this ); this.gameover = true; // when we crash, we'll publish an event to the world // that we can listen for to prompt to restart the game world.publish('lose-game'); return; } } }, // toggle player motion movePlayer: function( active ){ if ( active === false ){ this.playerMove = false; return; } this.playerMove = true; }, behave: function( data ){ // activate thrusters if playerMove is true this.player.thrust( this.playerMove ? 1 : 0 ); } }; }); });接下来我们可以声明js/playerandjs/player-behavior作为依赖库,并将它添加进我们的main.js文件的init()函数里,这样我们就可以使用了。
/ in init() var ship = Physics.body('player', { x: 400, y: 100, vx: 0.08, radius: 30 }); var playerBehavior = Physics.behavior('player-behavior', { player: ship }); // ... world.add([ ship, playerBehavior, //... ]);在我们看到我们的第二个场景之前,我们最后需要添加的东西是获取渲染的画布来跟踪用户的动作。这可以通过添加一些代码到stepevent listener来做到,在它调用world.render()方法前改变渲染的位置,就像下面这样:
// inside init()... // render on every step world.subscribe('step', function(){ // middle of canvas var middle = { x: 0.5 * window.innerWidth, y: 0.5 * window.innerHeight }; // follow player renderer.options.offset.clone( middle ).vsub( ship.state.pos ); world.render(); });
define( [ 'require', 'physicsjs', 'physicsjs/bodies/circle' ], function( require, Physics ){ Physics.body('ufo', 'circle', function( parent ){ var ast1 = new Image(); ast1.src = require.toUrl('images/ufo.png'); return { init: function( options ){ parent.init.call(this, options); this.view = ast1; }, blowUp: function(){ var self = this; var world = self._world; if (!world){ return self; } var scratch = Physics.scratchpad(); var rnd = scratch.vector(); var pos = this.state.pos; var n = 40; var r = 2 * this.geometry.radius; var size = r / n; var mass = 0.001; var d; var debris = []; // create debris while ( n-- ){ rnd.set( Math.random() - 0.5, Math.random() - 0.5 ).mult( r ); d = Physics.body('circle', { x: pos.get(0) + rnd.get(0), y: pos.get(1) + rnd.get(1), vx: this.state.vel.get(0) + (Math.random() - 0.5), vy: this.state.vel.get(1) + (Math.random() - 0.5), angularVelocity: (Math.random()-0.5) * 0.06, mass: mass, radius: size, restitution: 0.8 }); d.gameType = 'debris'; debris.push( d ); } setTimeout(function(){ for ( var i = 0, l = debris.length; i < l; ++i ){ world.removeBody( debris[ i ] ); } debris = undefined; }, 1000); world.add( debris ); world.removeBody( self ); scratch.done(); world.publish({ topic: 'blow-up', body: self }); return self; } }; }); });接下来,我们在main.js的innit()方法中 创建一些敌人。
// inside init()... var ufos = []; for ( var i = 0, l = 30; i < l; ++i ){ var ang = 4 * (Math.random() - 0.5) * Math.PI; var r = 700 + 100 * Math.random() + i * 10; ufos.push( Physics.body('ufo', { x: 400 + Math.cos( ang ) * r, y: 300 + Math.sin( ang ) * r, vx: 0.03 * Math.sin( ang ), vy: - 0.03 * Math.cos( ang ), angularVelocity: (Math.random() - 0.5) * 0.001, radius: 50, mass: 30, restitution: 0.6 })); } //... world.add( ufos );这里的数学运算只是为了让它们用这样一种方式随机地绕行地球,而且慢慢地朝地球蠕动过去。但现在,我们还不能射击他们,因此让我们向init()函数添加更多的代码来跟踪我们消灭了多少敌人,而如果我们消灭了它们,就会释放出AWIN-game事件。我们还将侦听这个世界中的碰撞:detected事件,并且任何冲突都会有激光在其中,如果它支持的话,此时我方会对其大发脾气。
// inside init()... // count number of ufos destroyed var killCount = 0; world.subscribe('blow-up', function( data ){ killCount++; if ( killCount === ufos.length ){ world.publish('win-game'); } }); // blow up anything that touches a laser pulse world.subscribe('collisions:detected', function( data ){ var collisions = data.collisions ,col ; for ( var i = 0, l = collisions.length; i < l; ++i ){ col = collisions[ i ]; if ( col.bodyA.gameType === 'laser' || col.bodyB.gameType === 'laser' ){ if ( col.bodyA.blowUp ){ col.bodyA.blowUp(); } else if ( col.bodyB.blowUp ){ col.bodyB.blowUp(); } return; } } });
// inside init()... // draw minimap world.subscribe('render', function( data ){ // radius of minimap var r = 100; // padding var shim = 15; // x,y of center var x = renderer.options.width - r - shim; var y = r + shim; // the ever-useful scratchpad to speed up vector math var scratch = Physics.scratchpad(); var d = scratch.vector(); var lightness; // draw the radar guides renderer.drawCircle(x, y, r, { strokeStyle: '#090', fillStyle: '#010' }); renderer.drawCircle(x, y, r * 2 / 3, { strokeStyle: '#090' }); renderer.drawCircle(x, y, r / 3, { strokeStyle: '#090' }); for (var i = 0, l = data.bodies.length, b = data.bodies[ i ]; b = data.bodies[ i ]; i++){ // get the displacement of the body from the ship and scale it d.clone( ship.state.pos ).vsub( b.state.pos ).mult( -0.05 ); // color the dot based on how massive the body is lightness = Math.max(Math.min(Math.sqrt(b.mass*10)|0, 100), 10); // if it's inside the minimap radius if (d.norm() < r){ // draw the dot renderer.drawCircle(x + d.get(0), y + d.get(1), 1, 'hsl(60, 100%, '+lightness+'%)'); } } scratch.done(); });