=encoding utf-8 =head1 Crossfire+ - ein graphisches MORPG mit Perl Crossfire+ ist ein stark von Nethack inspiriertes, graphisches Multiplayer-Online-RPG und gehört in die (sehr kleine) Klasse von Echtzeit-Multiplayer-Roguelike-Spielen. Im Gegensatz zu den meisten Roguelikes besitzt es nicht nur zufällig generierte Karten sondern auch über 5000 handgemachte Karten mit Rätseln und Quests. =begin latex \begin{center} \includegraphics{cfplus/scr2.eps} \end{center} =end latex =head2 Entstehung Crossfire+ ist aus Crossfire entstanden, das wiederum um 1994 entstanden ist. Um 2005 waren jedoch die meisten der Originalentwickler verschwunden, der Server crashte bei Benutzung täglich und die übriggebliebenen Maintainer waren nicht in der Lage, vorhandene Probleme zu lösen. Daher wurde 2005 Crossfire+ gegründet, ein Fork, der das Ziel hat, den (nicht mehr zeitgemäßen) Programmcode zu modernisieren und stabiler zu machen. =head2 Features Crossfire+ ist im Gegensatz zu Crossfire in C++ geschrieben und wesentliche Teile des Servers sind in Perl gehalten, was der Stabilität und der Erweiterbarkeit sehr zugute gekommen ist. So fängt Crossfire unter Last vieler Spieler an zu ruckeln, da es Karten synchron nachlädt, während Crossfire+ Laden und Speichern aller Daten im Hintergrund vornimmt (dank IO::AIO und Coro mit vertretbarem Aufwand). Das verhindert Ruckler, und dank des neugeschriebenen Parsers ist das Laden bis zu fünffach schneller. Doch auch das Objektsystem wurde stark verbessert und vor allem mit Perl kombiniert: An Karten, Monster, Spieler und viele andere Objekte lassen sich Perl-Erweiterungen binden die viele Details des Verhaltens regeln können. Zeitgleich wurde ein auf OpenGL-basierender und in Perl gschriebener neuer Client geschaffen (es gibt derzeit ca. 7 verschiedene Clients für unterschiedlichste Platformen). Um platformunabhägig zu bleiben, musste ein eigenes OpenGL-Toolkit implementiert werden. Die Welt von Crossfire+, von oben :) =begin latex \begin{center} \includegraphics{cfplus/world.eps} \end{center} =end latex =head2 Der Client Scorn bei Nacht, in CFPlus: =begin latex \begin{center} \includegraphics{cfplus/scr1.eps} \end{center} =end latex Wie schon erwähnt, wurde der Client größtenteils in Perl geschrieben. Ausnahmen sind der SDL- und SDL_Mixer-Code sowie die Schnitstelle zu OpenGL und einige Hilfsfunktionen (wie ein Pango-OpenGL-Renderer). Die Wahl fiel auf SDL um Platformunabhängigkeit zu erreichen. Leider kann man SDL_Perl seit Umstellung auf Module::Build wie viele andere derartige Module nicht mehr auf nicht-Unix-System zum Laufen bringen, weshalb wir gezwungen waren, unseren eigenen SDL-Schnitstellencode zu schreiben, was uzm Glück sehr einfach war, da´ wir SDL nur benutzen um ein Fenster mit OpenGL-Fähigkeit zu erhalten und dannach alles über OpenGL abwickeln. Die XS-Schnittstelle für OpenGL ist sehr dünn, selbst das Texturformat wird von Perl aufbereitet und einem direkten glTexImage-Aufruf übergeben: glTexImage2D GL_TEXTURE_2D, 0, $self->{internalformat}, $tw, $th, 0, $self->{format}, $self->{type}, $data; Das Toolkit selbst ist ebenfalls in Perl geschrieben und lehnt sich im Design an Toolkits wie Gtk+ oder Tk an. Um den transparenten unteren Teil des Clients (mit "Floorbox" und HP/Mana- etc. Pegel) zu erzeugen, wird z.B. folgender Code verwendet: sub make_gauge_window { my $win = new CFPlus::UI::Frame; $win->add (my $hbox = new CFPlus::UI::HBox children => [ (new CFPlus::UI::HBox expand => 1), (new CFPlus::UI::VBox children => [ (new CFPlus::UI::Empty expand => 1), (new CFPlus::UI::Frame bg => [0, 0, 0, 0.4], child => new CFPlus::UI::Table), ]), (my $vbox = new CFPlus::UI::VBox), ], ); $vbox->add (new CFPlus::UI::HBox expand => 1, children => [ (new CFPlus::UI::Empty expand => 1), (my $hb = new CFPlus::UI::HBox), ], ); $hb->add (my $hg = new CFPlus::UI::Gauge type => 'hp', tooltip => "#stat_health"); $hb->add (my $mg = new CFPlus::UI::Gauge type => 'mana', tooltip => "#stat_mana"); $hb->add (my $gg = new CFPlus::UI::Gauge type => 'grace', tooltip => "#stat_grace"); $hb->add (my $fg = new CFPlus::UI::Gauge type => 'food', tooltip => "#stat_food"); $vbox->add (my $exp = new CFPlus::UI::Label valign => 0, align => 1, can_hover => 1, can_events => 1, tooltip => "#stat_exp"); $vbox->add (my $rng = new CFPlus::UI::Label valign => 0, align => 1, can_hover => 1, can_events => 1, tooltip => "#stat_ranged"); } Eingabefelder fuktionieren ähnlich einfach: $vbox->add ( $HOST_ENTRY = new CFPlus::UI::Entry expand => 1, text => $CFG->{profile}{default}{host}, tooltip => "The hostname of the server to connect to", on_changed => sub { my ($self, $value) = @_; $CFG->{profile}{default}{host} = $value; 0 } ); Widgets selbst lassen sich in Perl erstaunlich kompakt realisieren. Als nichtriviales Beispiel soll hier kurz der gesamte Code für das Checkbox-Widget gezeigt werden: =begin latex \begin{center} \includegraphics{cfplus/checkbox.eps} \end{center} =end latex package CFPlus::UI::CheckBox; Die C-Basisklasse zeichnet lediglich einen (optional transparenten) Hintergrund und wird von vielen Widgets benutzt um einen definierten Untergrund zu erhalten: our @ISA = CFPlus::UI::DrawBG::; Hier werden die Texturen geladen, es gibt derer zwei, für aktiv/nicht aktiv: my @tex = map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 } qw(c1_checkbox_bg.png c1_checkbox_active.png); use CFPlus::OpenGL; Der Konstruktor erwartet Key-Value-Paare und setzt die Defaults: sub new { my $class = shift; $class->SUPER::new ( padding_x => 2, padding_y => 2, fg => [1, 1, 1], active_fg => [1, 1, 0], bg => [0, 0, 0, 0.2], active_bg => [1, 1, 1, 0.5], state => 0, can_hover => 1, @_ ) } C wird vom Layout-Algorithmus benutzt um die Wunschgröße eines Widgets in Erfahrung zu bringen. Checkboxen sind typischerweise unproblematisch, daher wird immer 6x6 angefordert. sub size_request { my ($self) = @_; (6) x 2 } C ist eine Hilfsfunktion, mit der man die Checkbox togglen kann bzw. die bei einer Änderung ein Signal auslöst an das man sich binden kann. C sorgt dafür, daß das gesamte Fenster neugezeichnet wird. OpenGL arbeitet indem es bei jeder Änderung den kompletten Schirm neu aufbaut. Anders als z.B. ego-shooter rendert CFPlus nicht so schnell die Hardware und Software es hergeben, sondern nur dann, wenn es wirklich Änderungen gab (CFPlus läuft bei uns den ganzen Tag, da ist es hilfreich, wenn es wenig Rechenzeit verbraucht :). sub toggle { my ($self) = @_; $self->{state} = !$self->{state}; $self->emit (changed => $self->{state}); $self->update; } Durch C-Methoden werden Eingabevents an die Widgets gereicht, hier der C-Event, mit dme man die Checkbox togglen kann. Die Methode prüft, ob der Klick im richtigen Bereich liegt und ruft gegebenenfalls C auf: sub invoke_button_down { my ($self, $ev, $x, $y) = @_; if ($x >= $self->{padding_x} && $x < $self->{w} - $self->{padding_x} && $y >= $self->{padding_y} && $y < $self->{h} - $self->{padding_y}) { $self->toggle; } else { return 0 } 1 } Die C<_draw>-Methode muss das Widget zeichnen, und zwar bei jedem Aufruf komplett. Diese Implementation ruft zuerst die SUPEr-Methode auf (von DrawBG) und benutzt dann C, eine Hilfsfunktion, die eine häufig benutze Folge von OpenGL-Aufrufen (glBlendFunc, einige glEnable, glAlphaFunc, glBindTexture, glBegin, glTexCoord, glVertex usf.) in C implementiert, was (hoffentlich) einen Geschwindigkeitsvorteil bringt (Funktionsaufrufe sind in Perl vergleichsweise kostpielig) aber vor allem den immer wiederkehrenden "mal mir die Textur dorthin"-Vorgang verkürzt (draw_quad_alpha wird allein im Toolkit-Code 13 mal aufgerufen). Je nachdem, ob die Checkbox gerade den Mouse-Focus hat oder nicht wird ausserdem mit einer anderen Farbe gemischt: sub _draw { my ($self) = @_; $self->SUPER::_draw; glTranslate $self->{padding_x} + 0.375, $self->{padding_y} + 0.375, 0; my ($w, $h) = @$self{qw(w h)}; my $s = List::Util::min $w - $self->{padding_x} * 2, $h - $self->{padding_y} * 2; glColor @{ $FOCUS == $self ? $self->{active_fg} : $self->{fg} }; my $tex = $self->{state} ? $tex[1] : $tex[0]; glEnable GL_TEXTURE_2D; $tex->draw_quad_alpha (0, 0, $s, $s); glDisable GL_TEXTURE_2D; } Der gesamte Toolkit-Code umfaßt zur Zeit 4000 Zeilen, bietet aber so komplexe Widgets wie einen Texteditor, Notebooks oder einen Pod-Viewer mit Bild- und Hyperlinkfähigkeit. =head2 Der Server Der Server ist, wie eingangs erwähnt, im Kern in C++ geschrieben. Der C++-Teil kümmert sich um die meisten zeitkritischen Aufgaben wie Wegefindung und Monsterbewegung (bei 10 Spielern sind im Schnitt ca. 20000 Objekte aktiv, die alle 120ms aufmerksamkeit erfordern, der Server verträgt momentan ca. 200000 aktive Objekte). Alles "höherwertige" wie die meisten Kommunikationskommandos und Administratives wie Login sind in Perl implementiert, wieso auch das Laden und Speichern von Dateien. =head3 Asynchrones Laden und Speichern Letzteres wurde zu einem Problem: lief im Hintergrund ein Backup stockte der Server bei jedem Mapwechseln - bei 20 Spielern gleichzeitig also andauernd. I/O in einen eigenen Thread zu verlegen schied aus, da zu viele globale Datenstrukturen existierten und verändert werden müssten und der Server-Code an allen Ecken und enden statische Variablen benutzte. Daher verlegten wir uns auf eine Lösung mit IO::AIO für den eigentliche I/O und Coro, um langwierige Vorgänge quasiparallel auszuführen ohne uns um fine-grained Locking Gedanken machen zu müssen und trotzdem noch rechtzeitig alle 120ms einen Update generieren zu können. Anders als Crossfire (bei dem alle Karten die bei einem Crash aktiv waren verlorengehen) schreibt Crossfire+ Karten und Spielerdaten ca. alle 20 Sekunden auf die Festplatte (zusätzlich schreibt es auch bei Crashes im allgemeinen alle Daten fehlerfrei auf die Festplatte bevor der Server sich beendet). Wie so vieles, ist dies durch eine Server-Erweiterung realisiert, den sogenannten C, die im wesentlichen aus einer Coroutine besteht die regelmäßig Karten speichert bzw. ganz aus dem Speicher löscht oder gar Karten zurücksetzt. C erzeugt eine Coroutine die bei Entfernen oder Neuladen der Extension automatisch gelöscht wird: our $SCHEDULER = cf::async_ext { Der Scheduler läuft einmal all paar Sekunden, und das immer wieder: my $schedule_interval = Coro::Event->timer (after => 1, interval => $SCHEDULE_INTERVAL); while () { $schedule_interval->next; Iteriert dabei über alle Karten: my @maps = keys %cf::MAP; for (@maps) { Möglicherweise wurde eine Karte schon gelöscht (zwischen dem keys-Aufruf und dem benutzen der Keys können viele Sekunden liegen): my $map = $cf::MAP{$_} or next; $map->valid or next; Falls etwas schiefgeht, logge den Fehler statt den (sehr wichtigen) Scheduler zu beenden: eval { Karten werden regelmäßig auf ihren Ursprungszustand zurückgesetzt, sonst gäbe es bald keine Monster zum töten mehr: if ($map->should_reset) { $map->reset; Wenn die Karte im Speicher ist, länger nicht benutzt wurde und keine Spieler auf ihr sind, speichere sie und lösche sie aus dem Speicher um die Last auf den Server zu verringern: } elsif ($map->in_memory == cf::MAP_IN_MEMORY) { if ($map->last_access + $SWAP_TIMEOUT <= $cf::RUNTIME && !$map->players) { $map->swap_out; Ansonsten speichere die Map einfach alle $SAVE_TIMEOUT Sekunden: } elsif ($map->{last_save} + $SAVE_TIMEOUT <= $cf::RUNTIME) { $map->save; } } }; warn $@ if $@; Gib anderen Teilen des Servers Rechenzeit (dies ist sehr wahrscheinlich unnötig da dies beim Speichern automatisch passiert und das iterieren durch alle Maps sehr wahrscheinlich nie lange dauert, aber es schadet nicht): Coro::cede; }; } }; =head3 Objekte steuern Eines der wenigen neuen Features von Crossfire aus jüngerer Zeit sind Transporte: Objekte, die von Spielern gesteuert werden können (z.B. Schiffe). Die Implementation in Crossfire bestand aus einem 23kB großem Patch an vielen Dateien und führte erwartunsggemäß alle paar Tage zu einem crash, weshalb er in Crossfire+ durch eine (weniger featurevolles dafür aber stabiles) Extension ersetzt wurde. Um eine Extension an ein ein Objekt zu binden, setzt man auf den "Archetyp" des Objektes (das ist so etwas wie das ursprüngliche Objekt von dem Kopien erstellt werden) ein C-Attribut, oder bindet sich gleich an alle Objekte einer bestimmten Klasse (z.b. C), wie hier: cf::object->attach ( type => cf::TRANSPORT, on_apply => sub { my ($tr, $ob) = @_; return unless $ob->type == cf::PLAYER; if ($ob->contr->attached ("transport_player_steer")) { $ob->message ("You stop steering the " . $tr->name . "."); $ob->contr->detach ("transport_player_steer"); } else { $ob->message ("You now steer the " . $tr->name . "."); $ob->contr->attach ("transport_player_steer"); } cf::override; }, ); Der obige Aufruf von C bindet sich auf den B-Event aller Objekte vom Typ C. Beim ersten Apply wird dynamisch eine weitere Erweiterung gebunden, "transport_player_steer" die den Spieler selbst bewegungsunfähig macht und statt dem Spielerobjekte den Transport (z.B. das Schiff) bewegt. Extensions können einen Namen erhalten auf die man sich z.B. beim Erstelle von Karten und neuen Objekten wie Monstern beziehen kann. Die "transport_player_steer"-Erweiterung selbst ist ganz einfach: cf::player::attachment transport_player_steer => on_move => sub { my ($pl, $dir) = @_; my $ob = $pl->ob; if (my $tr = find_transport $pl) { my @ontop; for (my $tr = $tr; $tr; $tr = $tr->more) { for (my $ob = $tr->above; $ob; $ob = $ob->above) { push @ontop, $ob; } } if ($tr->move ($dir, $ob)) { # do multiple loops in case some player/item blocks another # do not endlessly loop as the server is far too broken, e.g. # you can drop floors on top of non-floors etc. for (1..50) { @ontop or last; @ontop = map $_->move ($dir, $_) ? () : $_, @ontop; } } cf::override; } }, ; Sie fängt nur einen Event ab: B, der aufgerufen wird, wenn der Spieler ein Bewegungskommando auslöst. Sie sucht den Transport der zum Spieler gehört und merkt sich, was alles "auf" dem Transport liegt, in @ontop. Danach versucht es den Transport zu bewegen und, falls dies klappt (ein Schiff kann nicht auf Land segeln, zumindest nicht überall :), auch alle @ontop-Objekte. Da sich diese im Weg sein können, müsste man sie entwder sortieren oder, wie hier, man versucht es so lange bis sich alle erfolgreich bewegt haben (oder bis zu 50 Iterationen - lieber verlieren wir ein paar Objekte als den Server zu freezen. Echtzeit-Spiele-Server verlangen grundsätzlich nach anderen Herangehensweisen für Stabilitätsprobleme...) =head2 Links Die Crossfire+-Homepage: http://cf.schmorp.de/ Der CFPlus-Client: http://cf.schmorp.de/client.shtml Alle Karten von Crossfire+, online: http://cfmaps.schmorp.de/ Die Crossfire-Homepage, mit Handbuch und vielen Hintegrundmaterialien: http://crossfire.real-time.com/ =head2 Autor Marc Lehmann