Hexaverse

Kommentare anzeigen Worklog abonnieren
Gehe zu Seite 1, 2  Weiter

Worklogs Hexaverse

Schwierigkeitsgrade

Samstag, 27. Oktober 2012 von Xeres
Ein Thema, dass ich bei meinen Designideen noch nicht ausführlich erwähnt habe, betrifft das Thema Schwierigkeit.
Spiele, die eine Geschichte erzählen, brauchen Speicherstände – wenn man schon nicht überall speichern kann, dann in regelmäßigen Abständen. Niemand würde gerne Half Live wieder ganz von vorne Anfangen, weil man einen Sprung nicht geschafft hat.
In Jump & Runs ist es okay einmal zu sterben – z.B. wenn man eine Plattform verfehlt – und dann wieder vom Anfang des Levels zu starten. Das ist genug Bestrafung für den Spieler, damit er aufpasst, aber nicht so abschreckend, dass er es nicht noch mal versuchen würde (nachdem er seine Fassung wiedergefunden hat).

Das sind alles Spiele, die feste Level haben – sie wurden von Leveldesignern zusammen gebaut und sind für jeden Spieler gleich. Wenn die Geschichte alternative Pfade hat, gibt es mehr Level oder Content für jede Entscheidungsmöglichkeit, die der Spieler treffen kann. Aber: Im Prinzip kann der Spieler mit genug Zeit das Spiel in jeder Variation durchspielen. Natürlich soll der Spieler auch in den Genuss der Arbeitsleistung der Designer kommen und sollte sich nicht betrogen fühlen, weil er nicht alles im Spiel erreichen kann.

Allerdings kommt die Freiheit zu speichern und laden zu einem Preis: Entscheidungen verlieren schnell an Gewicht. In Fallout 3 basiert der Erfolg von Hacking oder Gesprächsoptionen auf Glück - und euren Fähigkeiten. Wenn man aber so lange laden und nochmal versuchen kann wie man möchte – wo ist der Zwang, seine Fähigkeiten hoch zu trainieren?
Natürlich braucht man sich nur daran zu halten, nur neu zu laden, wenn man gestorben ist… aber warum erlaubt das Spiel dann überhaupt etwas anderes? Das Spiel sollte die Regeln vorgeben, und dabei so restriktiv sein, keine einfachen Abkürzungen zu erlauben.

In Prozedural generierten Spielen, gibt es kein Problem damit, wenn Spieler nie das letzte Level zu sehen bekommen. Es ist in jedem Fall ein anderes in jedem Spiel. Ziel ist es nicht mehr einfach nach einer beliebigen Zeit zum Ende zu kommen, sondern tatsächlich das Spiel zu meistern.
Die Schwierigkeitsgrade für Hexaverse werden vielleicht einmal so aussehen:

Normal
- Permadeath
- Das Spiel wird nur gespeichert, wenn man es beendet.
- Wenn man stirbt, wird der Spieler mit erreichten Errungenschaften in einen Highscore eingetragen.

Simple
- Laden und Speichern von ~3 Spielständen wenn man sicher in einem Bewohnten System angedockt ist.

Custom
- Laden und Speichern überall nach Belieben.
- Man kann Dinge wie z.B. die Stärke von Gegnern in 5% Schritten nach eigenem ermessen einstellen.

Das sollte Anfängern und Fortgeschrittenen Spielern die Chance geben, am Spiel gleichermaßen Spaß zu haben, wobei die Anfänger encouragiert werden, das „richtige“ Spiel zu versuchen, indem es dort mehr Belohnungen für ein beendetes Spiel gibt.

Alpha 0.4 - „Full Power“

Mittwoch, 23. Mai 2012 von Xeres
Willkommen zurück!
Viele, kleine Änderungen summieren sich irgendwann zu einem vorzeigbarem ganzen! Hoffe ich jedenfalls. Man sieht relativ wenig von den großartigen Neuerungen.

Multiple Eigenschaften
Planeten aus der Restmasse der Sonnen zu formen hat sich als nicht besonders schlau erwiesen – selbst ein tausendstell eines Prozents bildet mehr Gasriesen, als einem lieb sein kann. Erst den Abstand und daraus Strahlungsintensität der Sonne (= Temperatur) zu ermitteln und darüber die Klasse zu bestimmen – auch keine tollen Ergebnisse. Immer gibt es zu viel oder zu wenig. Zudem fehlten Planeten noch wichtige Eigenschaften wie Atmosphäre und Ressourcen.
Alles lässt sich viel einfacher und geschickter lösen, wenn man alles in kleine Teilbereiche unterteilt und - genau wie bei der Sternenverteilung - dem Zufall die Arbeit überlässt.
Planetenklassen setzen sich nun also aus verschiedenen Wahrscheinlichkeiten für Masse, Temperatur, Atmosphäre und Ressourcen zusammen. Davon abgeleitet wird das Leben auf dem Planeten bestimmt: Pluspunkte gibt es für mittlere, Minuspunkte für Extremwerte.
Die gewichteten Wahrscheinlichkeiten (TProbability) werden in einem TDictionary nachgeschlagen und erhalten ihre festen Werte, die in einem TProperty Objekt festgehalten werden. Simpel, nicht?

user posted image

Zivilisationen werden nun nicht mehr irgendwo gegründet, sondern da, wo intelligentes Leben entstanden ist!
Die Population auf einem Planeten besitzt Eigenschaften wie Besiedlungsdichte und wirtschaftliche Ausrichtung um in Zukunft die Preise der richtigen Handelswaren zu verändern.
Die Zivilisation (das Volk) wird durch Bevölkerungswachstum und Regierungstyp genauer definiert. In Zukunft werden Zivilisationen während ihrer Entstehung - je nach gewaltbereitschaft - Kriege miteinander führen, und ihre besiedlungsvorlieben variieren.

Bis dahin kann man etwas Handel treiben, Planeten scannen und … ja, ich glaube das war’s.

user posted image

Full Power!
Aber dem Spieler stehen nun ein paar verschiedene Raumschiffe zur Verfügung, komplett mit brandneuen Geräten! Der Hyperantrieb muss mit einem Generator aufgeladen werden, welcher wiederrum Treibstoff verbraucht. Energie kann man auch in Speicherbänken einspeisen (später mal auch für Credits auf einem Planeten erwerben) bis man sie braucht.
Der Unterlichtantrieb verbrennt ebenso Treibstoff, benötigt oder verbraucht aber keine Energie – Elektro-Antriebe behalte ich aber im Hinterkopf.
Geräte benötigen kein Klassen-Objekt mehr, ich erstelle einfach ein Objekt aus den XML-Daten und benutzte das als Template, die ich für richtige Objekte kopiere. Die nötigen copy Methoden sind nicht all zu groß und es gibt weniger casting Gewusel.

