开源中国

我们不支持 IE 10 及以下版本浏览器

It appears you’re using an unsupported browser

为了获得更好的浏览体验,我们强烈建议您使用较新版本的 Chrome、 Firefox、 Safari 等,或者升级到最新版本的IE浏览器。 如果您使用的是 IE 11 或以上版本,请关闭“兼容性视图”。
使用 PhysicsJS 构建一个 2D 的浏览器游戏 - 技术翻译 - 开源中国社区

使用 PhysicsJS 构建一个 2D 的浏览器游戏 【已翻译100%】

oschina 推荐于 4年前 (共 14 段, 翻译完成于 12-05) 评论 7
收藏  
78
推荐标签: PhysicsJS 待读

PhysicsJS_header_2

作为一名Web开发人员同事又是物理极客,每当我试图使用JavaScript做2D物理的时候总觉得少了点什么。我想要一个可扩展的框架,紧随其他现代JavaScript API的脚步。我不希望它干涉性的假设我想要重力朝下,或者说我想要没有引力的物理环境... 这驱使我创造出了PhysicsJS,我希望它不会辜负其口号::

一个模块化,可扩展的,易于使用的JavaScript物理引擎.

还有一些新的JavaScript物理引擎正暂露头角,他们都有各自的优点。而我想告诉你的是我所觉得的PhysicsJS中真正很酷的东西,我也相信这会帮助其在其它引擎中脱颖而出。

LeoXu
 翻译得不错哦!

在这篇文章中,我将指导你完成一个完整的2D小行星风格的游戏,里面充满着火爆绚丽的图形。在这一过程中,你会看到我的一些开发风格,它会向你展示一些洞察PhysicsJS的更高级用法。你可以在GitHub上的PhysicsJS库(可以看到一些实例/飞船)中下载完整的演示代码,还可以看到CodePen的最终产品。而在我们开始之前,让我先阐述一个重要的声明:

PhysicsJS是一个新的库,并且我们正在努力让它在未来几个月内进入公测。该API正在不断变化中,而且在API中仍然有错误和漏洞。所以请记住这一点。 (我们正在开发PhysicsJS v0.5.2)。我正在寻找的任何水平有保证的贡献者。请即刻就到PhysicsJS GitHub并且指出其中谬误吧!我也会对StackOverflow上有关PhysicsJS的问题作答复,只要你在问题上标记了“physicsjs”。

LeoXu
 翻译得不错哦!

Stage 0: 空间的虚无

第一件要做的事情。我们需要设置好我们的HTML。我们将使用HTML  canvas进行渲染,所以就要从标准HTML5的样板代码开始。我将假定脚本放在body标签的尾部,使我们不用担心DOM的加载。我们也将使用RequireJS来加载所有的脚本。如果你还没有使用过RequireJS,并且不喜欢学习,那么你就必须拜托任何define()和任何require()的方法,并且将所有的JavaScript以正确的顺序组织在一起(而我则强烈推荐你使用RequireJS)。
下面是我工作时的目录结构:

index.html
images/
    ...
js/
    require.js
    physicsjs/
    ...

让我们继续,下载RequireJS,PhysicsJS-0.5.2以及相关的图像,并把它们放到上面这样的目录结构中。

LeoXu
 翻译得不错哦!

接下来,在头部添加CSS,根据游戏显示图片和信息的类名规定他们的样式:

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'
        }]
    },
    ...

OK,让我们开始创建第一个版本的main.js吧。

这将是一个——史无前例的——绝无仅有的——空前绝后的——无聊的游戏(靠!)。

cmy00cmy
 翻译得不错哦!

那么应该从哪开始呢?首先,我们以RequireJS的依赖库的形式加载PhisicsJS库。我们也加载一些其他的依赖库。这些是PhisicJS的扩展,可以为它添加一些功能,比如说循环体,碰撞检测等。当使用RequireJS(和AMD模块)时,我们需要像这样声明每一个必需的依赖。它允许我们只加载我们实际上使用的依赖库。

在我们的RequireJS工厂函数中,我们可以建立起我们的游戏。首先添加一个class名到body标签中,这样就可以显示“按下'Z'键开始”消息,然后我们监听“Z”键的键盘事件,触发时调用我们的helper中的newGame()方法。

接下来我们设置canvas渲染填充窗口并且监听窗体的resize事件以调整canvas的大小。

tnjin
 翻译得不错哦!

