Adding ChaiScript to a Cocos2d-x Project

Datetime:2016-08-23 04:29:25          Topic: Cocos2d-X           Share

The Problem

Most games have different words and levels – or their equivalent.

To define each level it is common to use a plist file, or similar XML equivalent, or even a simple text file, or maybe an SQLLite database. This works, and works well, but is limited in its flexibility.

As a simple example, let’s take a game of drafts (checkers). the board is 8×8 squares, and the starting position of the pieces is fixed – 3 rows for each colour, with all the pieces on only one colour squares – leaving two empty rows in the middle of the board.

I’ll create a (really *really*) simple example project capable simply of displaying one of two different sprites (our ‘black’ or ‘white’ piece) in an 8×8 grid.

When that’s up and running, I’ll add the Chaiscript engine to the project, and write a script to set up the starting level.

Creating the Project

To create a new project, from the Terminal:

cocos new CocosChai -p com.PooperPig.CocosChai -l cpp -d CocosChai

You can call the project whatever you like, of course, and replace the “com.PooperPig” with your own ‘company’ name.

Once created, open the project in your favourite IDE. I’m using a Mac so XCode is my IDE of choice. Build and run to make sure everything works up front.

Initial Set Up

Our game is going to use cut-down versions of a couple of classes I usually use:

Entity this is a class representing any ‘thing’ in my game. In this case each ‘piece’ in the game will be an Entity.

Entity.hpp

#ifndef Entity_hpp
#define Entity_hpp


USING_NS_CC;

class Entity : public cocos2d::Ref
{
public:
    CREATE_FUNC(Entity);
    bool init();
    bool initWithValues(Vec2 location,std::string name);
    // For simplicity I make the sprite property public - but it's not good form!
    cocos2d::Sprite* sprite;

private:
    Vec2 location;
    std::string name;
    
};

#endif /* Entity_hpp */

Entity.cpp

#include "Entity.hpp"

bool Entity::init()
{
    return true;
}
bool Entity::initWithValues(Vec2 location,std::string name)
{
    if (!this->init()) return false;
    
    this->location = location;
    this->name = name;
    
    this->sprite = cocos2d::Sprite::create(name);
    this->sprite->setPosition(location);
    
    return true;
}

EntityFactorythis is a class that knows how to create various entities. In this game it’s pretty simple, as we only really have one Entity type (or two if you count Black and White)

EntityFactory.hpp

#ifndef EntityFactory_hpp
#define EntityFactory_hpp

#include "EntityFactory.hpp"
#include "Entity.hpp"
#include "cocos2d.h"

USING_NS_CC;
class EntityFactory
{
public:
    cocos2d::Vector<Entity*> entities;
    void getEntitiesForLevel(int levelNumber);
    Entity* getEntityOfType(std::string classname, Vec2 location, std::string name);
};
#endif /* EntityFactory_hpp */

EntityFactory.cpp

#include "EntityFactory.hpp"
void EntityFactory::getEntitiesForLevel(int levelNumber)
{
    getEntityOfType("Test", Vec2(100,100), "CloseNormal.png");
}

// Add a new entity of the required type to the collection of entities
Entity* EntityFactory::getEntityOfType(std::string classname, Vec2 location, std::string name)
{
    auto entity = Entity::create();
    entity->initWithValues(location, name);
    entities.pushBack(entity);
    CCLOG("%s created at (%f,%f)", name.c_str(), location.x, location.y);
    return entity;
}

Now we need to add some code to actually use our factory to create some entities.

Edit HelloWorldScane.cpp

Add

#include "EntityFactory.hpp"

to the top of the source.

Replace the entire init method as follows:

bool HelloWorld::init()
{
    if ( !Layer::init() )
    {
        return false;
    }
    
    auto factory = new EntityFactory();
    auto entity = factory->getEntityOfType("Test1", Vec2(100,100), "CloseNormal.png");
    this->addChild(entity->sprite);
    
    return true;
}