Die erzeugte und verbrauchte Energie wird schon relativ gut miteinander verrechnet, zur Not werden Geräte abgeschaltet.
Auch der Schritt zu Waffen und Schutzschilden ist nicht mehr zu weit – pew, pew in Space undsoweiter.

user posted image

Die Readme liegt nun in html Form vor und enthält ein paar Informationen zu den möglichen Eigenschaften von Objekten.
Fliegt ein bisschen herum und entdeckt fremde Lebensformen und handelt, bis die Ressourcen eines Planten erschöpft sind.

Download Hexaverse Alpha0.4 (~2 MB, Zip)

Alpha 0.3 - „The Age of Empires“

Dienstag, 21. Februar 2012 von Xeres
user posted image

Git ist nicht nur toll, um verschiedene Versionen eines Projekts zu archivieren, auch beim Worklog schreiben kann ich einfach auf das Log zurückgreifen. Ein paar Kleinigkeiten haben sich summiert:

Viele viele Objekte
Positionen sind nun Objekte! Im Prinzip ist es eine Vektorklasse, die X und Y Koordinaten verwaltet. Es ist sauberer, als mit einzelnen Variablen zu hantieren insgesamt kürzer (auch wenn ab und an ein temporäres Objekt gebraucht wird) und – ich möchte es nicht verschweigen – hübscher.
Auch für Farben gibt es einen TColor-Type, mit dem RGB und Alpha verwaltet werden – color.Use() anstatt hard gecodetes SetColor/SetAlpha.
Die ganze Objektorientiertheit hat noch einen tieferen Sinn: Jedes Objekt besitzt eine Save/Load Methode, der man nur einen Stream zu reichen braucht, in den sie sich dann brav einspeichern bzw. auslesen. Galaxie und Spieler werden einfach hintereinander in eine Datei gegossen – falls der hintere Teil mit dem Spieler fehlt, wird ein neues Spiel gestartet ( => neues Schiff an zufälliger Position).
Die Rudimentären GUI-Objekte haben Zuwachs bekommen; nur damit mein sein Kreuzchen setzen kann, um der Galaxie beim entstehen zusehen zu können. Was gibt es da zu sehen? Nun…

The Age of Empires
Interne Umbauten sind schön und gut, aber womit hat sich die Version ihren Titel verdient?
Nachdem die Galaxie generiert wurde, werden ein paar Zivilisationskeime gestreut.
Ein zufälliges System wird gewählt und daraus ein zufälliger Planet. Die Nachbarsysteme werden auf Planeten der Klasse des ersten Planeten abgesucht (wir Menschen halten ja auch nach möglichst erdähnlichen Trabanten Ausschau) und die Zivilisation expandiert. Das wiederholt sich so lange, bis alle Zivilisationen genug Systeme besiedelt haben – und tadaa:

user posted image
Wessen Idee war es, ein System mit einem Schwarzen Loch zu besiedeln?!

Sternenreiche!

Die Planeten brauchen offensichtlich noch einen besseren Bildungsprozess, zufällige Wahl ist hier nicht angesagt. Ich stelle mir das so vor: Die Strahlungsintensität der Sonne nimmt mit 1/Abstand² ab, die Masse der Planeten nimmt erst zu (die Sonne hat das meiste angezogen) und dann wieder ab - eine schöne Glockenform. Mit dem Abstand der Sonne ließe sich also Temperatur & Masse bestimmen und die passende Planetenklasse setzten.
Erdähnliche Planeten, die um Schwarze Löcher kreisen, wird es dann nicht mehr geben.

Mit n-Halbleiters Tipp von letztem Mal läuft auch die Verwaltung der Ausrüstung ganz gut; jedes Device Objekt bekommt eine eindeutige ID zugewiesen, womit nur ein Cast durchgeführt werden muss, um das richtige zu erhalten. Etwas Code zur Veranschaulichung (oder Verwirrung, je nach dem):

BlitzMax: [AUSKLAPPEN]
Function Generate:TDevice(_DeviceName:String)

Local ThisClass:TDevice_Class = TDevice_Class.GetDeviceClass(_DeviceName)

Select ThisClass.TemplateID
Case HyperDriveTemplateID
Return TDevice_HyperDrive.Create(TDevice_HyperDrive_Class(ThisClass))
Case SubLightDriveTemplateID
Return TDevice_SubLightDrive.Create(TDevice_SubLightDrive_Class(ThisClass))
End Select

End Function


Da die Ausrüstungen zu verschieden sind, müssen sie ansonsten sowieso als Spezialfälle abgefragt werden, aber zum Speichern ist es so sehr hübsch.
Anstatt überhaupt Device-Klassen zu verwenden, könnte man Template-Objekte benutzen, die man Kopiert, um sie dann zu benutzen – ich werde in dieser Richtung mal etwas herum probieren.

In Zukunft gibt es noch viel zu tun:

  • Zivilisationen bräuchten etwas mehr Persönlichkeit.
  • Populationen der Planeten/Systeme sollten dann auch etwas mehr können, als nur da zu sein.
  • Unstrukturierte Organisationen wie z.B. Piraten sollten vll. auch bedacht werden.
  • Der Spieler könnte so langsam ein Menü vertragen, in welchem er sich Schiff / Ausrüstung / Startposition aussuchen kann.
  • ...aaaber dafür sollte es vielleicht mehr als ein Schiff und einen Ausrüstungsgegenstand geben.


Bis dahin seid ihr mal wieder eingeladen, mit der aktuellen Version herum zu spielen:

Download Hexaverse Alpha0.3 (~2 MB, Zip)

Es ist immer noch eine Alphaversion – man kann weder gewinnen, noch verlieren – höchstens der Treibstoff kann euch ausgehen...
Wer interessiert ist oder einen Bug vermutet, möge die debug.exe verwenden, sie schreibt euch ein paar kryptische Informationen auf.
Und benutzt keinen Hexeditor um in ein Savefile zu gucken – es lohnt sich nicht.

Alpha 0.1 „Spielbar“

