http://flippinawesome.org/2013/12/02/building-a-2d-browser-game-with-physicsjs/原文地址
步骤0:虚无的空间
在一切的开始。我们需要设置我们的html。我们使用HTML cavas来渲染。所以从你的标准HTML5模板开始。我假设scripts在body的末尾加载
这样就不用担心DOM加载了,我们打算使用 RequireJS 来加载我们的scripts。如果你没用过requirejs,也不想学,那么你不得不
摆脱所有的define()和require() 然后把所有你的jacascript以适当的顺序放在一起。(我强烈建议使用)
我的文件以这样的结构运行:
index.html
images/
...
js/
require.js
physicsjs/
...
Go ahead and download RequireJS, PhysicsJS-0.5.2,
and the images and
put them into a directory structure like this.现在让我们添加一些CSS在头部来设置"space"的背景图片和一些游戏角色的样式。
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。这个文件将不断修改在这个教程里。你需要修改一些requirejs设置,比如下面的。
// begin main.js
require(
{
// use top level so we can access images
baseUrl: ‘./‘,
packages: [{
name: ‘physicsjs‘,
location: ‘js/physicsjs‘,
main: ‘physicsjs‘
}]
},
...
第一版的main.js
呵呵一个无聊的游戏~。!
所以我们要添加点东西。首先我们加载physicsjs 库通过requirejs的依赖。我们也加载一系列其他的依赖项
如physicsjs的扩展,可以让我们能增加圆形刚体,弹性碰撞以及更多。当我们使用requirejs我们需要声明
每一个依赖项。它只加载那些我们需要的东西。
在我们的requirejs里,我们搭建我们的游戏。我们通过添加一个类名到body tag来显示“press ‘z‘ to start"的
游戏开始信息。然后我们监听‘z‘的键盘事件来调用我们的newGame();
接下来我们设置cavas渲染器(renderer)来绘制窗口监听窗口大小改变事件实时地改变cavas。
下一步我们建立重要的init()函数.在init()函数里调用physics()构造我们的游戏世界。在这个函数里我们需要以下东西
- 一个 “ship” (现在只是一个圆)
- a planet (一个有质量的大圆,配上图片)
- 在世界的每个角落装上触发器
这个行星的一部分代码。
planet.view = new Image();
planet.view.src = require.toUrl(‘images/planet.png‘);
我们创建一张图片,并把它存入.view里。这张图片以planet.png加载。(我们使用requirejs来设置路径).
但是为什么这样做?这样规定了如何显示我们的物件。canvas renderer 观察全局来找出哪里需要渲染。
如果缺失一些东西,就把它按类型(circle,polygon)绘制到canvas并认作图片来保存。(我想就是指
默认情况下的绘制方式)。这些图片都存在body的.view里。然而如果有图片在那里就用图片来加载。
好像有点扯远了。但有2个好消息要告诉你。
1.renderer可以简单地被替换成你想要的任何东西..只需要你建立不同的renderer。documentation about how to do this
2.在几个星期以内,会有个比赛。(明显是过期了,略)
回到正题。init()函数
// add things to the world
world.add([
ship,
planet,
Physics.behavior(‘newtonian‘, { strength: 1e-4 }),//万有引力,系数1e-4.
Physics.behavior(‘sweep-prune‘),//broad phase algorithm。添加碰撞物体后一定记得添加。
Physics.behavior(‘body-collision-detection‘),//监听sweep-prune事件,算出物体如何碰撞。注意只是监听,不做事。
Physics.behavior(‘body-impulse-response‘),//发生碰撞后做事的。碰撞以后物体的运动变化。
renderer
]);
现在已经有个一个真空的世界。我们还需要添加一些行为和动作进去。
接下来在main.js里部署newGame()。在这个函数里做清理(重新开始游戏前)并调用 Physics(init)。它也添加一些监听事件。
最后我们连接ticker来使world.step在每一帧都被调用。
步骤1:有序地创建物体
现在开始会越来越有趣。我们可以在这世界里任意飞翔。我们可以像做行星一样做其他物体。但是我们需要更多的纹理。
我们需要多种方法来操作物体。我们最好的动作教程是用physicsjs,这个能力可以扩展到任何物体。现在我们扩展我们的物体(body)。
~好吧翻译能力有限,things和body有点难翻译了。body是physicsjs里一个对象。通过physics.body()来创建。
我们创建一个player.js的文件。它将定义一个自定义的物体(body)叫做player。我们将假设一个特殊的物件(ship),扩展的circle body。
requirejs的定义
define(
[
‘require‘,
‘physicsjs‘,
‘physicsjs/bodies/circle‘,
‘physicsjs/bodies/convex-polygon‘
],
function(
require,
Physics
){
// code here...
});
现在我们获取一个扩展的circle body。convex-polygon body 作为碎片的扩展(要写爆炸)
// extend the circle body
Physics.body(‘player‘, ‘circle‘, function( parent ){
// private helpers
// ...
return {
// extension definition
};
});
player body 将被添加到physicsjs。我们添加一些私有的函数进去,返回一个对象(object 类似于c++的类)来扩展circle body
// private helpers 翻译中好几处help忽略了,主要我不是很理解。大概指一些辅助的函数或对象。
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;
};
下面是扩展的circle对象
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;
}
};
你可能注意到我们使用了physics.scratchpad.这是你最好的朋友。scratchpad 是个帮助我们回收临时对象(vectors)来减少创建
和垃圾回收的帮手。 read more about scratchpads here.
现在我们有个帮手,但是它连接任何用户输入。我们需要做的是创建一个player 行为(behavior) 来处理用户输入
所以创建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 );
}
};
});
});
So now we can declare js/player
and js/player-behavior
as
dependencies and use them in ourmain.js
file by adding
this to our init()
function:
// 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,
//...
]);
我们要做的最好一件事是加入我们第2版的renderer计划到用户行为上。这个要添加一些代码到step事件来
改变renderer offset right,在renderer调用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();
});
第2步操作后看起来就像个游戏了
步骤2:找麻烦
现在,这是个无聊的游戏。我们要升级它。让我们创建ufo(我看着像病毒...)
我们做的几乎是和创建player一样的事情。我们创建新的body(扩展 circle)。但是我们接下来做到简单多了。
新建ufo.js,仅仅给他一个简单的blowup():
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;
}
};
});
});
再对init()做一些更新:
// 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(),来确保我们消灭了多少并发出win-game事件如果我们全灭他们.我们也监听collisions:detected事件
如果任何物体碰到ufo,就消灭他们,接着我们将射出子弹。
// 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;
}
}
});
注意我们可以创建新的行为来管理ufo们。但是只有少量不必要的代码。我也想显示更多physicsjs能容纳
不同的风格(style)有很多方法可以这么做。
第3版的main.js
Fantastic! We’re almost done. There’s just one more thing I want to show you…
步骤3:找到的道路(写个雷达)
我们很容易在空间里迷失方向。很难看到你的四周,你需要一些帮助,为了解决这个,来创建一个雷达小地图吧。
我们将获取所有bodys的坐标。把他们变成点绘制在小的canvas区域在上右上角。尽管rendererb不是万能的
但还是有不是游泳的方法能满足我的要求。我们需要做的是关联renderer事件,当renderer结束渲染bodies时调用。
接下来看代码
// 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();
});
终结版