Wednesday, November 17, 2010

Serialisable Enums in C++ Using Templates and the Preprocessor

I have been too busy at work lately to spend any significant time on Battle Balloons. On the upside, it has been very interesting late-night work delving deep into console graphics (both Microsoft Xbox 360 and Sony PlayStation 3)!

Anyway, here's a little bit of unrelated C++ hackery I use in Battle Ballons code. It allows you to declare an enum once (even within a class) and have it automatically convertible to/from strings - all without any auto-generation scripts. I use it to allow my config files (specified in YAML) to reference C++ enums defined in code. Super-useful since Battle Balloons is heavily data-driven!

Common implementations out there seem to use code-generators or multiple #includes of the same file. This is similar to the latter, except I pass in the list of enums twice (automated for you through a small macro).

The template class is not entirely necessary - you could easily embed the same code into the macro. But the big advantage the template has here is that you can actually step through the code in a debugger (at least in Visual Studio you can)!

More details in the code snippets below. It is rough and references project-specific code not shown, but the general concept is there and should be possible to reproduce.

First, a quick example on how it is used:
ENUM_CLASS(TestEnum, TE_A, TE_B = 2, TE_C, TE_D = 6);

void testEnum
{
    ASSERT(TE_A == 0);
    ASSERT(TE_B == 2);
    ASSERT(TE_C == 3);
    ASSERT(TE_D == 6);
    ASSERT(MAX_TestEnum == 7);

    TestEnum te;
    ASSERT(te == TestEnum::max());
    ASSERT(te == MAX_TestEnum);

    te.setFromString("TE_A");
    ASSERT(te == TE_A);

    te.setFromString("TE_B");
    ASSERT(te == TE_B);

    ASSERT(TestEnum::getEnum("TE_C") == TE_C);
    ASSERT(TestEnum::getString(TE_D) == "TE_D");
}

Header file (.h):
#define ENUM_CLASS(Name, ...)   enum E##Name { __VA_ARGS__, MAX_##Name };                           \
                                struct Get##Name##EnumString                                        \
                                {                                                                   \
                                    static const Char *get()                                        \
                                    {                                                               \
                                        return #__VA_ARGS__ ", MAX_" #Name;                         \
                                    }                                                               \
                                };                                                                  \
                                typedef EnumT Name

void tokeniseEnumString(String *enumArray, Size enumArrayCapacity, const String &enumString);

template <class Enum, const Size MAX_ENUM, class GetEnumString>
class EnumT : public EnumBase
{
public:
    static const String &getString(Size i)
    {
        ASSERT(i <= MAX_ENUM);
        return enumNames()[i];
    }

    static Enum getEnum(const String &s)
    {
        for (Size i = 0; i < MAX_ENUM; ++i)
        {
            if (enumNames()[i] == s)
                return static_cast(i);
        }
        return static_cast<Enum>(MAX_ENUM);
    }

    static Size max() { return MAX_ENUM; }

    EnumT() : m_value(static_cast<Enum>(MAX_ENUM)) {}
    EnumT(Size value) : m_value(static_cast<Enum>(value)) {}
    EnumT(Enum value) : m_value(value) {}

    EnumT &operator=(Enum e) { m_value = e; return *this; }
    operator Enum() const { return m_value; } 

    virtual bool setFromString(const String &s) { m_value = getEnum(s); return m_value != MAX_ENUM; }
    const Char *str() const { return *getString(m_value); }

private:
    static String *enumNames()
    {
        static bool s_init = false;
        static String s_enumNames[MAX_ENUM + 1];
        if (!s_init)
        {
            s_init = true;
            tokeniseEnumString(s_enumNames, MAX_ENUM + 1, GetEnumString::get());
        }
        return s_enumNames;
    }

    Enum m_value;
};

Implementation file (.cpp), which is is just the tokenising function that splits the enum declaration string (passed by the ENUM_CLASS macro function) into its individual names (and also respecting explicit number values, if given). This is only called once (on first use) and the result is cached off for all future calls:
void tokeniseEnumString(String *enumArray, Size enumArrayCapacity, const String &enumString)
{
    Size enumArraySize = 0;

    Size cIndex = 0;
    while(cIndex < enumString.len())
    {
        String name;
        bool hasExplicitValue = false;

        for (; cIndex < enumString.len(); ++cIndex)
        {
            const Char nameChar = enumString[cIndex];

            if (nameChar == ',')
            {
                ++cIndex;
                break;
            }

            if (nameChar == '=')
            {
                hasExplicitValue = true;
                ++cIndex;
                break;
            }

            if (std::isspace(nameChar))
                continue;

            name += nameChar;
        }

        if (hasExplicitValue)
        {
            String numberStr;

            for (; cIndex < enumString.len(); ++cIndex)
            {
                const Char numberChar = enumString[cIndex];

                if (std::isspace(numberChar))
                    continue;

                if (numberChar == ',')
                {
                    ++cIndex;
                    break;
                }

                ASSERT(std::isdigit(numberChar));
                numberStr += numberChar;
            }

            Int intValue;
            ASSERT(numberStr.toInt(intValue));
            ASSERT(intValue >= 0);

            const Size value = static_cast(intValue);
            ASSERT(value >= enumArraySize);
            while (enumArraySize < value)
                ++enumArraySize;
        }

        enumArray[enumArraySize] = name;
        ++enumArraySize;
    }

    ASSERT(enumArraySize == enumArrayCapacity);
}

Thursday, October 28, 2010

Performance Work

So I decided to take a peek at performance lately. Although it was not bad (it was still capping out at 60 fps), I was still curious to see how I was spending my time. So I hooked up some timers and it turned out my occlusion tests were taking up around 50% of frame time (the other 50% roughly going to rendering - but that would include time waiting for vsync)! Some simple early returns using spherical tests brought it back down to 10% of frame time - a simple fix for an unexpected problem!

On top of this, I added some face merging/simplification code for my sprite-to-mesh generator. Again, not necessary - it was just for fun. Previously, I was generating vertices for each pixel from the source image, and using vertex colours to colour them. Ugly, I know, but dead simple and it worked.

Anyway, after a week of banging my head against a wall coming up with a working algorithm, I finally have results! It uses a fairly simple heuristic: try form the biggest face along the local horizontal (U-axis) and with that merged face, try form the biggest face along the local vertical (V-axis). It will not always find the most optimal result, but it is good enough and still a massive improvement from before.

This new mesh structure also means I cannot colour with vertex colours anymore. So colouring is now done through texture lookups of the original, source image. The texture is mapped in a following pass, once the simplified mesh has been generated.

Check out the images below. Happiness!

I do not have any hard performance numbers to show though. I think these changes would only be evident in GPU performance (not CPU), and I actually don't have any tools installed to measure that. I will hunt for some in the near future, but any recommendations in the meantime would be highly appreciated!

UPDATE: I realised this a few days after and thought I should note it here in case anyone tries it. The face merging algorithm described is bad. It creates T-junctions, which will produce rendering artifacts. The approach can be modified to correct this, but it is low on my priorities (I am not vertex-bound at the moment).

Before - no simplification

After - with simplification!

Saturday, October 23, 2010

Nearly Done With New Title Screen

Decided to put in some time to implement a proper physics-based character controller for walking. It was a bit tricky... but I'm fairly happy with the results. Hooked up a basic SFX and music system too. Just a bit more polish left and on to more things! Build is available for download here too (you can try move the players around if you want). Music is from Jorge Boscan and sound effects generated by as3sfxr!