用box2d制作完美绳子
前一段时间发现一个有意思的游戏叫 cut the rope ,凭借其简易的交互和巧妙的构思着实让我着迷了一把, 如果以一个游戏人的角度来分析这款游戏,最amazing的还是其流畅的物理场景,这类游戏往 往让玩家感受更多的真实性和趣味性. cut the rope是用cocos2d实现的,这里我用box2d来实现它的核心元素(rope),先看看demo:
(看不到请reload frame)
demo只是简单的实现了绳子,如果想看完整版的割绳子实现近期我会把实现的完整版放上来(完整版:http://mgp.qq.com/flash/76169/)
今天的主题是"make a perfect rope with box2d",so 如果大家感兴趣就跟着我这个 tutorial走吧.:)
box2d基础
为了不混淆你的代码,box2d的所有类型都以b2开头,整个box2d物理系统由b2world驱动,通过 b2Wrold的step方法来模拟时间经过.box2d的单位系统完全遵照现实世界,用米,秒,牛等单位, 所以使用代码时务必转换(有人因此吐槽box2d,为什么还要开发人员来转,原因是box2d根本 不需要关心你如何处理pixel,你如何显示资源,它就像我们无法触摸但真是存在的物理世界,= =).b2body则是物理系统中的物件,游戏中就是靠它们来模拟现实中的物理模型,结 合 b2Joint关节来连接 b2Body.
具体实现
好了,看了上面的介绍你应该对box有一些了解,现在上代码了,关键部分都有注释.
//为了效率能高点推荐使用requestAnimationFrame window.requestAnimFrame = (function(){ //requestAnimFrame fallback return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(/* function */ callback, /* DOMElement */ element){ window.setTimeout(callback, 1000 / 60); } })(); (function(){ //命名空间的导入 var b2Vec2 = Box2D.Common.Math.b2Vec2 , b2BodyDef = Box2D.Dynamics.b2BodyDef , b2Body = Box2D.Dynamics.b2Body , b2FixtureDef = Box2D.Dynamics.b2FixtureDef , b2Fixture = Box2D.Dynamics.b2Fixture , b2World = Box2D.Dynamics.b2World , b2MassData = Box2D.Collision.Shapes.b2MassData , b2PolygonShape = Box2D.Collision.Shapes.b2PolygonShape , b2CircleShape = Box2D.Collision.Shapes.b2CircleShape , b2DebugDraw = Box2D.Dynamics.b2DebugDraw ; //这里先创建一个box2d包装器,可以更方便的创建物体啥的而无须关心太多不必要的东西 window.b2dWrapper=function(stage,scale){ var gravity = new b2Vec2(0,10); var doSleep = true; this.world = new b2World( gravity, doSleep); this.debug=false; this.scale=scale; //setup debug draw var debugDraw = new b2DebugDraw(); debugDraw.SetSprite(stage); debugDraw.SetDrawScale(scale); debugDraw.SetFillAlpha(0.3); debugDraw.SetLineThickness(1.0); debugDraw.SetFlags(b2DebugDraw.e_shapeBit | b2DebugDraw.e_jointBit); this.world.SetDebugDraw(debugDraw); var updateListeners=[]; this.addUpdateListener=function(l){ updateListeners.push(l); } //shape工厂 this.makeShape=function(param){ //定义常用的参数,创建body时就不必传递太多不必要的参数 var defaults = { x: 3, y: 3, width: 3, height: 3, type:'box', scale: 0, linearDamping: 0, angularDamping: 0, fixedRotation: false, allowSleep: true, isSleeping: false, isSensor: false, isBullet: false, isDynamic: true, skin: null, //... scaleSkin: true, density: 1.0, friction: 0.5, restitution: 0.2, angle: 0.0, categoryBits:0x0001, maskBits:0xFFFF, groupIndex:0 }; var p=Object.extend(defaults,param); if (p.width) p.width /= 2; if (p.height) p.height /= 2; var bodyDef = new b2BodyDef(); var fixtureDef = new b2FixtureDef(); var shape=null; var body; if (p.scale == 0) p.scale = this.scale; bodyDef.position.x = p.x / p.scale; bodyDef.position.y = p.y / p.scale; bodyDef.angle = p.angle; bodyDef.linearDamping = p.linearDamping; bodyDef.angularDamping = p.angularDamping; bodyDef.fixedRotation = p.fixedRotation; bodyDef.allowSleep = p.allowSleep; bodyDef.linearDamping = p.linearDamping; bodyDef.angularDamping = p.angularDamping; bodyDef.awake = !p.isSleeping; bodyDef.bullet = p.isBullet; bodyDef.type = p.isDynamic ? b2Body.b2_dynamicBody : b2Body.b2_staticBody; //filter data fixtureDef.filter.groupIndex = p.groupIndex; fixtureDef.filter.maskBits = p.maskBits; fixtureDef.filter.categoryBits = p.categoryBits; fixtureDef.friction = p.friction; fixtureDef.density = p.density; fixtureDef.restitution = p.restitution; fixtureDef.isSensor = p.isSensor; if (p.type == 'circle')//only support two type now { shape = new b2CircleShape(p.width / p.scale); p.height = p.width; //workaround.... } else { shape = new b2PolygonShape(); if (p.type == 'box') { shape.SetAsBox(p.width / p.scale, p.height / p.scale); } else if (type == 'SHAPE_POLYGON') { throw new Error("to be implemented."); } } fixtureDef.shape = shape; if (p.parent) { body = p.parent; shape.SetLocalPosition(bodyDef.position); //根据父对象设置相对位置 } else { body = this.world.CreateBody(bodyDef); } //body.SetUserData(ud);//notice here body.CreateFixture(fixtureDef); return body; } var This=this; var update=function(){ This.world.Step(1/60,10,10); if(This.debug) This.world.DrawDebugData(); This.world.ClearForces(); if(updateListeners.length>0){ updateListeners.forEach(function(itm){if(itm)itm();}); } requestAnimFrame(update); } this.start=function(){ update(); } } })(); //现在基本框架已经出来了,现在开始造绳子了! b2dWrapper.prototype.createRope=function(bodyA,bodyB,params){ //package import var b2DistanceJointDef= Box2D.Dynamics.Joints.b2DistanceJointDef, b2Vec2 = Box2D.Common.Math.b2Vec2, b2Joint= Box2D.Dynamics.Joints.b2Joint, b2RevoluteJointDef= Box2D.Dynamics.Joints.b2RevoluteJointDef, b2RevoluteJoint= Box2D.Dynamics.Joints.b2RevoluteJoint; var This=this; //创建旋转关节 function revoluteJoint(bodyA,bodyB,anchorA,anchorB){ var revoluteJointDef = new b2RevoluteJointDef(); revoluteJointDef.localAnchorA.Set(anchorA.x,anchorA.y); revoluteJointDef.localAnchorB.Set(anchorB.x,anchorB.y); revoluteJointDef.bodyA=bodyA; revoluteJointDef.bodyB = bodyB; revoluteJointDef.collideConnected = true; revoluteJointDef.userData = 'chainParts'; //revoluteJointDef.enableLimit = true; //revoluteJointDef.referenceAngle = -30 * Math.PI / 180; return This.world.CreateJoint(revoluteJointDef); } //连接两个物体 function connectChain(bodyA,bodyB,chainLen/*CHAIN_LEN*/,isHead) { if(!chainLen) chainLen=5; return revoluteJoint(bodyA, bodyB, new b2Vec2( isHead?0:chainLen / 2 , 0 ), new b2Vec2( -chainLen / 2 , 0)); } var startP=bodyA.GetPosition().Copy(),endP=bodyB.GetPosition().Copy(); var detY=endP.y-startP.y,detX=endP.x-startP.x; var angle_rad=Math.atan2(detY,detX);//获取头尾两点的角度 var len=Math.sqrt(detX*detX*900+detY*detY*900);//总长 var subLen=len/10;//每个节点长 var offset=new b2Vec2(Math.cos(angle_rad)*subLen,Math.sin(angle_rad)*subLen);//获取每个 绳子节点相对于前一节点的偏移 for(var i=0;i<10;i++){ var box=this.makeShape(//创建绳子节点 { type:'box', isDynamic:true, width:subLen, height:2, angle:angle_rad, isSensor:true, x:bodyA.GetPosition().x*30+offset.x, y:bodyA.GetPosition().y*30+offset.y } ); var j=connectChain(bodyA,box,subLen/30,i==0); bodyA=box; } connectChain(bodyA,bodyB,subLen/30); }
后记
没用多少时间的实现了这个小demo,我想说的是互联网越来越开放,技术交流也应该如此, 人生苦短,拥抱开源 :>
豆瓣fm的全局快捷键
最近用上了pentadacty, 感觉不错,高效强大. 个人有一个习惯,只要不是思考密集的时间都会抽时间来听会歌,同时也有开大量tabs的习惯,所以在听豆瓣电台的时候免不了频繁的切换,于是就有了下面的代码:
"eval script in tabs js <<EOF function execScriptInTabs(filter,domjs){ tabs.allTabs.forEach(function(itm){ var ctab=gBrowser.getBrowserForTab(itm); if(filter(ctab)){ var s=ctab.contentDocument.createElement('script') s.textContent=domjs; ctab.contentDocument.body.appendChild(s); } }) } EOF "douban.fm utilities js <<EOF group.mappings.add( [modes.NORMAL], [",ds"],"skip", function(){ execScriptInTabs(function(ctab){ return ctab.contentDocument.location.host.match("douban\.fm"); },"DBR.act('skip')");//skip,pause,love } ); group.mappings.add( [modes.NORMAL], [",dd"],"ban", function(){ execScriptInTabs(function(ctab){ return ctab.contentDocument.location.host.match("douban\.fm"); },"DBnnR.act('ban')");//skip,pause,love } ); group.mappings.add( [modes.NORMAL], [",dl"],"like", function(){ execScriptInTabs(function(ctab){ return ctab.contentDocument.location.host.match("douban\.fm"); },"DBR.act('love')");//skip,pause,love } ); group.mappings.add( [modes.NORMAL], [",dp"],"pause", function(){ execScriptInTabs(function(ctab){ return ctab.contentDocument.location.host.match("douban\.fm"); },"DBR.act('pause')");//skip,pause,love } ); EOF