In this code we create a new factory, use it to create a new entity (using the CloseNormal.png that is already included in the project, then add the sprite as a child to this layer – allowing it to be displayed.

If you run the program now, you should see that single image displayed on screen.

The initial program running on the Mac

Getting ChaiScript

We’re about to add ChaiScript to our project – so first we need to download it.

Head to Chaiscript.com and download the source, and put it somewhere sensible! (I have a Projects folder in my Documents folder – so I added a Chaiscript-5.7.1 folder.

I want to do some maths in this example app – for which we also need to download the Chaiscript Extras source from https://github.com/ChaiScript/ChaiScript_extras

I added this to my Projects folder too – although we only actually need a couple of files out of this download.

The files we need are in the folder include/chaiscript/extras – so find this folder and copy it to the include/chaiscript/extras folder in the Chaiscript-5.7.1 folder. (You don’t actually have to do this – but it makes more sense to me to keep all my Chaiscript header files in the same place).

Adding ChaiScript to the Project

One of the great things about ChaiScript is that it is “Header-Only”. That is, all you need is to include the header(s) in your source and then use it!

So we need to tell our IDE where to find the headers!

In Xcode, highlight the Project in the Project Navigator, then select Build Settings. Find the Header Search Paths (or User header Search Paths) and add the path to the chaiscript include folder, and specify Recursive – so the compiler will search all the sub-folders rather than you having to specify the exact location of the header files. (This is optional, of course – you can specify the whole path of each header if you prefer).

Do not forget to do this for the correct Target!– I seem to always forget, and so it all works when I run for (say) IOS, but not for Mac!

Adding the header search path

Creating the Engine

I don’t know if Engine is the right word – but it makes sense to me. To use ChaiScript we need to perform two essential tasks: create the Engine, and then tell the engine about any classes, functions, fields etc. in our C++ program that we want our scripts to be aware of.

Add a new C++ file, ChaiscriptCreator.cpp, to your project:

ChaiscriptCreator.hpp

#ifndef CHAISCRIPTCREATOR
#define CHAISCRIPTCREATOR

namespace chaiscript
{
    class ChaiScript;
    class Module;
}

namespace CocosChai
{
    std::unique_ptr<chaiscript::ChaiScript> create_chaiscript();
}

#endif

ChaiscriptCreator.cpp

#include "ChaiscriptCreator.hpp"
#include "ChaiscriptBindings.hpp"
#include "chaiscript/chaiscript.hpp"
#include "chaiscript/chaiscript_stdlib.hpp"

namespace CocosChai
{
    std::unique_ptr<chaiscript::ChaiScript> create_chaiscript()
    {
        auto chai = std::unique_ptr<chaiscript::ChaiScript>(new chaiscript::ChaiScript(chaiscript::Std_Lib::library()));
        chai->add(createChaiscriptBindings());
        auto scriptFileName = cocos2d::FileUtils::getInstance()->fullPathForFilename("Setup.chai");
        chai->eval_file(scriptFileName);
        
        return chai;
    }    
}

You may notice that ChaiscriptCreator references ChaiscriptBindings.hpp – so we can’t build until we’ve written that code. Create the new C++ files, called ChaiscriptBinginfs.hpp and .cpp

ChaiscriptBindings.hpp

#ifndef CHAISCRIPTBINDINGS
#define CHAISCRIPTBINDINGS

namespace chaiscript
{
    class Module;
}

namespace CocosChai
{
    std::shared_ptr<chaiscript::Module> createChaiscriptBindings();
}

#endif

ChaiscriptBindings.cpp

#include "ChaiscriptBindings.hpp"
#include "chaiscript/chaiscript.hpp"

#include "chaiscript/extras/math.hpp"

#include "Entity.hpp"
#include "EntityFactory.hpp"

#define ADD_FUN(Class, Name) module->add(chaiscript::fun(&Class::Name), #Name )

namespace CocosChai
{
    std::shared_ptr<chaiscript::Module> createChaiscriptBindings()
    {
        auto module = std::make_shared<chaiscript::Module>();
        // add the math library so we can use math stuff in our script
        auto mathlib = chaiscript::extras::math::bootstrap();
        module->add(mathlib);
        
        // Add the Vec2 type so we can use them and return them in scripts
        module->add(chaiscript::user_type<cocos2d::Vec2>(), "Vec2");
        module->add(chaiscript::constructor<cocos2d::Vec2(float, float)>(), "Vec2");            // Define the Vec2 Constructor
        
        module->add(chaiscript::fun(&cocos2d::Vec2::x),"x");                                    // Define the Vec2 'properties'
        module->add(chaiscript::fun(&cocos2d::Vec2::y),"y");
        
        
        // Tell chaiscript about our classes
        module->add(chaiscript::user_type<Entity>(), "Entity"); // this isn't strictly necessary but makes error messages nicer
        module->add(chaiscript::user_type<EntityFactory>(), "EntityFactory");
        
        // Give chaiscript a reference to the methods we want it to access
        ADD_FUN(EntityFactory, getEntityOfType);
        
        ADD_FUN(Entity, initWithValues);
        
        return module;
    }
    
}

Ok – so this should build (it won’t do anything much, yet) but let’s walk through the code.

In ChaiscriptCreator.cpp there’s only one method – create_chaiscript() – that returns a unique pointer to a Chaiscript Engine.

It does this as follows:

10. Create a unique pointer to the Engine

11. Adds the result of the createChaiscriptBindings() method (which we’ll talk about shortly)

12. Gets the full pathname of a script file for us to load. (we’ll write some script soon)

13. Loads the script file into the Engine.

15. Returns the Engine

Incidentally, you don’t have to create a script file if you don’t want to – you can use strings in C++ and then have ChaiScript execute the strings as script – depending on your use-cases this might be a good option for you.

Binding C++ to ChaiScript

Let’s take a look at the single method in ChaiscriptBindings.cppThis is the file you will make most changes to in your own project. Here is where we tell the ChaiScript engine about, well, about anything we need it to know about!

9. Is a preprocessor macro to shorten what we need to write to bind methods of classes to ChaiScript

15. We create a ChaiScript Module

17. We create a ChaiScript Math ‘object’

18. We add the Math ‘object’ to the Module

21 – 25 we tell Chaiscript about the cocos2d::Vec2 structure, so we can use it in our script.

29. We tell ChaiScript about our Entity class

30. We tell ChaiScript about our EntityFactory class

33. We tell ChaiScript about the getEntityOfType method in our EntityFactory class

35. We tell ChaiScript about the initWithValues method in our Entity class

That’s it – nothing too complex!You can see that it would be quite simple to tell ChaiScript about more classes and methods or types, if that’s what you need.

Next, we’ll actually write some script!

Writing Script

ChaiScript syntax is similar to JavaScript – and I’m not going to try to teach you about its syntax – just enough for us to get some script executing.

First, let’s create a source file to write our script in.

In XCode, add a new file called “Setup.chai” (it can be called anything at all – but in the code we just wrote, that’s what we called it, so that’s what I’m using!).

As XCode doesn’t know what a file with suffix ‘chai’ is, there will be no syntax highlighting. Fix this by using the Menu Option EditorSyntax ColoringJavaScript

Here’s the function

Setup.chai

def HelloWorld(x)
{
    return "Hello " + x
}

Running the Script

Now we have a function called “HelloWord”, let’s run it.Change the HelloWorld::init function so it looks like this:

HelloWorld::init()

bool HelloWorld::init()
{

    if ( !Layer::init() )
    {
        return false;
    }
    
    // Get the Chaiscript Engine
    auto chaiscript = CocosChai::create_chaiscript();
    // Get a reference to the function in the script that takes a string parameter, returns a string, and is called "HelloWorld"
    auto fn = chaiscript->eval<std::function<std::string (std::string)>>("HelloWorld");
    
    // Call the function
    std::string s = fn("PooperPig");

    // Log the resutl
    CCLOG("%s", s.c_str());

    return true;
}

Line 10 gets the ChaiScript Engine object, using the code in our ChaiscriptCreator.cpp.

Line 12 defines a function pointer that will give our c++ code access to the ChaiScript function “HelloWorld”.

Line 15 calls the function, putting the result in the variable ‘s’.

Finally, line 18 logs the output to XCode’s output window.

Run the program; look at the console and, with luck, you’ll see “Hello PooperPig” there – generated by your script!

More Scripting

Ok – so we did a “Hello World” script, but now let’s get a little more realistic (but not too complex!)

We already have our EntityFactory ready to create Entity’s for us, so I”m going to write a script that takes a level number as a parameter, and creates a number of entities in a different pattern depending on the level number.There is already a C++ function called “getEntitiesForLevel” in EntityFactory. We’ll change this so that it calls the script function “SetUpLevel()”.

First, here’s the script.

Setup.chai

def setUpLevel(level)
{
    var black = "CloseNormal.png"
    var white = "CloseSelected.png"
    
    var x=0;
    var y=0;
    
    var w = 40
    var h = 40

    switch (level)
    {
        case(1)
        {
            for( x=0; x < 8; x += 2)
            {
                for( y = 0; y<3; ++y)
                {
                    Factory.getEntityOfType("Test", Vec2(20+x*w,20+y*h), black)
                }
            }
            for( x=0; x < 8; x += 2)
            {
                for( var y = 8; y>5; --y)
                {
                    Factory.getEntityOfType("Test", Vec2(20+x*w,20+y*h), white)
                }
            }

            break;
        }
        case(2)
        {
            x = 0;
            y = 0;
            Factory.getEntityOfType("Test", Vec2(20+x*w,20+y*h), white)
            x = 4
            y = 2;
            Factory.getEntityOfType("Test", Vec2(20+x*w,20+y*h), white)
            x = 3
            y = 6;
            Factory.getEntityOfType("Test", Vec2(20+x*w,20+y*h), black)
            
            break;
        }
        default
        {
        }
    }
}

It’s pretty simple stuff. Depending on the level number passed as a parameter, we create a different pattern of pieces.

As the EntityFactory will be calling functions from a script, it needs to have a ChaiScript Engine instance to use. We’ll pass an instance to it, so we need to make a change to getEntitiesForLevel to add a parameter to this method.

EntityFactory.hpp

#ifndef EntityFactory_hpp
#define EntityFactory_hpp

#include "EntityFactory.hpp"
#include "Entity.hpp"
#include "cocos2d.h"
#include "chaiscript/chaiscript.hpp"

USING_NS_CC;
class EntityFactory
{
private:
    std::function<void (int)> setUpLevel_script = NULL;
public:
    cocos2d::Vector<Entity*> entities;
    void getEntitiesForLevel(int levelNumber, chaiscript::ChaiScript &chai);

    Entity* getEntityOfType(std::string classname, Vec2 location, std::string name);
};
#endif /* EntityFactory_hpp */

We’ve added chaiscript.hpp to our #includes.

We’ve declared a std::function that takes a single int parameter and returns void, and we’ve called it “setUpLevel_script”

We’ve changed the signature of getEntitiesFor Level to include a reference to our ChaiScript engine.

EntityFactory.cpp – changes

void EntityFactory::getEntitiesForLevel(int levelNumber, chaiscript::ChaiScript &chai)
{
    if (setUpLevel_script == NULL)
    {
        setUpLevel_script = chai.eval<std::function<void (int)>>("setUpLevel");
    }
    setUpLevel_script(levelNumber);   
}

We’ve changed getEntitiesForLevel() to add a parameter to allow us to pass it a ChaiScript engine object, and the method now creates a std::function that references the ChaiScript function (setUpLevel) we just wrote – and then executes that function.

Finally, we need to make a few changes to HelloWorldScene.cpp.Add a #include “Entity.h”, then change the init function as below

HelloWorldScene.cpp – init function

bool HelloWorld::init()
{

    if ( !Layer::init() )
    {
        return false;
    }

    // Get a reference to the Chaiscript Engine
    auto chaiscript = CocosChai::create_chaiscript();

    // Instantiate our factory
    auto factory  = new EntityFactory();

    // tell ChaisCript about our factory
    chaiscript->addGlobal(chaiscript::var(factory), "Factory");
    
    // Ask the factory to get entities for this level - pass a reference to the ChaiScript engine so that the factory can use it
    factory->getEntitiesForLevel(2, *chaiscript);
    
    // Now the factory has created all of the Entities, add their sprites as children so we can actually see them
    for (auto entity:factory->entities)
    {
        this->addChild(entity->sprite);
    }
    return true;
}

The comments are hopefully self-explanatory.So now you can run the App. Change the first parameter in factory->getEntitiesForLevel(2, *chaiscript); to show level 1 or level 2.

Sure, it’s not pretty – but I think it shows the potential !

Level 2
Level 1




About List