Apro questo topic per parlarvi di PySpot.

Un po’ di storia

Dopo aver partecipato a delle game jam e sviluppato qualche giochino, ho cominciato a riflettere sul fatto che ogni volta che cambiavo una riga di codice per cambiare il comportamento delle entità, mi vedevo costretto a ricompilare e riavviare il gioco. Che noia.

Ho fatto un po’ di ricerche, molti usano Lua perché facile da implementare. Ero quasi caduto nel tranello, quando ho scoperto che fa partire gli indici degli array da uno. No.

Allora ho continuato a cercare e mi sono imbattuto in questa pagina stupenda che spiega come estendere Python con C/C++, e viceversa, e ho capito che in quel muro di testo c’era la risposta alla domanda fondamentale sul game scripting, sull’embedding di Python e tutto quanto.

Sette milioni e mezzo di anni dopo Un anno dopo, eccomi qui a presentarvi PySpot. Celebration!

P.S.

Se siete davvero impazienti, potete trovare un’introduzione in inglese sulla wiki ufficiale, altrimenti, abbiate pazienza e aspettate il prossimo post.

Ciaps.

Introduzione

Consideriamo un Game Loop pattern del genere:

input();
update();
draw();

La funzione input legge l’input dell’utente, la funzione update gestisce le business rules, e draw disegna a schermo.

Spesso e volentieri, vorremmo modificare il comportamento degli oggetti, ma scavare nell’update, trovare le classi e le funzioni che cerchiamo, non è un procedimento immediato e non vorremmo ricompilare ogni volta che ciò accade. La soluzione migliore, secondo me, consiste nell’allegare uno script, eseguibile a tempo di esecuzione, ai nostri oggetti. Per farlo, dobbiamo caricare gli script e darli in pasto ad un interprete Python.

Possiamo creare un interprete Python semplicemente istanziando la classe pyspot::Interpreter, che di default aggiunge <cwd>/script/ al proprio path:

#include <pyspot/Interpreter.h>

namespace pst = pyspot;

pst::Interpreter interpreter;

Ora, consideriamo un semplice script hello.py, contenente due metodi:

#!/usr/bin/python

def say_hello():
	return “Hello!”

def say_hello_to( name ):
	return “Hello %s!” % name

Possiamo caricarlo come modulo Python istanziando un pyspot::Module, al ché possiamo facilmente invocarne i metodi attraverso Module::Invoke.

pst::Module hello { "hello" };
pst::Object result { hello.Invoke( "say_hello" ) };

Fico, no?

Ma come facciamo a passare degli argomenti al metodo say_hello_to?

L’interprete Python si aspetta un parametro args come tupla python, quindi, in questo caso, dobbiamo costruire una tupla contenente una singola stringa Python. Con PySpot ciò è estremamente semplice:

pst::String name { "Nanni" };
pst::Tuple args { name };
pst::Object result { hello.Invoke( "say_hello_to", args ) };

Come avrete già notato, il tipo di ritorno di un metodo Python è pyspot::Object (aka Python Object Handle). È responsabilità del programmatore sapere il tipo esatto restituito da uno specifico metodo Python, ed interpretare il risultato nel modo giusto.

Esempio

Quindi, se volessimo fare uno script per delle gocce di pioggia? Ci piacerebbe indubbiamente scrivere roba del genere in un file drop.py:

#!/usr/bin/python
import sunspot

CLOUD_Y    = 128.0
GROUND_Y   =   0.0
DROP_SPEED =  42.0

def init( drop ):
	drop.transform.position.y = CLOUD_Y

def update( drop, delta ):
	drop.transform.position.y -= DROP_SPEED * delta
	if drop.transform.position.y <= GROUND_Y:
		drop.enabled = false