Freitag, 9. September 2011 von Xeres
Galaxien
Es können nun beliebige Galaxien erzeugt werden. Neben Name und Größe lässt sich auch der Zufallssamen angeben, mit dem zukünftig eine Galaxie 1:1 dupliziert werden kann – eine Mechanik, die simpel genug um zu setzen ist, aber für die Bug-Jagd und zum Sammeln & Tauschen wie gemacht ist. Eine simple Hash-Funktion bildet aus einem String den Samen, da sich Wörter einfach besser merken lassen – Benutzerfreundlichkeit ist auch wichtig.
BlitzMax: [AUSKLAPPEN]
Function HashSeed:Int(_str:String)
If Int(_str) = _str Then Return Int(_str) '* Reine Zahlen nicht als String verarbeiten!
Local h:Int
For Local j:Int = 0 Until _str.Length
h = 31 * h + _str[j]
Next
Return h
End Function
Natürlich ist diese Funktion weit von Perfektion entfernt und es wird ein paar Kollisionen geben (sprich: mit zwei verschiedenen Strings kommt man auf den gleichen Zahlenwert), aber ein wirkliches Problem entsteht daraus ja nicht.

Alles nur geklaut:
WP: Java hashCode()
MineCraft-Seed

Handel
Es gibt drei Handelswaren, mit denen man momentan mehr oder weniger etwas anfangen kann:
Treibstoff, Mineralien und Abfall. Die Handelspreise werden mit dem Techlevel variiert. Planeten die nur eine kleine Bergbaukolonie beheimaten, haben ein geringes Techlevel – Mineralien sind billig zu kaufen & Abfall billig los zu werden. Ein technisch fortgeschrittener Planet wird Rohstoffe gerne ankaufen um sie weiter zu verarbeiten, und eine Welt die vom Tourismus lebt, wird den Abfall zu besten Konditionen abgeben.

Diese Mechanik braucht noch etwas Ausarbeitung; Die Population eines Planeten gehört noch keiner Zivilisationen an, und Zivilisationen (Sternenreiche) werden nicht generiert, aber dahin werden wir mal kommen.
Momentan gibt es eine gewisse Chance, dass ein Planet Bevölkert ist, und Handel treibt. Mit Müll & Mineralien lässt sich Geld für Treibstoff verdienen… um mit mehr Müll & Mineralien zu handeln!
Gespeichert wird das allerdings auch noch nicht, also steckt nicht zu viel Zeit da rein.

Ausrüstung
Da nun Treibstoff existiert, gibt es auch Antriebe, die welchen verbrauchen. Mit dem normalen Antrieb kann man von Planet zu Planet reisen, der Hyperantrieb bring einen zum nächsten Stern. Die Lichtgeschwindigkeit liegt bei 20Feldern per Tick (oder Runde), und kann nicht mehr überschritten werden. Um einen Hypersprung aus zu führen braucht man mindestens eine Geschwindigkeit von ¾ c (also 15 F/t).
Das Funktioniert alles so weit, aber von der Codeseite her bin ich noch nicht sicher, ob das System so sinnig ist:

Obacht! Objektorientiertes Kauderwelsch!

Ich benutze ein TDevice_Class von der ich z.B. ein TDevice_HyperDrive _Class ableite (Ist das überhaupt der richtige Sprachgebrauch? Ich hoffe doch.) und ein TDevice von der ich ein TDevice_HyperDrive ableite. Die _Class Objekte, speichern mir die in XML definierten Geräte-Typen (z.B. die Maximale Reichweite), die anderen sind die „tatsächlichen“ Geräte und enthalten ihre Klasse, sowie nötige Zusatzinfos (Füllung des Tank und dergleichen).

Einerseits ist das ziemlich logisch und praktisch, da sich alle Geräte dieselben Grundlagen teilen, und das Ein- und Ausbauen einfach machen, andererseits komme ich an die zusätzlichen Fields nur über Getter-Methoden heran – und bei denen besteht BlitzMax darauf, dass sie auch für die Basisklasse definiert sind. Also besitzt TDevice eine Methode GetRange:Int() mit dem Inhalt Return 0 – und das soll ich für jedes neue Feld machen?
Außerdem brauche ich an irgendeiner Stelle sowieso extra-Abfragen, um dem Schiff den richtigen Antrieb in den Richtigen Slot zu schieben...

Irgendwas mache ich hier bestimmt falsch – Objekt orientiert programmiere ich mehr oder minder so sporadisch wie ich es nützlich finde. Hinweise & Ideen wie ihr (eure) Klassen & Objekte handhabt sind gern gesehen.

Nachtrag:
n-Halbleiter konnte mir einen guten Rat geben und ich schätze, das Problem ist gelöst. Erklärungen folgen.


Download
Genug für dieses mal. Wie immer gibt es die neueste Version zum herum Testen:
Download Hexaverse Alpha 0.1 Zip (~1,96 MB)

Zwischenziel erreicht

Donnerstag, 21. Juli 2011 von Xeres
user posted image

Sterne haben Planeten, man kann Fahrt aufnehmen und durch unbekannte Dimensionen in das nächste System springen. Momentan alles noch ohne Geschwindigkeits- oder Reichweitenbegrenzung, da das Schiff weder auf Maschinen noch Treibstoff, sondern allein auf gutem Willen basiert.

Testflug? Bittesehr:
Download Hexaverse WIP Test 03 Zip (~1,3 MB)

Tab wechselt von Galaxie- zu Systemansicht. Die Grüne Markierung in der Galaxie ist das derzeitige System. Wenn ihr ein System anklickt, wird es dunkellila umrandet und ein Druck auf Jump bringt euch direkt dorthin.
Um in den Weiten des Alls nicht ganz verloren zu gehen, gibt es links unten eine Minimap die euch Sonnen & Planeten anzeigt.

Der nächste Schritt wird vermutlich Laderaum, Treibstoff & Handelswaren involvieren, um etwas mehr Spiel zu bieten und Grundstein für Ausrüstung und Geräte zu legen.
Außerdem hätte ich gerne Asteroiden.

Spieletipp des Tages: Transcendence

Endlose Weiten

