【技术讨论】从弹弹堂说起,如何用2D物理引擎编写一个游戏<一>

呃,小儿科拿出来讨论一下。

巨注意:不是让大家去写外挂哦!纯属技术讨论。

先写一部分,测试是用GDI+写的,非常的简陋,而且整体只是一个雏形,抛砖引玉。

【一、物理引擎】

      所谓物理引擎,是通过为刚体赋予真实的物理属性的方式来计算它们的运动、旋转、碰撞等等的结果。也许你曾经编写过台球游戏,使用了大量的类似于碰撞检测,线相交和折返等等数学方法来解决问题,但那不是对真实世界的物理模拟,虽然可以使你的游戏看起来比较真实,但当游戏需要比较复杂的物体碰撞、滚动、滑动或者弹跳的时候(比如赛车类游戏或者保龄球游戏),通过编程的方法就比较困难了。而物理引擎则使用动量、扭矩等用高等数学手段来模拟真实物体,这将得到更真实的效果且使我们的编码更加容易。当然好的物理引擎允许有复杂的机械装置,像球形关节、轮子、气缸或者铰链。有些也支持非刚性体的物理属性,比如流体。

著名的物理引擎有:

Lagoa Multiphysics

Physx

Bullet

Havok

等等,它们多提供3D特性,用于在三维空间内来模拟物理运动——就像星际2、暗黑3使用的就是Havok。这些物理引擎当然可以应用到2D游戏当中,但它们的运算量将会大于2D引擎。很多物理引擎提供了图形界面功能,AI功能等诸多游戏要素,而且非常出色!但我们这里还是从比较简单的BOX2D入手,说它简单是它只负责计算物理运动(当然它也非常强大——除了不支持流体等)进而只提供了很少的一些接口,这有利于我们专注于物理引擎的使用。

出处:http://www.cnblogs.com/zcsor/

【二、BOX2D】

Box2D 是一个用于游戏的 2D 刚体仿真库。程序员可以在他们的游戏里使用它,它可以使物体的运动更加可信,让世界看起来更具交互性。从游戏的视角来看,物理引擎就是一个程序性动画(proceduralanimation)的系统,而不是由动画师去移动你的物体。你可以让牛顿来做导演。Box2D 是用可移植的 C++ 来写成的。引擎中定义的大部分类型都有 b2 前缀,希望这能消除它和游戏引擎之间的名字冲突。这就是BOX2D的概况。在学习如何使用时,我们先认识几个必要概念和它们存在的意义:

A、我想,质量,力,扭矩和冲量大家都非常清楚了,呃,如果不清楚那可以去百科恶补一下……

B、刚体:即说这个物体会在碰撞、挤压等力的作用后不变形。你应该注意我这个描述。(当然如果你愿意,可以告诉BOX2D改变它的任何属性)

C、形状:这决定了物体的外观以及碰撞等发生的位置,当然,还有摩擦的大小等

D、自由度:这个概念也许比较深奥,但在这里,我们只需要知道,2D物理引擎只模拟物体在一个平面上的运动——即你的游戏将是一个平面游戏,即使你用一些手段将地面和墙壁以及天空分开。。。其实不必在意,这种伎俩已经很少有游戏在用了,而且我相信当XNA向VB.NET微笑时,那些手段会被赶出你的脑袋。

E、世界:我们应该把所有物体限定在一个范围内——当物体跑出去了,引擎会抛出一个错误并停止对该物体进行演算,所以,我们的世界通常有足够大……

F、其他:当然指关节,齿轮等,这些与我们入门毫无关系。甚至在我的代码里也采用了一些卑劣的手段避开使用回调方式来处理物体的接触——这将使代码更“入门”,而且也足够应付这么简单的情况

【三、创建一个世界】

首先,确保你引用了BOX2D引擎。如果没有可以在其网站下载:http://www.box2d.org/

接下来我们创建一个世界,这是使用BOX2D引擎的第一步:

        '世界外边框
        Dim Wordaabb As Box2DX.Collision.AABB
        Wordaabb.LowerBound.Set(-10000.0F, -10000.0F)
        Wordaabb.UpperBound.Set(10000.0F, 10000.0F)
        '创建世界,第二个参数是重力,第三个是是否允许引擎休眠
        world = New Box2DX.Dynamics.World(Wordaabb, g, False)

