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 旋转矩阵的定义
即对象的本地坐标系(红色)相对于世界坐标系(黑色)顺时针旋转了θ,此时红色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 世界坐标和本地坐标的转换
现在我们将这个矩形放置在世界坐标系中的(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 阅读次数:6698
