HGE / Box2D 编程注意事项

记录 HGE / Box2D 编程中可能碰到的问题及解决办法。

最后更新 2014.2.9

引言

Haaf's Game Engine 是一个在国内非常流行的简单2D游戏引擎。
Box2D 是著名的2D物理模拟引擎。

(我所了解的) HGE 现状

HGE 在2008年以后官网就没有发布过新的版本了,但很神奇的是最近我有在网上看到一个 HGE 相关的消息,HGE 被移植到了 Mac 和 Linux 平台。文章里面提到作者是因为要将游戏 Hammerfight 移植到这些平台,而这个游戏也可能是采用 HGE 引擎中最为知名的一个。但从现在来看,HGE 应该算已经“死了”。好消息是现在你有更多更棒的选择。

从最初学习 HGE 到后来接触到 Flixel,我感觉 HGE 与其说是一个完整的游戏引擎不如说是一个方便的图形,声音,IO库。他可以让你能够简单的控制这些东西,却没有帮助你组织代码,没有碰撞检测,Sprite动画,滚轴控制这些东西。说实话当你熟悉 2D 游戏制作,或者编程基础殷实的话这些都是小问题。而我感觉往往初学者最开始就是会在这些问题上受挫折,况且 HGE 使用的 C++ 说实话的确够复杂。相比而言,现在有譬如 Flixel 这样的库,在 HGE 所提供的这类功能基础上还增加了上面提到的,做 2D 游戏非常需要的一些功能,这些东西真的可以大幅减少你做一个完整的小游戏出来的难度。另一个方面,HGE 作者没有继续开发,其使用者也越来越少,这也意味着你碰到问题更难找到解决的方法。这里我会列出我知道的一些流行的简单 2D 游戏引擎来给大家做个参考。

  1. Flixel 这个就是上面一直提到的 Flixel,是一个基于 Flash 的游戏引擎。它功能绝对是够完备,你要是想做一个 2D 平台动作游戏的话那么基本上只要设定好地图和人在哪里,游戏看起来就基本成型了。Flixel 的使用也十分广泛,很容易就能找到示例游戏的代码,而且作者最近也开始频繁更新,网站也经过了重新设计。Flash 使用的是 ActionScript3,但其实就我感觉这东西跟 Java 几乎一摸一样,而且 Flixel 封装的非常好,你不需要了解 Flash 平台上的 API,只用看看 Flixel 的文档就行了。总之就是强烈推荐,用了就是人生的赢家。

  2. GameMaker8 GameMaker 应该也是挺有名的一个软件。老实说我仅仅只看过几篇教程,没有自己用过。但从 GameMaker 做出来的作品来看它一定是够棒的。比如去年非常流行的一个独立游戏 Super Crate Box,以及你应该听说过的那个超棒的沙罗曼蛇同人游戏Hydorah 就是用它做的。

如果你觉得 HGE 提供的功能就已经足够,那么跟它类似的还有跨平台的 SDL,以及使用 Lua 语言的 LÖVE2D。这些也都有着大量的用户,且有开发者开发维护。

这里再额外列出一些我不太熟悉的但是很流行的工具,你可以自己搜来看看是否符合你的需求:Unity3D, cocos2d-x, SFML, FlashPunk, OpenFL。中心思想就是 HGE 已经不行啦,你应该看向别处。

最后,希望这篇文章的内容还是能够帮到你,以上~