这样,就创建完成了,非常简单吧!

【四、创建一个物体】

在BOX2D中,物体是这样创建的:

1、首先有一个“物体”(之所以这样叫,是因为到你附加形状并计算质量之前,它应该只是一个“抽象”的处于意识中的物体)被创建,但是他没有形态

2、指定物体的一些信息:位置,空气阻尼等

3、创建一个形态并设置由形态先关的信息:体积、密度、摩擦系数等

出处:http://www.cnblogs.com/zcsor/

4、为物体附加形态,让引擎计算其质量

可能这不是我们所习惯的,但你确实应该这样做:先设置形态,然后设置密度,因为决定物体运动状态的并不只是质量,形态将起到决定性作用!
        '创建动态物体,它的质量一定要大于0
        Dim bodyDef As New Box2DX.Dynamics.BodyDef

        '物体的位置
        bodyDef.Position.Set(X,Y)

        '空气的阻尼
        bodyDef.LinearDamping = Damping

        '实际物体
        Dim Body As Box2DX.Dynamics.Body

        '在世界中创建物体
        Body = world.CreateBody(bodyDef)
        '为物体附加一个多边形
        Dim shapeDef As New Box2DX.Collision.CircleDef
        shapeDef.IsSensor = IsSensor  '可否“休眠”即物体不动时引擎是否可以不计算它。(可能听起来有点别扭)
        shapeDef.Radius = R   '设置体积为1,以使得质量计算结果为1
        shapeDef.Density = Density    '密度
        'shapeDef.Friction = 1   '摩擦系数
        Dim shape = Body.CreateShape(shapeDef) '附加到物体
        Body.SetMassFromShapes()   '根据形状计算质量
至此,我们创建了一个动态物体。

 

 

【五、物体的运动】

好,我们先考虑一下,影响我们的“炮弹”运动轨迹的因素都有哪些:

1、重力

2、发射角度

3、发射力度

4、空气阻尼

5、风力

6、发射点(这在创建动态物体时已经被指定了,当然你可以告诉引擎来修改它的属性来使它停下并“瞬移”到你想要的位置重新开始运动。但我没有这样做——出于编码的原因当它无用时,我销毁它,下次计算时创建一个新物体,这样做效率并不很低)所以我们不关心它。

好,我们一个一个解决这些问题:

1、重力——这个属性是属于世界的,一般我们定义重力是向下的一个向量(BOX2D提供了用于内置计算的向量对象)

Dim g As New Box2DX.Common.Vec2(0.0F, 17.27F)               '重力

2、3、发射角度和力度——这将是瞬间完成的,不要指望去模拟一个在炮筒中的爆炸,那是得不偿失的——我们直接用一个向量作用于物体质心即可:

Body.ApplyImpulse(New Box2DX.Common.Vec2( Math.Cos(Angle) * Power , -Math.Sin(Angle) * Power), Body.GetPosition)

这个语句可能稍微复杂一些,实际上有两个参数,第一个就是冲量,第二个是物体当前位置。

第一个参数只是用了三角形的基本知识,将发射角度和力度进行换算,得到X,Y两个方向上的力——即得到一个向量^ ^。

4、空气阻尼——一个属于物体的性质:这也灰常的简单,只需要告诉BOX2D一个数值,它就会在迭代器模拟物理运动的计算过程中应用它。

bodyDef.LinearDamping = Damping

5、风力——这是一个BOX2D所没有提供的性质,换句话说,BOX2D的世界里没有风……果然是灰常的风和日丽……那怎么办呢?呃……

思考中……

思考中……

其实物理引擎无非是在一定的时刻(后面将提到步长)近似的求得物体的位置及速度(线速度和角速度),那么我们可以在每次计算时,都作用一个风力给物体。这是我的解决方案,当然,你也可以改变重力的大小和方向来达到目的(将风力和重力的合力作为重力),也许你还有更好的办法……

