top of page

IS IT HARD TO WRITE A GAME ENGINE? (PT 6)

Writer's picture: Dan HatchDan Hatch

(Part 6: Exceptionally good)


Last time…

The last post was quick run through of getting some input values, in that last post I vowed to get some scripting going and even had wild ideas of getting things moving on screen. This goal turned out to be a bit more involved than I’d like to put in a single post, so I’ve broken it down a bit over the next few posts. Why do Java developers wear glasses? Because they don’t C#.

Anyhoo…. I’d decided early on in my game engine adventures to try to use mono as the scripting solution, so I was straight to it. The way I saw it was that I had a few main areas of initial functionality to nail:

  • A way for native (C++) code to call a managed (C#) method

  • A way for managed code to call a native method

  • A way to tie together managed objects/scripts with native objects (so when you tell Giraffe-23 to run, he starts to run, rather than Elephant-406 starting to trumpet)

With these objectives in mind, I scooted over to the mono site to do some reading. It didn’t take long to find exactly what I needed: Embedding Mono BAM, it’s all there, practically on a plate! Do you speak-a my language? Extremely excited, I set to work on objective 1 — native to managed! I flapped about a little bit, copying and pasting examples, getting nowhere, then promptly decided to stop, take a step back and think about how best to approach this. I reasoned that just with the other major components of my engine, this is going to be a subsystem, so I’ll need an implementation of ISubSystem. I decided that’s the right place to do all the mono set up/tear down stuff. Excellent. Then I decided that I’ll need a way to represent my actual mono scripts, so I figured I’ll create an interface for a script — I thought IGameScript was fitting. I just need to get this working, so for now I started with a single method, I came to the conclusion that Update() will be a pretty useful method. Here’s my IGameScript:

#ifndef BFDE_IGAMESCRIPT_HPP
#define BFDE_IGAMESCRIPT_HPP

namespace bfde {
    class IGameScript {
    public:
        virtual ~IGameScript() {};
        virtual void Update() = 0;
    };
}

This was only half the story though, as this only covers the native world. I came to the conclusion that I also needed some set interface in the managed world too, and then I can marry the IGameScript calls with the managed methods. I opted for a really simple (of course!) way of achieving this, by creating a class that can be used as a base class for all future game scripts. I fired up MonoDevelop, started a new project (I called it BlueflameDigitalEngine) and added a new class called GameScript. It looked like this (WARNING! C#):

using System;

namespace BlueflameDigitalEngine {
    public class GameScript {
        public GameScript() {
        }

        public virtual void Update() {
        }
    }
}

I then created a new project called SampleProject, and added a new script called SampleScript that that looks like:

using System;
using BlueflameDigitalEngine;
using System.Diagnostics;

namespace SampleProject {
    public class SampleScript : GameScript {
        public SampleScript() {
        }

        public override void Update() {
            Debug.WriteLine(“Managed Update() called”);
        }
    }
}

I built this, and then dropped back into C++ to start implementing my script subsystem. There are no surprises in the declaration here, so I’ll just go straight in with some implementation details of what I’d dubbed MonoScriptSubSystem:

#include <stdlib.h>
#include <stdexcept>
#include <mono/jit/jit.h>
#include <mono/metadata/assembly.h>
#include <mono/metadata/mono-config.h>
#include "bfde.hpp"
#include "Script/Mono/MonoScriptSubSystem.hpp"

namespace bfde {
    MonoScriptSubSystem::MonoScriptSubSystem(
            const std::string& monoLibPath, 
            const std::string& monoLib)
        : eventManager_(eventManager),
        monoLibPath_(monoLibPath),
        monoLib_(monoLib) {
    }

    MonoScriptSubSystem::~MonoScriptSubSystem() {
        delete script_;
    }

    void MonoScriptSubSystem::Initialise() {
        // set mono path environment variable if not present
        setenv("MONO_PATH", monoLibPath_.c_str(), 0);
        std::string lib = monoLibPath_ + monoLib_;        

        mono_config_parse(NULL);
        monoDomain_ = mono_jit_init(lib.c_str());
        monoAssembly_ = mono_domain_assembly_open(
            monoDomain_, lib.c_str());

        if (!monoAssembly_) {
            throw std::runtime_error(
             "failed to create MonoBridge");
        }

        monoImage_ = mono_assembly_get_image(monoAssembly_);
        script_ = new MonoGameScript(monoDomain_, monoImage_,
            "SampleProject", "SampleScript");
    }

    void MonoScriptSubSystem::Terminate() {
        mono_runtime_cleanup(monoDomain_);
    }

    void MonoScriptSubSystem::Update() {
        script_->Update();
    }
}

I implemented MonoScriptSubSystem to take two string parameters: monoLibPath and monoLib.These point the script subsystem to a path and name for our managed assembly.

The Initialise method is pretty much lifted from the embedding mono link, but I’ll quickly run through the jist of it:

I set up a MONO_PATH environment variable as I found that this was the easiest way to get mono to find my libraries. I combined the path and library name into a single string for easy use

The mono_config_parse(NULL) loads up the default mono config file, I haven’t really read up too much on this, but I think it loads paths and stuff so that mono knows where to find standard libraries and suchlike.

Then I go on to the actual initialisation, first initialise mono itself, and then load/open the managed assembly with lame error checking. You should implement some better error checking, as I probably have by the time you’re reading this ;)

The last thing in this method is that I create a MonoGameScript object that is an implementation of IGameScript. I was just getting things going at this point, but my thinking is that I’ll have some kind of list or tree with references to my GameScripts. For trying stuff out though, one script is enough. The “SampleProject” and “SampleScript” that I pass to the constructor are the namespace and class name of the managed script, as you’ll see shortly.

I just took the cleanup stuff from the embedding mono article and popped that into the Terminate() method, and for now simply call the Update method of the script when Update on the subsystem is called.

On to my IGameScript implementation, MonoGameScript:

#include "bfde.hpp"
#include <mono/metadata/debug-helpers.h>
#include <mono/metadata/class.h>
#include "Script/Mono/MonoGameScript.hpp"

namespace bfde {
    MonoGameScript::MonoGameScript(MonoDomain* monoDomain,
        MonoImage* monoImage,
        const std::string& scriptNamespace,
        const std::string& scriptName)
        : monoDomain_(monoDomain),
        monoImage_(monoImage),
        scriptNamespace_(scriptNamespace),
        scriptName_(scriptName) {
    }

    MonoGameScript::~MonoGameScript() {
    }

    void MonoGameScript::Initialise() {
        MonoClass* monoClass = mono_class_from_name(monoImage_,
            scriptNamespace_.c_str(), scriptName_.c_str());

        // find constructor
        MonoMethod* ctor = FindMonoMethod(monoClass, ":.ctor");

        // find required methods
        monoUpdateMethod_ = FindMonoMethod(monoClass, ":Update");

        // create an instance
        monoObject_ = mono_object_new(monoDomain_, monoClass);

        // call constructor to initialise
        mono_runtime_invoke(ctor, monoObject_, NULL, NULL);
    }

    void MonoGameScript::Update() {
        mono_runtime_invoke(monoUpdateMethod_, monoObject_,
            NULL, NULL);
    }

    MonoMethod* MonoGameScript::FindMonoMethod(
        MonoClass* monoClass, const char* signature) {

        MonoMethodDesc* desc = mono_method_desc_new(signature, 0);

        MonoMethod *method = NULL;

        while (monoClass != NULL && method == NULL) {
            method = mono_method_desc_search_in_class(
                desc, monoClass);

            if (method == NULL)
                monoClass = mono_class_get_parent(monoClass);
        }

        mono_method_desc_free(desc);

        return method;
    }
}

This class takes a few things in the constructor that are required to find and interact with the mono class.

In the Initialise method we create a MonoClass object from the provided script name and namespace. This represents a CLASS, not an instance of the class. To be able to call into managed code, we also need to find the appropriate entry points for the methods we’re going to be calling. This is done using the class object and the entry points are represented by MonoMethod objects.

You’ll notice that I’ve written a method to find a mono method given the class and signature. In this method, I do a recursive search up the inheritance tree of the class. I’ve done this as my first attempt just used mono_method_desc_search_in_class() to try to find the method. However, that method ONLY searches in the MonoClass provided to it and no parent, and my managed SampleScript already inherits from GameScript. This came to light when I removed the Update() method on SampleScript (you’ll see why I removed it soon)

You need to call the constructor manually after creating a managed instance, so I added code to lookup the constructor, and of course I added code to look up the Update() method for later use.

Then, I added code to create an instance, and invoke the constructor. That’s all that’s needed for initialisation. I didn’t think it was too bad really!

In the Update() method of our GameScript, all we need to do is invoke the Update() method of our game script. This is just a simple one liner as we aren’t passing any parameters.


Hurray! An exception!

At this point, I was reasonably happy that we should be able to get some managed code being called, so I went back to the main method and integrated an instance of MonoScriptSubSystem. We have a hardcoded single script in there, so once that was done it was time to run it and see what happens.

So after running, it at least tried to do something, I ended up with:


I took this as a good sign, as that, my dear reader is a mono exception, which means that it must be running mono code to be raised, wahoo! At this point I commented out my Update() method in SampleScript, and then saw that my code didn’t find the Update() method, hence the need recursing FindMonoMethod() mentioned earlier.

Once I’d figured out the recursing thing, the code seemed to run OK. I put back in my Update() method to SampleScript and got the same exception. I then commented out just the Debug.WriteLine() and it seemed to run without throwing the exception. It didn’t do anything of interest now, but I was pretty sure that the method was being successfully called and run.


Next time…

Next time I’m moving on to scripting objective 2, which is getting managed code to call unmanaged code!



26 views0 comments

Recent Posts

See All

Comentarios


Contact

Main Office:

Blueflame Digital

The Old Fire Station

Salt Lane

Salisbury

SP1 1DU

 

01722 568 946 

 

hello@blueflamedigital.co.uk

We have worked with...
  • Facebook B&W
  • Twitter B&W
  • LinkedIn B&W
  • YouTubeBlack

© 2020 Blueflame Digital Ltd - Registered office: 41b Beach Road, Littlehampton, West Sussex, BN17 5JA - Company number: 03837054 - VAT number: 750344158

bottom of page