Version 1 (modified by lloydw, 10 years ago)
--

Python Event Tutorial

The Python interface to Rocket exposes a DOM API in a very similar way to Javascript. Python code can be attached to any event in the RML definition which is turn can dynamically update any part of the document including open other documents. Full source code to the final PyInvaders application can be found in the samples folder once the Python plugin has been installed.

Step 1: Setting up the Python environment

The first thing we need to do is initialise python in our application, once we have done this we can start executing scripts. We're going to make a PythonInterface class that will hide all the python intricacies.

/**
	Creates and maintains the python interface to Invaders.
 */

class PythonInterface
{
public:
    static bool Initialise();
    static void Shutdown();

private:
    PythonInterface();
    ~PythonInterface();
};

We then implement these methods.

NOTE: Its a good idea to forcibly import the rocket Python module, this ensures all boost bindings have been done and that you can proceed to expose your own classes that rely on these bindings having taken place.

NOTE: For more information Python initialisation and shutdown please see the Python documentation at http://docs.python.org

bool PythonInterface::Initialise()
{
    Py_Initialize();

    // Pull in the rocket python module.
    Py_XDECREF(PyImport_ImportModule("rocket"));
    return true;
}

void PythonInterface::Shutdown()
{
    Py_Finalize();
}

PythonInterface::Initialise should be called before rocket is initialised, this ensures the Python bindings are available when rocket starts up.

PythonInterface::Shutdown should be called after you've released all contexts but before you call Rocket::Shutdown(). This ensures all python objects are released before rocket does its final cleanup.

Step 2: Replacing the Event System

We can now completely remove the exiting event system from RocketInvaders as the Python bindings will do all the event management for us. We will however need some way of starting invaders from script. I suggest you do this with an autoexec.py script, that would look something like this:

import rocket

context = rocket.GetContext('main')
context.LoadDocument('data/background.rml').Show()
context.LoadDocument('data/main_menu.rml').Show()

To run this script, we simply need to import it at application start up. Add an import helper to the PythonInterface and call it just before the main shell loop.

bool PythonInterface::Import(const EMP::Core::String& name)
{
    PyObject* module = PyImport_ImportModule(name.CString());
    if (!module)
    {
        PrintError();
        return false;
    }

    Py_DECREF(module);
    return true;
}
PythonInterface::Import("autoexec");
Shell::EventLoop(GameLoop);

At this point the RocketInvaders will now run, however you'll get a nasty script error as Python attempts to execute the onload event in mainmenu.rml. Update the onload and onclose events to use Python script which will correctly display and hide the logo.

<body template="window" onload="document.context.LoadDocument('data/logo.rml').Show()" onclose="document.context.logo.Close()">

You will now have to go through each event defined in RML updating it with equivalent Python calls.

RocketPython parses any semi-colons in an event as a delimiter. So you can place multiple Python statements on a single line, semi-colon separated. This comes in useful when you want to execute two statements at once, for example you probably want to do the following for the Start Game button.

document.context.LoadDocument('data/start_game.rml').Show(); document.Close()

I've simplified this further by by placing a LoadMenu function in the shared template window.rml, that loads a new document, closing the existing one.

Step 3: Exposing Game Functions

The above takes care of most of the menu flow, except for a couple items, including the starting of the actual game and exiting. Lets tackle exiting first as that the easier of the two.

Our Python interface class will now have to expose a Python module (with the help of boost::python - for full documentation see http://www.boost.org).

BOOST_PYTHON_MODULE(game)
{
    python::def("Shutdown", &Shell::RequestExit);
}

This creates a module called game and places a Shutdown method within it. We now update the Initialise function to initialise this module at startup.

bool PythonInterface::Initialise()
{
    // Initialise python
    Py_Initialize();

    // Import rocket
    if (!Import("rocket"))
        return false;

    // Define our game specific interface
    initgame();

    return true;
}

We can now call the Shutdown function from main_menu.rml as follows

<button onclick="import game;game.Shutdown()">Exit</button>

If you have a lot of functions that call game, you can place the import game in the document header, or in one of your template files.

Using the above code we can extrapolate this throughout the game and have a complete functioning menu system. You will however need to expose more of the GameDetails class to Python so that the start_game screen can save the difficulty and colour selection.

Your game module should look something like this:

BOOST_PYTHON_MODULE(game)
{
    python::def("Shutdown", &Shell::RequestExit);
    python::def("SetPaused", &GameDetails::SetPaused);
    python::def("SetDifficulty", &GameDetails::SetDifficulty);
    python::def("SetDefenderColour", &GameDetails::SetDefenderColour);

    python::enum_<GameDetails::Difficulty>("difficulty")
        .value("HARD", GameDetails::HARD)
        .value("EASY", GameDetails::EASY)
    ;
}

Step 4: Custom Elements

The next problem we'll hit when converting RocketInvaders is the ElementGame does not have a Python interface. Thus we can't give it focus when we start the game which means the defender cannot be moved until the user clicks the game with the mouse. To fix this, we need to define ElementGame to Python and register the Python instancer with Rocket::Factory instead of the C++ instancer.

Lets define a static method on ElementGame to do this and call it from our game modules initialisation.

void ElementGame::InitialisePythonInterface()
{
    PyObject* object = python::class_<ElementGame, 
                                      Rocket::Core::Python::ElementWrapper<ElementGame>,
                                      python::bases<Rocket::Core::Element>, 
                                      boost::noncopyable >("ElementGame", python::init<const char*>())
    .ptr();

    Rocket::Core::Factory::RegisterElementInstancer("game", 
                                                    new Rocket::Core::Python::ElementInstancer(object))->RemoveReference();
}

Step 5: Key and end game bindings

We can now get into the game, however the game will never finish as theres no key bindings for processing the ESCAPE key and nothing will make the game exit when the game is over. Fixing the key binding is easy, simply drop in a OnKeyDown handler and make it launch the pause menu.

OnGameOver is a bit more tricky, as the old Invaders would call EventManager::LoadWindow directly from C++. We're going to have to add a game_over flag to game and make the GameElement check this state every update and fire a custom OnGameOver event.

// Updates the game.
void ElementGame::OnUpdate()
{
	game->Update();

	if (game->IsGameOver())
		DispatchEvent("gameover", EMP::Core::Dictionary(), false);
}

Step 6: Python Data Formatters

We're still using C++ data formatters, these can be moved into python for simplicity.

class NameDataFormatter(rocket.DataFormatter):
    def __init__(self):
        rocket.DataFormatter.__init__(self, "name")
		
    def FormatData(self, raw_data):
	""" 
        Data format:
        raw_data[0] is the name.
        raw_data[1] is a bool - True means the name has to be entered. False means the name has been entered already.
        """
		
        formatted_data = ""

        if (raw_data[1] == "1"):
            formatted_data = "<input id=\"player_input\" type=\"text\" name=\"name\" onchange=\"game.SetHighScoreName(event.value)\" />"
        else:
            formatted_data = raw_data[0]
			
        return formatted_data

A lot more code could be moved from C++ into Python, for example the HighScore system. Its just a matter of taking the principles you have learnt here and applying them.