好了,这个风力代码我是这样写的:

 '添加位置到返回数组

        Dim ret(MaxStep) As Box2DX.Common.Vec2
        For i As Integer = 0 To MaxStep
            Body.ApplyForce(f, Body.GetPosition)    '应用风力
            world.Step(timeStep, MaxStep, 1)         '步进计算,第一个参数是步长(多久计算一次),第二个参数是计算的最多步数,第三个,呃是迭代数(每次计算时用迭代器的次数)
            ret(i) = New Box2DX.Common.Vec2(Body.GetPosition.X, Body.GetPosition.Y)               '当前步位置
            '检测进入
            'Dim s(0) As Box2DX.Collision.Shape

‘出处:http://www.cnblogs.com/zcsor/
            'If RsEvnt = False AndAlso world.Query(mAABB, s, 1) = 1 Then
            '    RsX = Body.GetPosition.X * 10
             '   RsY = Body.GetPosition.Y * 10
             '   RsEvnt = True
            'End If
        Next

为了看起来更清晰,我仅保留了记录当前位置和应用风力的代码——注释掉的是检测是否击中目标的代码。

至此,我们已经可以得到一个物体的运动轨迹,也许——你应该在GDI+上绘制一下他们了。

 

【六、冲突检测】

BOX2D提供了AABB检测和其他的冲突检测方式。我们这里简要的提一下AABB,如果你感兴趣,完全可以使用“接触监听器”或者“传感器”来完成你的代码。

创建一个AABB检测和使用它将非常简单:只需要设置AABB的两个坐标点——像创建世界外边框那样,指定左上角和右下角。
        mAABB.LowerBound.Set(X1, Y1)
        mAABB.UpperBound.Set(X2, Y2)

而检测代码已经在上面的代码中了——还记得吗,我把他们注释掉了。

【后记】

上面讲到的类,代码如下:

 

代码
Public Class Box2DEngine
    
Dim world As Box2DX.Dynamics.World
    
Dim g As New Box2DX.Common.Vec2(0.0F, 17.27F)               '重力
    Dim Damping As Single = 0.1F                                '空气阻尼
    Dim WapOffset As PointF = New PointF(2.5F, -2.5F)           '相对于人物中心的武器的偏移量
    Dim RevisePower As Single = 4.0F                            '力度
    Dim ReviseWind As Single = 2.474F                           '风速
    Dim Density As Single = 5.0929579415893746                  '密度
    Dim timeStep As Single = 1 / 25                             '时间步数——游戏每帧时间
    Dim mAABB As New Box2DX.Collision.AABB                      '范围检测,这里用的不是冲突而是范围内是否有物体
    Public Event InAABB(ByVal power As SingleByVal x As SingleByVal y As SingleByVal vs() As Box2DX.Common.Vec2)
    
Public Event NotInAABB(ByVal power As SingleByVal x As SingleByVal y As SingleByVal vs() As Box2DX.Common.Vec2)

    
Public Enum ForRight As Integer
        Yes 
= 1
        No 
= -1
    
End Enum

    
Sub New()
        
'世界外边框
        Dim Wordaabb As Box2DX.Collision.AABB
        Wordaabb.LowerBound.Set(
-10000.0F, -10000.0F)
        Wordaabb.UpperBound.Set(
10000.0F, 10000.0F)
’出处:http://www.cnblogs.com/zcsor/
        
'允许引擎休眠
        Dim dosleep As Boolean = False ' True
        '创建世界
        world = New Box2DX.Dynamics.World(Wordaabb, g, dosleep)
    
End Sub

    
Sub SetAABB(ByVal p As PointF, ByVal mSize As SizeF)
        mAABB.LowerBound.Set(p.X 
/ 10 - mSize.Width, p.Y / 10 - mSize.Height)
        mAABB.UpperBound.Set(p.X 
/ 10 + mSize.Width, p.Y / 10 + mSize.Height)
    
End Sub

    
''' <summary>
    
''' 获取指定状态下的关键点
    
''' </summary>
    
''' <param name="PlayerPoint">玩家位置(玩家中心位置的屏幕坐标)</param>
    
''' <param name="ForRight">玩家是否朝向右面</param>
    
''' <param name="Power">射击力度</param>
    
