How to manage multiple scripts


#1

First a little background… In the game engine that I am developing my world consists of a bunch of entities. Each entity is composed of a bunch of components. One such component is a Script component class. This class represents a single chaiscript file. My main loop simply calls the update() method on every component attached to every entity in the game world. The Script class’s update() method just calls the update() function defined within the chaiscript file.

Each entity can have more than one Script component, each representing different chaiscript files. Ideally, I would like each script to be sandboxed so that one does not affect the other. Also, the state of each script should persist across update calls so that any global variables defined in one iteration of the main loop are available in the next.

I see two possible options for achieving this:

  1. Each Script instance has its own chaiscript::ChaiScript object.
  2. Use a single chaiscript::ChaiScript object. Each script then stores a chaiscript::ChaiScript::State object, which is restored before update() and saved after update() call.

I’m curious as to which option is preferable from a performance standpoint. Bear in mind that the game world can have hundreds of entities each containing several Script components. Using the second option would require copying and restoring the chaiscript::ChaiScript::State object hundreds of times per frame.

Thank you for any insight.


How to handle multiple scripts / functions
#2

From a pure performance perspective, having one ChaiScript engine per object would be the best. But it would use far too much RAM. Expect ~2MB per object.

In a simple test I just ran, I can do approximately 5000 save/restores per second on my system (Linux GCC 4.8). I can almost certainly increase the performance of that operation since I have literally spent no time trying to optimize it so far.

However, I personally wouldn’t do either. I would set it up so that each game object is treated as an object, so the script creates and returns a ChaiScript object that contains the update() method. Using OOP to perform your encapsulation.

-Jason


#3

Thanks Jason, I really appreciate your time.

I’m not sure that I fully understand your preferred method. Are you suggesting that my ChaiScript script file defines a class and then instantiates it? For example…

class SomeBehaviour
{
    var m_updateCount

    def SomeBehaviour() {
        this.m_updateCount = 0
    }

    def update(dt) {
        this.m_updateCount = this.m_updateCount + 1
    }
}

var instance = SomeBehaviour()

The same script may be used by different entities. How would I then go about creating multiple instances of the same class without overwriting older instances?


#4

I can think of many ways to compose this, but something to keep in mind is that the result value of any script is the last statement executed. You can use this to your advantage to simply have the script return the object you want to use, unnamed, then deal with it however you want to in C++ land, possibly adding back into the engine with a name you control.

I perhaps (definitely) got a bit carried away here, but I wanted to show you the possibilities. Here I’m greatly blurring the lines between C++ and ChaiScript, by having the script create an unnamed function object that is returned and called (typesafely) from within C++.

This code, which is a bit more realistic for how much time you might need to spend on an object update, can do ~70,000 object updates / second on my system with an optimized build:

#include <chaiscript/chaiscript.hpp>
#include <chaiscript/chaiscript_stdlib.hpp>

int main()
{
  chaiscript::ChaiScript chai(chaiscript::Std_Lib::library());

  std::vector<std::function<chaiscript::Boxed_Value (int)>> updaters;

  chai.eval(R"(
        class SomeBehaviour
        {
          var m_updateCount
          var m_name

          def SomeBehaviour(t_name) {
            this.m_updateCount = 0
            this.m_name = t_name
          }

          def update(timestep) {
            ++this.m_updateCount
          }

          def description() {
            "${this.m_name} Updated ${this.m_updateCount} times"
          }
        }
  )");

  auto creator = chai.eval<std::function<std::function<chaiscript::Boxed_Value (int)> (const std::string &)>>(R"(
        // this lambda function is the last statement and therefor its value is simply
        // returned when the script is done executing
        fun(name) { 
          var f = fun(obj, timestep) { 
            obj.update(timestep)
            return obj;
          }
          var s = SomeBehaviour(name);
          return bind(f, s, _)
        }
     )");

  updaters.push_back(creator("Obj1"));
  updaters.push_back(creator("Obj2"));


  for (int i = 0; i < 30000; ++i)
  {
    for(auto &updater : updaters)
    {
      updater(i);
    }
  }
}