Donnerstag, 28. April 2011 von Xeres
Kleiner Nachtrag zur letzten Ausgabe:
Ob ein Punkt (z.B. der Mauszeiger) innerhalb eines Sechsecks liegt, lässt sich schon recht schnell mit der angegeben Methode herausfinden, aber es geht viel Simpler. Da ich ja ein ganzes Netz aus aneinander liegenden Sechsecken verwende, schließe ich zuerst alle Tiles aus, die eine schnelle In-Rechteck-Prüfung nicht bestehen. Da bleiben nur maximal 3 Möglichkeiten über. Von diesen drei wird die Entfernung zum Tile-Mittelpunkt berechnet – und der kleinste Abstand ist das Sechseck, über dem der Mauszeiger schwebt. Dank an BladeRunner für den Hinweis Wink

Endlose Weiten

Tilemaps sind schön und gut, aber wozu wurden und werden sie verwendet? Um mit wenigen Bildern möglichst schnell den Bildschirm voll zu zeichnen. Anders gesagt: man geht davon aus, es müsse etwas gezeichnet werden.
Der Weltraum aber, ist größtenteils leer.
Eigentlich ist auch Materie selbst zum größten Teil leer, aber diese Tatsache tritt schnell in den Hintergrund, wenn man sich den Zeh anstößt.
Bei der ersten WIP Demo habe ich also ein riesiges, leeres Array verwendet, was definitive Grenzen hatte. Um in den nächsten Sektor zu schauen, müsste man die angrenzenden Arrays ebenfalls im Speicher halten. Das ist alles wenig Optimal und darum habe ich einen neuen Ansatz gewagt.
Ich generiere kein Array, sondern unterteile den Bildschirm - rein mathematisch - in Hextiles. Der schwierige Part dabei ist, das Koordinatensystem so um zu formen, dass wir wieder eine verstauchte Tilemap erhalten. So gibt es Sechs eindeutig definierte Richtungen und man kann sich - egal ob gerade oder ungerade Zeile - hübsch linear in beliebige Entfernung bewegen. Die Array-zu-Pixel Umrechnung hat mich dafür eine schlaflose Nacht gekostet, aber hey, es sieht jetzt alles schon ganz hübsch aus.

user posted image
Eigentlich sieht es fast genau so, wie bei der 1. WIP Demo aus – ich hoffe, man erkennt die innere Schönheit?


Auch bei der Galaxie hat sich etwas getan: Es gibt eine. Noch fehlen den Sternen Planeten und noch befindet sich der Spieler nicht tatsächlich an einem definierten Ort in der Galaxie – aber das wird schon noch.

user posted image

Und wer Lust und Zeit hat, darf sich das alles persönlich anschauen:

Download Hexaverse WIP Test 02 Zip (~2 MB)

Zugegeben, wirklich neues Gameplay gibt es noch nicht ganz, aber man kann das Raumschiff nun mit der Maus steuern.
Eine Galaxie wird automatisch beim ersten Start generiert – wenn ihr mit den Lua Skripten experimentieren wollt, die die Sternenverteilung und Benennung definieren, denk daran, das GALAXY File zu löschen, sonst seht ihr keine Änderung.
Die statistische Verteilung der Sterne findet ihr in dem XML File – denkt daran, dass es gekoppelte Werte sind, keine Prozentangaben. Massen und Temperaturen haben noch keinen Effekt.

Nächste Stationen:
- Sterne mit Planeten bestücken.
- Spieler tatsächlich in der Galaxie platzieren.
- …was auch immer mir interessant erscheint.

Drinnen oder Draußen

Mittwoch, 13. April 2011 von Xeres
Wie findet man heraus, ob der Mauscursor (oder ein beliebiger Punkt) in einem Sechseck ist, oder nicht?
Wir kennen:
- Den Mittelpunkt des Sechsecks
- Die Größe (Länge, Breite) des Sechsecks

1) Ist die Maus irgendwo in der Nähe des Sechsecks?
user posted image
Ein Punkt in einem Rechteckigen Bereich zu finden, ist wirklich simpel und geht schnell; man spare sich die Aufwändigeren Schritte für später auf.

2) Ist die Maus in dem großen Teil in der Mitte?
user posted image
Noch ein großer, eindeutiger Bereich...

3) Steckt sie in einem Dreieck?
user posted image

Ah, der Interessante Teil: Wie gehen wir hier vor? Wenn man den Ansatz kennt, klingt es schon mal ganz einfach: Man überprüfe, ob die Fläche des Dreiecks der ursprünglichen Punkte gleich der Fläche der Dreiecke mit dem zu überprüfendem Punkt ist.

user posted image

Wir berechnen die Fläche von 4 Dreiecken: Einmal das Dreieck, von dem wir wissen wollen, ob der Punkt darin ist (Gelb) und dann tauschen wir je ein Punkt von diesem Dreieck mit dem gesuchten Punkt aus, und addieren die Flächen zusammen. Liegt der Punkt nun außerhalb des Dreiecks…

user posted image

…sind die Flächen größer, als sie sein dürften.

user posted image

Die äußeren beiden Punkte der Dreiecke verschiebe ich um den Sicherheitsabstand von 1px nach innen – so wird verhindert, dass mehr als ein Feld markiert wird, wenn mehrere nebeneinander liegen.

Das ergibt diesen Code (ja, ohne weiteres lauffähig):
BlitzMax: [AUSKLAPPEN]
SuperStrict

AppTitle = "Maus im Sechseck?"

Global gfx_w:Int = 512, gfx_h:Int = 512
Graphics(gfx_w, gfx_h)
AutoMidHandle(True)
SeedRnd(MilliSecs())
Local FrameTimer:TTimer = TTimer.Create(60)

Global mx:Int, my:Int

SetClsColor(255, 255, 255)

Repeat
Cls

mx = MouseX()
my = MouseY()

If PointInHex(gfx_w *.5, gfx_h *.5, mx, my, 196) Then
SetColor(0, 0, 200)
Else
SetColor(0, 200, 0)
EndIf

DrawHex(gfx_w *.5, gfx_h *.5, 196)


Flip(0)
FrameTimer.Wait()
Until KeyHit(KEY_ESCAPE) Or AppTerminate()
End

Function DrawHex(_x:Float, _y:Float, _size:Float)
DrawPoly([_x, _y - _size / 2, ..
_x + _size / 2, _y - _size / 4, ..
_x + _size / 2, _y + _size / 4, ..
_x, _y + _size / 2, ..
_x - _size / 2, _y + _size / 4, ..
_x - _size / 2, _y - _size / 4])
End Function


Function PointInHex:Int(X:Int, Y:Int, px:Int, py:Int, Size:Int)
Rem
X und Y sind Mittelpunkt des Hexfelds
Das Hexfeld ist Size breit und Size hoch
endrem


