ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/cvsroot/docs/pws2007/cfplus.pod
Revision: 1.2
Committed: Sat Jul 18 05:59:11 2009 UTC (17 years, 2 months ago) by root
Branch: MAIN
CVS Tags: HEAD
Changes since 1.1: +1 -1 lines
Log Message:
riddify us of meta.yml garbage in manifest

File Contents

# Content
1 =encoding utf-8
2
3 =head1 Crossfire+ - ein graphisches MORPG mit Perl
4
5 Crossfire+ ist ein stark von Nethack inspiriertes, graphisches
6 Multiplayer-Online-RPG und gehört in die (sehr kleine) Klasse von
7 Echtzeit-Multiplayer-Roguelike-Spielen. Im Gegensatz zu den meisten
8 Roguelikes besitzt es nicht nur zufällig generierte Karten sondern auch
9 über 5000 handgemachte Karten mit Rätseln und Quests.
10
11 =begin latex
12
13 \begin{center}
14 \includegraphics{cfplus/scr2.eps}
15 \end{center}
16
17 =end latex
18
19 =head2 Entstehung
20
21 Crossfire+ ist aus Crossfire entstanden, das wiederum um 1994 entstanden
22 ist. Um 2005 waren jedoch die meisten der Originalentwickler verschwunden,
23 der Server crashte bei Benutzung täglich und die übriggebliebenen
24 Maintainer waren nicht in der Lage, vorhandene Probleme zu lösen. Daher
25 wurde 2005 Crossfire+ gegründet, ein Fork, der das Ziel hat, den (nicht
26 mehr zeitgemäßen) Programmcode zu modernisieren und stabiler zu machen.
27
28 =head2 Features
29
30 Crossfire+ ist im Gegensatz zu Crossfire in C++ geschrieben und
31 wesentliche Teile des Servers sind in Perl gehalten, was der Stabilität
32 und der Erweiterbarkeit sehr zugute gekommen ist. So fängt Crossfire
33 unter Last vieler Spieler an zu ruckeln, da es Karten synchron nachlädt,
34 während Crossfire+ Laden und Speichern aller Daten im Hintergrund
35 vornimmt (dank IO::AIO und Coro mit vertretbarem Aufwand). Das verhindert
36 Ruckler, und dank des neugeschriebenen Parsers ist das Laden bis zu
37 fünffach schneller.
38
39 Doch auch das Objektsystem wurde stark verbessert und vor allem mit Perl
40 kombiniert: An Karten, Monster, Spieler und viele andere Objekte lassen
41 sich Perl-Erweiterungen binden die viele Details des Verhaltens regeln
42 können.
43
44 Zeitgleich wurde ein auf OpenGL-basierender und in Perl gschriebener
45 neuer Client geschaffen (es gibt derzeit ca. 7 verschiedene Clients für
46 unterschiedlichste Platformen). Um platformunabhägig zu bleiben, musste
47 ein eigenes OpenGL-Toolkit implementiert werden.
48
49 Die Welt von Crossfire+, von oben :)
50
51 =begin latex
52
53 \begin{center}
54 \includegraphics{cfplus/world.eps}
55 \end{center}
56
57 =end latex
58
59 =head2 Der Client
60
61 Scorn bei Nacht, in CFPlus:
62
63 =begin latex
64
65 \begin{center}
66 \includegraphics{cfplus/scr1.eps}
67 \end{center}
68
69 =end latex
70
71 Wie schon erwähnt, wurde der Client größtenteils in Perl
72 geschrieben. Ausnahmen sind der SDL- und SDL_Mixer-Code sowie
73 die Schnitstelle zu OpenGL und einige Hilfsfunktionen (wie ein
74 Pango-OpenGL-Renderer).
75
76 Die Wahl fiel auf SDL um Platformunabhängigkeit zu erreichen. Leider kann
77 man SDL_Perl seit Umstellung auf Module::Build wie viele andere derartige
78 Module nicht mehr auf nicht-Unix-System zum Laufen bringen, weshalb wir
79 gezwungen waren, unseren eigenen SDL-Schnitstellencode zu schreiben, was
80 uzm Glück sehr einfach war, da´ wir SDL nur benutzen um ein Fenster mit
81 OpenGL-Fähigkeit zu erhalten und dannach alles über OpenGL abwickeln. Die XS-Schnittstelle für
82 OpenGL ist sehr dünn, selbst das Texturformat wird von Perl aufbereitet und einem direkten glTexImage-Aufruf übergeben:
83
84 glTexImage2D GL_TEXTURE_2D, 0,
85 $self->{internalformat},
86 $tw, $th,
87 0,
88 $self->{format},
89 $self->{type},
90 $data;
91
92 Das Toolkit selbst ist ebenfalls in Perl geschrieben und lehnt sich im
93 Design an Toolkits wie Gtk+ oder Tk an. Um den transparenten unteren Teil
94 des Clients (mit "Floorbox" und HP/Mana- etc. Pegel) zu erzeugen, wird
95 z.B. folgender Code verwendet:
96
97 sub make_gauge_window {
98 my $win = new CFPlus::UI::Frame;
99
100 $win->add (my $hbox = new CFPlus::UI::HBox
101 children => [
102 (new CFPlus::UI::HBox expand => 1),
103 (new CFPlus::UI::VBox children => [
104 (new CFPlus::UI::Empty expand => 1),
105 (new CFPlus::UI::Frame bg => [0, 0, 0, 0.4],
106 child => new CFPlus::UI::Table),
107 ]),
108 (my $vbox = new CFPlus::UI::VBox),
109 ],
110 );
111
112 $vbox->add (new CFPlus::UI::HBox
113 expand => 1,
114 children => [
115 (new CFPlus::UI::Empty expand => 1),
116 (my $hb = new CFPlus::UI::HBox),
117 ],
118 );
119
120 $hb->add (my $hg = new CFPlus::UI::Gauge type => 'hp', tooltip => "#stat_health");
121 $hb->add (my $mg = new CFPlus::UI::Gauge type => 'mana', tooltip => "#stat_mana");
122 $hb->add (my $gg = new CFPlus::UI::Gauge type => 'grace', tooltip => "#stat_grace");
123 $hb->add (my $fg = new CFPlus::UI::Gauge type => 'food', tooltip => "#stat_food");
124
125 $vbox->add (my $exp = new CFPlus::UI::Label
126 valign => 0, align => 1, can_hover => 1,
127 can_events => 1, tooltip => "#stat_exp");
128 $vbox->add (my $rng = new CFPlus::UI::Label
129 valign => 0, align => 1, can_hover => 1,
130 can_events => 1, tooltip => "#stat_ranged");
131 }
132
133 Eingabefelder fuktionieren ähnlich einfach:
134
135 $vbox->add (
136 $HOST_ENTRY = new CFPlus::UI::Entry
137 expand => 1,
138 text => $CFG->{profile}{default}{host},
139 tooltip => "The hostname of the server to connect to",
140 on_changed => sub {
141 my ($self, $value) = @_;
142 $CFG->{profile}{default}{host} = $value;
143 0
144 }
145 );
146
147 Widgets selbst lassen sich in Perl erstaunlich kompakt realisieren. Als
148 nichtriviales Beispiel soll hier kurz der gesamte Code für das
149 Checkbox-Widget gezeigt werden:
150
151 =begin latex
152
153 \begin{center}
154 \includegraphics{cfplus/checkbox.eps}
155 \end{center}
156
157 =end latex
158
159 package CFPlus::UI::CheckBox;
160
161 Die C<DrawBG>-Basisklasse zeichnet lediglich einen (optional
162 transparenten) Hintergrund und wird von vielen Widgets benutzt um einen
163 definierten Untergrund zu erhalten:
164
165 our @ISA = CFPlus::UI::DrawBG::;
166
167 Hier werden die Texturen geladen, es gibt derer zwei, für aktiv/nicht
168 aktiv:
169
170 my @tex =
171 map { new_from_file CFPlus::Texture CFPlus::find_rcfile $_, mipmap => 1 }
172 qw(c1_checkbox_bg.png c1_checkbox_active.png);
173
174 use CFPlus::OpenGL;
175
176 Der Konstruktor erwartet Key-Value-Paare und setzt die Defaults:
177
178 sub new {
179 my $class = shift;
180
181 $class->SUPER::new (
182 padding_x => 2,
183 padding_y => 2,
184 fg => [1, 1, 1],
185 active_fg => [1, 1, 0],
186 bg => [0, 0, 0, 0.2],
187 active_bg => [1, 1, 1, 0.5],
188 state => 0,
189 can_hover => 1,
190 @_
191 )
192 }
193
194 C<size_request> wird vom Layout-Algorithmus benutzt um die Wunschgröße
195 eines Widgets in Erfahrung zu bringen. Checkboxen sind typischerweise
196 unproblematisch, daher wird immer 6x6 angefordert.
197
198 sub size_request {
199 my ($self) = @_;
200
201 (6) x 2
202 }
203
204 C<toggle> ist eine Hilfsfunktion, mit der man die Checkbox togglen kann
205 bzw. die bei einer Änderung ein Signal auslöst an das man sich binden
206 kann. C<update> sorgt dafür, daß das gesamte Fenster neugezeichnet
207 wird. OpenGL arbeitet indem es bei jeder Änderung den kompletten
208 Schirm neu aufbaut. Anders als z.B. ego-shooter rendert CFPlus nicht so
209 schnell die Hardware und Software es hergeben, sondern nur dann, wenn es
210 wirklich Änderungen gab (CFPlus läuft bei uns den ganzen Tag, da ist es
211 hilfreich, wenn es wenig Rechenzeit verbraucht :).
212
213 sub toggle {
214 my ($self) = @_;
215
216 $self->{state} = !$self->{state};
217 $self->emit (changed => $self->{state});
218 $self->update;
219 }
220
221 Durch C<invoke_>-Methoden werden Eingabevents an die Widgets gereicht,
222 hier der C<button_down>-Event, mit dme man die Checkbox togglen kann. Die
223 Methode prüft, ob der Klick im richtigen Bereich liegt und ruft
224 gegebenenfalls C<toggle> auf:
225
226 sub invoke_button_down {
227 my ($self, $ev, $x, $y) = @_;
228
229 if ($x >= $self->{padding_x} && $x < $self->{w} - $self->{padding_x}
230 && $y >= $self->{padding_y} && $y < $self->{h} - $self->{padding_y}) {
231 $self->toggle;
232 } else {
233 return 0
234 }
235
236 1
237 }
238
239 Die C<_draw>-Methode muss das Widget zeichnen, und zwar bei jedem Aufruf
240 komplett. Diese Implementation ruft zuerst die SUPEr-Methode auf (von
241 DrawBG) und benutzt dann C<draw_quad_alpha>, eine Hilfsfunktion, die eine
242 häufig benutze Folge von OpenGL-Aufrufen (glBlendFunc, einige glEnable,
243 glAlphaFunc, glBindTexture, glBegin, glTexCoord, glVertex usf.) in C
244 implementiert, was (hoffentlich) einen Geschwindigkeitsvorteil bringt
245 (Funktionsaufrufe sind in Perl vergleichsweise kostpielig) aber vor allem
246 den immer wiederkehrenden "mal mir die Textur dorthin"-Vorgang verkürzt
247 (draw_quad_alpha wird allein im Toolkit-Code 13 mal aufgerufen).
248
249 Je nachdem, ob die Checkbox gerade den Mouse-Focus hat oder nicht wird
250 ausserdem mit einer anderen Farbe gemischt:
251
252 sub _draw {
253 my ($self) = @_;
254
255 $self->SUPER::_draw;
256
257 glTranslate $self->{padding_x} + 0.375, $self->{padding_y} + 0.375, 0;
258
259 my ($w, $h) = @$self{qw(w h)};
260 my $s = List::Util::min $w - $self->{padding_x} * 2, $h - $self->{padding_y} * 2;
261
262 glColor @{ $FOCUS == $self ? $self->{active_fg} : $self->{fg} };
263 my $tex = $self->{state} ? $tex[1] : $tex[0];
264 glEnable GL_TEXTURE_2D;
265 $tex->draw_quad_alpha (0, 0, $s, $s);
266 glDisable GL_TEXTURE_2D;
267 }
268
269 Der gesamte Toolkit-Code umfaßt zur Zeit 4000 Zeilen, bietet aber so
270 komplexe Widgets wie einen Texteditor, Notebooks oder einen Pod-Viewer mit
271 Bild- und Hyperlinkfähigkeit.
272
273 =head2 Der Server
274
275 Der Server ist, wie eingangs erwähnt, im Kern in C++ geschrieben. Der
276 C++-Teil kümmert sich um die meisten zeitkritischen Aufgaben wie
277 Wegefindung und Monsterbewegung (bei 10 Spielern sind im Schnitt ca. 20000
278 Objekte aktiv, die alle 120ms aufmerksamkeit erfordern, der Server
279 verträgt momentan ca. 200000 aktive Objekte).
280
281 Alles "höherwertige" wie die meisten Kommunikationskommandos und
282 Administratives wie Login sind in Perl implementiert, wieso auch das Laden
283 und Speichern von Dateien.
284
285 =head3 Asynchrones Laden und Speichern
286
287 Letzteres wurde zu einem Problem: lief im Hintergrund ein Backup stockte
288 der Server bei jedem Mapwechseln - bei 20 Spielern gleichzeitig also
289 andauernd. I/O in einen eigenen Thread zu verlegen schied aus, da zu viele
290 globale Datenstrukturen existierten und verändert werden müssten und der
291 Server-Code an allen Ecken und enden statische Variablen benutzte. Daher
292 verlegten wir uns auf eine Lösung mit IO::AIO für den eigentliche I/O
293 und Coro, um langwierige Vorgänge quasiparallel auszuführen ohne uns
294 um fine-grained Locking Gedanken machen zu müssen und trotzdem noch
295 rechtzeitig alle 120ms einen Update generieren zu können.
296
297 Anders als Crossfire (bei dem alle Karten die bei einem Crash aktiv waren
298 verlorengehen) schreibt Crossfire+ Karten und Spielerdaten ca. alle 20
299 Sekunden auf die Festplatte (zusätzlich schreibt es auch bei Crashes im
300 allgemeinen alle Daten fehlerfrei auf die Festplatte bevor der Server sich
301 beendet).
302
303 Wie so vieles, ist dies durch eine Server-Erweiterung realisiert, den
304 sogenannten C<map-scheduler>, die im wesentlichen aus einer Coroutine
305 besteht die regelmäßig Karten speichert bzw. ganz aus dem Speicher
306 löscht oder gar Karten zurücksetzt. C<async_ext> erzeugt eine Coroutine
307 die bei Entfernen oder Neuladen der Extension automatisch gelöscht wird:
308
309 our $SCHEDULER = cf::async_ext {
310
311 Der Scheduler läuft einmal all paar Sekunden, und das immer wieder:
312
313 my $schedule_interval = Coro::Event->timer (after => 1, interval => $SCHEDULE_INTERVAL);
314 while () {
315 $schedule_interval->next;
316
317 Iteriert dabei über alle Karten:
318
319 my @maps = keys %cf::MAP;
320 for (@maps) {
321
322 Möglicherweise wurde eine Karte schon gelöscht (zwischen dem keys-Aufruf
323 und dem benutzen der Keys können viele Sekunden liegen):
324
325 my $map = $cf::MAP{$_}
326 or next;
327 $map->valid or next;
328
329 Falls etwas schiefgeht, logge den Fehler statt den (sehr wichtigen) Scheduler zu beenden:
330
331 eval {
332
333 Karten werden regelmäßig auf ihren Ursprungszustand zurückgesetzt,
334 sonst gäbe es bald keine Monster zum töten mehr:
335
336 if ($map->should_reset) {
337 $map->reset;
338
339 Wenn die Karte im Speicher ist, länger nicht benutzt wurde und keine
340 Spieler auf ihr sind, speichere sie und lösche sie aus dem Speicher um
341 die Last auf den Server zu verringern:
342
343 } elsif ($map->in_memory == cf::MAP_IN_MEMORY) {
344 if ($map->last_access + $SWAP_TIMEOUT <= $cf::RUNTIME && !$map->players) {
345 $map->swap_out;
346
347 Ansonsten speichere die Map einfach alle $SAVE_TIMEOUT Sekunden:
348
349 } elsif ($map->{last_save} + $SAVE_TIMEOUT <= $cf::RUNTIME) {
350 $map->save;
351 }
352 }
353 };
354 warn $@ if $@;
355
356 Gib anderen Teilen des Servers Rechenzeit (dies ist sehr wahrscheinlich
357 unnötig da dies beim Speichern automatisch passiert und das iterieren
358 durch alle Maps sehr wahrscheinlich nie lange dauert, aber es schadet
359 nicht):
360
361 Coro::cede;
362 };
363 }
364 };
365
366 =head3 Objekte steuern
367
368 Eines der wenigen neuen Features von Crossfire aus jüngerer Zeit
369 sind Transporte: Objekte, die von Spielern gesteuert werden können
370 (z.B. Schiffe). Die Implementation in Crossfire bestand aus einem 23kB
371 großem Patch an vielen Dateien und führte erwartunsggemäß alle
372 paar Tage zu einem crash, weshalb er in Crossfire+ durch eine (weniger
373 featurevolles dafür aber stabiles) Extension ersetzt wurde.
374
375 Um eine Extension an ein ein Objekt zu binden, setzt man auf den
376 "Archetyp" des Objektes (das ist so etwas wie das ursprüngliche Objekt
377 von dem Kopien erstellt werden) ein C<attach>-Attribut, oder bindet sich
378 gleich an alle Objekte einer bestimmten Klasse (z.b. C<TRANSPORT>), wie
379 hier:
380
381 cf::object->attach (
382 type => cf::TRANSPORT,
383 on_apply => sub {
384 my ($tr, $ob) = @_;
385
386 return unless $ob->type == cf::PLAYER;
387
388 if ($ob->contr->attached ("transport_player_steer")) {
389 $ob->message ("You stop steering the " . $tr->name . ".");
390 $ob->contr->detach ("transport_player_steer");
391 } else {
392 $ob->message ("You now steer the " . $tr->name . ".");
393 $ob->contr->attach ("transport_player_steer");
394 }
395
396 cf::override;
397 },
398 );
399
400 Der obige Aufruf von C<attach> bindet sich auf den B<appl>-Event aller
401 Objekte vom Typ C<TRANSPORT>. Beim ersten Apply wird dynamisch eine
402 weitere Erweiterung gebunden, "transport_player_steer" die den Spieler
403 selbst bewegungsunfähig macht und statt dem Spielerobjekte den Transport
404 (z.B. das Schiff) bewegt.
405
406 Extensions können einen Namen erhalten auf die man sich z.B. beim
407 Erstelle von Karten und neuen Objekten wie Monstern beziehen kann. Die
408 "transport_player_steer"-Erweiterung selbst ist ganz einfach:
409
410 cf::player::attachment transport_player_steer =>
411 on_move => sub {
412 my ($pl, $dir) = @_;
413
414 my $ob = $pl->ob;
415
416 if (my $tr = find_transport $pl) {
417 my @ontop;
418
419 for (my $tr = $tr; $tr; $tr = $tr->more) {
420 for (my $ob = $tr->above; $ob; $ob = $ob->above) {
421 push @ontop, $ob;
422 }
423 }
424
425 if ($tr->move ($dir, $ob)) {
426 # do multiple loops in case some player/item blocks another
427 # do not endlessly loop as the server is far too broken, e.g.
428 # you can drop floors on top of non-floors etc.
429 for (1..50) {
430 @ontop or last;
431 @ontop = map $_->move ($dir, $_) ? () : $_, @ontop;
432 }
433 }
434
435 cf::override;
436 }
437 },
438 ;
439
440 Sie fängt nur einen Event ab: B<move>, der aufgerufen wird, wenn der
441 Spieler ein Bewegungskommando auslöst. Sie sucht den Transport der zum
442 Spieler gehört und merkt sich, was alles "auf" dem Transport liegt, in
443 @ontop.
444
445 Danach versucht es den Transport zu bewegen und, falls dies klappt
446 (ein Schiff kann nicht auf Land segeln, zumindest nicht überall
447 :), auch alle @ontop-Objekte. Da sich diese im Weg sein können,
448 müsste man sie entwder sortieren oder, wie hier, man versucht es
449 so lange bis sich alle erfolgreich bewegt haben (oder bis zu 50
450 Iterationen - lieber verlieren wir ein paar Objekte als den Server zu
451 freezen. Echtzeit-Spiele-Server verlangen grundsätzlich nach anderen
452 Herangehensweisen für Stabilitätsprobleme...)
453
454 =head2 Links
455
456 Die Crossfire+-Homepage:
457
458 http://cf.schmorp.de/
459
460 Der CFPlus-Client:
461
462 http://cf.schmorp.de/client.shtml
463
464 Alle Karten von Crossfire+, online:
465
466 http://cfmaps.schmorp.de/
467
468 Die Crossfire-Homepage, mit Handbuch und vielen Hintegrundmaterialien:
469
470 http://crossfire.real-time.com/
471
472 =head2 Autor
473
474 Marc Lehmann <crossfire@schmorp.de>
475
476