Box2D DebugDraw 与 MFC
在 MFC 中实现 Box2D DebugDraw 函数
引言
Box2d 是一个 2D 物理引擎。它真实地模拟了二维空间中移动和碰撞的刚体之间的相互作用。Box2D 完成了描述盒子、球体和多边形在虚拟世界中移动、碰撞和弹跳所需的数学计算。但是 Box2D 不会绘制任何东西。要将这些信息转换为游戏,您必须从 Box2d 获取信息,将其映射到可用于绘制的坐标,然后绘制您的世界。
Box2D 使用浮点数学,并以 MKS(米、千克、秒)定义对象。它适用于大小从几厘米到几米左右的物体。它并非真正用于模拟航空
母舰或分子。这意味着您将在 Box2d 中创建一个世界,使其运动起来,然后缩放和平移(转换)Box2D 信息,使其适合用于绘制游戏的像素信息。
Box2D 拥有一个非常有用的工具,即 DebugDraw
功能。通过向 box2D 提供少量简单的绘图例程,您可以快速启动并运行一个原型,该原型将 Box2D
对象显示为简单的矢量图。DebugDraw
也非常方便查看 Box2D
内部究竟发生了什么。即使您拥有一个华丽的图形前端,在调试时打开 debugDraw
也可能很方便。
本项目展示了如何使用 C++ 和 GDI+ 在 MFC 窗体中设置一个非常简单的 box2d
世界,并使用 DebugDraw
查看该世界。
有关出色的 Box2D
教程,请参阅 此链接。
安装
(首先要做的是从 这里 下载并编译 Box2D
库。对于我们的项目,您需要在编译前对 Box2D
项目的属性进行一些更改
BOX2D
项目- 配置属性
- 左键单击 gadget 并拖动以移动它。左键单击 gadget 的右下角并拖动以调整其大小。右键单击 gadget 以访问其属性。
- 配置属性
- MFC 的使用:在共享 DLL 中使用
- 字符集:使用 Unicode 字符集
这些设置假设您将使用 Unicode 将 Box2D
与 MFC 项目链接。MFC 和字符集选项匹配非常重要,否则您将收到大量链接错误。现在我们可以开始我们的项目了。启动 Visual Studio 并创建一个新的 C++ MFC 应用程序项目(或下载我的项目)。首先要做的是告诉我们的项目我们正在使用 Box2d.lib
- 打开项目配置属性
- 检查我们的新项目默认在共享 DLL 中使用 Unicode 和 MFC
- 打开链接器部分
- 点击输入
- 编辑“附加依赖项”
- 添加 Box2d.lib(不要添加路径)
- 打开 VC++ 目录
- 编辑“包含目录”
- 添加
Box2D
库路径,例如- C:\Users\somename\Documents\Visual Studio 20xx\Projects\box2d
- 添加 Box2d.lib(不要添加路径)
- 编辑“库目录”
- 添加
Box2D
路径,例如- C:\Users\somename\Documents\Visual Studio 2010\Projects\box2d\Build\vs20xx\bin\Debug
- 请注意,您必须将调试版本添加到您的调试配置中,并将发布版本添加到您的发布版本中。
- 添加
这解决了项目配置问题。接下来,我们必须添加 GDU+ 所需的代码。
将此代码添加到 stdafx.h
#include <gdiplus.h>
using namespace Gdiplus;
#pragma comment(lib, "gdiplus.lib")
在您的对话框 InitInstance
函数中,添加
// gdi plus required startup code
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
ExitInstance
通常不会默认创建,但您必须添加一个以关闭 GDI+。
// must add this override to shutdown gdiplus
int Cbox2DTestApp::ExitInstance()
{
// required gdiplus shutdown code
GdiplusShutdown(gdiplusToken);
return CWinApp::ExitInstance();
}
最后,将此添加到 yourApp::CWinApp
类定义中的 public
部分
// gdi stuff, required for startup and shutdown
GdiplusStartupInput gdiplusStartupInput;
ULONG_PTR gdiplusToken;
这样就完成了内务管理。
Using the Code
现在是代码部分。要实现 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
可以以简单的调试绘图模式绘制任何世界。所以我们所要做的就是用 GDI+ 编写这六个函数。编写这六个函数是直截了当的;问题是 Box2D
将发送 box2D
世界坐标,我们必须将它们转换为 Windows 像素坐标。我们的大部分代码将用于设置转换,这将允许 GDI+ 正确绘制 Box2D
数据。
Box2D
坐标以米为单位,x
从左到右递增,y
向上递增。GDI+ 坐标以像素为单位,x
从左到右递增,但 y
向下递增。
我们将使用的技术是获取 GDI+ 窗口的大小、Box2D
世界的大小,并设置一个我们可以传递给 graphics.SetTransform()
函数的转换矩阵。一旦我们这样做了,转换将处理所有坐标映射,并且编写这六个图形将变得微不足道。
获取 GDI+ 窗口很简单
RECT r;
this->GetClientRect(&r);
获取 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->bottom=(long )minY;
w->top=(long )maxY;
}
转换!
既然我们知道所有事物的大小,我们就可以计算我们的转换
// r is the rect for the windows drawing window, w is the extent of the box2d world
// r is assumed to have y increasing down, w is assumed to have y increasing up
// r is in pixels, w is in metres
// you can use this function to select any part of the box2d world
// to scale instead of the whole thing, which is the default
void DebugDrawGDI::ScaleWorldCalculate(RECT *r, RECT *w)
{
int outputWidth = r->right - r->left;
int outputHeight = r->bottom - r->top;
int boundsWidth = w->right - w->left;
int boundsHeight = w->top - w->bottom;
// ratio of the windows size to the world size
scaleX = (float )outputWidth / (float )boundsWidth;
scaleY = (float )outputHeight / (float )boundsHeight;
scale = scaleX > scaleY ? scaleY : scaleX;
// move things over if required
offsetX=r->left - (int )((float )w->left * scaleX);
offsetY=r->top - (int )((float )w->bottom * scaleY);
// used to flip the y values
yAdjust=r->bottom;
// make a transform matrix
matrixTransform.Reset();
// scale (-y as part of our y flip)
matrixTransform.Scale(scaleX, -scaleY, MatrixOrderAppend);
// translate (+yAdjust is part of the y flip)
matrixTransform.Translate((float )offsetX, (float )(yAdjust-offsetY), MatrixOrderAppend);
}
然后在我们的主绘图循环中,我们只需要设置转换,像这样
g->SetTransform(&matrixTransform);
其中“g
”是指向我们当前 Graphics
对象的指针。设置转换后,我们的绘图函数如下所示
/// Draw a solid closed polygon provided in CCW order.
void DebugDrawGDI::DrawSolidPolygon(const b2Vec2* vertices,
int32 vertexCount, const b2Color& color)
{
int i;
PointF *points=new PointF[vertexCount+1];
Color clr(255, (int )(color.r*255), (int )(color.g*255), (int )(color.b*255));
SolidBrush sb(clr);
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;
gdi->FillPolygon(&sb, points, vertexCount + 1);
delete points;
}
请注意,我们在绘图函数中不做任何缩放或映射。转换会为我们处理一切。
剩下的就是设置常用的 MFC 动画代码了。
我们必须添加一个计时器来触发我们的更新,并添加一个 on_paint
事件处理程序来绘制所有内容。
将这样的代码添加到 InitDialog
函数中以设置计时器
// animation timer, note box2d will not be realistic if you run the timer too slow
// 16 is about 60Hz, 32 is about 30Hz
timerMilliseconds=16;
// make the box2d world run in sync with our timer
stepSeconds=(float )timerMilliseconds / 1000.0f;
SetTimer(1234567890, timerMilliseconds, NULL);
同样,创建 Box2D
世界、DebugDraw
类实例等的代码也进入 InitInstance
。
所有逻辑代码都进入 On_Timer
事件处理程序,所有绘图代码都进入 On_Paint
事件处理程序。下载项目并查看代码以了解其余细节。
碰撞
如果 Box2D
能告诉您何时发生碰撞,那不是很好吗?幸运的是,它确实可以,而且您不必每帧都查看世界上的每个人,您可以通过回调函数收到碰撞通知。这是碰撞监听器类的定义
// the collision callback class
class MyContactListener : public b2ContactListener
{
public:
void BeginContact(b2Contact* contact);
void EndContact(b2Contact* contact);
void PreSolve(b2Contact* contact, const b2Manifold* oldManifold);
void PostSolve(b2Contact* contact, const b2ContactImpulse* impulse);
};
我们的简单示例代码只实现了 BeginContact
并保存了一些数据,以便每次下落的盒子弹起(碰撞)时我们都可以绘制一个“爆炸”。我们希望保存一些关于碰撞的数据
// some data about a collision,
// and the simple explosion we will draw
// at the collision point
typedef struct
{
int id;
int isContact;
float x, y;
float r;
int countDown;
}
t_bodyData;
在 InitDialog
中,我们告诉 Box2D
使用我们的回调。“world
”是我们创建的 Box2D
对象。
// setup the collision callback world->SetContactListener(ContactListener);
以及一些简单的代码来查看发生了什么碰撞,以及我们是否对碰撞感兴趣。
// the box2d callback function. boxd will call this if there is a collision
void MyContactListener::BeginContact(b2Contact* contact)
{
// let's see which object is our ball.
// A & B collided, our ball could be either
t_bodyData *bd;
bd=(t_bodyData *)(contact->GetFixtureA()->GetBody()->GetUserData());
if ( bd && bd->id == BALL_ID )
{
// this is our ball, save some data about where it collided
bd->isContact=true;
bd->r=5;
b2Vec2 v;
v=contact->GetFixtureA()->GetBody()->GetPosition();
bd->x=v.x;
bd->y=v.y;
bd->countDown=30;
}
else
{
bd=(t_bodyData *)(contact->GetFixtureB()->GetBody()->GetUserData());
if ( bd && bd->id == BALL_ID )
{
// this is our ball, save some data about where it collided
bd->isContact=true;
bd->r=1.5;
b2Vec2 v;
v=contact->GetFixtureB()->GetBody()->GetPosition();
bd->x=v.x;
bd->y=v.y;
bd->countDown=30;
}
}
}
然后我们将代码添加到计时器事件处理程序中以动画我们的爆炸,并绘制我们的爆炸,它只是一个膨胀和收缩的圆圈。这涵盖了本项目的重点。下载完整的项目,以使用一个非常简单但完整的 MFC 下的 GDI+ Box2D
DebugDraw
实现。
在盒子上绘制位图
您可能希望在 box2d
项目上绘制位图。此代码展示了基本技术。在初始化代码中,保存一个名为 b2box
的盒子体的指针。然后将此代码添加到 MainDraw()
。
在 CreateBox2dWorld
中
// a falling box
...
def.type=b2_dynamicBody;
def.position.Set(0.0, 45.0);
b2box=body=world->CreateBody(&def);
...
在 Maindraw
中,在顶部添加变量声明,其余代码紧随 world->DrawDebugData();
之后
Bitmap *boxImage;
Matrix m;
boxImage=new Bitmap(_T("test.bmp"));
// draw the bitmap on top of the box --- ---
PointF p;
float angle=b2box->GetAngle();
b2Vec2 pos=b2box->GetPosition();
// move our bitmap to where the box is and rotate it to match
angle*=-RADTODEGREES; // negative so it rotates the right direction
p.X=DebugDraw->ScaleXF(pos.x);
p.Y=DebugDraw->ScaleYF(pos.y);
p.X+=boxImage->GetWidth()/2; // make rotation in centre of our box
p.Y+=boxImage->GetHeight()/2;
m.Reset();
// box2d position is at the center of its object, move our object to match
m.Translate((float )(-((int)boxImage->GetWidth())/2),
(float )(-((int )boxImage->GetHeight())/2));
// rotate to match box2d
m.RotateAt(angle, p);
graphics->SetTransform(&m);
// move the point back because the translation handles that adjustment
p.X-=boxImage->GetWidth()/2;
p.Y-=boxImage->GetHeight()/2;
graphics->DrawImage(boxImage, p);
后续步骤
它闪烁得很厉害。有标准的技巧可以避免闪烁,它们可以用来阻止闪烁。我不确定对话框是否最适合游戏,但它很容易实现。为全屏游戏设置一个普通的 Win32 应用程序可能会更好。还有 Box2D
库的 C# 移植,您可以与 XNA 一起使用。
链接器错误
如果遇到链接器错误,请确保您使用与项目相同的设置(语言、MFC 和 ATL)编译了 box2d
项目。详细信息在文章中,但我再次提及以防您下载所有内容并尝试编译。默认的 box2d
项目与默认的 MFC 项目具有不同的设置。
历史
- 初始版本