Local Size2:Int = Size / 2
Local Size4:Int = Size / 4

'* 1px Sicherheitsabstand, damit nicht mehr als 1 Feld markiert wird:
Local SD:Int = 1

'* Schneller Check: Im Gebiet?
If px <= (X - Size2) And px >= (X + Size2) Then
If py <= (Y - Size2) And py >= (Y + Size2) Then
'* Außerhalb des Size x Size Quadrates
Return False
EndIf
EndIf

'* Mittelquader:
If px >= (X - Size2 + SD) And px <= (X + Size2 - SD) Then
If py >= (Y - Size4 + SD) And py <= (Y + Size4 - SD) Then
Return True
EndIf
EndIf

'* Oben Rechts:
If PointInTriangle(X, Y - Size4 + SD, X, Y - Size2 + SD, X + Size2 - SD, Y - Size4 + SD, px, py) Then Return True
'* Oben Links:
If PointInTriangle(X, Y - Size4 + SD, X, Y - Size2 + SD, X - Size2 + SD, Y - Size4 + SD, px, py) Then Return True
'* Unten Rechts:
If PointInTriangle(X, Y + Size4 - SD, X, Y + Size2 - SD, X + Size2 - SD, Y + Size4 - SD, px, py) Then Return True
'* Unten Links:
If PointInTriangle(X, Y + Size4 - SD, X, Y + Size2 - SD, X - Size2 + SD, Y + Size4 - SD, px, py) Then Return True

Function PointInTriangle:Int(x1:Float, y1:Float, x2:Float, y2:Float, x3:Float, y3:Float, px:Float, py:Float)
Local A1:Float = TriangleArea(px, py, x2, y2, x3, y3)
Local A2:Float = TriangleArea(px, py, x1, y1, x3, y3)
Local A3:Float = TriangleArea(px, py, x1, y1, x2, y2)
Local AT:Float = TriangleArea(x1, y1, x2, y2, x3, y3)
If ((A1 + A2 + A3) > AT) Then
Return False
Else
Return True
End If

Function TriangleArea:Float(x1:Float, y1:Float, x2:Float, y2:Float, x3:Float, y3:Float)
Local a:Float = (x1 - x3)
Local b:Float = (y1 - y3)
Local c:Float = (x2 - x3)
Local d:Float = (y2 - y3)
Return (0.5 * Abs ((a * d) - (b * c)))
End Function

End Function

End Function


Schlimmstenfalls berechnet man mit dieser Methode 4x4=16 Dreiecksflächen. Das ließe sich optimieren, indem man das Vergleichsdreieck zuerst berechnet und schon jede weitere Fläche damit vergleicht. Wenn der Punkt weit entfernt liegt, übersteigt schon eine Fläche das Soll und weitere Flächen müssen nicht mehr berechnet werden.

Quelle - Danke Will Turnage!

Outsourcing zum Mond

Freitag, 10. Dezember 2010 von Xeres
BlitzMax hat so viele schicke Dinge eingebaut, man wird relativ schnell mal abgelenkt. Mit BRL.MaxLua lassen sich leicht Lua Skripte einbinden. Damit kann man Funktionen auslagern um z.B. Berechnungen variabel zu halten.

Lua – ein paar interessante Punkte:
  • Basiert auf C
  • Case Sensitiv
  • Variablen haben keinen direkten Typ – nur deren Werte
  • Zahlen und Zeichenketten werden automatisch ineinander umgewandelt wenn nötig
  • Multiple Zuweisungen sind möglich:
    Code: [AUSKLAPPEN]
    x, y, z = 5, 10, 15

  • Multiple Rückgabewerte sind möglich:
    Code: [AUSKLAPPEN]
    x, y, z = EineFunktion()

  • Gerechnet wird mit Fließkommazahlen (Doubles)
  • Lua hat seinen eigenen Garbage Collector


Also so wirklich beeindruckt bin jetzt ja noch nicht…

Okay, kleines Beispiel: Lagern wir eine Rechnung aus.

user posted image
BlitzMax: [AUSKLAPPEN]
SuperStrict
AppTitle = "Punktverteilung"
Rem
Dieses Beispiel lagert eine Rechnung für eine Punktverteilung aus.
Endrem


Local Lua_class:TLuaClass = TLuaClass.Create(LoadString("Verteilung.lua"))
Local Lua_instance:TLuaObject = TLuaObject.Create(Lua_class, Null)

Local Size:Int = 512, Methode:String = "A"
Local time:Int = MilliSecs()

Print("Lua Start~n")
Local LuaPoints:String = String(Lua_instance.Invoke("Verteilung", [String(Size), Methode]))
Print("~n")
Print("Lua End (" + (MilliSecs() - time) + " ms)")
'Print("D: " + LuaPoints)
Local Koords:String[] = LuaPoints.Split(";")


Graphics(Size, Size)

For Local i:Int = 0 Until Koords.Length
Local Points:String[] = Koords[i:1].Split(",")
DrawOval((Size / 2) + Int(Points[0]), (Size / 2) + Int(Points[1]), 4, 4)
Next

Flip
WaitKey()
End

Lua Code: [AUSKLAPPEN]
--[[
    Dieses Beispiel lagert eine Rechnung für eine Punktverteilung aus.
]]

function Verteilung(size, m)
    -- Zufallssamen aus aktueller Zeit:
    math.randomseed( os.time() )
    -- Neuer Table mit 2 Dimensionen (für x und y).
    -- in der Form {<nr>, <x/y>}
    local t = {}

    --[[
    t[0] = {}
    table.insert(t[0], 1, "X")
    table.insert(t[0], 2, "Y")
    ]]

    for i=0, 512 do
        -- Zufälliger Winkel, gleich in Rad umrechnen
        local w = math.random(0, 359) * (math.pi/180)
        -- Zufälliger Radius
        local r = math.random(0, (size/2))

        t[i] = {} -- Table-Eintrag i selbst zu einem Table machen.

        -- Möglichkeit A
        if string.upper(m) == "A" then
            table.insert(t[i], 1, math.cos(w)*r)
            table.insert(t[i], 2, math.sin(w)*r)
        -- Möglichkeit B
        elseif string.upper(m) == "B" then
            table.insert(t[i], 1, math.random(-(size/2), (size/2)))
            table.insert(t[i], 2, math.random(-(size/2), (size/2)))
        end

    end

    -- "2D Array" zurückgeben
    local re="" -- Als String festlegen für '..' Operator

    for i in pairs(t) do
        re = re..table.concat(t[i], ",")..";"
    end

    -- Letztes ";" wieder abschneiden.
    re = string.sub(re, 0, string.len(re)-1)
    return re
