Touhou ECL tutorial (German ver)

Inhalt

Installation
Wie ECL funktioniert (Anfang)
Unser Zweiter Gegner
Kugeln, und warum ECL so toll ist
Funktionen und Routinen
Mehr Kugeln!



Es wird mal zeit für ein anständiges ECL tutorial.

ECL ist eines der 7 scriptformate, die Touhou benutzt. Es kümmert sich dabei um Gegner- und Kugelformationen.

Installation

Vorerst: Ich zeige hier nur einen einfachen weg, sich eine Umgebung für ECL-Bearbeitung aufzubauen. Wenn du bereits einen anderen Weg benutzt, kannst du ihn gerne weiter benutzen.

Wir werden für unsere umgebung Thcrap und Touhou Toolkit benutzen. Also downloade die am besten schonmal.

Also, Mal ganz schnell:
Entpacke Thcrap und starte es
Wähle "Skipgame" und das spiel, das du bearbeiten möchtest (Für dieses Tutorial am besten Touhou 15), den shortcut am besten schonmal in die Taskleiste packen, den werden wir noch oft brauchen
Gehe zu thpatch/skipgame und gehe in den ordner mit der nummer deines spieles und lösche alle dateien darin (Oder erstell ihn, wenn er nicht existiert)
erstelle einen Unterordner mit dem Namen thtk und entpacke Touhou Toolkit da rein
Erstelle eine Datei mit dem Namen st01.txt und schreibe da rein:
sub main()
{
var;
ins_1();
}
Erstelle eine weitere Datei mit dem Namen convert.bat und schreibe da rein:
thtk\thecl c15 (Nummer eures Spiels, 15 für th15) st01.txt st01.ecl
pause
Nun starte zum testen einmal convert.bat.
Wenn alles richtig lief, kannst du nun eine Datei st01.ecl in deinem Ordner finden.
Nun kannst du deine ECL Scripts in st01.txt schreiben und convert.bat nutzen, um sie in eine ECL-Datei zu konvertieren.

Wie ECL funktioniert (Anfang)

Ich habe ja vorhin schon kurz erklärt, das ECL für stages, gegner- und bulletpattern benutzt wird. ECL ist eigentlich eine recht einfache Sprache, auch wenn sie durch die (momentan recht unübersichtliche) übersetzung zuerst etwas abschreckend wirken kann...
Also, Gehen wir erstmal das Script durch, das wir in Part 1 geschriben haben:

sub main()
{
var;
ins_1();
}

sub

Sub steht für Subroutine.
Subroutinen funktioniert so ähnlich wie Funktionen in anderen Sprachen. Es gibt ein paar kleine Unterschiede, auf die wir nachher zu sprechen kommen.
Die subroutine „main“ wird automatisch am Anfang einer Stage aufgerufen.

Var

Var wird immer an den Anfang einer Routine geschrieben und ist dafür da, die Variablen zu definieren, die man in einer subroutine benutzen möchte.
Wir werden aber noch einige weile ohne Variablen auskommen.

ins_1();

Alle befehle in ECL lauten ins_ und danach eine nummer zwischen 0 und 1005.
Eine Liste aller Befehle findest du Hier
Der Befehl ins_1 bedeutet soviel wie „Funktion sterben lassen“.
Und ja, ich hab gesagt „Sterben lassen“ und nicht „beenden“



Wenn wir dieses Script jetzt also in Touhou öffnen, haben wir eine Leere Stage, wir können weder Schießen noch fokussieren.
Das passiert, wenn keine Funktionen aktiv sind. Logisch, wir haben ja auch gerade die einzige getötet. Normalerweise ist dies eine Situation, die man um jeden fall vermeiden möchte. Der einzige Ausweg daraus ist das Menü zu öffnen und die Stage zu verlassen.

Wenn eine Routine aktiv ist, ist es uns möglich zu schießen, und zu fokussieren. Deswegen wollen wir versuchen unsere Funktion diesmal am leben zu lassen.

Allerdings stirbt eine Funktion auch automatisch, wenn sie am ende ihres Codes ist. Der Ausweg daraus: die Funktion unendlich Warten zu lassen. Der Befehl für warten ist

ins_23(int frames)

