Box2D 和 Direct2D
使用 DirectX 2D 和 win32 实现的 Box2D DebugDraw
引言
点击此处获取 GDI+ 版本。GDI+ 版本.
Box2d 是一个 2D 物理引擎。它能真实地模拟二维世界中运动和碰撞的刚体之间的交互。Box2D 会进行计算,以描绘盒子、球体和多边形在一个虚拟世界中移动、碰撞和反弹。但是 Box2D 本身不绘制任何东西。要将这些信息转化为游戏,你需要从 Box2d 获取信息,将其映射到可用于绘制的坐标,然后绘制你的世界。
Box2D 使用浮点数数学,并以 MKS(米、千克、秒)为单位定义对象。它适用于尺寸从几厘米到几米的物体。它并不适用于模拟航空母舰或分子。这意味着你将在 Box2d 中创建一个世界,使其运动起来,然后缩放和平移(变换)Box2D 信息,使其适合用于绘制游戏。
Box2D 的一个非常有用的工具是 DebugDraw(调试绘制)功能。通过提供 handful of simple drawing routines(一些简单的绘图例程)给 box2d,你可以快速搭建一个原型,显示 Box2D 对象作为简单的矢量图形。DebugDraw 还有助于查看 Box2D 内部发生的具体情况。即使你有一个漂亮的图形前端,在调试时开启 DebugDraw 也会很有用。
本项目展示了如何在裸 Win32 DirectX 2D 程序中设置一个非常简单的 box2d 世界,并使用 DebugDraw 来查看这个世界。获取优秀的 Box2D 教程,请点击此处。
安装
首先,你需要从 http://box2d.org/. 下载并编译 Box2D 库。对于我们的项目,在编译之前,你需要在 Box2D 项目的属性中进行一些更改。
- BOX2D
- 配置属性
- 通用
- 字符集:使用 Unicode 字符集
这些设置假定你将把 Box2D 与使用 Unicode 的 win32 项目链接。确保字符集选项匹配,否则你会遇到大量链接器错误,这一点非常重要。
现在我们可以开始我们的项目了。启动 visual studio 并创建一个新的 Win32 应用程序项目(或者下载我的项目)。选择 C++/Win32/Win32 Project。确保设置项目名称。首先要做的是告诉我们的项目我们正在使用 Box2d.lib。
- 打开项目配置属性
- 检查我们的新项目是否默认使用
- Unicode
- 打开 Linker(链接器)部分
- 点击 Input(输入)
- Additional Dependancies"(附加依赖项)"
- 添加 Box2d.lib(不要添加路径)
- 打开 VC++ directories(VC++ 目录)
- 编辑 "Include Directories"(包含目录)
- 添加 Box2D 库路径,例如:C:\Users\somename\Documents\Visual Studio 20xx\Projects\box2d
- 编辑 "Library directories"(库目录)
- 添加 Box2D 路径,例如:C:\Users\somename\Documents\Visual Studio 2010\Projects\box2d\Build\vs20xx\bin\Debug
请注意,你必须将调试版本添加到调试配置中,并将发布版本添加到发布版本中。
项目配置就完成了。接下来,我们需要添加 DirectX 2D 所需的头文件。在 stdafx.h 的底部添加:
#include <d2d1.h>
#include <Box2D/Box2D.h>
在你的主 cpp 文件中,添加以下代码,告诉编译器你正在使用 DirectX 2D:
#pragma comment(lib, "d2d1")
这样就完成了所有准备工作。
代码
现在转向代码。要实现 Box2D DebugDraw
,你需要实现一个基于 Box2d 类 b2draw 的类。这是必须实现的最少部分:
class DebugDrawGDI : public b2Draw
{
public:
DebugDrawGDI();
~DebugDrawGDI();
// these are the box2d virtual functions we have to implement
/// Draw a closed polygon provided in CCW order.
virtual void DrawPolygon(const b2Vec2* vertices, int32 vertexCount, const b2Color& color);
/// Draw a solid closed polygon provided in CCW order.
virtual void DrawSolidPolygon(const b2Vec2* vertices, int32 vertexCount, const b2Color& color);
/// Draw a circle.
virtual void DrawCircle(const b2Vec2& center, float32 radius, const b2Color& color);
/// Draw a solid circle.
virtual void DrawSolidCircle(const b2Vec2& center, float32 radius,
const b2Vec2& axis, const b2Color& color);
/// Draw a line segment.
virtual void DrawSegment(const b2Vec2& p1, const b2Vec2& p2, const b2Color& color);
/// Draw a transform. Choose your own length scale.
/// @param xf a transform.
virtual void DrawTransform(const b2Transform& xf);
};
通过实现这六个绘图函数,Box2D 可以在简单的调试绘制模式下绘制任何世界。所以我们只需要用 DirectX 2D 来编写这六个函数。编写这六个函数很简单;但棘手之处在于 Box2D 将传入 box2D 世界坐标,而我们需要将它们转换为窗口像素坐标。我们的大部分代码将用于设置变换矩阵,以便将该矩阵传递给 renderTarget->SetTransform ()
函数。一旦我们这样做了,变换矩阵就会处理所有的坐标映射,编写六个图形函数就会变得微不足道。
Box2D 坐标单位是米,x 轴向右增加,y 轴向上增加。DirectX 2D 坐标单位是像素,x 轴向右增加,但 y 轴向下增加。
我们将采用的技术是获取 DirectX 2D 窗口的大小、Box2D 世界的大小,然后设置一个变换矩阵,我们可以将其传递给 renderTarget->SetTransform ()
函数。一旦我们做到这一点,变换矩阵就会处理所有的坐标映射,编写六个图形函数就变得非常简单了。
获取 win32 窗口大小需要主窗口的 HWND
。为了方便起见,我添加了一个全局变量来保存它:
HWND hWndGlobal; // we will save the hwnd here for future use
然后,在 InitInstance()
中,在调用 ShowWindow()
之前添加这一行:
hWndGlobal=hWnd; // save a copy, it's handy to know
现在我们可以在 _twinMain() 中通过以下方式获取窗口大小:
RECT rect;
GetClientRect(hWndGlobal, &rect);
获取 BoxD2 世界大小需要更多工作。首先,你需要创建一个 Box2D 世界,向其中添加一些“刚体”,然后查询“世界”以了解其大小。为了测试,我创建了一个带有矩形地面、屋顶和墙壁的世界,并在这些墙壁内添加了几个动态对象。一旦我们创建了世界,我们就可以调用一些 Box2d 函数来遍历所有刚体并获取世界大小。
// set w to the box2D world AABB
// use this to help scale/transform our world
void DebugDrawGDI::GetBoundBox2DBounds(RECT *w, b2World *world)
{
// iterate over ALL the bodies, and set the w to max/min
b2Body *b;
b2Fixture *fix;
b2AABB bound;
float minX, maxX, minY, maxY;
minX=minY=1000000.0;
maxX=maxY=-1000000.0;
b=world->GetBodyList();
while ( b )
{
fix=b->GetFixtureList();
while ( fix )
{
bound=fix->GetAABB(0);
if ( bound.lowerBound.x < minX )
minX=bound.lowerBound.x;
if ( bound.upperBound.x > maxX )
maxX=bound.upperBound.x;
if ( bound.lowerBound.y < minY )
minY=bound.lowerBound.y;
if ( bound.upperBound.y > maxY )
maxY=bound.upperBound.y;
fix=fix->GetNext();
}
b=b->GetNext();
}
maxX+=2.0;
maxY+=2.0;
minX-=2.0;
minY-=2.0;
w->left=(long )minX;
w->right=(long )maxX;
w->top=(long )maxY;
w->bottom=(long )minY;
}
现在我们知道了所有的大小,就可以计算我们的变换矩阵了。
// set w to the box2D world AABB
// use this to help scale/transform our world
void DebugDrawGDI::GetBoundBox2DBounds(RECT *w, b2World *world)
{
// iterate over ALL the bodies, and set the w to max/min
b2Body *b;
b2Fixture *fix;
b2AABB bound;
float minX, maxX, minY, maxY;
minX=minY=1000000.0;
maxX=maxY=-1000000.0;
b=world->GetBodyList();
while ( b )
{
fix=b->GetFixtureList();
while ( fix )
{
bound=fix->GetAABB(0);
if ( bound.lowerBound.x < minX )
minX=bound.lowerBound.x;
if ( bound.upperBound.x > maxX )
maxX=bound.upperBound.x;
if ( bound.lowerBound.y < minY )
minY=bound.lowerBound.y;
if ( bound.upperBound.y > maxY )
maxY=bound.upperBound.y;
fix=fix->GetNext();
}
b=b->GetNext();
}
maxX+=2.0;
maxY+=2.0;
minX-=2.0;
minY-=2.0;
w->left=(long )minX;
w->right=(long )maxX;
w->top=(long )maxY;
w->bottom=(long )minY;
}
然后在我们的主绘图循环中,我们只需要设置变换矩阵,如下所示:
renderTarget->SetTransform(matrixTransform);
其中 renderTarget
是指向我们当前 DirectX 2D 渲染工厂对象的指针。有了变换矩阵,我们的绘图函数看起来就像这样:
/// Draw a solid closed polygon provided in CCW order.
void DebugDrawGDI::DrawSolidPolygon(const b2Vec2* vertices, int32 vertexCount, const b2Color& color)
{
int i;
ID2D1PathGeometry *geo;
ID2D1GeometrySink *sink;
ID2D1SolidColorBrush *brush;
D2D1::ColorF dColor(color.r, color.g, color.b);
D2D1_POINT_2F *points=new D2D1_POINT_2F [vertexCount+1];
HRESULT hr;
// create a direct2d pathGeometry
hr=factory->CreatePathGeometry(&geo);
hr=geo->Open(&sink);
sink->SetFillMode(D2D1_FILL_MODE_WINDING);
// first point
sink->BeginFigure(D2D1::Point2F(vertices[0].x, vertices[0].y), D2D1_FIGURE_BEGIN_FILLED);
// middle points
vertices++;
vertexCount--;
for (i = 0; i < vertexCount; i++, vertices++)
{
points[i].x = vertices->x;
points[i].y = vertices->y;
}
points[vertexCount].x = points[0].x;
points[vertexCount].y = points[0].y;
sink->AddLines(points, vertexCount);
// close it
sink->EndFigure(D2D1_FIGURE_END_CLOSED);
sink->Close();
SafeRelease(&sink);
renderTarget->CreateSolidColorBrush(dColor, &brush);
renderTarget->FillGeometry(geo, brush);
delete points;
SafeRelease(&geo);
}
请注意,我们在绘图函数中没有进行任何缩放或映射。变换矩阵为我们处理了所有这些。剩下的是设置一些动画和计时代码。我们需要更改默认的消息循环,使其不仅仅等待消息。如果你的绘图代码只在按下某个键时调用,那么动画效果不会太多。使用这段代码来处理消息:
// prime the message structure
PeekMessage( &msg, NULL, 0, 0, PM_NOREMOVE);
// run till completed
while (msg.message!=WM_QUIT)
{
// is there a message to process?
if (PeekMessage( &msg, NULL, 0, 0, PM_REMOVE))
{
// dispatch the message
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
else
{
// no windows messages, do our game stuff
if ( ajrMain.MainLogic() )
{
ajrMain.MainDraw();
}
}
}
ajrMain.MainLogic()
负责所有对象的移动和计时,而 ajMain.MainDraw()
负责绘制当前世界。
我创建了一个新类来处理所有的 DirectX 2D 相关事务、计时和逻辑。我在消息循环上方添加了这段代码来创建和初始化该对象:
CAjrMain ajrMain(&rect, hWndGlobal); // my main class to do everything ajrMain.CreateBox2dWorld();
CAjrMain 类包含初始化 DirectX 2D、加载位图、执行简单游戏逻辑和计时等代码。我将它放在一个单独的 cpp 文件中。我使用了 QueryPerformanceCounter();
来为动画计时。
此外,创建 Box2D 世界、DebugDraw
类实例等的代码也包含在 CAjrMain
类中。
有关简单的碰撞检测代码,请参阅 GDI+ 版本(文章顶部有链接)。Box2d 使用回调函数支持非常好的碰撞检测。
历史
- 初始版本。