end


[i]Mh, nicht ganz unpraktisch – zugegeben – aber wenn Lua nichts mit meinen Objekten anfangen kann…

Oh, Lua kann – man muss sie nur miteinander vertraut machen.
Hier kann man das grüne Quadrat mit der Maus bewegen und wird von drei roten Punkten verfolgt – die Bewegung wird per Lua gesteuert.

user posted image
BlitzMax: [AUSKLAPPEN]
SuperStrict
AppTitle = "Lua AI"
Rem
Anstatt einen Interpreter zu bauen, wird Lua Benutzt
Endrem


Global Lua_class:TLuaClass = TLuaClass.Create(LoadString("LuaAI.lua"))

Global gfx_w:Int = 512, gfx_h:Int = 512
Local FrameTimer:TTimer = TTimer.Create(60)
Graphics(gfx_w, gfx_h)

Type TGameObjekt Abstract
Field X:Float, Y:Float
Global list:TList = New TList

Method New()
list.AddLast(Self)
End Method

Method Update() Abstract

Function UpdateAll()
For Local o:TGameObjekt = EachIn list
o.Update()
Next
End Function
End Type

Type TPlayer Extends TGameObjekt

Function Create()
Local p:TPlayer = New TPlayer
p.X = gfx_w / 2
p.Y = gfx_h / 2
LuaRegisterObject(p, "Player")
End Function

Method Update()
Local mx:Int = MouseX(), my:Int = MouseY()

Local vx:Float = mx - Self.X
Local vy:Float = my - Self.Y
Local vz:Float = Sqr(vx * vx + vy * vy)

vx = (vx / Abs(vz))
vy = (vy / Abs(vz))

If vz >.1 Then
Self.X:+vx * vz / 64
Self.Y:+vy * vz / 64
EndIf

SetColor(0, 255, 0)
DrawRect(Self.X - 4, Self.Y - 4, 8, 8)

End Method
End Type

Type TEnemy Extends TGameObjekt
Field x:Int, y:Int
Field Lua_ins:TLuaObject

Function Create()
Local e:TEnemy = New TEnemy
e.X = Rand(0, gfx_w)
e.Y = Rand(0, gfx_h)
e.Lua_ins = TLuaObject.Create(Lua_class, Null)
End Function

Method Update()
LuaRegisterObject(Self, "Enemy")
Self.Lua_ins.Invoke("DoAI", Null)

SetColor(255, 0, 0)
DrawOval(Self.X - 4, Self.Y - 4, 8, 8)
End Method
End Type

TPlayer.Create()
TEnemy.Create()
TEnemy.Create()
TEnemy.Create()

Repeat
Cls

TGameObjekt.UpdateAll()

Flip(0)
FrameTimer.Wait()
Until KeyHit(KEY_ESCAPE) Or AppTerminate()
End

Lua Code: [AUSKLAPPEN]
--[[
    Lua AI
]]

function DoAI()

    -- Vektor generieren:
    local vx = Player.x - Enemy.x
    local vy = Player.y - Enemy.y
    local vz = math.sqrt(vx^2 + vy^2)

    -- Bewegen:
    Enemy.x = Enemy.x + (vx / vz)
    Enemy.y = Enemy.y + (vy / vz)

end


Auch schön, aber vollkommen überzeugt bin ich nicht. Sieht nicht so aus, als hätte Lua BlitzMax wirklich was voraus.

Wie wäre es denn hiermit:
BlitzMax: [AUSKLAPPEN]
Local funktion:String = "1/4 * x^3 + 1/2 * x^2 + x + cos(pi)"

Wie könnte man den die Funktionswerte für beliebige X in BMax Berechnen?

Uhm… Um den Ausdruck zu Parsen könnte man ihn in Token zerlegen. Rechenregeln für Potenz vor Punkt vor Strich, Funktionen… nicht ganz simpel, aber nicht unmöglich.
Siehe auch: Noobodys Matheparser BMax Archiv, BB Archiv

In Lua lässt man den String als Lua Code ausführen. Yeah, nimm das, Logik!

BlitzMax: [AUSKLAPPEN]
SuperStrict
AppTitle = "Lua Rechnet"
Rem
Anstatt einen Interpreter zu bauen, wird Lua Benutzt
Endrem


Local Lua_class:TLuaClass = TLuaClass.Create(LoadString("Rechnung.lua"))
Local Lua_instance:TLuaObject = TLuaObject.Create(Lua_class, Null)

Local funktion:String = "1/4 * x^3 + 1/2 * x^2 + x + cos(pi)"
Local X:Int = 2
Local time:Int = MilliSecs()

Print("Lua Start")
Local LuaErgebnis:String = String(Lua_instance.Invoke("Rechnung", [String(funktion), String(X)]))
Print("Lua End (" + (MilliSecs() - time) + " ms)")
Print("Funktion: " + funktion)
Print("mit x=" + X)
Print("= " + LuaErgebnis)
End

Lua Code: [AUSKLAPPEN]
--[[
    Luaskript um einen Funktion zu berechnen.
]]

function Rechnung(funk, v)
    io.write("Lua hier :-) \n")
    -- X Einsetzen (ja, das bekommt BM auch hin - wartet ab!):
    funk = string.gsub(funk, "x", v)

    -- Funktionen ggf. für Lua ersetzen:
    -- Obacht: BM Grad <-> Lua Rad
    funk = string.gsub(funk, "cos", "math.cos")
    funk = string.gsub(funk, "sin", "math.sin")
    funk = string.gsub(funk, "tan", "math.tan")
    funk = string.gsub(funk, "pi", "math.pi")

    --[[ Ausdruck berechnen:
        Die Funktion "loadstring" lässt uns Lua Code laden,
        jetzt nur noch ein "return" anfügen, damit auch was zurück gegeben wird...
    ]]
    funk = "return "..funk

    -- Und den geladenen 'Code' als Funktion ausführen...
    return loadstring(funk)()

end


Output:
Code: [AUSKLAPPEN]
Lua Start
Lua hier :-)
Lua End (5 ms)
Funktion: 1/4 * x^3 + 1/2 * x^2 + x + cos(pi)
mit x=2
= 5