Für das unendliche brauchen wir eine Schleife. Das erreichen wir in ECL mit gotos. Der befehl für ein einfaches goto ist

ins_14(Label label, int a)

Also, mit diesen Neuerungen sollte unser Script ungefähr so aussehen:

sub main()
{
    var;
    ins_23(120);
loop_start:
    ins_23(1000);
    ins_12(loop_start, 0);
    ins_1();
}

(Das ins_23 vor loop_start muss da sein, da eine funktion nicht mit einem Label beginnen kann)
Dann mal los! Stage starten, und.. wir können Schießen! Und jetzt nicht mehr... Die Main-Routine ist wieder gestorben. Diesmal aber nicht durch einen befehl, sondern weil wir sie abgeschossen haben.

Jetzt wird’s Kompliziert, denn
Das ist der Unterschied zwischen ECL-Routinen und normalen funktionen: sie haben einige unsichtbare variablen, die automatisch beim start der Funktion erstellt werden:

Leben, Position, Hitboxen, Grafik die an der Position angezeigt wird, Score-bonus beim Abschießen, Item-bonus beim Abschießen, Informationen, wie die Routine aufgerufen wurde, Schalter für die Routine, eine Routine, die aufgerufen wird, wen die Routine Stirbt, eine Routine, die nach einer bestimmten zeit aufgerufen wird, wenn die Routine noch am leben ist und noch ein paar freie Variablen, die Teilweise mit der aufrufenden Routine verbunden sind.

Eine Funktion „Stirbt“ - Siehst du jetzt, warum ich den begriff gewählt habe? - normalerweise, wenn

Der Befehl ins_1 aufgerufen wird.
Das Ende der Routine erreicht wird
Das Leben der Routine unter 0 fällt
Die Position der Routine zu weit außerhalb des Spielfeldes ist



Wie die Variablen belegt werden, hat damit zu tun, wie die Routine ausgeführt wird. Die Main-Funktion ist ein besonderer Fall, denn da sie von Touhou selbst aufgerufen wird, sind alle ihre Werte 0
Aber, auch wen die Hitbox gleich null ist, zählt sie leider immer noch wie 1, also ist es uns möglich, die Routine abzuschießen, und von ihr getroffen zu werden, wenn wir uns an die stelle 0/0 begeben.
Das ist natürlich nicht das, was wir wollen, denn die Main-Routine sollte ja die Ganze zeit Laufen.

Hier kommt der Schalter einer Funktion ins Spiel. Der Schalter einer Funktion ist eine Binäre Zahl von 0 bis 65535 (Also mit 15 ziffern), und jenachdem, welche dieser Ziffern 1 sind, hat eine Routine unterschiedliche privilegien.
Die Modi sind:

01No hitbox
12No killbox
24unit is allowed to go out of the left and right bounds.
38unit is allowed to go out of the top and bottom bounds
416Hide life bar and invunerable
532Hide the unit
664Every boss has it at the beginning, But the effect is unknown
7128Function can't be cancelled (by canceling functions like 525)
8256Unknown
9512Can be grazed (like laser)
101024Unknown
112048On hit kill by bomb
124096Unknown. Used only on Taiko of DDC extra
138192Don't effect by time stop
1416384Unused
1532768Unused


Die Privilegien, die wir wollen, sind Keine Hit- und killbox, Verstecken der Einheit, und anti-cancelling, also ist unsere Schalterzahl 163.
Der Befehl, um den Schalter einer Funktion zu setzen ist ins_502, mit dem argument Schalter.
Also fügen wir unserer main-Routine voran:

 
sub main()
{
    var;
    ins_502(163);
    ins_23(120);
loop_start:
    ins_23(1000);
    ins_12(loop_start, 0);
    ins_1();
}

Nun funktioniert alles. Die Stage ist zwar noch etwas Leer, aber immerhin können wir Schießen!


Unser zweiter Gegner

Es wird Zeit, unsere Stage mit Leben zu füllen!
Das lustige ist: Wir haben eigendlich schon einen Gegner auf dem Bildschirm: unsere Main-Funktion.
Da wir sie aber am leben lassen müssen, können wir sie nicht in den Kampf schicken. Also erstellen wir uns eine neue Subroutine für unseren Gegner.
Dafür werden wir diese Befehle brauchen:

ins_301(routine r, float x, float y, int life, int bonus, int item);
ins_302(int anm_index);
ins_303(int slot, int instruction);
ins_306(int slot, int instruction);
ins_401(int time, int mode, float x, foat y);
ins_500(float x, float y);
ins_501(float x, float y);

Also: Dann erstellen wir erstmal wieder eine endlos wartende Routine für unseren Gegner:

sub Gegner()
{
    var;
    ins_23(1);
loop_start:
    ins_23(100);
    ins_12(loop_start, 0);
    ins_1();
}

hab keine Angst, du kannst soviele Label mit demselben namen haben wie du willst, solange ein Label nicht zweimal in derselben Routine auftaucht.
Wir haben jetzt unseren Gegner erstellt, aber wir müssen ihn noch aufrufen. Das tun wir mit ins_301.
Errinerst du dich noch, das ich sagte, die unsichtbaren Variablen einer Routine sind davon abhängig, wie sie aufgerufen wird?
Nun, ins_301 Erlaubt uns, einige der unsichtbaren Variablen zu definieren:

X, Y, Leben, Leben, Score-Bonus, Item-Bonus

Dann wollen wir mal: Fürs erste setzen wir den Gegner mal an die position {0,0}, geben ihm 100 Leben, score und 1 als Bonus. (1=Power-Item) Und rufen ihn in der Main-Routine auf:

ins_301(Gegner, 0.0f, 0.0f, 100, 100, 1);

X und Y sind floats, also müssen sie immer als Kommazahlen mit f am ende geschrieben werden.

Nun, jetzt haben wir unseren Gegner erstellt, aber er ist immernoch unsichtbar, und hat eine Hitbox von 0/0.
Dann wollen wir unserem gegner mal einen Sprite geben.
Das einzige Problem ist: ECL enthält keine Grafiken. Die Dateien, die Grafiken enthalten, sind ANM Dateien.
Da dieses Tutorial aber um ECL und nicht um ANM geht, werden wir hier nur von Touhou preperierte ANM dateien benutzen.
Eine dieser dateien ist enemy.anm. Diese Datei enthält sprites für Feen und Ying-Yang Bälle. Um die Datei in unserer ECL-Datei zu benutzen, müssen wir sie einfügen. Das tun wir, indem wir

anim { "enemy.anm"; }

an den anfang unserer Datei schreiben.
Als nächstes müssen wir Touhou mitteilen, das wir Gegner einen Sprite aus dieser ANM-Datei geben wollen.
Da Touhou schon von Haus aus zwei ANM-Dateien einbindet (bullet.anm und effect.anm), ist unsere ANM-Datei index 2, also ist unser befehl

ins_302(2);

Nun kommen wir zu speziellen instruktionen in ANM-Dateien.
ANM Dateien enthalten nicht nur Bilder, sondern auch Befehle, wie diese Bilder zu Sprites verändert und animiert werden sollen. diese Befehle Werden dann als "Scripts" mit einer nummer versehen und gespeichert. Eine normale, Blaue Fee, die nach Vorne guckt, besitzt die nummer 20, eine Fee, die nach rechts fliegt die nummer 21, und eine Fee, die nach links fliegt die nummer 22.
Touhou gibt jedem Gegner slots, in die Grafiken gesetzt werden können, um diese mit einfachen animationen zu besetzen, wird der Befehl ins_303 benutzt. Allerdings kann Touhou auch automatisch die Animation verändern, wenn sich der Gegner bewegt. Das geht allerdings nur bei Slot 0 und dem Befehl ins_306 (der genauso wie 303 aufgebaut ist).
Das kommt uns recht gelegen, also setzen wir den index für die blaue Fee (20) auf Slot 0:

ins_302(2);
ins_306(0, 20);

Als letztes wollen wir die Hitboxen erstellen. Die Befehle dafür sind 500 und 501. Die Hitboxen für feen sind 16 und 24

ins_500(24.0f, 24.0f);
ins_501(16.0f, 16.0f);