注意事项

  1. 用VS编译后的hge程序不能在其他机器上运行:
    首先先确定hge.dll和bass.dll还有其他相关文件是否在hge程序的目录 下或者其找得到的地方. 然后请确认项目属性中公共语言运行库是否为关闭 具体做法项目属性(project properties)->配置属性(configuration p roperties)->公共语言运行库(common language runtime support) 将该项关闭。
    其次请确认Release的运行时库为MT模式 具体做法在之前提到配置属性中->C/C++->代码生成(code genera tion)->运行时哭(runtime library)中修改。

  2. 使用hgeSprite类中的GetBoundingBox()时,注意其接受一个hgeRect 指针参数,将得到的sprite的boundingbox存储在该指针所指的对象中 因此,改指针指向的对象必须要先初始化后才能使用。 例如:

    m_boundingbox = new hgeRect();
    m_sprite = GetBoundingBox(m_posX, m_posY, m_boundingBox);
    
  3. hgeSprite中的SetHotSpot的功用是设定该sprite的热点(锚点)。其接 受的参数a,b代表改热点据sprite坐上角的相对横,纵距离。设置热点 是初始化一个sprite过程中的一部分。之后再对该sprite进行处理时其 坐标就是其热点坐标。例如:

    hgeSprite* spr = new hgeSprite(0, 0, 0, 20, 20)//20x20
    spr.SetHotSpot(10, 10);//Hot spot (10, 10) in center
    
  4. 当要将FrameFunc和RenderFunc封装到类里面时,注意用System_SetState 是无法将类成员函数直接设为hge的FrameFunc和RenderFunc 的。 因为类成员函数属于一个具体的对象,不能直接从类调用他。 例如:

    class GameState
    {
    public:
        bool FrameFunc();
        bool RenderFunc();
    }
    hge->System_SetState(HGE_FRAMEFUNC, 
                      GameState::FrameFunc);//ERROR
    hge->System_SetState(HGE_RENDERFUNC, 
                      GameState::RenderFunc);//ERROR
    

    代替的方法是将该类建立为Singleton,为其提供Static的FrameFunc 和RenderFunc来调用成员函数进行真正的工作。例如:

    class GameState
    {
    public:
        static instance;
        static bool FrameFunc() {
            return instance.doUpdate();
        }
        static bool RenderFunc() {
            return instance.doRender();
        }
    private:
        bool doUpdate();
        bool doRender();
    }
    
  5. 使用hge中Input系列函数时,区分GetKeyStateKeyDown的区别并 选择你需要的使用。KeyDown 用于侦测按键被按下的事件,适合于 监测单次按下键做出反应的情况。GetKeyState则是侦测某个键是否 被按下的状态。用于按下并保持按下按键进行操作情况比较合适。

  6. 我们一般用hge指针来操作hge。但是hge指针不需要是全局变量。由 于hge是Singleton,每次采用hgeCreate返回的指针都是指向同一个h ge类。所以任何时候你需要用到hge中的功能就能用hgeCreate 来得 到一个指向hge的指针。
    要记得使用release来释放hge。

  7. 尽量使用hgeResourceManager类。因为他它真的很好用。 如果没有使用他,那么注意在初始化texture/font时一定要确保hge 已经初始化过(System_Initiate已成功)。否则会产生运行时错误。

  8. 如果你将hgeSprite,hgeFont这些类封装在你自定义的类中,记得不 要把delete放在析构函数中。如果这样将导致游戏退出时出错。 将需要执行的delete操作提取出来放入一个函数中(如命名为Clear) 在合适的地方手动调用。 例如:

    if (hge->System_Initiate())
    {
        RenderUnit::instance();//singleton
        hge->System_Start();
        RenderUnit::instance().clearup();//clearup includes deletes
    }
    
  9. HGE的Color格式为0xAARGGBB
    其中A - alpha R -red G - green B - blue
    但事实上传统C++颜色值表示为
    0xBBGGRR
    记得不要弄错。

  10. HGE和Box2D混用时很容易采用一比一的单位, 即1个像素代表1个 Box2D中的一个单位。但这样是严重错误的。Box2D中1代表的单位 为1米。所以HGE中渲染一个30像素的正方形在Box2D中意味着一栋 楼房。Box2D在其文档中明确指出,Box2D只能模拟0,1至10米的物 体,对于超出范围以外的物体的物理模拟将会很不正常,一般表现 为速度慢,惯性大且难以加速到很大的速度。
    所以一定不能在HGE和Box2D直接采用一比一单位比。

  11. Box2D中如果为了给物体一个线性速度,比较好的方法是使用SetLi nearVelocity()而不是ApplyImpulse()来设置冲量。

  12. HGE和Box2D中获得的radius是相同的。所以下面语句能正确的将Bo x2D模拟的情况反应到屏幕上。

    objspr->RenderEx(dynbody->GetPosition().x, 
    dynbody->GetPosition().y, dynbody->GetAngle())
    

    但是务必注意10条中的内容,一比一的单位制必然导致灾难性结果。

  13. HGE中sprite的淡入淡出效果可以通过SetColor不断更改sprite的颜色 值来做到。 对于一个带有自定义的texture的sprite,将其颜色设置为0xFFFFFFF 在一般的BlendMode设定下为最标准的值。如果更改为0x88FFFFFF 则正好是半透明状态。以此类推0x00FFFFFF时则为完全透明。 下面给出一个简单的渐变色的类的例子。

    一个简单的办法是设置一个int的factor,将其乘以0x1000000对颜色 进行加减来控制其alpha值。 但要注意的是HGE中颜色值的类型是

    typedef unsigned long DWORD
    

    是非符号数。
    如果用下面这种方式进行操作必然导致错误。

    colorword -= factor*0x1000000;
    if (colorword < 0x00FFFFFF) {
        colorword = 0x00FFFFFF;
        return false;//fade finished
    }
    return true;//fading
    
    //由于colorword是非符号数,if判断支内语句永远无法执行可以改为下面的方法:
    
    DWORD delta = factor*0x1000000;
    if (colorword < delta) {
       colorword = 0x00FFFFFF;
       return false;
    } else {
       colorword -= delta;
       return true;
    }
    
  14. Box2D中一个Body的默认弹性系数是0,即不反弹。这个设定使得 在初期进行渲染测试时容易产生误解。 如果要设置一个Body使其能够反弹,首先检查其是否创建了Shape 然后看是否执行了SetMasses类语句,之后设置Restitution设置弹性 如果还是不能反弹则需检查该Body是否冻结(用IsFrozen()检查)

  15. 第10条中提到了不能在HGE和Box2D直接直接用一比一的单位制。 比较好的方法是预先建立你想要大小的Sprite,将Box2D模拟的结 果乘以系数加上偏移来渲染到屏幕上。 例如

    //建立一个Box2D中的物件,尺寸为5m x 5m正方形
    b2BodyDef dynBodyDef;
    b2PolygonDef dynPolyDef;
    dynPolyDef.SetAsBox(2.5f, 2.5f);
    dynbody = world->CreateBody(&dynBodyDef);
    dynbody->CreateShape(&dynPolyDef);
    dynbody->SetMassFromShapes();
    
    //根据上面Box3D中物件的尺寸创建Sprite,这里是1:10
    dynspr = new hgeSprite(dyntex, 0, 0, 50.0f, 50.0f);
    dynspr->SetHotSpot(25.0f, 25.0f);
    
    //渲染,计算坐标要计入全局的偏移量和缩放量
    dynspr->RenderEx(dynbody->GetPosition().x * GLOBSCALE
        + GLOBSHIFT, dynbody->GetPosition().y * GLOBSCALE 
        + GLOBSHIFT, dynbody->GetAngle());
    

    容易看出来对Body/Shape建立相应的Sprite时要用到Shape的尺寸 所以最好在Body的ShapeDef可见的地方就建立Sprite。

  16. 对于包装FrameFunc的类对象,慎用Singleton模式。 Singleton只要实例化后很难销毁重建。对于需要完成游戏重置的 情况会早成很大的麻烦。 含有RenderFunc的对象偶尔可以考虑用Singleton模式。 总的来说,程序进程中需要重置的对象最好都不要用Singleton。
    EDIT:
    后来发现对于这些对象还是尽量使用Singleton比较好 这样有一个比较可靠的可以全局取得的实例在 可以避免很多类似两个头文件需要互相包含的问题 对于重置问题 建议设置两个函数分别负责初始化和数据清除,在重置时可以先调用清 除函数,再进行初始化即可。注意要照顾到所有数据成员。

  17. Box2D中实现"子弹时间"效果,只要动态修改world的timestep大小 即可。而且效果相当完美。

  18. HGE中的卷轴问题,可以用Gfx_SetTransform解决。但是这个函数 貌似有点问题。

    void Gfx_SetTransform(
        float x = 0,
        float y = 0,
        float dx = 0,
        float dy = 0,
        float rot = 0,
        float hscale = 0,
        float vscale = 0
    );
    

    虽然每个都有默认参数但是如果根据源码中,如果忽略设置vscale 那么之前的参数都会被忽略。这应该是个bug。 所以如果要使用SetTransform必须将提供所有参数。

    hge->Gfx_SetTransform(0,0, 10.0f, 10.0f, 0, 1.0f, 1.0f);
    
  19. Box2D中隐藏的函数 有一些简单的计算函数在Box2D API文档中貌似是找不到的 比如b2vec2这个向量类的点乘和差乘操作,在b2Vec2类中没有相关描述 实际上在b2math.h中有定义一系列的相关函数,比如b2dot就是对两个向 量进行点乘。当你发现一些很常用的操作没有相关函数时不妨自己打开相应的源代码翻看一下。

  20. 在 ContactListener 回调函数中是不允许调用 b2World 成员函数的 比如说你想在两个 body 碰撞的时候在他们之间建立一个 joint,你是不能在回调 函数中做的(这样 Box2D 会报错),需要在 step 函数结束后再进行处理。

To the extent possible under law, the person who associated CC0 with this work has waived all copyright and related or neighboring rights to this work.

home | top