CLOSE

Archive:

2022
2021
Subscribe:Atom Feed

"Embedding Wren, pt1"

Posted: 11 August, 2022

It’s always been my intention to make the EH500 with some sort of scripting language behind it, else, what’s the point? And originally, I wanted to use this as an excuse to get back into Scheme. I wrote a basic MUD in Scheme, at Uni, and have fond memories of it. (Not to mention SICP, which must be one of the greatest programming books ever written...)

Unfortunately, this project’s not going to be the one that gets me back into Scheme...

I pulled down Guile, Chibi, s7 and Bigloo, and went through the examples on how to embed each and... none of them quite did it for me. Either the code base was too large for my taste/needs, or the type of glue code required was hard to grok, or just way too many lines of code. I’m after something simpler, that ideally, is consistent for various types of call.

It was at this point that I stumbled upon Wren, a C-based language, pretty much built for my use-case; embedding in a game engine.

Compiling Wren

For simplicity I want to compile the language into my VC and here Wren excels. You can drop a couple of folders containing the source – ~20 files – add a couple of GLOB_RECURSEs to CMAKE and it just works. Wren compiles cleanly, on Windows, just like that.

I don’t know why this surprised me, but it did. If you want to compile and share a .dll, there’re included VS solutions to build release and debug versions, and those also just work.

(Linux wasn’t quite as simple. Wren has a few warnings for unused-params/vars that the author wants to leave in, for whatever reason, which is fine, but I don’t want to look at them. I added COMPILE_OPTIONS "-w" to the Wren include folders, to turn off the warnings...)

Using Wren

The basic embedding example in the documentation is fine, but it’s unlikely to suit many people'ss needs. My use-case may be more typical. I want to load a specific .wren file, along with my level, and use it as a fixed entry point. I also want to Tick the scripts, once per frame.

Loading Wren Scripts

This is discussed in the “Modularity” section of the Wren docs. The embedding program must provide a function to load files from the resource bundle, or file system, along these lines:

static WrenLoadModuleResult Script_LoadModule(__attribute__((unused))WrenVM* vm, const char* sModuleName) 
{ 
    char* sBuff = malloc(sizeof(char)*1024); 
    memset(sBuff,0, sizeof(char)*1024); 
    
    char* sFileBytes = NULL; 
    WrenLoadModuleResult Ret; 
    Ret.source = NULL; 

    sprintf(sBuff, "%s/%s.wren", Cart_GetFilepathForLevel(), sModuleName); 
    
    if(!TDI_FileExists(sBuff)) { LOG_ERROR("Unable to find module %s for script!", sModuleName); return Ret; } 
    
    sFileBytes = TDI_FileLoad(sBuff); 
    free(sBuff); 

    if(NULL == sFileBytes) { LOG_ERROR("Unable to load module %s for script!", sModuleName); return Ret; } 

    Ret.source = sFileBytes; 
    return Ret; 
}

I've actually extended this to search a 'global' scripts folder, so modules can be re-used.

Initialising Wren, and Ticking it...

The init example in the Wren docs shows the VM parsing a string, but now we've got a file loader, we can provide the VM with an entry module and call a specific method within it.

To do this we have to use a Call Handle. The nice thing about call handles is that they can be re-used and stored, so we don't need to create them in the hot-path. Even better, a call handle can call any method with the same signature, in any module. Handy!

Here's a short example:

s_pWH_BeginPlay = wrenMakeCallHandle(s_pWrenVM, "BeginPlay()");
s_pWH_Tick = wrenMakeCallHandle(s_pWrenVM, "Tick(_)");
s_pWH_EndPlay = wrenMakeCallHandle(s_pWrenVM, "EndPlay()");

wrenEnsureSlots(s_pWrenVM, 1);
wrenGetVariable(s_pWrenVM, "main", "LevelMode", 0);
s_pWH_LevelMode_ClassHandle = wrenGetSlotHandle(s_pWrenVM, 0);

if(wrenCall(s_pWrenVM, s_pWH_BeginPlay) != WREN_RESULT_SUCCESS)
{
    ...
}

The full Init looks like this:

void Init_Script()
{
    LOG_INFO("Initialising Script Engine:");

    WrenConfiguration VMConfig;
    wrenInitConfiguration(&VMConfig);
    VMConfig.writeFn = &Script_Print;
    VMConfig.errorFn = &Script_Error;
    VMConfig.loadModuleFn = &Script_LoadModule;

    s_pWrenVM = wrenNewVM(&VMConfig);

    char* sFilepath = Cart_GetFilepathForEntryScript();
    if(!TDI_FileExists(Cart_GetFilepathForEntryScript())) {
        LOG_ERROR(" -- Unable to find initial script file: %s", sFilepath);
        Framework_Guru(SCRIPT_ERROR);
        return;
    }

    char* sScriptBytes = TDI_FileLoad(sFilepath);
    if(NULL == sScriptBytes) {
        LOG_ERROR(" -- Unable to load %s!", sFilepath);
        Framework_Guru(SCRIPT_ERROR);
        return;
    }

    WrenInterpretResult result = wrenInterpret(s_pWrenVM, "main", sScriptBytes);
    switch (result)
    {
        case WREN_RESULT_COMPILE_ERROR:
        {
            LOG_ERROR(" -- Script Compile Error!\n");
            Framework_Guru(SCRIPT_ERROR);
            return;
        }

        case WREN_RESULT_RUNTIME_ERROR:
        {
            LOG_ERROR(" -- Script Runtime Error!");
            Framework_Guru(SCRIPT_ERROR);
            return;
        }

        case WREN_RESULT_SUCCESS: LOG_INFO(" -- LevelMode loaded and compiled OK!"); break;
        default: break;
    }


    s_pWH_BeginPlay = wrenMakeCallHandle(s_pWrenVM, "BeginPlay()");
    s_pWH_Tick = wrenMakeCallHandle(s_pWrenVM, "Tick(_)");
    s_pWH_EndPlay = wrenMakeCallHandle(s_pWrenVM, "EndPlay()");

    wrenEnsureSlots(s_pWrenVM, 1);
    wrenGetVariable(s_pWrenVM, "main", "LevelMode", 0);
    s_pWH_LevelMode_ClassHandle = wrenGetSlotHandle(s_pWrenVM, 0);

    if(wrenCall(s_pWrenVM, s_pWH_BeginPlay) != WREN_RESULT_SUCCESS)
    {
        LOG_ERROR(" -- Interpretor error, calling LevelMode->BeginPlay");
        Framework_Guru(SCRIPT_ERROR);
        return;
    }

    LOG_INFO("Script Engine Initialised!");
}

And yeah, I've nicked UE's style here. The idea being, I call BeginPlay on a fixed entry point, each level, and that pulls in any and all scripts it needs to setup the level, calling BeginPlay in each.

Once that's done the VM can sit there, waiting for Tick(fDeltaSeconds) to be called, once a frame. E.G:

void Tick_Script()
{
    wrenEnsureSlots(s_pWrenVM, 2);
    wrenSetSlotHandle(s_pWrenVM, 0, s_pWH_LevelMode_ClassHandle);
    wrenSetSlotDouble(s_pWrenVM, 1, GetGameDeltaSeconds());

    WrenInterpretResult result = wrenCall(s_pWrenVM, s_pWH_Tick);
    switch (result)
    {
        case WREN_RESULT_COMPILE_ERROR:
        {
            LOG_ERROR(" -- Script Compile Error!\n");
            Framework_Guru(SCRIPT_ERROR);
            return;
        }

        case WREN_RESULT_RUNTIME_ERROR:
        {
            LOG_ERROR(" -- Script Runtime Error!");
            //Framework_Guru(SCRIPT_ERROR);
            return;
        }

        case WREN_RESULT_SUCCESS: break;
        default: break;
    }
}

You'll notice that I'm passing a float -- Delta Seconds -- into the second slot, which the Wren VM will pass as the argument to the Tick() method.

Initial Thoughts

I'm not a galaxy-brain coder. I like simple things. And well, this is one of the simplest things I've done in a very, very long time. What's left is the glue-code, to expose the very limited functionality of my VC to the scripts, but the documentation seems much clearer here. It's just grunt work.

I do need to write more Wren in anger, but it's clear, from what I've written so far, that it's designed for a C-coder who wants a simple high-level language to do some hacking about in, and well, yeah. That's exactly what I want.

To be honest, I'm kinda blown away by how good an experience this has been. I can't wait to get the glue-code done and start fucking about... but that'll have to wait.

My holiday is over, so it's back to work for me.

I very nearly have a virtual console, though. Which is kinda cool :)