ECL Tutorial

Table of Contents

Back

Introduction

Headstart

First off, there are three main version of ecl that are compatible with this tutorial, games before MoF use a different System.

V2
used from MoF to SA
V3
used from UFO to GFW
V4
used from DS until now

Compiling ECL

ECL scripts need to be compiled into the ECL bytecode format to be understood by the games. The easiest way to do this is by using thtk.

Notice: You will need a mnemonic map for your ECL-version for the examples to compile. A mnemonic map is used to set the 'names' of the instructions. You can download them from the ECL-Standard page.

To compile a script, use thecl -m [map] -c [id] ecl_file.ect ecl_file.ecl with your mnemonic map and game id.

Notice: For this tutorial, we use the file ending .ecl to indicate ECL-bytecode and .ect to indicate ECL scripts. If you want, you can use anything you want (.txt, for example)

Running ECL

To test your scripts, use thpatch. If you want an easy testing environment, select the skipgame patch, and place your ECL files under /thpatch/skipgame/thxxx/, where thxxx is the ID of the game you want work with.
After that, you only have to quit out of a stage and go back into it to reload after a change.

File names

ECL file names follow a set pattern:

for main games before UFO,
stage0x.ecl is loaded at the start of a stage.
for main games after UFO,
st0x.ecl is loaded at the start of a stage.
for photo games,
ecl_xxxx.ecl is loaded at the start of a scene.
  • Examples:

    If you want to edit the first stage of MoF, your file is thpatch/skipgame/th10/stage01.ect
    If you want to edit the second stage of TD, your file is thpatch/skipgame/th13/st02.ect

The first Enemy

Routines and Instructions

The main routine

  • Starting your script

    ECL scripts start at an entry routine. In the main games this routine is main, in photo games it is Appear. The syntax is as follows:

    sub main() {
      big_return(); 
    }
    

    sub is the keyword for subroutines, which contain blocks of instructions. Instructions are defined by their name or ins_ followed by a number.

    Every subroutine has to end in a return instruction, which terminates the sub. There are two types of return: small_return and big_return. for now we will use big_return.

    If you compile the script above and start it, you'll notice that the game will be stuck in the stage without anything happening. This is the state with no subroutines active.

    To stop the routine from dying, we need to add instructions that pass time. A fitting instruction for this is wait, which waits a specified time in frames (60 frames = 1 second at normal speed) before continuing. So to let the routine stay active for 10 seconds, we have

    sub main() 
    {
        wait(600);
        big_return();
    }
    

    Now the main instruction stays alive for 10 Seconds and we can start shooting.

  • Flags and Loops

    One thing you will notice if you run this Script, is, that you can actually shoot down the main sub. This is because every sub is bound to an entity, an object in the game with a set of parameters:

    • Flags
    • Position
    • Life
    • Reward
    • Local Variables

    The entity that our main sub belongs to is the main entity, it has all it's parameters set to zero.
    To make an object not-shootable, we have to activate it's hide unit flag. For that we use ent_set_flag, which takes the flag id we want to set as a parameter. The hide unit flag is 32, so we set

    ent_set_flag(32);
    

    Next, we want our sub to wait infinitely. We'll do this with a goto instruction. We set a label, wait and then jump to it:

    loop_start:
      wait(10000);
      goto loop_start @ 0;
    

    The @ 0 part sets the local time of the sub, but we'll come to that later.

    So now, our code looks like this:

    sub main() 
    {
        ent_set_flag(32);
    
      loop_start:
        wait(10000);
        goto loop_start @ 0;
        big_return();
    }
    

Entities and Animations