Note that C++11’s raw string literals are a HUGE help to embedding ChaiScript inside your C++ of you are so inclined.

Let me know if you need any of the code explained.


#5

Thanks Jason, that was extremely helpful and gave me a fair few ideas.


#6

Yes please!
Let me see if I’ve got at least some of this:
My c++ prog has a vector of functions that take a string as a parameter and return a Boxed_Value (int).
Q1. I do not understand what a Boxed_Value(int) is

I have a script that defines a class called “SomeBehavior” that contains three functions - SomeBehaviour(), update() and description()

I define a functor called creator.
Q2. I do not understand what this script is doing. More confusing the more I look at it!

Q3. What does bind(f, s, _) do? I haven’t come across the underscore before.

I now store two functors created by passing “Obj1” and “Obj2” to this lambda function that I don’t understand.

Now I loop round and call the functions returned by the functors, passing i as the integer parameter.

I’m quite confused.


#7

Q1:

Let’s break it down:

// this is a function that takes an int and returns a Boxed_Value
// Boxed_Value is how ChaiScript manages data internally. 
// It can contain anything
std::function<chaiscript::Boxed_Value (int)> 

Then next is this part:

// This is a function that takes a string and returns the thing described above:
// That is, it's a function that takes a string and returns a new function
std::function<std::function<chaiscript::Boxed_Value (int)> (const std::string &)>

So finally, this line:

// Evalute this block of ChaiScript <...> and return back a function that takes a string, that returns 
// a new function that takes an int and returns a Boxed_Value
chai.eval<std::function<std::function<chaiscript::Boxed_Value (int)> (const std::string &)>>(...)

So essentially, it’s possible to generate new functions whenever you need to, at runtime, but it’s
not a beginner example for sure.

Q2: See Q1

Q3:

This is similar to std::bind http://en.cppreference.com/w/cpp/utility/functional/bind

let’s take a more simple example:

def add(x, y) {
  return x+y;
}

add(2,3) // returns 5

var add2 = bind(add, 2, _); // the underscore means "leave this parameter empty"

add2(10) // return 12

// and to drive the point home

var add2plus12 = bind(add, 2, 12);
add2plus12(); // returns 14

This example is a little old and I don’t really recommend using bind anymore. You can accomplish the same thing by returning an unnamed function:

var add2 = fun(y) { return add(2, y); }
add2(3); // returns 5

Hopefully this makes sense and isn’t just more confusing.

If not, it might be most appropriate to go back to your other topic or make a new one to specifically discuss your implementation. (Or share a link to a github project or something if you have it.)


#8

Thanks so much for your reply - it is starting to come together!

Seems that each time I am moving two steps forward and one step back - so making progress!

I’ll try to post individual questions as I find issues I don’t understand rather than bombarding you with so much stuff!

Again - thanks for your patience and help!


#9

Yeah, I was wondering when reading through this because the bind usage seemed strange when you should be able to write:

      var s = SomeBehaviour(name);
      return fun(timestep) { 
          s.update(timestep)
      }

Am I correct in that s would be copied into fun implicitly?


#10

s would not be copied in implicitly. I opted for more of the C++ model (mostly because it was easier to implement) then the javascript model. You can do this however:

return fun[s](timestep) {
  s.update(timestep);
}

to do an explicit capture.


#11

Oh, that’s actually much better! I didn’t realize the syntax was even possible. It doesn’t seem to be reflected in the docs anywhere really.

Honestly I love chaiscript, but little things like this would be great to see some documentation about (again, I might just be looking in the wrong places). :slight_smile:


#12

I’ve gone a couple of different rounds of documentation. It’s extremely difficult to maintain technical docs and I’ve learned that no one reads them.

So, I have this: https://github.com/ChaiScript/ChaiScript/blob/develop/cheatsheet.md

I think every language feature is covered on there. PLEASE submit pull requests / issues if you see something that should be added to it.

-Jason