AngelScript Integration Tests


AngelScript is awesome. It’s a C#-like scripting language you can integrate into a C++ game engine, with wicked-fast hot-reloads, so wicked-fast iteration speed. The amazing Hazelight gamedev team integrated it into Unreal Engine and have made it open source. I wrote a longer post here that I’ll copy below.

source: https://angelscript.hazelight.se/

One of its great features is exposing Unreal’s test automation framework and turning them into easy integration tests. This existing work was incredible. There were just two sharp edges for my use-case (testing a singleplayer game) - the test map names were based off the test function names, and it was using a client/server model instead of standalone, so a bunch of singleplayer-code assumptions didn’t hold and were erroring out.

I’ve submitted a PR to add optional fixes for these and updated the docs, so if you pull down the latest version and read the docs soonish (assuming they’re both merged), you’ll get all these benefits. I’m posting a full example here early.

Angelscript Integration Test Full Example

/**
 * An example of an integration test to test that saves are backwards compatible (via
 * upgrades/migrations). This could be in a file called
 * Testing/UpgradeSaveGame_IntegrationTest.as, or
 * AnythingElse/DoesntMatter_IntegrationTest.as.
 *
 * Assume that we changed the protected variable that ExampleGameMode::GetCash() uses,
 * between v1 and v2, and we want to ensure that our upgrade code (not shown)
 * successfully copies it from the old variable to the new.
 *
 * Note that angelscript does a lot of lifting around turning the automation framework
 * into integration tests. See the code for more details.
 *
 * Other references: https://angelscript.hazelight.se/scripting/script-tests/
 */

// define the overall test. The naming standard is important. you run this from Session
// Frontend -> Automation Tab. Search for e.g. "V1" to show this function, then tick it
// to select it.
void IntegrationTest_UpgradeSaveGameV1(FIntegrationTest& T)
{
    // queue the object that can run for more than one frame, to validate a long-running test
    T.AddLatentAutomationCommand(UTestUpgradeSaveGameV1());
}

// A function that returns an FString, with the same name as the integration test + a
// _GetMapName() suffix allows us to override the default behaviour of requiring a map
// name matching the test name.
FString IntegrationTest_UpgradeSaveGameV1_GetMapName()
{
    return "/Game/IS/Maps/ISMainMap";
}

// bulk of the work is here. You can have multiple of these
class UTestUpgradeSaveGameV1 : ULatentAutomationCommand
{
    // the sentinal value we expect to see in the loaded save. in the v1 save this is
    // stored in ExampleGameMode::Cash. In the v2 save, ExampleGameMode::GetCash()
    // should be able to retrieve this from the new CashTest variable.
    // different to what CashTest defaults to.
    float CashFromV1Save = 12345.0;

    // manually create a save in the previous version to use in the test here.
    FString V1SaveFileName = "IntegrationTest_UpgradeSaveGameV1";

    // This runs at the start of this command's lifetime in the test. GetWorld(), and
    // therefore all the automatic context places it's used, should be valid here
    // (unless you try changing the map)
    UFUNCTION(BlueprintOverride)
    void Before()
    {
        auto GM = ExampleGameMode::Get();
        auto ExampleSaveSystem = UExampleSaveSystem::Get();
        ExampleSaveSystem.SelectSaveFile(V1SaveFileName);
        // can't change the map in an integration test, so don't do a full map reload. Just deserialize
        ExampleSaveSystem.LoadWithoutReload(V1SaveFileName);
    }

    // runs each tick. Return true to pass the test. The test fails if the timeout
    // (default 5 seconds) is hit and this hasn't returned true.
    UFUNCTION(BlueprintOverride)
    bool Update()
    {
        auto GM = ExampleGameMode::Get();
        // if the gamemode is loaded from the save, and the upgrade code has run
        // successfully, the values should match.
        if (GM != nullptr && Math::IsNearlyEqual(CashFromV1Save, GM.GetCash()))
        {
            return true;
        }
        return false;
    }