Dann wollen wir mal gucken, ob die Animation auch Funktioniert!
Dazu müssen wir unsere Fee dazu bringen, sich zu bewegen. Dies geht mit dem Befehl 401. Wir wollen unseren Gegner mal zuerst in die Mitte des bildschirms ({0, 128}) fliegen lassen, dann 2 Sekunden warten, und ihn nachher nach Rechts ({120, 128}) fliegen lassen.

ins_401(60, 0, 0.0f, 128.0f);
ins_23(180);
ins_401(60, 0, 120.0f, 128.0f);

Also, Unser Script sollte jetzt ungefähr so aussehen:

anim { "enemy.anm"; }

sub Gegner()
{
    var;
    ins_302(2);
    ins_306(0, 20);
    ins_500(24.0f, 24.0f);
    ins_501(16.0f, 16.0f);
    ins_401(60, 0, 0.0f, 128.0f);
    ins_23(180);
    ins_401(60, 0, 120.0f, 128.0f);
loop_start:
    ins_23(100);
    ins_12(loop_start, 0);
    ins_1();
}
 
sub main()
{
    var;
    ins_502(163);
    ins_23(120);
    ins_301(Gegner, 0.0f, 0.0f, 100, 100, 1);
loop_start:
    ins_23(1000);
    ins_12(loop_start, 0);
    ins_1();
}

Soweit sollte alles Funktionieren!
Im nächsten Part wollen wir unsere fee dann ein paar Kugeln schießen lassen!



Kugeln, und warum ECL so toll ist

Für das, was du bis jetzt gelernt hast, siehst du wahrscheinlich keinen Grund, ECL irgendeiner anderen Danmaku-sprache vorzuziehen. Das wird sich hoffentlich jetzt ändern.
Der vorteil an ECL ist, das nicht jede Kugel einzeln definiert und abgeschossen werden muss, sondern Touhou automatisch Kreis-, Diamanten-, Zufalls- und Linienformationen erstellt, wenn man es darum bittet.
Fangen wir aber erstmal am anfang an. Für's erste lassen wir unsere Fee mal eine einfache Kugel schießen.
Die Befehle, die dafür nötig sind, sind:

ins_600(int nr);
ins_601(int nr);
ins_602(int nr, int Typ, int Farbe);
ins_604(int nr, float Richtung, float Richtungsoffset);
ins_605(int nr, float Geschwindigkeit, float Geschwindigkeitsoffset);
ins_606(int nr, int Kreiskugeln, int Stackkugeln);
ins_607(int nr, int Spreadtyp);

Keine sorge, ich werde noch alles erklären!
Kugeln werden in eigenen Variablen gespeichert, bis sie abgeschossen werden. Diese Variable ist die Nummer, die am Anfang jeder Kugelfunktion steht. Wir werden für unsere Kugel die Variable #0 benutzen.
Diese Variable initialisieren wir mit

ins_600(0);

Wir werden auf die Folgenden Funktionen später noch eingehen, also wird die Erklährung hier etwas spährlicher ausfallen.
Zur Position der Kugel: Eine Kugel Spawnt automatisch auf dem Punkt des Gegners, aus derer Subroutine sie Abgeschossen wurde. Wenn du die Position trotzdem noch verändern möchtest, kannst du dazu die Funktion 603 nutzen.
Nun haben wir unsere Variable. Als nächstes sollten wir die Grafik und Farbe unserer Kugel wählen. Für's erste nehmen wir die Grafik 0 und die Farbe 0 (Kleine, Weiße Kugel).

ins_602(0, 0, 0);

Jetzt müssen wir Touhou mitteilen, das wir eine Kugel in eine Festgesetzte richtung schießen wollen.
Mit ins_606 können wir die Anzahl der Kugeln bestimmen. Da wir nur Eine Kugel haben, setzen wir beide Werte auf 1:

ins_606(0, 1, 1);

Als nächstes wollen wir Touhou sagen, das wir die Kugel nicht zielgerichtet, sondern in eine festgesetzte Richtung schießen wollen.
Das tun wir mit der Funktion 607 und dem Argument 1. (0 währe zielgerichtet)

ins_607(0, 1);