Wenn das mal nicht cool ist! Hat ein bisschen was davon, sich an den eigenen Haaren aus dem Sumpf zu ziehen. Wenn man kein Externes Skript benutzen möchte, kann man das Lua Progrämmchen auch im BMax Quellcode in einen String packen (siehe Doku).

Wo war ich doch gleich…? Uh, eine Galaxie mit Sternen befüllen genau!

Dum-di-dum-di-dum…

Siehe auch: Lua.org

Unwahrscheinlicher Zufall

Freitag, 19. November 2010 von Xeres
Zufall ist eine feine Sache, um den Spielablauf etwas unvorhersehbar zu machen. Das Problem dabei: Was Standardmäßig geboten wird, ist kaum mehr als ein beliebig großer Eimer mit fairen Würfeln beliebiger Seitenzahl. Alle Zahlen werden in etwa gleich oft gezogen.

user posted image
BlitzMax: [AUSKLAPPEN]
SuperStrict

AppTitle = "Ein Eimer Würfel"

Global gfx_w:Int = 256, gfx_h:Int = 256
Graphics(gfx_w, gfx_h)
SeedRnd(MilliSecs())
SetClsColor(255, 255, 255)

Local DiceCount:Int = 100 '* Anzahl der Würfel
Local DiceResult:Int[6] '* Wie oft kam die Augenzahl?

'* Den Würfeleimer Auskippen und auszählen:
For Local i:Int = 0 Until DiceCount
DiceResult[Rand(0, 5)]:+1
Next

'* Diagramm auftragen:
Cls
For Local i:Int = 0 Until DiceResult.length
If i Mod 2 = 0 Then
SetColor(128, 64, 64)
Else
SetColor(64, 128, 64)
EndIf
DrawRect(i * 32, gfx_h, 32, -DiceResult[i] * 8)

SetColor(0, 0, 0)
DrawText(i + 1, 12 + i * 32, 8) '* Augenzahl
DrawText(DiceResult[i], 12 + i * 32, gfx_h - 16) '* Häufigkeit
Next

Flip()
WaitKey()
End


Für die Fälle, in denen man einen einzelnen Wert benötigt ist das gut und schön – aber was, wenn ich nicht einzelne, sondern verknüpfte Werte haben möchte? Beispielsweise „6 aus 49“: Man darf hier keine Zahl doppelt ziehen dürfen. Man könnte nun Würfeln, die Zahl aufschreiben und wenn die nächste Zahl schon gezogen wurde, nochmal würfeln.
Wenn man tatsächlich nur ein paar wenige Zahlen aus einer ganzen Menge braucht, kann das gut gehen. Für den Fall, dass man eine Liste an Zahlen un-sortieren möchte (also so lange weitermacht, bis alle Zahlen einmal gezogen wurden), könnte das aber eine (unvorhersehbar lange) Weile dauern. Wir kehren den Ansatz etwas um:
Wir haben eine Liste an Zahlen und nehmen zufällig eine heraus – bei jeder weiteren Ziehung ist die Zahl schon nicht mehr da und man muss sich keine weiteren Gedanken machen.

user posted image
BlitzMax: [AUSKLAPPEN]
SuperStrict

AppTitle = "6 aus 49"

Global gfx_w:Int = 256, gfx_h:Int = 256
Graphics(gfx_w, gfx_h)
SeedRnd(MilliSecs())
SetClsColor(255, 255, 255)

Local AllNumbers:Int[49] '* Platz für alle Verfügbaren Zahlen
'* Zahlen eintragen:
For Local i:Int = 0 Until AllNumbers.length
AllNumbers[i] = i + 1
Next

Local Numbers:String[6] '* Platz für die Sechs Zahlen, die herausgezogen werden

'* Zahlen ziehen:
For Local i:Int = 0 Until Numbers.length
Numbers[i] = GetRandomFromArray(AllNumbers)
Next

'* Zahlen ausgeben:
Cls
SetColor(0, 0, 0)
DrawText("Ziehung: ", 8, 16)
DrawText(" - ".join(Numbers), 8, 32)
DrawText("Restliche Zahlen (49-6): " + AllNumbers.length, 8, 64)

Flip()
WaitKey()
End

Function GetRandomFromArray:Int(_N:Int[] Var)
Local rp:Int = Rand(0, _N.length - 1) '* Eine Zufällige Position des Arrays wählen
Local rn:Int = _N[rp] '* Die Zahl an dieser Position ermitteln
_N = _N[..rp] + _N[(rp + 1)..] '* Die Zahl aus dem Array "ausschneiden" (Slices)
Return rn
End Function


Ganz toll, aber: Wer sagt eigentlich, dass ich einen fairen Würfel will? Ob ich zwei gleiche Sternentypen hintereinander generiere ist mir eigentlich schnurz – viel wichtiger wäre mir, zu steuern, wie die Prozentuale Wahrscheinlichkeit ist.
Denkt an PowerUps oder auch Gegenstände: Man möchte doch hauptsächlich die schwächeren PowerUps / Wertlosen Schund für den Spieler generieren. Wenn man von den ersten Monstern 3 mal das „Schwert der ultimativen Vernichtung“ bekommt, weil es dieselbe Dropchance hat, wie die „Looserkappe die dich gar nicht schützt“...
Hier benutzen wir zur Hilfe 2 Arrays: Einmal eine Liste mit den zur Auswahl stehenden Objekten und eine Liste, die die Gewichtung enthält.

user posted image
BlitzMax: [AUSKLAPPEN]
SuperStrict

AppTitle = "Gewichtete Wahrscheinlichkeit"

Global gfx_w:Int = 256, gfx_h:Int = 256
Graphics(gfx_w, gfx_h)
SeedRnd(MilliSecs())
SetClsColor(255, 255, 255)

Local Item:String[] = ["A", "B", "C"]
Local ItemWeight:Int[] = [15, 10, 5]
Local weightMax:Int
For Local i:Int = EachIn ItemWeight
weightMax:+i
Next

'* Statistische Verteilung:
Local Zufall:Int[3], Maxcount:Int = 1000
For Local i:Int = 0 Until Maxcount
Select String(GetWeightedRandom(Item, ItemWeight))
Case "A"; Zufall[0]:+1
Case "B"; Zufall[1]:+1
Case "C"; Zufall[2]:+1
End Select
Next

'* Darstellen:
Cls
SetColor(0, 0, 0)
DrawText("Ein Objekt: " + String(GetWeightedRandom(Item, ItemWeight)), 8, 16)
DrawText("Statistische Verteilung:", 8, 32)