''' <param name="Angle">射击角度(无左右方向,有上下方向)</param>
    
''' <param name="Wind">风力及方向,向右为正,向左为负</param>
    
''' <param name="MaxStep">最大计算步数,每一秒计算25次</param>
    
''' <returns>从当前位置开始模拟运动时,每一步的坐标</returns>
    
''' <remarks></remarks>
    Public Function GetPath(ByVal PlayerPoint As PointF, ByVal ForRight As ForRight, ByVal Power As SingleByVal Angle As DoubleByVal Wind As SingleByVal MaxStep As SingleAs Box2DX.Common.Vec2()
        
Dim RsEvnt As Boolean
        
Dim RsX, RsY As Single
        
Dim ret(MaxStep) As Box2DX.Common.Vec2
        
'创建一个动态物体
        Dim Body As Box2DX.Dynamics.Body = CreateBody(PlayerPoint, ForRight, 0.5F, False, Angle)
        
'发射——对其质心应用一个冲力
        Body.ApplyImpulse(New Box2DX.Common.Vec2(CInt(ForRight) * Math.Cos(Angle) * Power * RevisePower, -Math.Sin(Angle) * Power * RevisePower), Body.GetPosition)
        
'一个持久作用力——风力
        Dim f As Box2DX.Common.Vec2 = New Box2DX.Common.Vec2(Wind * ReviseWind, 0)
        
'添加位置到返回数组
        For i As Integer = 0 To MaxStep
            Body.ApplyForce(f, Body.GetPosition)    
'应用风力
            world.Step(timeStep, MaxStep, 1)        '步进计算
            ret(i) = New Box2DX.Common.Vec2(Body.GetPosition.X, Body.GetPosition.Y)               '当前步位置
            '检测进入
            Dim s(0As Box2DX.Collision.Shape
            
If RsEvnt = False AndAlso world.Query(mAABB, s, 1= 1 Then
                RsX 
= Body.GetPosition.X * 10
                RsY 
= Body.GetPosition.Y * 10
                RsEvnt 
= True
            
End If
        
Next
        
'销毁这次用的BODY,下次将重新创建
        world.DestroyBody(Body)
        Body.Dispose()
        Body 
= Nothing
        
If RsEvnt Then
            
RaiseEvent InAABB(Power, RsX, RsY, ret)
        
Else
            
RaiseEvent NotInAABB(Power, RsX, RsY, ret)
        
End If
        
Return ret
    
End Function

    
Private Function RotateV2(ByVal p As PointF, ByVal a As DoubleAs PointF
        
Dim ret As New PointF
        
Dim b As Double = -a
        ret.X 
= p.X * Math.Cos(b) - p.Y * Math.Sin(b)
        ret.Y 
= p.X * Math.Sin(b) + p.Y * Math.Cos(b)
        Debug.Print(b)
        Debug.Print(ret.ToString)
        
Return ret
    
End Function

    
''' <summary>
    
''' 根据玩家位置和是否向右,来创建一个炮弹,这个炮弹的具体位置和发射者位置及角度相关。
    
''' </summary>
    
''' <param name="PlayerPoint">玩家位置</param>
    
''' <param name="ForRight">是否向右</param>
    
''' <param name="R">半径</param>
    
''' <param name="IsSensor">是否是感应器</param>
    
''' <param name="Angle">发射角度</param>
    
''' <returns></returns>
    
