PhysicsJS

JS物理引擎库。瞬间让虚拟世界变得真实。
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 RequireJSPhysicsJS-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();
});
终结版


Wrapping up

Voila! That’s it! Thanks for bearing with me through that long tutorial.

Obviously the gameplay experience isn’t the best, and there’s more to do (like minifying our scripts with the RequireJS build tool), but my goal was to give you an idea of the capabilities of PhysicsJS, and how useful it can be even in it’s early stages. Hopefully this has given you enough to tinker around with. Remember, if you have any questions, feel free to post in the comments, or on*.

Thanks to MillionthVector for the sprites

Remember there’s a PhysicsJS contest you should enter at ChallengePost! Check it out!

PhysicsJS,布布扣,bubuko.com

PhysicsJS

上一篇:深入dwr2之三 Dwr2页面请求处理机制分析之engine.js


下一篇:ASP.NET中的Image和ImageButton控件