For Local i:Int = 0 Until 3
DrawText(Item[i] + ": " + Zufall[i] + " - " + Int(Zufall[i] * 100.0 / Maxcount) + "% (" + Int((ItemWeight[i] * 100.0) / weightMax) + "%)", 8, 64 + i * 12)
Next

Flip()
WaitKey()
End


Function GetWeightedRandom:Object(_obj:Object[], _weight:Int[])
Local weightMax:Int, r:Int, Z:Int
'* Gesammt Gewicht bestimmen:
For Local i:Int = EachIn _weight
weightMax:+i
Next

'* Einen zufälligen Wert erzeugen
r = Rand(0, weightMax)
For Z = 0 Until _weight.length - 1
'* Ein Objekt mit einer Höheren Gewichtung wird öfters ausgegeben
If _weight[Z] > r Then Return _obj[Z]
r = r - _weight[Z]
Next
Return _obj[Z]
End Function


Es gibt natürlich wieder kleinere Abweichungen von den Idealwerten, aber das Prinzip funktioniert.
Jetzt kann ich eine Galaxie statistisch korrekt - näherungsweise - bevölkern. Da die großen, hellen O-Klasse Sterne sehr schnell ihren Wasserstoff verbrennen, gibt es sie sehr viel seltener (1 von 10 Millionen) als z.B. die M-Klassen Sterne am anderen Ende der Hauptreihe (1 von 8 Sternen).
Für Hexaverse werde ich vielleicht nochmal etwas an der Verteilung drehen – zumal die Statistik mit Vorsicht zu genießen ist. Unsere nähere Umgebung ist (natürlicher Weise) besser erforscht und muss nicht zwangsläufig Beispielhaft für die gesamte Galaxie (oder gar andere Galaxien) sein.

Von den Gesetzen der Natur

Freitag, 12. November 2010 von Xeres
Mittlerweile gibt es einen hübschen Sektor, den man mit einem Raumschiff abfliegen kann. Noch gibt es keine Maussteuerung, dass sollte noch Zeit haben – Tastatursteuerung reicht vorerst aus. Wer ein bisschen herumspielen will ist herzlich eingeladen.

user posted image

Steuerung:
In Richtung Beschleunigen: U, I, H, K, N, M (F1 ingame sollte das klarer machen)
Nicht beschleunigen: J
Zufälliger Sprung in der näheren Umgebung: R
Auf der Karte herumscrollen: Pfeiltasten (+Shift)
Kamera auf den Planeten zentrieren: Enter
Kamera auf das Schiff zentrieren: Backspace

Download Hexaverse WIP Test 01 (675,5 KB)

Für die Steuerung hatte ich eine „reale“ Variante geplant – Ein Raumschiff kann bis zu einer Maximalgeschwindigkeit (Lichtgeschwindigkeit) beschleunigt werden (momentan kein Tempolimit – dafür kann man den Sektor auch nicht verlassen…), und behält seine Geschwindigkeit bis man abbremst – das Beste von Newton & Einstein (nein, Zeitdilatation wird es nicht geben)!
Die Trennung von „Beschleunigung“ und „Gravitation“ sorgt vielleicht für eine gewisse Irritation, hat aber folgende Bewandtnis: Gravitation ist nicht zwangsläufig eine notwendige Kraft (spieltechnisch gesehen). Wenn es die meisten Spieler zu sehr aus der Bahn wirft, kann man auch darauf verzichten.
Man könnte das Spiel auch in Stopp-Motion spielen: Das Schiff wird – je nach Triebwerksleistung – um ein paar Felder versetzt, nimmt aber kein Momentum auf und bleibt nach dem Zug wieder komplett stehen. Das ist natürlich unrealistisch, kann die Steuerung aber auch wesentlich vereinfachen. Welche Variante würde euch am meisten Spaß bringen?
Ihr müsst euch jetzt noch nicht endgültig festlegen Wink

Um die Strecken zwischen Sonnensystemen zurück zu legen, braucht es – wie immer im Sci-Fi-Genre – einen kleinen Trick. Da die Lichtgeschwindigkeit eine Fundamentale Konstante darstellt, weicht man auf Dimensionen aus, in denen es keine bzw. eine angenehmere Reisegeschwindigkeit gibt, oder man Faltet die Raumzeit in bisschen zusammen.
Anstatt Sprungtore (X Universum / EVE Online) zu verwenden, die Stationär herumhängen, wird jedes Schiff - ganz nach Elite - mit einem Hypersprungantrieb ausgestattet. Schließlich soll man die Galaxie auf eigene Faust erkunden können.

user posted image
Ohja: und zwischen den Sternen stranden kann man dann auch… Irgendwer wird schon einen Anhalter mitnehmen.

Bevor ein anderes Sonnensystem angesteuert werden kann, brauchen wir erst einmal eines! Nachdem die Funktionen für das Karten-handling einigermaßen stehen, kann ich mich darum kümmern, eine Galaxie zu erzeugen. Ich stelle mir das so vor: Ich erstelle ein Hex-Array und fülle es Balkenförmig auf, dann werden einzelne Ringe um das Zentrum der Galaxie gedreht – und Schwupps, eine Spirale mit mehr oder weniger großem Balken. Natürlich ließe sich auch eine Archimedische Spirale oder einfach eine radiale Wahrscheinlichkeitsverteilung realisieren.

user posted image

Die wichtigsten Spektralklassen werden vertreten sein, auch wenn ich stark überlege, lieber ein eigenes (logischeres / leicht zu merkendes) Kategorisierungssystem zu verwenden (offenbar benutzen Astronomen furchtbar gerne komische Merksätze).
user posted image
Spektralklasse (WP), Klassifikation von Sternen (eng. YT)

Wie wichtig ist euch die korrekte Klassifizierung von Sternen im Spiel? Planeten haben noch kein offizielles Kategorisierungssystem (und die offizielle Definition, was ein Planet überhaupt ist, ist etwas… strittig), da müsste ich also sowieso etwas erfinderisch werden.
Jeder Stern erhält erst einmal 0 bis 3 Planeten (Doppel-, Mehrfachsternensysteme und Monde sind für diese Entwicklungsstufe noch ausgeschlossen) – und mehr erfordert das Zwischenziel aus dem letzten Eintrag nicht wirklich.

Gehe zu Seite 1, 2  Weiter