Nello script, drop e delta rappresentano oggetti passati dal nostro engine, ma – attenzione – Python accetta solo oggetti Python come argomenti per i suoi metodi, quindi dobbiamo creare dei moduli Python custom, e implementarli in C/C++. Ci servono moduli per le componenti, entità, ed ogni parte dell’engine che vorremmo esporre come API. Focca la bindella, è un sacco di codice da scrivere.

Scioltissimi! PySpot ha degli script fighissimi in grado di generare queste classi per voi da semplici descrizioni json.

Già non state più nella pelle? Nel prossimo post mettiamo le mani sull’Extension Generator! 😮

Extension Generator

Ora le cose si fanno più interessanti. Possiamo usare il PySpot Extension Generator per creare estensioni. Per estensione intendo un modulo Python, implementato in C/C++, che possiamo importare nei nostri script. Prendendo come riferimento l’esempio nel post precedente, il nome dell’estensione sarebbe sunspot, e dovrebbe contenere definizioni di una classe entity (drop), con un transform, contenente una position. Di seguito è mostrata la struttura delle cartelle e dei file json.

extension
├─ Sunspot.json
├─ component
│  └─ Transform.json
├─ entity
│  └─ Entity.json
└─ math
   └─ Vec3.json

Sunspot.json

{
	"import": [],
	"name": "Sunspot",
	"methods": [],
	"components": [
		"entity::Entity",
		"component::Transform",
		"math::Vec3"
	]
}

Vec3.json

{
	"namespace": "math",
	"name": "Vec3",
	"members": [
		{
			"name": "x",
			"type": "float"
		},
		{
			"name": "y",
			"type": "float"
		},
		{
			"name": "z",
			"type": "float"
		}
	]
}

Transform.json

{
	"namespace": "component",
	"name": "Transform",
	"members": [
		{
			"name": "position",
			"type": "math::Vec3"
		}
	]
}

Entity.json

{
	"namespace": "entity",
	"name": "Entity",
	"members": [
		{
			"name": "transform",
			"type": "component::Transform"
		}
	]
}

Gli script del generatore prendono in input questi file e generano i sorgenti necessari. Possiamo anche scegliere quale versione di Python vogliamo (2 o 3).

# Extension.h
> python generate-extension.py {2|3} <Extension.json> -h <output/directory/Extension.h>

# Estension.cpp
> python generate-extension.py {2|3} <Extension.json> <output/directory/Extension.cpp>

# Component.h
> python generate-component.py {2|3} <Extension name> <Component.json> <output/directory/Component.h>

Per la vostra gioia, sotto la cartella cmake folder, potete trovare un modulo molto utile: ExtensionGenerator.cmake. Lo potete includere nel vostro progetto cmake ed invocare la macro generate_extension, macro che genererà questi file aggiungendone i path ad una lista (EXTENSION_SOURCES) che potete usare per aggiungerli ai target sources. Ricordatevi di impostare la variabilePST_SCRIPT_DIR, altrimenti l’Extension Generator non troverà gli script Python (generate-extension.py e generate-component.py).

set(EXTENSION_DIR ${CMAKE_CURRENT_SOURCE_DIR}/extension)
set(EXTENSION_NAME Sunspot)
set(PST_SCRIPT_DIR ${CMAKE_CURRENT_SOURCE_DIR}/pyspot/script)

include(ExtensionGenerator)
generate_extension(`${EXTENSION_DIR} $`{EXTENSION_NAME})

list(APPEND SUNSPOT_SOURCES ${EXTENSION_SOURCES})

A questo punto, quando proverete a compilare il progetto cmake, verranno automaticamente generati i seguenti file:

build
├─ include
│  └─ sunspot
│     ├─ component
│     │  └─ Transform.h
│     ├─ entity
│     │  └─ Entity.h
│     ├─ extension
│     │  └─ Sunspot.h
│     └─ math
│        └─ Vec3.h
└─ src
   └─ sunspot
      └─ extension
         └─ Sunspot.cpp

Wow! 🤯

Nel prossimo post vedremo cosa c’è scritto in questi file e come possiamo utilizzarli.