Creating a second Entity

  • Creating Entities

    To create a new entity, we use ent_create. It's parameters are ent_create(sub, x, y, life, score reward, item reward)
    But first, we have to create a new sub to attach to it:
    (We'll use ent_set_anmscr for now to give our sub a sprite)

    sub entity()
    {
        ent_set_anmscr(0, 158);
    
      loop_start:
        wait(10000);
        goto loop_start @ 0;
        big_return();
    }
    
    sub main()
    {
        ent_set_flag(32);
        ent_create("entity", 0.0f, 10.0f, 1, 1, 0);
    
      loop_start:
        wait(10000);
        goto loop_start @ 0;
        big_return();
    }
    
  • Movement

    There are three main types of movement:

    By Position
    moves the object to a position
    functions are ent_set_pos(x, y) and ent_chg_pos(time, type, x, y)
    X coordinates are centered, Y coordinates aren't.
    By Direction
    moves the object in a direction
    functions are ent_set_dir(direction, speed) and ent_chg_dir(time, type, direction, speed)
    Direction is in radians (right is 0, down is pi/2), speed in coordinates-per-frame.
    Special
    animated movements like B├ęzier curves

    A movement by Position or Direction can be either instant (set) or animated (chg). Animations can be

    Linear (0)
    moves at a constant speed.
    Accelerated (1)
    moves slow at first and gets faster.
    Decellerated (4)
    moves fast at first and gets slower.
    Smooth-step (9)
    accelerates and decelerates

    So we could, for example, move our entity from its spawn point to (0.0f, 228.0f) in 60 frames by writing

    ent_chg_pos(60, 0, 0.0f, 228.0f);
    

Changing attributes

  • Life and Hitboxes

    The life of our unit is normally set by ent_create, but we can also set it inside an entity by calling ent_set_life(life)
    Hitboxes are not set by default. There are two types of them, one for collision with bullets (hitbox) and one for collision with the player (killbox).
    They are set by ent_set_hitbox(x, y) and ent_set_killbox(x, y)
    Typical hitboxes are (48.0f, 48.0f) for big and (24.0f, 24.0f) for normal fairies. Typical killboxes are (48.0f, 48.0f) for big and (16.0f, 16.0f) for normal fairies.

  • Sprites

    The sprites and animations are not defined in ecl files. This is handled by another language called anm. We'll not learn how to write it in this tutorial, but we can use already made anm's.
    To use an anm file in a game, we have to include it. Sprites for faries are contained in the file enemy.anm. You include it like this:

    anim {
      "enemy.anm";
    }
    

    The file effect.anm (or bullet.anm in some games) containing bullet sprites and clear/transition effects is always included.
    To use an animation from a anm file, you have to first specify the file your animation is in. this is done by ent_set_anmfile(file id). effects.anm is id 0, your includes take the IDs from 1 up.
    After that you can assign animations from that file to the entity your sub is attached to. Entities can have multiple animations active on seperate animation slots. You assign an animation to one with ent_set_anmscr(slot, animation id). Generally you use the slot 0 for the main sprite.

    So, lets assign an animation to our enemy. You can find an animation of a fairy in enemy.anm with the animation-id 0:

    anim {
      "enemy.anm";
    }
    
    sub entity()
    {
        ent_set_anmfile(1);
        ent_set_anmscr(0, 0);
    
        ent_chg_pos(60, 0, 0.0f, 228.0f);
    
      loop_start:
        wait(10000);
        goto loop_start @ 0;
        big_return();
    }
    
    sub main()
    {
        ent_set_flag(32);
        ent_create("entity", 0.0f, 10.0f, 1, 1, 0);
    
      loop_start:
        wait(10000);
        goto loop_start @ 0;
        big_return();
    }
    

    This works well when your entity is just moving up and down, but eventually you'll need animations that change with movement. If you try out some animation id's you'll discover that there already are other animations for our fairy:

    ID Movement
    0 Facing forward
    1 Moving right
    2 Moving left
    3 Stopping from Right
    4 Stopping from Left

    Because this is a common problem, there is an instruction for this: ent_set_anmscr_i(slot, animation_id)
    So if we just change that in our code, the fairy animation changes with movement.

    anim {
      "enemy.anm";
    }
    
    sub entity()
    {
        ent_set_anmfile(1);
        ent_set_anmscr_i(0, 0);
    
        ent_chg_pos(60, 0, 80.0f, 128.0f);
        wait(120);
        ent_chg_pos(60, 0, -80.0f, 128.0f);
        wait(120);
        ent_chg_pos(60, 0, -80.0f, 228.0f);
    
      loop_start:
        wait(10000);
        goto loop_start @ 0;
        big_return();
    }
    
    sub main()
    {
        ent_set_flag(32);
        ent_create("entity", 0.0f, 10.0f, 1, 1, 0);
    
      loop_start:
        wait(10000);
        goto loop_start @ 0;
        big_return();
    }
    

Local Time and action composition

If you are planning things like movement paths or spawning of entities, having a lot of wait instructions like

ent_chg_pos(60, 0, 80.0f, 128.0f);
wait(120);
ent_chg_pos(60, 0, -80.0f, 128.0f);
wait(120);
ent_chg_pos(60, 0, -80.0f, 228.0f);

Can look a bit messy, and all timings rely on the timings of the other.
An easier way is the use of specific timings:

0:
  ent_chg_pos(60, 0, 80.0f, 128.0f);
120:
  ent_chg_pos(60, 0, -80.0f, 128.0f);
240:
  ent_chg_pos(60, 0, -80.0f, 228.0f);

All timings are sub-specific and start at 0 on sub creation. You can change the time of your sub with the @ time argument of the goto instruction.

So, Let's start shooting bullets!

Firering a bullet

About Bullet handles

Unlike some other Danmaku-Scripts, in ECL you normally don't fire each bullet individually. Instead, bullets are grouped by appearance and behaiviour and then controlled by a bullet handle, which handles transformations and the such.
This has the advantage that you can create template-Handles on entity creation, and then tweak their settings by difficulty and wave.
Bullet handles are saved by an ID. They are created with bh_create(id) and started by bh_start(id).

There are a lot of things you can do with bullet handles; here are the most common options:

Sprites and Colors

Sprites and colors somehow depend on ANM id's in effect.anm. They are propably hardcoded, and I don't have a comprehensive list of them at the moment, but they are set with bh_set_color(id, sprite, color).
Some Sprites have 16 colors, some 8 and some 4.

Rows and Columns

Generally there are two ways in which bullet patterns can spread (Rows and Columns), and two ways they can align (Ring and Wall):

Spread/Align Ring Wall
Rows ring_row.png wall_row.png
Columns ring_column.png wall_column.png

You can set the Alignment with bh_set_arrange_mode(id, mode)
The modes are:

Mode Explanation
0 Wall, aimed
1 Wall
2 Ring, aimed
3 Ring
4 Away
5 Ring, with extra offset
6 Random
7 Ring, random speeds
8 Arc, random speeds

You can set the number of rows and columns with bh_set_count(id, rows, columns)

Let's test these out with a simple formation:
We want to shoot one ring of 20 bullets. For that we need to set the Arrange mode to Ring (or Ring, aimed), the number of rows to 20, and the number of columns to 1:

bh_create(0);
bh_set_arrange_mode(0, 2);
bh_set_count(0, 20, 1);
bh_start(0);

Spawn Offset

Something that might bother you is that the bullets are spawning directly on the boss image. You can avoid that by setting a spawn offset with bh_set_ang_offs(id, offset):

With an offset of 0.0f With an offset of 28.0f
on_enemy.png around_enemy.png

You should not set a spawn offset bigger than the enemies killbox to avoid creating spawn-safespots.

Speed Offset

If you are fireing multiple columns, they are all fired at the same time on the same spot. So, if you set them all to the same speed, they all fly on top of each other.

10 columns, Speed set to 2 10 columns, Speed from 1 to 3
speed_2.png speed_1_3.png

To make the columns different from each other, you assign them different speeds. To set a speed for your bullets, you typically use bh_set_speed(id, start, end).
If you have only one column, then all bullets fire at the start speed. If you have multiple columns, columns take ascending speeds from start to finish.

Angles

There are two values for setting angles, both of them are set with bh_set_angle(id, angle, offset)
The angle is always moving the center of the bullets. You can use it to aim the bullets away from something.

The offset depends on the arrange mode:

When firing walls,
the offset is used to change the offset between rows (the bullets all arrange around the middle bullet).
When firing rings,
the offset is used to change the offset between columns (the offset is appended to each row, the first column aims at the center)

Here are some examples:

angle, offset Wall Ring
0.00, 0.00 wall_0_0.png ring_0_0.png
0.25, 0.00 wall_25_0.png ring_25_0.png
0.00, 0.25 wall_0_25.png ring_0_25.png
0.25, 0.25 wall_25_25.png ring_25_25.png

Moving and shooting

So after playing aroud with movements and bullets, you propably have a mix of timings, movement, and bullet instructions in your entity. Maybe something like this:

anim {
  "enemy.anm";
}

sub entity()
{
    ent_set_anmfile(1);
    ent_set_anmscr_i(0, 0);

    bh_create(0);
    bh_set_arrange_mode(0, 2);
    bh_set_count(0, 20, 1);

    ent_chg_pos(60, 0, 80.0f, 128.0f);
  60:
    bh_start(0);
  120:
    ent_chg_pos(60, 0, -80.0f, 128.0f);
  180:
    bh_start(0);
  240:
    ent_chg_pos(60, 0, -80.0f, 228.0f);
  300:
    bh_start(0);

  loop_start:
    wait(10000);
    goto loop_start @ 0;
    big_return();
}

sub main()
{
    ent_set_flag(32);
    ent_create("entity", 0.0f, 10.0f, 1, 1, 0);

  loop_start:
    wait(10000);
    goto loop_start @ 0;
    big_return();
}

Entity sub-structure

You usually split your Entity into the subroutines sub [Color][Type][ID]() for setting sprites, sub [Type][ID] for movement paths, and sub [Type][ID]_at for attacks.
In our example, [Color] Would be B (for blue), [Type] would be Girl (ZUN's naming scheme for faries), and [ID] would be 00 (because this is our first enemy):

sub BGirl00() {} 
sub Girl00() {}
sub Girl00_at() {}

Typically you create an entity with [Color][Type][ID], which then calls the movmement sub which stats the attack-sub.

There are two ways subs can run: as calls (sub_call(sub name)) and coroutines (ent_start_sub_bg(sub name)). When a sub is run as a call, the calling sub waits for the called to finish before continuing. When a subs are run as a coroutine, the calling and the called sub run in sequence.

Now you also need to know the difference between big_return and small_return:
Every entity has a main stack of subroutines witch contains the main sub of the entity (the one you call with ent_create) and all its calls (not its coroutines). If one of these subroutines uses big_return, the entity is also destroyed.
This didn't bother us until now because we only had one sub per entity, but if we want to call a subroutine and then continue execution after it returns, we have to use small_return.
Normally, you use big_return in the main function of your entity, and then use small_return in all coroutines and calls, even if they never return.

Our example from above would fit into that like this:

anim {
  "enemy.anm";
}

sub BGirl00() 
{
    ent_set_anmfile(1);
    ent_set_anmscr_i(0, 0);

    sub_call(Girl00);
    big_return();
}

sub Girl00() 
{
    ent_start_sub_bg(Girl00_at);

    ent_chg_pos(60, 0, 80.0f, 128.0f);
  120:
    ent_chg_pos(60, 0, -80.0f, 128.0f);
  240:
    ent_chg_pos(60, 0, -80.0f, 228.0f);

  loop_start:
    wait(10000);
    goto loop_start @ 0;
    small_return();
}

sub Girl00_at() 
{
    bh_create(0);
    bh_set_arrange_mode(0, 2);
    bh_set_count(0, 20, 1);

  loop_start:
  60:
      bh_start(0);
  120:
      goto loop_start @ 0;

    small_return();
}

sub main()
{
    ent_set_flag(32);
    ent_create("BGirl00", 0.0f, 10.0f, 1, 1, 0);

  loop_start:
    wait(10000);
    goto loop_start @ 0;
    big_return();
}

The obvious advantage of this is that you can easily recycle Enemy behaviour.
For example, if you want your fairy in a different color, you can simply create a RGirl00(), which then calls Girl00. Or, if you want your fairy to go a different path, you can create a BGirl01 and Girl01, which then still uses Girl00_at.
This is also the way enemies are structured in the original games.

Variables, Waves and the Stack

If you are editing your scripts, you propably encounter the instance that you want to spawn a lot of enemies in a row.
You could solve this by doing something like

ent_create("enemy", -100.0f, 100.0f, ...);
ent_create("enemy", -80.0f, 100.0f, ...);
ent_create("enemy", -60.0f, 100.0f, ...);
...

But there is an easier way: The decrement loop.
This is just a loop that counts down a number for each interation, and breaks if the number reaches zero.
Because this number should change, we need to store it in a Variable.

Variables

You declare all variables for a sub at the beginning of the sub, using the keyword var:

sub s() {
  var variable_1 variable_2;
  small_return();
}

There are no commas between the variable names.

There are two types of variables in ECL:
S (int) and f (float).

You should only use one type per variable.
To access a S variable, use $variable_name
To access a f variable, use %variable_name

With this you can use simple math, like:

sub s() {
  var A B C;
  $A = 0;
  $B = $A + 10;
  %C = 24.0f;
  big_return();
}

if you have to change the type of a variable, you have to use a cast:

sub s() {
  var C D E;
  %C = 24.0f;
  $D = 10;
  $E = $D + _S(%C);
  %C = %C + _f($E);
  small_return();
}

If you have to concaternate more than one equation, also use casts:

sub s() {
  var A;
  %A = _f(10.0f * _f( _f(100.0f / 16.0f) + 1.0f));
  ent_set_life(_S(10 + _S(%A)));
  small_return();
}

With that we have everything we need to write our decrement loop.
The only thing left is a goto instruction that only executes if the counter is greater than 0. There is a if [EQ] goto label @ time construct for this in ECL:

sub spawn() {
  var i;

  $i = 10;
 loop_start:
   ent_create(BGirl00, _f(-100.0f + _f(20.0f * _f($i))), 100.0f, 100, 100, 0);
  $i = $i - 1;
  if $i > 0 goto loop_start @ 0;

  small_return();
}

And, there is also a special instruction just for decrementing and comparing (var_dec), that we can use by writing

sub spawn() {
  var i;

  $i = 10;
 loop_start:
   ent_create(BGirl00, _f(-100.0f + _f(20.0f * _f($i))), 100.0f, 100, 100, 0);
  if $i-- goto loop_start @ 0;

  small_return();
}

However, this construct differs from the other implementation: $i-- does subtract the variable after checking it against zero, so this loop acually runs an iteration longer than the one above.

Last Update: 2019-03-27 Mi 11:08

Validate