8.第一次代码重构

前面的几篇文章我们已经建立起了一个基本的物理引擎,下面就要开始处理碰撞了,对于碰撞来说,难的不是物理而是数学,为了让引擎的结构更好地适应碰撞的处理,我们需要进行一些调整,让它更健壮。

添加数学和变换辅助类

在引擎中添加Common目录,在这个目录中添加2D向量类Vector2.cs,3D向量类Vector3.cs,矩阵类Matrix.cs,数学函数辅助类MathHelper.cs,这四个函数在XNA框架中都是自带的,但如果引擎中工作在Silverlight环境中,就无法使用XNA框架中的这四个类,因此需要自己创建,其中Vector2类我们已经在前面写过了,只需要自己写另外三个类。幸运的是有现成代码可以使用,这就是 MonoXna(www.monoxna.org),它可以看成XNA的开源代码,并支持多平台,但现在好像很久没有更新了,不过我们可以参考(也可以说是照抄)它的源代码,以上四个类的代码就来自于它的源代码(http://code.google.com/p/monoxna/source/browse/#svn%2Ftrunk%2Fsrc)。

源代码比较长,就不贴出了,只需要说明一点,以Vector2.cs为例:

#if(!XNA) 

using System;
using System.Runtime.InteropServices;
using System.Text; 

namespace Microsoft.Xna.Framework
{
    [StructLayout(LayoutKind.Sequential)] 
    public struct Vector2 : IEquatable<Vector2>
    {
        […]
    }
}

#endif

从代码中我们可以看到使用了条件编译#if(!XNA),因此当编译目标不是XNA时(这里是Silverlight),就不会编译这个类,而且这四个类的命名空间都是Microsoft.Xna.Framework。这样做带来的好处是Stun2Dphysics的Silverlight和XNA的版本的代码文件是完全一样了,无需进行微调避免不必要的错误,在Silverlight平台上引擎会编译这个Vector2类,而XNA平台就不会编译这个类而使用XNA框架自带的Vector2类。

在Common目录下还有一个Math.cs文件,它由四部分组成

1.Mat22结构体

它是一个自定义的2x2矩阵,代码如下:

/// 
/// 2*2的列主序矩阵。
/// 
public struct Mat22
{
    /// 
    /// 矩阵的第一列数据。 
    /// 
    public Vector2 Col1;
    
    /// 
    /// 矩阵的第二列数据。 
    /// 
    public Vector2 Col2; 
    
    /// 
    /// 使用两列构建矩阵。 
    /// 
    /// 第一列对应的二维矢量。 
    /// 第二列对应的二维矢量。 
    public Mat22(Vector2 c1, Vector2 c2) 
    {
         Col1 = c1;
         Col2 = c2;
     }
     
     /// 
     /// 使用4个标量构建矩阵。 
     /// 
     /// a11。 
     /// a12。 
     /// a21。 
     /// a22。 
     public Mat22(float a11, float a12, float a21, float a22) 
     {
         Col1 = new Vector2(a11, a21);
         Col2 = new Vector2(a12, a22); 
     }
     
     /// 
     /// 使用角度构建矩阵。此时这个矩阵成为一个正交旋转矩阵。 
     /// 
     /// 角度。 
     public Mat22(float angle) 
     {
         float c = (float)Math.Cos(angle), s = (float)Math.Sin(angle);
         Col1 = new Vector2(c, s);
         Col2 = new Vector2(-s, c); 
     }
     
     /// 
     /// 从矩阵中提取旋转角度。
     /// 
     public float Angle
     {
         get { return (float)Math.Atan2(Col1.Y, Col1.X); } 
     }
     
     /// 
     /// 计算逆矩阵
     /// 
     public Mat22 Inverse 
     {
         get 
         {
             float a = Col1.X, b = Col2.X, c = Col1.Y, d = Col2.Y;
             // 计算矩阵行列式
             float det = a * d - b * c;
             if (det != 0.0f)
             {
                 det = 1.0f / det;
             }
             Mat22 result = new Mat22();
             result.Col1.X = det * d; 
             result.Col1.Y = -det * c;
             result.Col2.X = -det * b; 
             result.Col2.Y = det * a; 
             return result; 
         }
     }
     
     ///     
     /// 使用列设置矩阵。
     /// 
     /// 第一列对应的二维矢量。 
     /// 第二列对应的二维矢量。 
     public void Set(Vector2 c1, Vector2 c2) 
     {
         Col1 = c1;
         Col2 = c2;
     }
     
     /// 
     /// 使用旋转角度值设置矩阵。
     /// 这个矩阵就成为一个正交旋转矩阵。
     /// 
     /// 旋转角度。 
     public void Set(float angle)
     {
         float c = (float)Math.Cos(angle);
         float s = (float)Math.Sin(angle);
         Col1.X = c;
         Col2.X = -s; 
         Col1.Y = s;
         Col2.Y = c;
     }
     
     /// 
     /// 设置为单位矩阵。 
     /// 
     public void SetIdentity()
     {
         Col1.X = 1.0f;
         Col2.X = 0.0f;
         Col1.Y = 0.0f;
         Col2.Y = 1.0f;
     }
     
     /// 
     /// 矩阵清零。 
     /// 
     public void SetZero()
     {
          Col1.X = 0.0f;
          Col2.X = 0.0f;
          Col1.Y = 0.0f;
          Col2.Y = 0.0f;
      }
      
      /// 
      /// 矩阵相加
      /// 
      /// 第一个矩阵  
      /// 第二个矩阵  
      /// 相加结果矩阵 
      public static void Add(ref Mat22 A, ref Mat22 B, out Mat22 R) 
      {
          R.Col1 = A.Col1 + B.Col1; R.Col2 = A.Col2 + B.Col2; 
      }
 } 

XNA框架自带的Matrix是4x4结构,其中左上角的3*3矩阵包含了旋转和缩放的信息,同理,要处理2D的情况,我们可以使用一个2x2矩阵表示2D物体的旋转(无需处理缩放的情况),例如,一个对象顺时针旋转θ,可以用下图表示:

图1 旋转矩阵的定义

图1 旋转矩阵的定义

即对象的本地坐标系(红色)相对于世界坐标系(黑色)顺时针旋转了θ,此时红色x轴在世界坐标系中的坐标(不考虑缩放,即认为长度为1)为(cosθ,sinθ),y轴的坐标为(-sinθ,cosθ),因为Mat22是列主序的,所以将这两个坐标作为两列构成一个2x2矩阵,而正是这个矩阵包含了物体的旋转信息,即角位移信息。

旋转矩阵

其实只用一个浮点数θ就可以表示2D空间中的旋转,为什么需要使用4个浮点数构成矩阵?这是因为矩阵最主要的优点是可以立即进行向量的旋转,而且图形API就是使用矩阵来描述旋转。同理,在3D空间中只需使用3个浮点数表示三个欧拉角,但我们使用的却是9个浮点数构成的3*3矩阵。

2.Transform结构

在XNA中,我们使用一个4*4矩阵描述3D空间中物体的位置和朝向,同理,在2D空间中我们可以使用3*3矩阵表示物体的位置和朝向,但为了减少数据冗余,我们使用一个Vector2表示位置,一个Mat22表示朝向,组合成Transform结构。这个结构就可以描述Body的位置和旋转了。代码如下:

/// <summary> 
/// 包含平移和旋转的变换,用于表示刚体的位置和朝向。
/// </summary>
public struct Transform
{
    /// <summary> 
    /// 位置
    /// </summary>
    public Vector2 Position;
    
    /// <summary>
    /// 表示旋转的2*2矩阵
    /// </summary>
    public Mat22 RMatrix;
    
    /// <summary>
    /// 使用一个位置矢量和一个旋转矩阵进行初始化。
    /// </summary>
    // <param name="position">位置矢量。</param>
    /// <param name="r">旋转矩阵。</param> 
    public Transform(ref Vector2 position, ref Mat22 rotation)
    {
        Position = position;
        RMatrix = rotation;
    }
    
    /// <summary>
    /// 计算旋转矩阵表示的旋转角度。
    /// </summary>
    public float Angle
    {
        get { return (float)Math.Atan2(RMatrix.Col1.Y, RMatrix.Col1.X);}
    }
    
    /// <summary>
    /// 设置为单位变换。即将位置设置为零矢量,旋转矩阵设置为单位矩阵
    /// </summary>
    public void SetIdentity()
    {
        Position = Vector2.Zero;
        RMatrix.SetIdentity();
    }
    
    /// <summary>
    /// 基于位置和旋转角度设置变换。
    /// </summary>
    /// <param name="position">位置矢量。</param>
    /// <param name="angle">旋转角度。</param> 
    public void Set(Vector2 position, float angle)
    {
        Position = position;
        RMatrix.Set(angle);
    }
}

3.Sweep结构

我们还创建了一个结构保存Body的质心位置和旋转信息,代码如下:

/// 
/// 保存Body质心位置和旋转的结构体。
/// 
public struct Sweep
{
    /// 
    /// 世界空间中的旋转量
    /// 
    public float A; 
    
    /// 
    /// 世界空间中的旋转量备份 
    /// 
    public float A0;
    
    /// 
    /// 质心的世界坐标
    /// 
    public Vector2 C; 
    
    /// 
    /// 质心的世界坐标备份
    /// 
    public Vector2 C0;
    
    /// 
    /// 质心的本地坐标
    /// 
    public Vector2 LocalCenter;
}

为什么这个结构叫做Sweep?这会涉及到以后(可能是很久以后)将要讨论的连续碰撞检测(Continuous Collision Detection,简称CCD),其中有个扫掠体(swept volume)的概念。

4.静态MathUtils类

这个类包含一些前面的MathHelper类没有包含的辅助方法,代码如下:

public static class MathUtils
{
    /// <summary> 
    /// 计算二维矢量的绝对值
     /// </summary> 
     /// <param name="v">二维矢量</param> 
     public static Vector2 Abs(Vector2 v) 
     {
          return new Vector2(Math.Abs(v.X), Math.Abs(v.Y)); 
     } 
     
     /// <summary> 
     /// 交换两个对象的值。
      /// </summary> 
      /// <typeparam name="T">对象类型。</typeparam> 
      /// <param name="a">第一个对象。</param> 
      /// <param name="b">第二个对象。</param> 
      public static void Swap<T>(ref T a, ref T b) 
      {
          T tmp = a;
          a = b;
          b = tmp; 
      } 
      
      /// <summary> 
      /// 判断一个浮点数是否是有效值,即是不是NaN或无穷大。
      /// </summary> 
      public static bool IsValid(float x) 
      {
          if (float.IsNaN(x)) 
          {
              // NaN.
              return false;
          }
          return !float.IsInfinity(x);
      } 
      
      /// <summary> 
      /// 判断一个二维矢量是否是有效值。 
      /// </summary> 
      /// <param name="x"></param> 
      /// <returns></returns>
      public static bool IsValid(this Vector2 x) 
      {
          return IsValid(x.X) && IsValid(x.Y); 
      }
      
      /// <summary> 
      /// 拟叉乘,两个二维矢量拟叉乘的结果是一个浮点数 
      /// </summary> 
      /// <param name="a">第一个二维矢量</param>
      /// <param name="b">第二个二维矢量</param> 
      public static float Cross(Vector2 a, Vector2 b)
      {
          return a.X * b.Y - a.Y * b.X; 
      }
      
      /// <summary> 
      /// 拟叉乘,两个二维矢量拟叉乘的结果是一个浮点数 
      /// </summary>
      /// <param name="a">第一个二维矢量</param> 
      /// <param name="b">第二个二维矢量</param>
      /// <param name="c">结果浮点数。</param> 
      public static void Cross(ref Vector2 a, ref Vector2 b, out float c) 
      {
          c = a.X * b.Y - a.Y * b.X; 
      }
      
      /// <summary>
       /// 使用一个2*2矩阵对二维矢量进行变换,只变换旋转。 
      /// </summary> 
      /// <param name="A">2*2矢量。</param> 
      /// <param name="v">二维矢量。</param> 
      /// <returns>经过变换的二维矢量。</returns> 
      public static Vector2 Multiply(ref Mat22 A, Vector2 v) 
      {
          return Multiply(ref A, ref v); 
      }
      
      /// <summary> 
      /// 使用一个2*2矩阵对二维矢量进行变换,只变换旋转。
      /// </summary>
      /// <param name="A">2*2矢量。</param> 
      /// <param name="v">二维矢量。</param> 
      /// <returns>经过变换的二维矢量。</returns> 
      public static Vector2 Multiply(ref Mat22 A, ref Vector2 v) 
      {
          return new Vector2(A.Col1.X * v.X + A.Col2.X * v.Y, A.Col1.Y * v.X + A.Col2.Y * v.Y); 
      } 
      
      /// <summary> 
      /// 对一个二维矢量进行变换,同时变换平移和旋转。 
      /// </summary> /// <param name="T">变换。</param> 
      /// <param name="v">二维矢量。</param> 
      /// <returns>经过变换的二维矢量。</returns> 
      public static Vector2 Multiply(ref Transform T, Vector2 v) 
      {
          return Multiply(ref T, ref v); 
      }
           
      
      /// <summary> 
      /// 对一个二维矢量进行变换,同时变换平移和旋转。 
      /// </summary> 
      /// <param name="T">变换。</param> 
      /// <param name="v">二维矢量。</param> 
      /// <returns>经过变换的二维矢量。</returns> 
      public static Vector2 Multiply(ref Transform T, ref Vector2 v)
      {
          return new Vector2(T.Position.X + T.RMatrix.Col1.X * v.X + T.RMatrix.Col2.X * v.Y, T.Position.Y + T.RMatrix.Col1.Y * v.X + T.RMatrix.Col2.Y * v.Y); 
      } 
      
      /// <summary> 
      /// 对一个整数进行截取。
       /// </summary> 
       /// <param name="a">要截取的整数。</param> 
      /// <param name="low">下限。</param> 
      /// <param name="high">上限。</param> 
      public static int Clamp(int a, int low, int high) 
      {
          return Math.Max(low, Math.Min(a, high)); 
      } 
      
      /// <summary> 
      /// 对一个浮点数进行截取。 
      /// </summary> 
      /// <param name="a">要截取的浮点数。</param> 
      /// <param name="low">下限。</param> 
      /// <param name="high">上限。</param> 
      public static float Clamp(float a, float low, float high) { return Math.Max(low, Math.Min(a, high)); }
      
      /// <summary> 
      /// 对一个二维矢量进行截取。 
      /// </summary> 
      /// <param name="a">要截取的二维矢量。</param> 
      /// <param name="low">下限。</param> 
      /// <param name="high">上限。</param> 
      public static Vector2 Clamp(Vector2 a, Vector2 low, Vector2 high) 
      {
          return Vector2.Max(low, Vector2.Min(a, high)); 
      }
  }

Body类

先贴出Body类的代码,然后就细节部分进行解释:

using System;
using System.Diagnostics;
using Stun2DPhysics4SL.Common;
using Microsoft.Xna.Framework;

namespace Stun2DPhysics4SL.Dynamics
{
    /// <summary>
    /// Body类型。
    /// </summary>
    public enum BodyType
    {
        /// <summary>
        /// 速度为零即静止不动,可以手动移动,注意:静止Body也有质量。
        /// </summary>
        Static,
        /// <summary>
        /// 质量为正值, 施加力可以产生速度。
        /// </summary>
        Dynamic,
    }

    [Flags]
    public enum BodyFlags
    {
        None = 0,
        Enabled = (1 << 0),
        IgnoreGravity = (1 << 1),
    }

    public class Body : IDisposable
    {        
        /// <summary>
        /// Body编号
        /// </summary>
        public int BodyId;
        
        private static int _bodyIdCounter;          // Body编号,它是一个静态变量
        internal BodyFlags Flags;                   // 包含Body属性信息的标志
        private BodyType _bodyType;                 // Body类型,目前有Static(静态)和Dynamic(动态)两种
        
        private float _mass;                        // 质量
        internal float InvMass;                     // 质量倒数
        internal Vector2 LinearVelocityInternal;    // 引擎内部调用的线速度
        internal Vector2 Force;                     // 施加在Body上的力

        private float _inertia;                     // 以质心为转轴的转动惯量
        internal float InvInertia;                  // 转动惯量的倒数
        internal float AngularVelocityInternal;     // 引擎内部调用的角速度
        internal float Torque;                      // 施加在Body上的力矩

        internal Transform Xf;                      // Body原点的变换
        internal Sweep Sweep;                       // 保存Body质心位置和旋转信息的结构体
        
        internal World World;

        /// <summary>
        /// 创建一个Body对象
        /// </summary>
        /// <param name="world">对World的引用。</param>
        public Body(World world)
        {
            BodyId = _bodyIdCounter++;
            BodyType = BodyType.Dynamic  ;
            Enabled = true;
            Xf.RMatrix.Set(0);

            World = world;
            world.AddBody(this);
        }

        #region 属性        

        /// <summary>
        /// 获取或设置Body类型。
        /// </summary>
        public BodyType BodyType
        {
            get { return _bodyType; }
            set
            {
                if (_bodyType == value)
                {
                    return;
                }

                _bodyType = value;

                // 如果将Body类型设置为静态的,则需要将线速度和角速度都设置为0
                if (_bodyType == BodyType.Static)
                {
                    LinearVelocityInternal = Vector2.Zero;
                    AngularVelocityInternal = 0.0f;
                }

                Force = Vector2.Zero;
                Torque = 0.0f;
            }
        }

        /// <summary>
        /// 获取或设置质心的线速度。
        /// </summary>
        public Vector2 LinearVelocity
        {
            set
            {
                // 如果是静态对象,则直接返回。
                if (_bodyType == BodyType.Static)
                    return;

                LinearVelocityInternal = value;
            }
            get { return LinearVelocityInternal; }
        }

        /// <summary>
        /// 获取或设置角速度,单位为弧度每秒(rad/s)。
        /// </summary>
        public float AngularVelocity
        {
            set
            {
                // 如果是静态对象,则直接返回。
                if (_bodyType == BodyType.Static)
                    return;

                AngularVelocityInternal = value;
            }
            get { return AngularVelocityInternal; }
        }

        /// <summary>
        /// 获取或设置线性阻尼系数。
        /// </summary>
        public float LinearDamping{ get;set; }

        /// <summary>
        /// 获取或设置角度阻尼系数。
        /// </summary>
        public float AngularDamping{ get;set; }
        

        /// <summary>
        /// 设置Body的激活状态。
        /// </summary>
        public bool Enabled
        {
            set
            {
                if (value == Enabled)
                {
                    return;
                }

                if (value)
                {
                    Flags |= BodyFlags.Enabled;
                }
                else
                {
                    Flags &= ~BodyFlags.Enabled;
                }
            }
            get { return (Flags & BodyFlags.Enabled) == BodyFlags.Enabled; }
        }

        /// <summary>
        /// 获取或设置Body在世界坐标系中的初始位置。
        /// </summary>
        public Vector2 Position
        {
            get { return Xf.Position; }
            set
            {
                SetTransform(ref value, Rotation);
            }
        }

        /// <summary>
        /// 获取或设置旋转量,单位为弧度。
        /// </summary>
        public float Rotation
        {
            get { return Sweep.A; }
            set
            {
                SetTransform(ref Xf.Position, value);
            }
        }

        /// <summary>
        /// 获取或设置Body是否处是静态的。
        /// </summary>
        public bool IsStatic
        {
            get { return _bodyType == BodyType.Static; }
            set
            {
                if (value)
                    BodyType = BodyType.Static;
                else
                    BodyType = BodyType.Dynamic;
            }
        }

        /// <summary>
        /// 获取或设置Body是否忽略本身的重力。
        /// </summary>
        public bool IgnoreGravity
        {
            get { return (Flags & BodyFlags.IgnoreGravity) == BodyFlags.IgnoreGravity; }
            set
            {
                if (value)
                    Flags |= BodyFlags.IgnoreGravity;
                else
                    Flags &= ~BodyFlags.IgnoreGravity;
            }
        }

        /// <summary>
        /// 获取质心在世界坐标系中的坐标。
        /// </summary>
        public Vector2 WorldCenter
        {
            get { return Sweep.C; }
        }

        /// <summary>
        /// 获取或设置质心的本地坐标。
        /// </summary>
        public Vector2 LocalCenter
        {
            get { return Sweep.LocalCenter; }
            set
            {
                if (_bodyType != BodyType.Dynamic)
                    return;

                // 更新存储在Sweep中的质心本地坐标和世界坐标。
                Vector2 oldCenter = Sweep.C;
                Sweep.LocalCenter = value;
                Sweep.C0 = Sweep.C = MathUtils.Multiply(ref Xf, ref Sweep.LocalCenter);

                // 更新质心的速度。
                Vector2 a = Sweep.C - oldCenter;
                LinearVelocityInternal += new Vector2(-AngularVelocityInternal * a.Y, AngularVelocityInternal * a.X);
            }
        }

        /// <summary>
        /// 获取或设置质量,单位为千克(kg)。
        /// </summary>
        public float Mass
        {
            get { return _mass; }
            set
            {
                if (_bodyType != BodyType.Dynamic)
                    return;

                _mass = value;

                if (_mass <= 0.0f)
                    _mass = 1.0f;

                InvMass = 1.0f / _mass;
            }
        }

        /// <summary>
        /// 获取或设置body相对于本地坐标原点的转动惯量,单位为kg-m^2。
        /// </summary>
        public float Inertia
        {
            get { return _inertia + Mass * Vector2.Dot(Sweep.LocalCenter, Sweep.LocalCenter); }
            set
            {
                if (_bodyType != BodyType.Dynamic)
                    return;

                if (value > 0.0f)
                {
                    _inertia = value - Mass * Vector2.Dot(LocalCenter, LocalCenter);
                    InvInertia = 1.0f / _inertia;
                }
            }
        }

        #endregion        

        #region 施加力、力矩、冲量、角冲量的方法

        /// <summary>
        /// 在质心上施加一个力。
        /// </summary>
        /// <param name="force">力。</param>
        public void ApplyForce(ref Vector2 force)
        {
            ApplyForce(ref force, ref Xf.Position);
        }

        /// <summary>
        /// 在质心上施加一个力。
        /// </summary>
        /// <param name="force">力。</param>
        public void ApplyForce(Vector2 force)
        {
            ApplyForce(ref force, ref Xf.Position);
        }

        /// <summary>
        /// 在世界坐标系中某点施加一个力。
        /// 如果这个力没有通过质心,则还会产生一个力矩并影响到角速度。
        /// </summary>
        /// <param name="force">世界坐标系中的力,单位为牛顿(N)。</param>
        /// <param name="point">世界坐标系中的点位置。</param>
        public void ApplyForce(Vector2 force, Vector2 point)
        {
            ApplyForce(ref force, ref point);
        }

        /// <summary>
        /// 在世界坐标系中某点施加一个力。
        /// 如果这个力没有通过质心,则还会产生一个力矩并影响到角速度。
        /// </summary>
        /// <param name="force">世界坐标系中的力,单位为牛顿(N)。</param>
        /// <param name="point">世界坐标系中的点位置。</param>
        public void ApplyForce(ref Vector2 force, ref Vector2 point)
        {
            if (_bodyType == BodyType.Dynamic)
            {
                Force += force;
                Torque += (point.X - Sweep.C.X) * force.Y - (point.Y - Sweep.C.Y) * force.X;
            }
        }

        /// <summary>
        /// 施加一个力矩。这会影响到角速度但不会影响质心的线速度。
        /// </summary>
        /// <param name="torque">关于z轴(指向屏幕之外)的力矩,单位为N·m。</param>
        public void ApplyTorque(float torque)
        {
            if (_bodyType == BodyType.Dynamic)
            {
                Torque += torque;
            }
        }

        /// <summary>
        /// 在世界坐标系中施加一个冲量,可以立即改变速度。
        /// </summary>
        /// <param name="impulse">世界坐标系中的冲量矢量,单位为N·seconds或kg·m/s。</param>
        public void ApplyLinearImpulse(Vector2 impulse)
        {
            ApplyLinearImpulse(ref impulse);
        }

        /// <summary>
        /// 在世界坐标系中施加一个冲量,可以立即改变速度。
        /// </summary>
        /// <param name="impulse">世界坐标系中的冲量矢量,单位为N·seconds或kg·m/s。</param>
        public void ApplyLinearImpulse(ref Vector2 impulse)
        {
            if (_bodyType != BodyType.Dynamic)
            {
                return;
            }
            LinearVelocityInternal += InvMass * impulse;
        }

        /// <summary>
        /// 在世界坐标系中某点施加一个冲量,可以立即改变速度。
        /// 如果这个冲量不通过质心,则还会改变角速度。
        /// </summary>
        /// <param name="impulse">世界坐标系中的冲量矢量,单位为N·seconds或kg·m/s。</param>
        /// <param name="point">世界坐标系中的点位置。</param>
        public void ApplyLinearImpulse(Vector2 impulse, Vector2 point)
        {
            ApplyLinearImpulse(ref impulse, ref point);
        }

        /// <summary>
        /// 在世界坐标系中某点施加一个冲量,可以立即改变速度。
        /// 如果这个冲量不通过质心,则还会改变角速度。
        /// </summary>
        /// <param name="impulse">世界坐标系中的冲量矢量,单位为N·seconds或kg·m/s。</param>
        /// <param name="point">世界坐标系中的点位置。</param>
        public void ApplyLinearImpulse(ref Vector2 impulse, ref Vector2 point)
        {
            if (_bodyType != BodyType.Dynamic)
                return;
            
            LinearVelocityInternal += InvMass * impulse;
            AngularVelocityInternal += InvInertia * ((point.X - Sweep.C.X) * impulse.Y - (point.Y - Sweep.C.Y) * impulse.X);
        }

        /// <summary>
        /// 施加一个角冲量。
        /// </summary>
        /// <param name="impulse">角冲量,单位为kg*m*m/s。</param>
        public void ApplyAngularImpulse(float impulse)
        {
            // 如果Body是静态的,则施加角冲量是无用的,直接退出
            if (_bodyType != BodyType.Dynamic)
            {
                return;
            }
            AngularVelocityInternal += InvInertia * impulse;
        }

        #endregion

        #region 世界坐标和本地坐标的变换

        /// <summary>
        /// 设置Body原点的位置和旋转。
        /// </summary>
        /// <param name="position">Body原点在世界坐标系中位置。</param>
        /// <param name="rotation">世界空间中的旋转量,以弧度为单位。</param>
        public void SetTransform(ref Vector2 position, float rotation)
        {
            Xf.RMatrix.Set(rotation);
            Xf.Position = position;

            // 更新存储在Sweep中的质心世界坐标和旋转量
            Sweep.C0 =
                Sweep.C =
                new Vector2(Xf.Position.X + Xf.RMatrix.Col1.X * Sweep.LocalCenter.X + Xf.RMatrix.Col2.X * Sweep.LocalCenter.Y,
                            Xf.Position.Y + Xf.RMatrix.Col1.Y * Sweep.LocalCenter.X + Xf.RMatrix.Col2.Y * Sweep.LocalCenter.Y);
            Sweep.A0 = Sweep.A = rotation;
        }

        /// <summary>
        /// 设置Body原点的位置和旋转。
        /// </summary>
        /// <param name="position">Body原点在世界坐标系中位置。</param>
        /// <param name="rotation">世界空间中的旋转量,以弧度为单位。</param>
        public void SetTransform(Vector2 position, float rotation)
        {
            SetTransform(ref position, rotation);
        }

        /// <summary>
        /// 获取Body的变换数据。
        /// </summary>
        /// <param name="transform">输出的Body的变换信息。</param>
        public void GetTransform(out Transform transform)
        {
            transform = Xf;
        }
        
        /// <summary>
        /// 给定本地空间中的点,返回世界空间中的点。
        /// </summary>
        /// <param name="localPoint">本地空间中的点。</param>
        /// <returns>世界空间中的点。</returns>
        public Vector2 GetWorldPoint(ref Vector2 localPoint)
        {
            return new Vector2(Xf.Position.X + Xf.RMatrix.Col1.X * localPoint.X + Xf.RMatrix.Col2.X * localPoint.Y,
                               Xf.Position.Y + Xf.RMatrix.Col1.Y * localPoint.X + Xf.RMatrix.Col2.Y * localPoint.Y);
        }

        /// <summary>
        /// 给定本地空间中的点,返回世界空间中的点。
        /// </summary>
        /// <param name="localPoint">本地空间中的点。</param>
        /// <returns>世界空间中的点。</returns>
        public Vector2 GetWorldPoint(Vector2 localPoint)
        {
            return GetWorldPoint(ref localPoint);
        }

        /// <summary>
        /// 给定世界空间中的点,获取本地空间中的点。
        /// </summary>
        /// <param name="worldPoint">世界空间中的点。</param>
        /// <returns>本地空间中的点。</returns>
        public Vector2 GetLocalPoint(ref Vector2 worldPoint)
        {
            return
                new Vector2((worldPoint.X - Xf.Position.X) * Xf.RMatrix.Col1.X + (worldPoint.Y - Xf.Position.Y) * Xf.RMatrix.Col1.Y,
                            (worldPoint.X - Xf.Position.X) * Xf.RMatrix.Col2.X + (worldPoint.Y - Xf.Position.Y) * Xf.RMatrix.Col2.Y);
        }

        /// <summary>
        /// 给定世界空间中的点,获取本地空间中的点。
        /// </summary>
        /// <param name="worldPoint">世界空间中的点。</param>
        /// <returns>本地空间中的点。</returns>
        public Vector2 GetLocalPoint(Vector2 worldPoint)
        {
            return GetLocalPoint(ref worldPoint);
        }

        /// <summary>
        /// 给定本地空间中的矢量,返回世界空间中的矢量。
        /// 矢量只需关心旋转而不考虑位置。
        /// </summary>
        /// <param name="localVector">本地空间中的矢量。</param>
        /// <returns>世界空间中的矢量。</returns>
        public Vector2 GetWorldVector(ref Vector2 localVector)
        {
            return new Vector2(Xf.RMatrix.Col1.X * localVector.X + Xf.RMatrix.Col2.X * localVector.Y,
                               Xf.RMatrix.Col1.Y * localVector.X + Xf.RMatrix.Col2.Y * localVector.Y);
        }

        /// <summary>
        /// 给定本地空间中的矢量,返回世界空间中的矢量。
        /// 矢量只需关心旋转而不考虑位置。
        /// </summary>
        /// <param name="localVector">本地空间中的矢量。</param>
        /// <returns>世界空间中的矢量。</returns>
        public Vector2 GetWorldVector(Vector2 localVector)
        {
            return GetWorldVector(ref localVector);
        }
        

        /// <summary>
        /// 给定世界空间中的矢量,获取本地空间中的矢量。
        /// 这个矢量只考虑旋转不考虑位置。
        /// </summary>
        /// <param name="worldPoint">世界空间中的矢量。</param>
        /// <returns>本地空间中的矢量。</returns>
        public Vector2 GetLocalVector(ref Vector2 worldVector)
        {
            return new Vector2(worldVector.X * Xf.RMatrix.Col1.X + worldVector.Y * Xf.RMatrix.Col1.Y,
                               worldVector.X * Xf.RMatrix.Col2.X + worldVector.Y * Xf.RMatrix.Col2.Y);
        }

        /// <summary>
        /// 给定世界空间中的矢量,获取本地空间的矢量。
        /// 这个矢量只考虑旋转不考虑位置。
        /// </summary>
        /// <param name="worldPoint">世界空间中的矢量。</param>
        /// <returns>本地空间中的矢量。</returns>
        public Vector2 GetLocalVector(Vector2 worldVector)
        {
            return GetLocalVector(ref worldVector);
        }

        /// <summary>
        /// 获取世界空间中的线速度矢量。
        /// </summary>
        /// <param name="worldPoint">世界空间中的点位置。</param>
        /// <returns>点在世界空间中的线速度矢量。</returns>
        public Vector2 GetLinearVelocityFromWorldPoint(Vector2 worldPoint)
        {
            return GetLinearVelocityFromWorldPoint(ref worldPoint);
        }

        /// <summary>
        /// 获取世界空间中的线速度矢量。
        /// </summary>
        /// <param name="worldPoint">世界空间中的点位置。</param>
        /// <returns>点在世界空间中的线速度矢量。</returns>
        public Vector2 GetLinearVelocityFromWorldPoint(ref Vector2 worldPoint)
        {
            return LinearVelocityInternal +
                   new Vector2(-AngularVelocityInternal * (worldPoint.Y - Sweep.C.Y),
                               AngularVelocityInternal * (worldPoint.X - Sweep.C.X));
        }

        /// <summary>
        /// 获取世界空间中的线速度矢量。
        /// </summary>
        /// <param name="localPoint">本地空间中的点位置。</param>
        /// <returns>点在世界空间中的线速度矢量。</returns>
        public Vector2 GetLinearVelocityFromLocalPoint(Vector2 localPoint)
        {
            return GetLinearVelocityFromLocalPoint(ref localPoint);
        }

        /// <summary>
        /// 获取世界空间中的线速度矢量。
        /// </summary>
        /// <param name="localPoint">本地空间中的点位置。</param>
        /// <returns>点在世界空间中的线速度矢量。</returns>
        public Vector2 GetLinearVelocityFromLocalPoint(ref Vector2 localPoint)
        {
            return GetLinearVelocityFromWorldPoint(GetWorldPoint(ref localPoint));
        }     

        /// <summary>
        /// Sweep变化后同步改变Transform中的值
        /// </summary>
        internal void SynchronizeTransform()
        {
            // 改变Transform中的旋转量
            Xf.RMatrix.Set(Sweep.A);
            
            // 改变Transform中的原点位置
            float vx = Xf.RMatrix.Col1.X * Sweep.LocalCenter.X + Xf.RMatrix.Col2.X * Sweep.LocalCenter.Y;
            float vy = Xf.RMatrix.Col1.Y * Sweep.LocalCenter.X + Xf.RMatrix.Col2.Y * Sweep.LocalCenter.Y;
            Xf.Position.X = Sweep.C.X - vx;
            Xf.Position.Y = Sweep.C.Y - vy;
        }

        #endregion      

        #region IDisposable Members

        public bool IsDisposed { get; set; }

        public void Dispose()
        {
            if (!IsDisposed)
            {

                World.RemoveBody(this);
                IsDisposed = true;
                GC.SuppressFinalize(this);
            }
        }

        #endregion        
    }
}

相比前一个版本的Body类,主要有以下变化:

1.添加了BodyType枚举

如果Body的类型只有静态和动态两种的话,那么使用bool值IsStatic就完全够用了,设置BodyType是多此一举,但是以后还会添加第三种状态,所以需要这个枚举。

2.添加了BodyFlags标志

如果一个类中有很多bool变量,比较好的组织形式是创建一个位标志变量。一个位标志中的一个位有0和1两种状态,正好对应true和false。以BodyFlag中的IgnoreGravity位为例,代码IgnoreGravity = (1 << 1)表示IgnoreGravity对应的是BodyFlag变量的倒数第二个位,要将IgnoreGravity位设置为1,应该使用运算:

Flags |= BodyFlags.IgnoreGravity; 

要将IgnoreGravity位设置为0,应该使用与非运算:

Flags &= ~BodyFlags.IgnoreGravity; 

要判断IgnoreGravity位是1还是0,可使用以下代码,如果为1则返回true:

(Flags & BodyFlags.IgnoreGravity) == BodyFlags.IgnoreGravity 

这篇文章XNA中的位标志也可供参考。

3.速度和位移的积分运算移至World类中进行处理

这样做可以提高运行速度,在这篇文章http://www.sgtconker.com/2011/05/high-end-performance-optimizations-on-the-xbox-360-and-windows-phone-7/中Manually inlining high frequency提及了这个技巧。

4.世界坐标和本地坐标的变换

首先需要清楚一个事实,即Body的原点O和质心C不一定在同一个点上。原点O是Body的旋转中心,而质心C是由Body的形状和质量分布决定的。例如,一个64×32大小的质量分布均匀的矩形,我们指定它的左上角为转轴,即原点O位于左上角,它的本地坐标为(0,0);而质心C在板的几何中心上,即本地坐标为(32,16),如下图所示。

图2 世界坐标和本地坐标的转换

图2 世界坐标和本地坐标的转换

现在我们将这个矩形放置在世界坐标系中的(32,32),并顺时针旋转30度,此时这个Body的Transform结构中的Position应为(32,32),旋转矩阵RMatrix应为

旋转矩阵

如果我想获得质心在世界空间中的坐标,应该调用GetWorldPoint方法,其代码为:

/// 
/// 给定本地空间中的点,返回世界空间中的点。
/// 
/// 本地空间中的点。</param name="localPoint" > 
/// 世界空间中的点。
public Vector2 GetWorldPoint(ref Vector2 localPoint) 
{
    return new Vector2(Xf.Position.X + Xf.RMatrix.Col1.X * localPoint.X + Xf.RMatrix.Col2.X * localPoint.Y,Xf.Position.Y + Xf.RMatrix.Col1.Y * localPoint.X + Xf.RMatrix.Col2.Y * localPoint.Y); }

即新坐标为(32+32cos30°-16sin30°,32+32sin30°+16cos30°)=(51.72,,61.86)。

其他的用于坐标变换的代码原理类似不再赘述。

5.转动惯量的平行轴定理

在创建Body时,我们会在下面提及的BodyFactory类中自动计算物体相对于质心的转动惯量。但从图2可以看到,转轴不一定与质心重合,此时的转动惯量就要发生变化。

相关物理知识告诉我们:若有任一轴与过质心的轴平行,且该轴与过质心的轴相距为d,刚体对其转动惯量为I,则有:

I=IC+md2

其中IC表示相对通过质心的轴的转动惯量。

这个定理称为平行轴定理

对于图2的情况,若转轴在质心C处时转动惯量为m(642+322)/12,而当转轴变为左上角O时,转动惯量变为m(642+322)/12+m(322+162)。这也是Body类中的Inertia属性代码的原理:

/// <summary>
/// 获取或设置body相对于本地坐标原点的转动惯量,单位为kg-m^2。
/// </summary>
public float Inertia 
{
    get { return _inertia + Mass * Vector2.Dot(Sweep.LocalCenter, Sweep.LocalCenter); }
    set 
    { 
        if (_bodyType != BodyType.Dynamic) 
            return; 
        if (value > 0.0f)
        {
            _inertia = value - Mass * Vector2.Dot(LocalCenter, LocalCenter); 
            InvInertia = 1.0f / _inertia;
        }
    }
}

BodyFactory类的变化

在很多游戏环境中,根据形状密度计算质量是很有意义的,这能确保物体有合理和一致的质量,通常我们将物体的密度设为相同的值,这样大的物体就有大的质量,非常直观。当然,在某些情况中,你也可以手动设置质量。因此工厂方法传入的参数由质量mass变为密度density,创建矩形Body的代码如下:

public static Body CreateRectangle(World world, float width, float height, float density, Vector2 position) 
{
    if (width <= 0)
        throw new ArgumentOutOfRangeException("width", "矩形的宽不得小于0");
    if (height <= 0)
        throw new ArgumentOutOfRangeException("height", "矩形的高不得小于0"); 
    if(density <=0)
        throw new ArgumentOutOfRangeException("height", "密度不得小于0");
        
    Body body = CreateBody(world, position);
    float area = width * height;
    body.Mass = density * area; 
    body.Inertia = body.Mass * (width * width + height * height) / 12; 
    return body; 
}

World类

将PhysicsSimulator类改名为World,将此类中的Update方法改名为Step,完整代码如下:

using System.Collections.Generic; 
using Microsoft.Xna.Framework;
using Stun2DPhysics4SL.Common;

namespace Stun2DPhysics4SL.Dynamics
{
    public class World
    {
        // 添加或移除的Body集合,在World的每次Step之前调用, 
        // 在World的Body集合中添加或移除这两个集合中的对象。 
        private HashSet<Body> _bodyAddList = new HashSet<Body>(); 
        private HashSet<Body> _bodyRemoveList = new HashSet<Body>();
        
        /// <summary> 
        /// 如果设置为false,整个模拟器就会停止。 
        /// </summary> 
        public bool Enabled = true;
        
        // <summary> 
        /// 创建一个新<see cref="World"/>类对象。
        /// </summary> 
        private World() 
        {
            BodyList = new List<Body>(32); 
        }
       
        /// <summary>
        /// 创建一个新<see cref="World"/>类对象。
        /// </summary> 
        /// <param name="gravity">重力加速度矢量。</param> 
        public World(Vector2 gravity) : this() 
        {
            Gravity = gravity;
        }
        
        /// <summary>
        /// 所有Body共享的重力加速度矢量。 
        /// </summary>
        public Vector2 Gravity; 
        
        /// <summary> 
        /// World中的Body集合。 
        /// </summary> 
        public List<Body> BodyList { get; private set; }
        
        /// <summary> 
        /// 在World中添加一个Body。
        /// </summary> 
        internal void AddBody(Body body) 
        {
            if (!_bodyAddList.Contains(body)) 
                 _bodyAddList.Add(body); 
        }
        
        /// <summary>
        /// 在World中移除一个Body。 
        /// </summary> 
        /// <param name="body">要移除的Body。</param> 
        public void RemoveBody(Body body) 
        { 
            if (!_bodyRemoveList.Contains(body)) 
                _bodyRemoveList.Add(body); 
        }
        
        /// <summary> 
        /// 在每次World的步进中缓存要添加或移除的Body对象。
        /// 然后在每次更新前调用这个方法处理添加和移除的Body对象。 
        /// </summary> 
        public void ProcessChanges() 
        {
            ProcessAddedBodies(); 
            ProcessRemovedBodies();
        }
         
         // 将_bodyAddList集合中的Body对象添加到World的Body集合中
         private void ProcessAddedBodies()
         {
             if (_bodyAddList.Count > 0)
             {
                 foreach (Body body in _bodyAddList)
                 {
                     // 添加到World的Body集合中。 
                     BodyList.Add(body);
                 }
                 _bodyAddList.Clear();
             }
         }
         
         // 从World的Body集合中移除_bodyRemoveList中的Body对象 
         private void ProcessRemovedBodies()
         {
             if (_bodyRemoveList.Count > 0)
             {
                 foreach (Body body in _bodyRemoveList)
                 {
                     // 从World的Body集合中移除Body对象
                     BodyList.Remove(body); 
                 }
                 _bodyRemoveList.Clear(); 
             }
         }
         
         /// <summary> 
         /// 以一个时间步进为参数,处理Body集合中所有对象的积分运算。
         /// </summary>
         /// <param name="dt">物理模拟的时间间隔。</param> 
         public void Step(float dt) 
         {
              ProcessChanges();
              if (dt == 0 || !Enabled)
              {
                  return;
              }
                
              // 对速度进行积分运算, 然后对位置进行积分计算。 
              Solve(dt);
              ClearForces();
          }
            
          /// <summary>
          /// 每次调用Step方法之后需要调用这个方法将力和力矩设置为零。 
          /// </summary>
          public void ClearForces() 
          {
              for (int i = 0; i < BodyList.Count; i++)
              {
                  Body body = BodyList[i];
                  body.Force = Vector2.Zero;
                  body.Torque = 0.0f; 
              }
          }
          
          private void Solve(float dt)
          {
              // 遍历Body数组
              for (int i = 0; i < BodyList.Count; i++)
              {
                  Body b = BodyList[i];
                  if (b.BodyType != BodyType.Dynamic)
                  {
                       continue;
                   }
                   
                   // 对速度进行积分运算,根据Body的IgnoreGravity属性决定是否添加重力的影响
                   if (b.IgnoreGravity) 
                   {
                       b.LinearVelocityInternal.X += dt * (b.InvMass * b.Force.X);
                       b.LinearVelocityInternal.Y += dt * (b.InvMass * b.Force.Y);
                       b.AngularVelocityInternal += dt * b.InvInertia * b.Torque;
                   }
                   else
                   {
                       b.LinearVelocityInternal.X += dt * (Gravity.X + b.InvMass * b.Force.X); 
                       b.LinearVelocityInternal.Y += dt * (Gravity.Y + b.InvMass * b.Force.Y); 
                       b.AngularVelocityInternal += dt * b.InvInertia * b.Torque; 
                   }
                   
                   // 施加影响线速度和角速度的阻力。
                   // 动力学方程: dv/dt + c * v = 0 
                   // 解方程: v(t) = v0 * exp(-c * t) 
                   // 每个时间步进: v(t + dt) = v0 * exp(-c * (t + dt)) = v0 * exp(-c * t) * exp(-c * dt) = v * exp(-c * dt) 
                   // v2 = exp(-c * dt) * v1 
                   // 泰勒级数展开: // v2 = (1.0f - c * dt) * v1 
                   b.LinearVelocityInternal *= MathUtils.Clamp(1.0f - dt * b.LinearDamping, 0.0f, 1.0f); 
                   b.AngularVelocityInternal *= MathUtils.Clamp(1.0f - dt * b.AngularDamping, 0.0f, 1.0f); 
                   
                   // 对位置进行积分运算 
                   b.Sweep.C.X += dt * b.LinearVelocityInternal.X; 
                   b.Sweep.C.Y += dt * b.LinearVelocityInternal.Y; 
                   b.Sweep.A += dt * b.AngularVelocityInternal; 
                   
                   // 将改变同步到Transform 
                   b.SynchronizeTransform(); 
               }
           }
       }
   }

1._bodyAddList和_bodyRemoveList的作用

当在Step()方法中进行物理计算时,如果此时你在Body集合中添加或移除一个Body对象,就有可能会引发冲突,因此额外还需要两个HashSet集合保存要添加或移除的Body对象,在每次Step()的一开始都会调用ProcessChanges()方法就缓存在集合中的Body变更到BodyList集合中,这样就避免了程序冲突。

2.阻尼代码的解释

在Step()方法中我们不仅对Body集合中的每个集合对象进行了速度和位置的积分运算,而且还添加了阻力效果。

首先在Body类中添加了线性阻尼LinearDamping和角度阻尼系数AngularDamping。对于线性运动来说,物理知识告诉我们,物体在运动过程中受到的空气阻力在速度较小时与v成正比(斯托克斯公式),在速度较大时与v2成正比,为了简化问题,我们取与v成正比的情况。有微分方程:

dv/dt =-Cv

解得:

vt=v0•e-ct

然后进行泰勒级数展开: vt=v(1-ct-c2t2/2 +……)

忽略高次项,取vt=v(1-ct)。最后将(1-ct)限制在[0,1]之间。角度阻尼的算法是一样的。

Silverlight代码

Silverlight中的代码变化不大,添加了一些代码可以指定控件的初始大小,自己看源代码吧,偷懒不写了。

点击数字键1、2、3可以分别让三个矩形发生旋转,而且由于阻尼的作用,旋转一段时间就会停止。

文件下载(已下载 1048 次)

发布时间:2011/7/6 上午12:59:24  阅读次数:6120

2006 - 2024,推荐分辨率 1024*768 以上,推荐浏览器 Chrome、Edge 等现代浏览器,截止 2021 年 12 月 5 日的访问次数:1872 万 9823 站长邮箱

沪 ICP 备 18037240 号-1

沪公网安备 31011002002865 号