Kiedy chcemy skorzystać z jakiejś nietrywialnej funkcjonalności w naszym programie, mamy zazwyczaj do wyboru dwie możliwości. Albo sami wymyślamy koło, albo idziemy do sklepu (względnie komisu), aby się w takowe zaopatrzyć. Ponieważ programiści to z natury stworzenia na wskroś leniwe, z reguły będą starali się zminimalizować wkład czasowo-intelektualny w tworzone oprogramowanie. Tak było w tym przypadku: do implementacji skryptów w KNAT zdecydowaliśmy się na użycie SqPlus, będącego rozszerzeniem do Squirrel.
Korzystanie z gotowych rozwiązań (zwłaszcza w przypadku FLOSS) ma jednak często sporą niedogodność. Otóż spojrzenie do kodu rzadko potrafi z miejsca ujawnić całą intelektualną głębię autora, i z tego to powodu zmuszeni jesteśmy poprzestać na lekturze materiałów pochodnych — a w szczególności dokumentacji. Jeżeli tylko autor biblioteki zdecydował się na tak szalony i ekscentryczny krok, jakim jest dokumentacja…
Nie jest jednak tak źle, jak mogłoby się wydawać. Choć dołączone do biblioteki przykłady — wbrew zapowiedziom — nie wyczerpują tematu na tyle, aby można już pełni korzystać ze wszystkich dobrodziejstw Wiewióra, a lektura wiki i forów — wbrew pokładanych w nich nadziejom — nie rozwiewa wszystkich naszych lęków, to po pewnym wysiłku ze strony własnej mózgownicy jesteśmy w stanie całość uruchomić.
Ale do rzeczy.
Założenie było takie, że na początku implementujemy zdarzenia typu MapEvent. Polegają one na tym, że gdy nasz — oby jego lufa zawsze lśniła w słońcu — czołg wjedzie na wyznaczone pole na planszy, uruchamiane jest odpowiednie zdarzenie. Całość miała wyglądać tak:
struct MapEvent
{
MapEvent();
virtual ~MapEvent();
int x;
int y;
virtual void DoEvent();
};
W odpowiednich momentach gra pobiera potrzebne zdarzenie i uruchamia na nim DoEvent(). No dobra, ale co z implementacją tego? Założenie było takie, żeby trzymać logikę gry w skryptach. W tym celu trzeba było nieco dostosować model zdarzenia.
struct MapEvent
{
int x;
int y;
CMapEvent() {x=y=0;};
virtual ~CMapEvent() {};
void AddEvent();
void CallEvent();
HSQOBJECT scriptObjHandle;
[...]
};
W wykropkowanej części ukryte są magiczne formuły, do których wrócimy dopiero na końcu. Co się zmieniło? Zrezygnowaliśmy z wirtualnego handlera DoEvent, ponieważ nie było prostego sposobu, aby go rozszerzać w Squirrelu i używać z powrotem w C++, natomiast dodane zostały AddEvent, który po prostu dodaje zdarzenie do gry; a także CallEvent, które wykonuje zdarzenie (wywołujemy je z C++). To, jak wygląda AddEvent oraz jak zdarzenia są przechowywane można pominąć, ponieważ nie jest to dla sprawy aż tak istotne; powiedzmy sobie za to, jak zdarzenia definiować.
Skrypt przykładowej mapy może wyglądać następująco:
my_event <- class extends MapEvent
{
event = null;
}();
my_event.event = function()
{
KNAT.DisplayInfo("Super zdarzenie!");
}
my_event.x = 4;
my_event.y = 1;
my_event.AddEvent();
Co tu się dzieje? Tworzymy obiekt my_event, będący instancją anonimowej klasy pochodnej od MyEvent. Ponieważ w Sq nie ma jakiegoś bardzo rygorystycznego wiązania typów, tworzymy po prostu pole o nazwie event, do którego następnie przypisujemy funkcję (KNAT.DisplayInfo to zaimportowana metoda wypisująca na konsoli podany tekst). Potem ustawiamy (x, y), czyli współrzędne zdarzenia na mapie (rysujemy w tym miejscu złowieszczo wyglądające skalne pole) a następnie prosimy zdarzenie, aby łaskawie dodało się do gry. Tak też się dzieje.
Jak wygląda jego wywołanie? Całkiem prosto:
void MapEvent::CallEvent()
{
try
{
SquirrelObject o(scriptObjHandle);
SquirrelObject func = o.GetValue("event");
SquirrelVM::BeginCall(func, o);
SquirrelVM::EndCall();
}
catch (SquirrelError & e)
{
scprintf(_T("Error: %s, %s\n"),e.desc,_T("Squirrel::error"));
}
}
O co tym razem chodzi? Na początku pobieramy wskaźnik do obiektu. Musi być to wskaźnik wiewiórczego świata, więc potrzebujemy scriptObjHandle. Ostatnia trudność będzie polegać na uzyskaniu powyższego. Następnie pobieramy wartość event (warto zauważyć, że zdarzenie jest traktowane jak zmienna, mimo, że jest funkcją) i tworzymy wywołanie funkcji func na rzecz obiektu o. I już!
Jeszcze magiczne scriptObjHandle. Aby je uzyskać, musimy do obiektu dodać wiewiórkowy konstruktor:
static int construct(HSQUIRRELVM v)
{
StackHandler sa(v);
MapEvent* self = new MapEvent();
self->scriptObjHandle = sa.GetObjectHandle(1);
sq_addref(SquirrelVM::GetVMPtr(), &self->scriptObjHandle);
return SqPlus::PostConstruct(v, self, release);
}
SQ_DECLARE_RELEASE(CMapEvent)
Ta magiczna funkcja sprawia, że scriptObjHandle staje się pomostem ze świata C++ do świata wiewiórek.
Mam nadzieję, że lektura rozjaśniła komuś umysł; i uświadomiła, że wiewiórki wcale nie są tak złe, na jakie wyglądają.
Tomasz Dąbrowski
Pierwsza publikacja: devblog KNI