    // The output in the automation test log. Show expected success condition and
    // current state for debugging when it fails. Important to check GetWorld() in case
    // it runs too early.
    UFUNCTION(BlueprintOverride)
    FString Describe() const
    {
        float ActualCash = -1.0;
        if (GetWorld() != nullptr)
        {
            auto GM = ExampleGameMode::Get();
            if (GM != nullptr)
            {
                ActualCash = GM.GetCash();
            }
        }
        return f"Expected cash: {CashFromV1Save}, Actual cash: {ActualCash} (-1 is null)";
    }
}

Original Post

Apologies if this sounds evangelical, I’ve been absolutely radicalised here.

If you’ve been using unreal for a while, you’ve probably bumped up against the limitations and sharp edges of both blueprint and C++. Blueprint has fast iteration times and discoverability, but lacks all the benefits of text-based programming. C++ has the benefits of text-based programming, but it’s iteration speed when working on gameplay is a killer (yes, hot-reload exists, but even 10 seconds is enough to make you lose your flow, it’s not always 10 seconds, and you can’t quickly iterate on headers).

If you’re anything like me, you’ve probably also stood in the shower wondering “why don’t they just implement a scripting language that has the iteration speed of blueprints”, then pictured some amalgamation of unreal’s custom C++ setup + C#.

Well I’m here to tell you it exists, and if you’ve been banging your head against the aforementioned sharp edges, it might even bring a (happy) tear to your eye. It’s Hazelight’s (It Takes Two developer) fork of Unreal with AngelScript added in. It is eerily similar to the language you pictured in the shower.

You can almost write it like unreal-C++, but the compile time is near instant - by the time you’ve alt-tabbed back into the editor after adding a new UPROPERTY, it’s ready to use. And there are no separate header files, it’s all single-file classes like C#. You can also Cast to your heart’s content without worrying about blueprint hard-asset references.

Think about your usual process for adding a new class in C++: from thinking about it, all the way to being able to use it in the editor. Then think about this: in angelscript it’s as quick as adding this to a file then alt-tabbing back to the editor:

class AExampleActor : AActor
{
}

Whoops, I forgot to add a member. Alt-tab, add, alt-tab, it’s ready.

class AExampleActor : AActor
{
    UPROPERTY()
    int32 productivity = 100;
}

You’ve never felt a flow-state like this.

They didn’t just integrate angelscript into unreal though, they went ahead and wrote a full vscode plugin that gives you syntax highlighting, autocomplete, go-to-definition, find-all-references, and rename functionality. It even includes inline errors and warnings with suggestions.

But wait! There’s more - it has full integration with vscode’s debugger, so you can step through every single line like you would in blueprint (and it’s all automatically in scope - no more adding #PRAGMA_DISABLE_OPTIMIZATIONS then recompiling).

The plugin’s code suggestions and autocomplete are amazing btw: for example, as soon as you type class A in the example above, it will offer to autocomplete the class name based on the filename. Little helpful things like that. It’s a masterclass in developer-experience design.

And this is all open-source. The gift that the lead dev Lucas has given to the unreal community is beyond comprehension. It no longer matters if Verse is any good really, there’ll always be angelscript. Tbh I can’t believe this wasn’t used in the engine from the start, or added in later.

This is at least a 10x productivity increase. If you’re currently programming any of your gameplay in C++ you need to check this out. I can’t believe this exists.

Full docs and instructions here: https://angelscript.hazelight.se/

If anything isn’t listed there, search the discord (linked there too).

Bonus: If that didn’t convert you, this one surely will. When debugging you can just do this:

float TestVal1 = 20.0;
float TestVal2 = 40.0;
Print(f"IT HAS FORMAT STRINGS. {TestVal1}. THIS ONE HAS THE VARIABLE NAME. {TestVal2=}");

Videogames

Hey, do you like videogames? If so please check out my game Grab n' Throw on Steam, and add it to your wishlist. One gamemode is like golf but on a 256 km^2 landscape, with huge throw power, powerups, and a moving hole. Another is like dodgeball crossed with soccer except instead of balls you throw stacks of your own teammates. And there's plenty more!

See full gameplay on Steam!


See also