Zum schluss müssen wir nur noch die Richtung und Geschwindigkeit eingeben!
Die Offsets können wir vorerst ignorieren, wir setzen sie einfach auf denselben wert wie das andere Argument.
Touhou benutzt für Richtungen die basis π*2 für einen Kreis, wobei 0 nach Rechts zeigt. Um die Kugel beispielsweise nach unten zu schießen, währe die Richtung π/2.
Also, Setzen wir die Richtung mal auf π/2 und die Geschwindigkeit auf 1.

ins_604(0, 1.57079632679f, 1.57079632679f);
ins_605(0, 1.0f, 1.0f);

Und schießen die Kugel mit 601 ab.

ins_601(0);

Nun wollen wir mal diese Funktionen in unseren Gegner einbauen!



Funktionen und Routinen

Kugeln schießen tut man normalerweise in einer eigenen Funktion.
Warum, ist bei einer einzigen Kugel vielleicht noch nicht so klar, aber spätestens, wenn du große Bulletpattern hast, und asynchron dazu den Boss bewegen willst, werden dir eigene Bulletfunktionen das Leben deutlich leichter machen.
Eine Funktion wird im Grunde genauso geschrieben wie eine Subroutine, allerdings mit den Befehlen 11 oder 15 aufgerufen. Dadurch wird sie nicht zu einer eigenen Routine mit Variablen, sondern teilt sich alle Variablen mit der Funktion, aus der sie aufgerufen wird (und wird automatisch beendet, wenn die Hauptroutine "Stirbt").
Wird eine Funktion mit ins_11 aufgerufen, wartet die Routine, bis die Funktion beendet wird, und läuft dann weiter. Wird eine Funktion allerdings mit ins_15 aufgerufen, läuft die Funktion parrallel zu der Hauptroutine mit. Wir nutzen ins_15, warum wird später noch wichtig.
Das Script sollte jetzt ungefähr so aussehen:

anim { "enemy.anm"; }

sub Gegner()
{
    var;
    ins_302(2);
    ins_306(0, 20);
    ins_500(24.0f, 24.0f);
    ins_501(16.0f, 16.0f);
    ins_401(60, 0, 0.0f, 128.0f);
    ins_23(180);
    ins_15(Gegner_at);
    ins_401(60, 0, 120.0f, 128.0f);
loop_start:
    ins_23(100);
    ins_12(loop_start, 0);
    ins_1();
}

sub Gegner_at()
{
    var;
    ins_600(0);
    ins_602(0, 0, 0);
    ins_604(0, 1.57079632679f, 1.57079632679f);
    ins_605(0, 1.0f, 1.0f);
    ins_606(0, 1, 1);
    ins_607(0, 1);
    ins_601(0);
    ins_1();
}
 
sub main()
{
    var;
    ins_502(163);
    ins_23(120);
    ins_301(Gegner, 0.0f, 0.0f, 100, 100, 1);
loop_start:
    ins_23(1000);
    ins_12(loop_start, 0);
    ins_1();
}    

Schön, nicht war?



Mehr Kugeln!

Eine einzige Kugel zu schießen ist ja langweilig, deswegen versuchen wir uns mal an einem Kugelvorhang.

Kugelvorhang

Wir nehmen mal einen Kugelvorhang aus 5 Kugeln, die sich alle 0.15 Grad voneinander unterscheiden - also ein Richtungsoffset von 0.15f haben. wir müssen also

ins_604(0, 1.57079632679f, 1.57079632679f);
ins_606(0, 1, 1);

Verändern in

ins_604(0, 1.57079632679f, 0.15f);
ins_606(0, 5, 1);

Die richtung wird dabei so angepasst, das die mittlere kugel direkt nach unten schießt. -Praktisch, nicht war?

Kreis

Ein Kreis Funktioniert genauso wie ein Vorhang, nur das das Kugeloffset automatisch so angepasst wird, das die Kugeln einen Kreis bilden.
Dazu stellt man einfach das Spread-Pattern auf 2 (zielgerichtet) oder 3 (nicht zielgerichtet)

ins_607(0, 3);

Übersicht über die einfachen Pattern:

Wand

ins_607(0, 0) oder ins_607(0, 1)

Kreis

ins_607(0, 2) oder ins_607(0, 3)

Zufall

ins_607(0, 6)

"Up and Down"

ins_607(0, 11)




Komplexere Pattern

--Comming Soon---