接下来,使用init()函数初始化我们的基本元素。init()函数将被传给Physics()构造器,用来创建一个世界。在这个函数中,我们需要进行一些设置:

  • 一艘“船”(现在也就是个〇而已)
  • 一个星球 (另一个〇,不过要给它加上图片)
  • 一个监听器,用来监听世界中发生的操作

对于星球,我可能需要作出一些额外的解释。事实上,它是这样的:

planet.view = new Image();
planet.view.src = require.toUrl('images/planet.png');

这是什么鬼东西0.0?

上面的操作是用来创建一个图片并将其储存在view属性中。该图片将会载入planet.png(使用RequireJS解析图片路径)。但是我们为什么要这么做呢?这是为了定制我们的对象的显示方式。cancas渲染器会监视所有它需要渲染的元素,如果没有设置这个属性,那么canvas将会画出指定的元素(圆形、多边形)并作为图片存在缓存中,图片就存在body的view属性中。如果属性中已存在图片,那么渲染器将会使用这个现成的图片。

cmy00cmy
 翻译得不错哦!

注意: 这远非定制渲染过程的最佳方案,但是这里有两个好消息要告诉你:

  1. 渲染器极易扩展或被其他你想要的东西替代,这完全取决于你是否要创建一个完全不同的渲染器。此处有关于这方面的详细文档
  2. 在未来的几周, ChallengePost将会为PhysicsJS搭建功能更加丰富和强大的渲染器。你可以考虑使用这个渲染器。即使你不用,至少你也可以得到些好处。

扯远了,让我们继续。现在我们继续看init()函数,这是完成这个函数的最后一步:

// 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
]);

我们一次性奖所有的组件都加入世界:创建的船、星球、一些动作、渲染器。下面我们逐一分析这些动作:

  • newtonian: 为世界加入牛顿引力(newtonian gravity)。物体会被1e-4的平方大小的相互作用力吸引.
  • sweep-prune: 这是一个广义相位算法,加速碰撞检测.如果你的游戏涉及到物体碰撞,那么最好用上这个东西。
  • body-collision-detection: 这个狭义碰撞检测算法用来监听sweep-prune事件并用像GJK一类的算法计算出碰撞效果。这只会检测碰撞,而不会对碰撞做出任何反应。
  • body-impulse-response: 这才是对碰撞做出响应的相关动作。它监听碰撞事件并通过对碰撞的物体应用一定的冲量对碰撞做出响应。
cmy00cmy
 翻译得不错哦!

希望就在前方!你现在已经开始接触PhysicsJS模块化的本质了。无论是物体亦或是世界本身都不包含任何物理逻辑,如果你想要在你的世界中运用物理规律而不是在真空环境中,你需要创建一些行为并附加在世界中、表现在物体上。

接下来我们在main.js脚本中加入一个新的Game()辅助函数,通过调用Physics(init)来达到清理和创建新游戏的目的。它同时还监听新世界中的用户事件来终止和重启游戏。现在这些事件还不会做出任何响应,不过快了。

最后,我们与心跳建立连接(requestAnimationFramehelper)让每个框架都能调用world.step函数。

cmy00cmy
 翻译得不错哦!

步骤1: 组织元素

越来越有趣了不是么?特别是当我们可以开着飞船移动的时候。和我们对星球所做的操作一样,我们也可以对飞船应用相应的操作,但是在此我们需要多个自定义皮肤。我们需要一些方法来操纵飞船,这里最佳的选择也是我最喜欢PhysicsJS的地方:几乎无限制的扩展。在这个例子中,我们将会对body进行扩展。

创建一个新的文件,命名为player.js。这个文件用来定义一个名为player的自定义物体。我们假设一个特别的飞船 (奶牛飞船), 然后向WIKI中说的那样扩展PhysicsJS。

为模块定义RequireJS:

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
    };
});

body将会被附加如PhysicsJS,在其中加入些死人辅助函数并传回一个普通对象用以扩展圆形的主体。

cmy00cmy
 翻译得不错哦!

这是我们的私有helper:

// 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();
});
我们第二个迭代现在看起来更像一个游戏了。
tnjin
 翻译得不错哦!
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们
评论(7)
Ctrl/CMD+Enter

结束语:瞧!就是这样!谢谢你忍受了我这篇常常的教程。不是常常,是长长吧
很好很强大,谢谢翻译
看完了,顺便还看了右面的“石墓阵刷猪,升级刷装备”!
不明觉厉
问下,这个应该不支持ie6

引用来自“yuyuyuyu”的评论

问下,这个应该不支持ie6

HTML5
新手求解,ship, planet这些not defined
顶部