''' <remarks></remarks>
    Private Function CreateBody(ByVal PlayerPoint As PointF, ByVal ForRight As ForRight, ByVal R As SingleByVal IsSensor As BooleanByVal Angle As DoubleAs Box2DX.Dynamics.Body
        
'Debug.Print(CInt(ForRight))
        '创建动态物体,它的质量一定要大于0
        Dim bodyDef As New Box2DX.Dynamics.BodyDef
        WapOffset 
= RotateV2(New PointF(2.50), Angle)
        bodyDef.Position.Set((PlayerPoint.X 
/ 10 + CInt(ForRight) * WapOffset.X), (PlayerPoint.Y / 10 + WapOffset.Y))
        bodyDef.LinearDamping 
= Damping
        
Dim Body As Box2DX.Dynamics.Body
        Body 
= world.CreateBody(bodyDef)
        
'为物体附加一个多边形
        Dim shapeDef As New Box2DX.Collision.CircleDef
        shapeDef.IsSensor 
= IsSensor
        shapeDef.Radius 
= R   '设置体积为1,以使得质量计算结果为1
        shapeDef.Density = Density    '密度
        'shapeDef.Friction = 1   '摩擦系数
        Dim shape = Body.CreateShape(shapeDef) '附加到物体
        Body.SetMassFromShapes()   '根据形状计算质量
        Return Body
    
End Function

    
Protected Overrides Sub Finalize()
        world.Dispose()
        
MyBase.Finalize()
    
End Sub
End Class

 

而我所用的测试代码如下:

 

代码


Public Class Form1
    
Dim gr As Graphics      '窗体的画布
    Dim CompassRect As Rectangle    '罗盘
    Dim po As Point     '罗盘中心
    Dim p1 As Point     '指针终点
    Dim powerRect As Rectangle  '力度框
    Dim mb As Point '目标位置
    Dim wind As Single  '风力
    Dim ang As Integer  '角度
    Dim Userpower As Single '力度*5
    Dim mFont As Font   '字体
    Dim WithEvents eg As New Box2DEngine    '封装引擎
    Dim v2s() As Box2DX.Common.Vec2     '路径点
    Dim img As Bitmap       '背景图
    Dim grp As Graphics     '背景图或最终图的画布
    Dim pnt As Bitmap       '最终图

    
Private Sub Form1_KeyPress(ByVal sender As ObjectByVal e As System.Windows.Forms.KeyPressEventArgs) Handles Me.KeyPress
        
Select Case e.KeyChar
            
Case " "
                
If My.Computer.Keyboard.ShiftKeyDown Then
                    
If Userpower > 0 Then Userpower -= 2.5
                
Else
                    
If Userpower < 500 Then Userpower += 2.5
                
End If
            
Case "w""W"
                
If ang > -90 Then ang -= 1
            
Case "s""S"
                
If ang < 90 Then ang += 1
            
Case "r""R"
                
Randomize()
                wind 
= CInt(Int((100 * Rnd()))) / 10 - 5
                mb 
= New Point(800 * Rnd() + 180500 * Rnd())
                eg.SetAABB(
New PointF(mb.X, mb.Y), New Size(2020))
                Userpower 
= 0
        
End Select
        Form1_Paint(
MeNew PaintEventArgs(gr, Me.ClientRectangle))
    
End Sub

    
Private Sub Form1_KeyUp(ByVal sender As ObjectByVal e As System.Windows.Forms.KeyEventArgs) Handles Me.KeyUp
        
Select Case e.KeyData
            
Case Keys.Space, Keys.Shift Or Keys.Space, Keys.ShiftKey Or Keys.Space, Keys.W, Keys.S
                v2s 
= eg.GetPath(New PointF(po.X, po.Y), Box2DEngine.ForRight.Yes, Userpower / 5-ang * Math.PI / 180, wind, 300)
        
End Select
    
End Sub

    
Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
        img 
= New Bitmap(1000600)
        grp 
= Graphics.FromImage(img)
        
Dim alppen As Pen = New Pen(Color.FromArgb(&H40FFFFFF))
        grp.DrawString(
"按W增加角度" & vbCrLf & "按S减少角度" & vbCrLf & "按空格增加力度" & vbCrLf & "按SHIFT+空格减少力度" & vbCrLf & "按R重置"New Font("宋体"18), Brushes.DarkCyan, 00)
        
For x As Integer = 0 To 1000 Step 100
            grp.DrawLine(Pens.White, x, 
595, x, 600)
            grp.DrawLine(alppen, x, 
0, x, 595)
            grp.DrawString(x 
/ 100, Font, Brushes.White, x, 590)
        
Next
        
For y As Integer = 0 To 600 Step 60
            grp.DrawLine(alppen, 
0, y, 1000, y)
        
Next
        pnt 
= New Bitmap(1000600)
        grp.Dispose()
        grp 
= Graphics.FromImage(pnt)
        gr 
= Me.CreateGraphics
        CompassRect 
= New Rectangle(0300100100)
        po 
= New Point(CompassRect.X + CompassRect.Width / 2, CompassRect.Y + CompassRect.Height / 2)
        p1 
= New Point(CompassRect.X + CompassRect.Width / 2 + 35, CompassRect.Y + CompassRect.Height / 2)
        powerRect 
= New Rectangle(15057050220)
        mFont 
= New Font("宋体"24, FontStyle.Bold)
    
End Sub

    
Private Sub Form1_Paint(ByVal sender As ObjectByVal e As System.Windows.Forms.PaintEventArgs) Handles Me.Paint
        grp.Clear(Color.DarkGreen)
        grp.DrawImage(img, PointF.Empty)
        
Dim offset As Point = RotateV2(p1 - po, -ang) + po
        grp.DrawLine(Pens.Blue, po, offset)

        grp.DrawString(
-ang, mFont, Brushes.Blue, po.X - 15, po.Y + 10)
        grp.FillPie(Brushes.White, 
New Rectangle(po.X - 10, po.Y - 102020), 0360)
        grp.DrawRectangle(Pens.White, powerRect)
        grp.FillRectangle(Brushes.Wheat, 
New Rectangle(powerRect.X + 1, powerRect.Y + 1, Userpower, 19))
        grp.DrawString(Userpower 
/ 5, mFont, Brushes.Blue, powerRect.X, powerRect.Y - 35)
        grp.DrawString(Math.Abs(wind), mFont, Brushes.Blue, 
49010)
        grp.DrawString(
IIf(wind < 0""""), mFont, Brushes.Blue, 49030)
        
If mb.X > 0 AndAlso mb.Y > 0 Then grp.FillPie(Brushes.Black, New Rectangle(mb.X - 10, mb.Y - 102020), 0360)
        
If v2s IsNot Nothing Then
            
For Each v2 As Box2DX.Common.Vec2 In v2s
                grp.DrawArc(Pens.White, 
CSng(v2.X * 10.0F - 0.5), CSng(v2.Y * 10 - 0.5), 1.0F, 1.0F, 0.0F, 360.0F)
            
Next
        
End If
        v2s 
= Nothing
        gr.DrawImage(pnt, PointF.Empty)
    
End Sub

    
Private Function RotateV2(ByVal p As Point, ByVal a As IntegerAs Point
        
Dim ret As New Point
        
Dim b As Double = -* Math.PI / 180
        ret.X 
= p.X * Math.Cos(b) - p.Y * Math.Sin(b)
        ret.Y 
= p.X * Math.Sin(b) + p.Y * Math.Cos(b)
        
Return ret
    
End Function

    
Private Sub eg_InAABB(ByVal power As SingleByVal x As SingleByVal y As SingleByVal vs() As Box2DX.Common.Vec2) Handles eg.InAABB
        v2s 
= vs
        Form1_Paint(
MeNew PaintEventArgs(gr, Me.ClientRectangle))
    
End Sub

    
Private Sub eg_NotInAABB(ByVal power As SingleByVal x As SingleByVal y As SingleByVal vs() As Box2DX.Common.Vec2) Handles eg.NotInAABB
        v2s 
= vs
        Form1_Paint(
MeNew PaintEventArgs(gr, Me.ClientRectangle))
    
End Sub
End Class

 

好了,只是抛砖引玉,希望大家能够开发出自己的游戏,毕竟别人的游戏只是一种借鉴,学会思考才是最重要的。

PS:代码中使用了坐标缩放,这是因为物理引擎模拟运动时,只在一定范围内(例如十米以内)有较高的准确度,超出这个范围将会降低准确度。

PS:BOX2D是使用单精度浮点数来进行计算的——这不会对准确度影响较大。因为它使用了一些通用的公差。

原文:http://www.cnblogs.com/zcsor/

贴了这么多出处我也没办法。因为有些同学脸皮太薄了——将将比城墙厚一点点!

posted @ 2010-09-09 16:55  zcsor~流浪dè风  Views(4655)  Comments(3Edit  收藏  举报