Author Topic: Player script tutorial  (Read 17893 times)

Stuffman

  • *
  • We're having a ball!
Player script tutorial
« on: April 26, 2009, 06:34:23 PM »
In this tutorial we will go through all the steps involved in creating a player script, using Sakuya A as the subject.

This tutorial is Intermediate/Advanced. It is advised that you understand how to use effect objects with vertices before proceeding.



The first step when creating a character is to first have a concept a mind. That's kind a "well duh" kind of thing, but the more details you have laid out in your head the easier a time you'll have.

Before you can get anywhere making your character, you'll need to have your resources assembled: at a minimum, the character's sprite and shot graphics, and you'll also need any sound effects and graphics for bombs or other miscellaneous effects you want to use.



These are the graphics we're using for Sakuya. Her spritesheet "sakuya.png", which also includes her shot graphics and life stock icon, her cutin "sakuya_select.png" for spellcards (which will also be her select screen portrait), and the special knives "sakuya_spell.png" for her bombs. There's also a sound effect called "knife.wav" that I'll use for bombs.

There's one more file we need to put together before we get started, and that's a custom shot definition file. This is necessary because player scripts can't use the default bullets like RED01, but it's also pretty simple to put together. You create a txt file with #PlayerShotData as the header, and structure it like so:

Code: [Select]
#PlayerShotData
ShotImage=".\sakuya.png"
ShotData{
  id=1
  rect=(1,49,47,95)
  render=ALPHA
  alpha=160
}
ShotData{
  id = 2
  rect = (49,49,95,95)
  render = ALPHA
  alpha=160
}
ShotData{
  id = 11 //ID
  rect = (1,49,47,95)
  render = ALPHA
  alpha=200
}
ShotData{
  id = 12 //ID
  rect = (49,49,95,95)
  render = ALPHA
  alpha=200
}

Sakuya only has two bullets, her blue knives and her purple ones, so this is pretty straightforward. Bullets 11 and 12 are identical to 1 and 2, but with higher alpha. I'll use these for the focus bullets since I want them to be brighter.
  • ShotImage is the path to the spritesheet with the bullets. You can just put ".\" before it to signify that it's in the same directory.
  • id is the number that you use for this bullet's graphic.
  • rect is the coordinates for the bullet on the spritesheet.
  • render is the rendering method used for the bullet. ALPHA is normal, ADD is for glowing bullets.
  • alpha is the transparency of the bullet if you're using ALPHA render, with 0 being totally clear and 255 being solid. You generally don't want player bullets to be solid, since you want enemy bullets to stand out more.

There are some other things you can do with bullets, like animation, but we won't cover that in this tutorial.

Anyway, with that put together, now we can get started. This is what the skeleton of a player script looks like:

Code: [Select]
#“Œ•?’e–‹•—[Player]
#ScriptVersion[2]
#Menu[Player name]
#Text[Player description
Can put in multiple lines]
#Image[Player image]
#ReplayName[Short player name]

script_player_main{
  @Initialize{
    LoadPlayerShotData
    SetPlayerLifeImage
    SetSpeed
  }
  @MainLoop{
  }
  @Missed{
  }
  @SpellCard{
  }
  @DrawLoop{
  }
  @Finalize
  }
}

script_spell ScriptName{
  @Initialize{
  }
  @MainLoop{
  }
  @Finalize{
  }
}

As you can see, it looks a lot like an enemy spellcard script, and it is similar in most ways.

script_player_main is your primary bulk of code. Within it, you probably recognize @Initialize, @MainLoop, @DrawLoop, and @Finalize. The additional brackets are @Missed, which is code that will execute when you die and respawn, and @SpellCard, which will be called when you press the bomb button.

There is also an entirely different script type for players called script_spell, which is used for making Bombs. You can have as many of these as you want in a script, though most players have just two. You call script_spells from your script_player_main, usually inside @SpellCard.

Now, to begin structuring the script, let's first examine how Sakuya A works, and decide what we'll need to do to script it.
  • Sakuya herself fires blue knives, and has two options that fire purple ones.
  • Sakuya's unfocused shot is a simple forward spread shot.
  • Sakuya's focused shot will stream her knives together into a limited homing attack. The knives will only target the enemy if it's somewhere above Sakuya, within about a 60 degree arc. Otherwise, they fly straight.
  • Sakuya's unfocused bomb, Indiscriminate, will fire knives in every direction that arc slightly as they fly.
  • Sakuya's focused bomb, Killer Doll, will release spinning knives in a circle around her, which will target an enemy and fly straight at them, stretching into a laser-like line.
If you've fooled around with advanced spellcard scripts at all you'll know that bullets with unique behavior sometimes require being made as Object Bullets, using tasks. For Sakuya, we'll use a task for her focused shot knives, for her options, and also her spell knives.

Additionally, you'll notice the three functions under @Initialize. These are unique to players and are basically required. LoadPlayerShotData is used to load the shot definition file we made, SetPlayerLifeImage is used to specify an image for the player's life icons in the sidebar (in this case, I'm using the watch in the spritesheet). Note that the life image you specify is shrunk to half size for the icon, so double its size in the spritesheet if you want it to use the full resolution. The final function, SetSpeed, is used to define both your normal and focused speed. For reference, Reimu's speeds are 4/1.5, and Marisa's speeds are 5/2. Sakuya traditionally has low normal speed but relatively fast focus speed, so I'm putting her at 3.5/2.5.

Updating our skeleton to load our resources, create tasks, and set the basic functions listed above, this is what we have now:
Code: [Select]
#“Œ•?’e–‹•—[Player]
#ScriptVersion[2]
#Menu[Sakuya A]
#Text[Sakuya Izayoi - Illusion Sign

Shot:
Unfocused: Jack the Ludo Bile (spread)
Focused: Jack the Ripper (homing)

Spell Card:
Illusion Sign �uIndiscriminate�v
Illusion Sign �uKiller Doll�v]
#Image[.\sakuya_select.png]
#ReplayName[SakuyaA]

script_player_main{
  let img_sakuya = GetCurrentScriptDirectory()~"sakuya.png";
  let img_cutin = GetCurrentScriptDirectory()~"sakuya_select.png";

  task Option(position){
  }
  task JackTheRipper(x,y,graphic){
  }
  @Initialize{
    LoadGraphic(img_sakuya);
    LoadGraphic(img_cutin);
    LoadPlayerShotData(GetCurrentScriptDirectory()~"sakuya_shotdata.txt");
    SetPlayerLifeImage(img_sakuya, 96, 48, 143, 95);
    SetSpeed(3.5, 2.5);
  }
  @MainLoop{
    yield;
  }
  @Missed{
  }
  @SpellCard{
  }
  @DrawLoop{
  }
  @Finalize{
    DeleteGraphic(img_sakuya);
    DeleteGraphic(img_cutin);
  }
}

script_spell Indiscriminate{
  task Spell_Knife(x,y,angle,type){
  }
  @Initialize{
  }
  @MainLoop{
  }
  @Finalize{
  }
}

script_spell KillerDoll{
  task Spell_Knife(x,y,angle,type){
  }
  @Initialize{
  }
  @MainLoop{
  }
  @Finalize{
  }
}

First, let's get Sakuya visible onscreen. I didn't give Sakuya a lot of frames so this is pretty simple.

Code: [Select]
@DrawLoop{
  SetTexture(img_sakuya);
  if(GetKeyState(VK_LEFT)==KEY_PUSH || GetKeyState(VK_LEFT)==KEY_HOLD){
    SetGraphicRect(49.5, 1.5, 95.5, 47.5); // left movement frame
  }else if(GetKeyState(VK_RIGHT)==KEY_PUSH || GetKeyState(VK_RIGHT)==KEY_HOLD){
    SetGraphicRect(97.5, 1.5, 143.5, 47.5); // right movement frame
  }else{
    SetGraphicRect(1.5, 1.5, 47.5, 47.5); // neutral frame
  }
  DrawGraphic(GetPlayerX(), GetPlayerY());
}

The new function of interest here is GetKeyState, which reads information from your keyboard (or gamepad, if applicable). GetKeyState returns the current state of the specified key. The keys are VK_LEFT, VK_RIGHT, VK_UP, VK_DOWN, VK_SHOT, VK_BOMB, and VK_SLOWMOVE, which are all quite self-explanatory. The returned codes are KEY_FREE and KEY_HOLD, which means the button is off and on respectively, but there's also KEY_PUSH, which will tell you that the button was just pressed this frame, and KEY_PULL, which means it was just released this frame. In this case, we're simply checking if the player is pressing left or right, and setting Sakuya's frame accordingly.

With that put together, the next order of business is to create Sakuya's options, since she'll be firing some of her bullets from them. Her options will be Effect objects maintained by the task. For this tutorial, we'll assume you already know how objects work. Now, one thing we'll need to keep in mind is that there are going to be two options, one on either side of Sakuya. These two options, while similar, are not identical, so we won't bother to make two tasks but we will branch it so that the task will land in one of two loops, "LEFT" or "RIGHT". This is the purpose of the "position" string declared for Option. Later, we'll call the options as Option("LEFT") and Option("RIGHT"). We're also creating two new variables, optionxpos and optionypos. These indicate the distance from Sakuya's center that the orbs will be at; we're making it a variable so that we can move them around. Since the options are always parallel we only need one set of optionxpos/optionypos.

Code: [Select]
let optionxpos=16;
let optionypos=0;

task Option(position){
  let objoption=Obj_Create(OBJ_EFFECT);
  Obj_SetAlpha(objoption,200);
  ObjEffect_SetTexture(objoption,img_sakuya);  //uses star orb from spritesheet
  ObjEffect_SetRenderState(objoption,ALPHA);
  ObjEffect_SetPrimitiveType(objoption,PRIMITIVE_TRIANGLEFAN);
  ObjEffect_CreateVertex(objoption,4);         // square object with 4 vertexes
  ObjEffect_SetVertexUV(objoption,0,145,1);    // four coordinates of orb on spritesheet
  ObjEffect_SetVertexUV(objoption,1,159,1);    // object is 15x15
  ObjEffect_SetVertexUV(objoption,2,159,15);
  ObjEffect_SetVertexUV(objoption,3,145,15);
  if(position=="LEFT"){
    while(!Obj_BeDeleted(objoption)){
      ObjEffect_SetVertexXY(objoption,0,GetPlayerX-optionxpos-8,GetPlayerY+optionypos-7);
      ObjEffect_SetVertexXY(objoption,1,GetPlayerX-optionxpos+6,GetPlayerY+optionypos-7);
      ObjEffect_SetVertexXY(objoption,2,GetPlayerX-optionxpos+6,GetPlayerY+optionypos+7);
      ObjEffect_SetVertexXY(objoption,3,GetPlayerX-optionxpos-8,GetPlayerY+optionypos+7);
      yield; // note that the left orb is shifted one pixel to the left to make the total width even
    }
  }else{
    while(!Obj_BeDeleted(objoption)){
      ObjEffect_SetVertexXY(objoption,0,GetPlayerX+optionxpos-7,GetPlayerY+optionypos-7);
      ObjEffect_SetVertexXY(objoption,1,GetPlayerX+optionxpos+7,GetPlayerY+optionypos-7);
      ObjEffect_SetVertexXY(objoption,2,GetPlayerX+optionxpos+7,GetPlayerY+optionypos+7);
      ObjEffect_SetVertexXY(objoption,3,GetPlayerX+optionxpos-7,GetPlayerY+optionypos+7);
      yield;
    }
  }
}

Did you get all that? Creating the object was standard operating procedure, but the location of the vertices is updated in the loop, creating a 15x15 square (the dimensions of the image) centered on the optionxpos/optionypos offset from the player. So after we create these options, all we need to do is change optionxpos and optionypos to move the options. Keen!

With that done, we'll simply add this to @Initialize, which will create the options.

Code: [Select]
Option("LEFT");
Option("RIGHT");

Put that all together, and this is our current code.

Code: [Select]
#“Œ•?’e–‹•—[Player]
#ScriptVersion[2]
#Menu[Sakuya A]
#Text[Sakuya Izayoi - Illusion Sign

Shot:
Unfocused: Jack the Ludo Bile (spread)
Focused: Jack the Ripper (homing)

Spell Card:
Illusion Sign �uIndiscriminate�v
Illusion Sign �uKiller Doll�v]
#Image[.\sakuya_select.png]
#ReplayName[SakuyaA]

script_player_main{
  let img_sakuya = GetCurrentScriptDirectory()~"sakuya.png";
  let img_cutin = GetCurrentScriptDirectory()~"sakuya_select.png";
  let optionxpos=16;
  let optionypos=0;
 
  task Option(position){
    let objoption=Obj_Create(OBJ_EFFECT);
    Obj_SetAlpha(objoption,200);
    ObjEffect_SetTexture(objoption,img_sakuya);  //uses star orb from spritesheet
    ObjEffect_SetRenderState(objoption,ALPHA);
    ObjEffect_SetPrimitiveType(objoption,PRIMITIVE_TRIANGLEFAN);
    ObjEffect_CreateVertex(objoption,4);         // square object with 4 vertexes
    ObjEffect_SetVertexUV(objoption,0,145,1);    // four coordinates of orb on spritesheet
    ObjEffect_SetVertexUV(objoption,1,159,1);    // object is 15x15
    ObjEffect_SetVertexUV(objoption,2,159,15);
    ObjEffect_SetVertexUV(objoption,3,145,15);
    if(position=="LEFT"){
      while(!Obj_BeDeleted(objoption)){
        ObjEffect_SetVertexXY(objoption,0,GetPlayerX-optionxpos-8,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,1,GetPlayerX-optionxpos+6,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,2,GetPlayerX-optionxpos+6,GetPlayerY+optionypos+7);
        ObjEffect_SetVertexXY(objoption,3,GetPlayerX-optionxpos-8,GetPlayerY+optionypos+7);
        yield; // note that the left orb is shifted one pixel to the left to make the total width even
      }
    }else{
      while(!Obj_BeDeleted(objoption)){
        ObjEffect_SetVertexXY(objoption,0,GetPlayerX+optionxpos-7,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,1,GetPlayerX+optionxpos+7,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,2,GetPlayerX+optionxpos+7,GetPlayerY+optionypos+7);
        ObjEffect_SetVertexXY(objoption,3,GetPlayerX+optionxpos-7,GetPlayerY+optionypos+7);
        yield;
      }
    }
  }
  task JackTheRipper(x,y,graphic){
  }
  @Initialize{
    LoadGraphic(img_sakuya);
    LoadGraphic(img_cutin);
    LoadPlayerShotData(GetCurrentScriptDirectory()~"sakuya_shotdata.txt");
    SetPlayerLifeImage(img_sakuya, 96, 48, 143, 95);
    SetSpeed(3.5, 2.5);
    Option("LEFT");
    Option("RIGHT");
  }
  @MainLoop{
    yield;
  }
  @Missed{
  }
  @SpellCard{
  }
  @DrawLoop{
    SetTexture(img_sakuya);
    if(GetKeyState(VK_LEFT)==KEY_PUSH || GetKeyState(VK_LEFT)==KEY_HOLD){
      SetGraphicRect(49.5, 1.5, 95.5, 47.5); // left movement frame
    }else if(GetKeyState(VK_RIGHT)==KEY_PUSH || GetKeyState(VK_RIGHT)==KEY_HOLD){
      SetGraphicRect(97.5, 1.5, 143.5, 47.5); // right movement frame
    }else{
      SetGraphicRect(1.5, 1.5, 47.5, 47.5); // neutral frame
    }
    DrawGraphic(GetPlayerX(), GetPlayerY());
  }
  @Finalize{
    DeleteGraphic(img_sakuya);
    DeleteGraphic(img_cutin);
  }
}

script_spell Indiscriminate{
  task Spell_Knife(x,y,angle,type){
  }
  @Initialize{
  }
  @MainLoop{
  }
  @Finalize{
  }
}

script_spell KillerDoll{
  task Spell_Knife(x,y,angle,type){
  }
  @Initialize{
  }
  @MainLoop{
  }
  @Finalize{
  }
}

If you run it, you should get a Sakuya that looks like this:



At this stage, you can move Sakuya around and her options will follow her, but that's all. Next, we'll add some bullets.
« Last Edit: March 05, 2010, 09:11:08 AM by Stuffman »

Stuffman

  • *
  • We're having a ball!
Re: Player script tutorial
« Reply #1 on: April 26, 2009, 06:34:40 PM »
Now we're going to start putting together Sakuya's @MainLoop, which will handle the bulk of her shot function.

We'll do it the same way we do it for enemies, but on a much tighter scale; we'll create a variable called "count" that will go up by one in each iteration of @MainLoop, which tracks the number of frames past. Then we can make it fire bullets on certain frame numbers. The difference here is that the player won't always be shooting bullets; therefore, bullets will only be shot if count is 0 or more, and we'll have it sit at -1 if the button isn't being pressed.

Code: [Select]
let count=-1;
let i=0;

@MainLoop{
  if((GetKeyState(VK_SHOT)==KEY_PUSH || GetKeyState(VK_SHOT)==KEY_HOLD) && count==-1){
    count = 0;
  }
  if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
    if(count%6 == 0){

    }
  }else{
    if(count%8 == 0){
     
    }
  }
  if(count >= 0){
    count++;
  }
  if(count >= 6 && (GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD)){
    count=-1;
  }
  if(count >= 8){
    count=-1;
  }
  yield;
}

See how this will pan out? First it checks if you're pressing the button when it wasn't pressed before, setting count to 0. Count can then start counting up each frame. When it runs up to 6 or 8 (depending on if you're focusing or not) the count will reset to -1, requiring the button to be checked again. Why did I make the cycle different for focus and unfocused? Because I want her focused shot to have a higher fire rate. Now let's add the unfocused shot first, since it's simpler. It's a fan of knives that flies straight.

Code: [Select]
@MainLoop
~~~
  if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
    if(count%6 == 0){

    }
  }else{
    if(count%8 == 0){
      i=-3;
      while(i<=3){
        CreatePlayerShot01(GetPlayerX(), GetPlayerY()-8, 10, 270+(i*5), 2.5, 1, 1);
        i++;
      }
    }
  }
~~~

CreatePlayerShot01 is the only bullet function for players; anything else will need to be an object shot. Anyway, it's pretty similar to the enemy version, CreateShot01. The difference is the addition of two arguments, which is the two next-to-last. The first (2.5) is the damage the bullet will do. The second (1) is the penetration, or the number of hits the bullet can score, once per frame. If you set the penetration to some huge number the bullet will be able to shoot through every enemy it hits, but be sure to set the damage low if you do something like that! The final argument is the bullet graphic, in this case the blue knives we defined earlier. Player shots cannot have delay.

So anyway yeah, that's our unfocused shot. It's a fan of 7 knives that fly in 5 degree increments in front of you. But wait! That's only half the attack! We still have the options' shots. Let's add a similar function to our options.

Code: [Select]
  task Option(position){
    let objoption=Obj_Create(OBJ_EFFECT);
    Obj_SetAlpha(objoption,200);
    ObjEffect_SetTexture(objoption,img_sakuya);  //uses star orb from spritesheet
    ObjEffect_SetRenderState(objoption,ALPHA);
    ObjEffect_SetPrimitiveType(objoption,PRIMITIVE_TRIANGLEFAN);
    ObjEffect_CreateVertex(objoption,4);         // square object with 4 vertexes
    ObjEffect_SetVertexUV(objoption,0,145,1);    // four coordinates of orb on spritesheet
    ObjEffect_SetVertexUV(objoption,1,159,1);    // object is 15x15
    ObjEffect_SetVertexUV(objoption,2,159,15);
    ObjEffect_SetVertexUV(objoption,3,145,15);
    if(position=="LEFT"){
      while(!Obj_BeDeleted(objoption)){
        ObjEffect_SetVertexXY(objoption,0,GetPlayerX-optionxpos-8,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,1,GetPlayerX-optionxpos+6,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,2,GetPlayerX-optionxpos+6,GetPlayerY+optionypos+7);
        ObjEffect_SetVertexXY(objoption,3,GetPlayerX-optionxpos-8,GetPlayerY+optionypos+7);
        if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
          if(count%6 == 3){

          }
        }else{
          if(count%8 == 4){
            i=-2;
            while(i<=2){
              CreatePlayerShot01(GetPlayerX()-optionxpos-1, GetPlayerY()+optionypos-8, 10, 254+(i*8), 2, 1, 2);
              i++;
            }
          }
        }
        yield;
      }
    }else{
      while(!Obj_BeDeleted(objoption)){
        ObjEffect_SetVertexXY(objoption,0,GetPlayerX+optionxpos-7,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,1,GetPlayerX+optionxpos+7,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,2,GetPlayerX+optionxpos+7,GetPlayerY+optionypos+7);
        ObjEffect_SetVertexXY(objoption,3,GetPlayerX+optionxpos-7,GetPlayerY+optionypos+7);
        if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
          if(count%6 == 3){

          }
        }else{
          if(count%8 == 4){
            i=-2;
            while(i<=2){
              CreatePlayerShot01(GetPlayerX()+optionxpos, GetPlayerY()+optionypos-8, 10, 286+(i*8), 2, 1, 2);
              i++;
            }
          }
        }
        yield;
      }
    }
  }

There, pretty simple, even if it did make Option() a lot bigger. Each option will now fire a fan of five purple knives on an alternating count with the blue ones. The purple ones are angled to side a bit to increase coverage, but do less damage. Note that "count" is a global variable, so all tasks in script_player_main can use it, so we don't need to do with count what we did in @MainLoop.

Now, let's compile all that and see if it works.

Code: [Select]
#“Œ•?’e–‹•—[Player]
#ScriptVersion[2]
#Menu[Sakuya A]
#Text[Sakuya Izayoi - Illusion Sign

Shot:
Unfocused: Jack the Ludo Bile (spread)
Focused: Jack the Ripper (homing)

Spell Card:
Illusion Sign �uIndiscriminate�v
Illusion Sign �uKiller Doll�v]
#Image[.\sakuya_select.png]
#ReplayName[SakuyaA]

script_player_main{
  let img_sakuya = GetCurrentScriptDirectory()~"sakuya.png";
  let img_cutin = GetCurrentScriptDirectory()~"sakuya_select.png";
  let optionxpos=16;
  let optionypos=0;
  let count=-1;
  let i=0;
 
  task Option(position){
    let objoption=Obj_Create(OBJ_EFFECT);
    Obj_SetAlpha(objoption,200);
    ObjEffect_SetTexture(objoption,img_sakuya);  //uses star orb from spritesheet
    ObjEffect_SetRenderState(objoption,ALPHA);
    ObjEffect_SetPrimitiveType(objoption,PRIMITIVE_TRIANGLEFAN);
    ObjEffect_CreateVertex(objoption,4);         // square object with 4 vertexes
    ObjEffect_SetVertexUV(objoption,0,145,1);    // four coordinates of orb on spritesheet
    ObjEffect_SetVertexUV(objoption,1,159,1);    // object is 15x15
    ObjEffect_SetVertexUV(objoption,2,159,15);
    ObjEffect_SetVertexUV(objoption,3,145,15);
    if(position=="LEFT"){
      while(!Obj_BeDeleted(objoption)){
        ObjEffect_SetVertexXY(objoption,0,GetPlayerX-optionxpos-8,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,1,GetPlayerX-optionxpos+6,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,2,GetPlayerX-optionxpos+6,GetPlayerY+optionypos+7);
        ObjEffect_SetVertexXY(objoption,3,GetPlayerX-optionxpos-8,GetPlayerY+optionypos+7);
        if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
          if(count%6 == 3){

          }
        }else{
          if(count%8 == 4){
            i=-2;
            while(i<=2){
              CreatePlayerShot01(GetPlayerX()-optionxpos-1, GetPlayerY()+optionypos-8, 10, 254+(i*8), 2, 1, 2);
              i++;
            }
          }
        }
        yield;
      }
    }else{
      while(!Obj_BeDeleted(objoption)){
        ObjEffect_SetVertexXY(objoption,0,GetPlayerX+optionxpos-7,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,1,GetPlayerX+optionxpos+7,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,2,GetPlayerX+optionxpos+7,GetPlayerY+optionypos+7);
        ObjEffect_SetVertexXY(objoption,3,GetPlayerX+optionxpos-7,GetPlayerY+optionypos+7);
        if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
          if(count%6 == 3){

          }
        }else{
          if(count%8 == 4){
            i=-2;
            while(i<=2){
              CreatePlayerShot01(GetPlayerX()+optionxpos, GetPlayerY()+optionypos-8, 10, 286+(i*8), 2, 1, 2);
              i++;
            }
          }
        }
        yield;
      }
    }
  }
  task JackTheRipper(x,y,graphic){
  }
  @Initialize{
    LoadGraphic(img_sakuya);
    LoadGraphic(img_cutin);
    LoadPlayerShotData(GetCurrentScriptDirectory()~"sakuya_shotdata.txt");
    SetPlayerLifeImage(img_sakuya, 96, 48, 143, 95);
    SetSpeed(3.5, 2.5);
    Option("LEFT");
    Option("RIGHT");
  }
  @MainLoop{
    if((GetKeyState(VK_SHOT)==KEY_PUSH || GetKeyState(VK_SHOT)==KEY_HOLD) && count==-1){
      count = 0;
    }
    if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
      if(count%6 == 0){
 
      }
    }else{
      if(count%8 == 0){
        i=-3;
        while(i<=3){
          CreatePlayerShot01(GetPlayerX(), GetPlayerY()-8, 10, 270+(i*5), 2.5, 1, 1);
          i++;
        }
      }
    }
    if(count >= 0){
      count++;
    }
    if(count >= 6 && (GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD)){
      count=-1;
    }
    if(count >= 8){
      count=-1;
    }
    yield;
  }
  @Missed{
  }
  @SpellCard{
  }
  @DrawLoop{
    SetTexture(img_sakuya);
    if(GetKeyState(VK_LEFT)==KEY_PUSH || GetKeyState(VK_LEFT)==KEY_HOLD){
      SetGraphicRect(49.5, 1.5, 95.5, 47.5); // left movement frame
    }else if(GetKeyState(VK_RIGHT)==KEY_PUSH || GetKeyState(VK_RIGHT)==KEY_HOLD){
      SetGraphicRect(97.5, 1.5, 143.5, 47.5); // right movement frame
    }else{
      SetGraphicRect(1.5, 1.5, 47.5, 47.5); // neutral frame
    }
    DrawGraphic(GetPlayerX(), GetPlayerY());
  }
  @Finalize{
    DeleteGraphic(img_sakuya);
    DeleteGraphic(img_cutin);
  }
}

script_spell Indiscriminate{
  task Spell_Knife(x,y,angle,type){
  }
  @Initialize{
  }
  @MainLoop{
  }
  @Finalize{
  }
}

script_spell KillerDoll{
  task Spell_Knife(x,y,angle,type){
  }
  @Initialize{
  }
  @MainLoop{
  }
  @Finalize{
  }
}



Yep. So far so good.

Now we need to make the focused shot, which will have homing capabilities. This is a bit trickier, and we're going to use a task to make this shot. Let's start by looking at the skeleton we placed earlier.

Code: [Select]
  task JackTheRipper(x,y,graphic){
  }

Each instance of JackTheRipper will be used to generate one homing knife. x and y will be the coordinates at which it is spawned, and graphic will be the shot ID it uses. Now, this task is going to need to do two things: first it will need to pick a target and find the angle to it, and then throw a knife at it. Thankfully, because the knife itself doesn't need to do anything special once its been launched, it doesn't need to be an object bullet. We just need to find the angle to throw it at, then we can make it with CreatePlayerShot01 as normal. First, I'll make the code that finds an enemy target.

Code: [Select]
  task JackTheRipper(x,y,graphic){
    let enemy_target=-1;
    ascent(i in EnumEnemyBegin..EnumEnemyEnd) {
      enemy_target=EnumEnemyGetID(i);
    }
  }

Now, we COULD just use GetEnemyX and GetEnemyY to calculate where we're throwing this, but the problem with it is that it doesn't have error checking and can cause slowdown if there are no enemies to target. So we're going to do it in a way that's a bit more classy. First we've made the variable "enemy_target" which will keep track of what enemy we're targetting. Now we have two functions you may not be familiar with, EnumEnemyBegin and EnumEnemyEnd. Now, I am not entirely familiar with how these work, but I do know that each enemy onscreen is stored in an index, and EnumEnemyBegin has the first number of that index and EnumEnemyEnd has the last. We want to sort through this index and find a suitable target. The enemy index is saved to "i" each loop, and we use that index with EnumEnemyGetID to return its ID to enemy_target. We can now use the value in enemy_target for the GetEnemyInfo functions we're about to add. By the way, if no enemies are onscreen, the loop will fail, and enemy_target will remain at -1. Now let's add some lines to check the angle of the target we've obtained.

Code: [Select]
  task JackTheRipper(x,y,graphic){
    let enemy_target=-1;
    let test_angle=0;
    ascent(i in EnumEnemyBegin..EnumEnemyEnd) {
      enemy_target=EnumEnemyGetID(i);
      test_angle=atan2(GetEnemyInfo(enemy_target,ENEMY_Y)-GetPlayerY, GetEnemyInfo(enemy_target,ENEMY_X)-GetPlayerX);
      if(test_angle<=-60 && test_angle>=-120){
        i=EnumEnemyEnd;
      }else{
        enemy_target=-1;
      }
    }
  }

New variable: test_angle. We'll use this to check the angle the enemy is at compared to Sakuya. Now, the formula we're returning to test_angle may look really complicated, so let's simplify it in plain english:

test_angle = atan2(enemy y - player y, enemy x - player x);

  • atan2 returns the arc tangent (angle) to a given point from 0,0, so we give it the enemy y and x values, adjusted for the position of Sakuya by subtracting her y and x from each.
  • GetEnemyInfo returns the info you ask for about the enemy with the given ID. You can put in ENEMY_X, ENEMY_Y, or ENEMY_LIFE to get any of that data. In this case we want the x and y positions of the enemy, so we use this function for enemy y and enemy x.
  • GetPlayerY and GetPlayerX do exactly what they say, so we use these functions for player y and player x.

Make sense? So now test_angle has the angle from Sakuya to the enemy in enemy_target. Now we want to see if that angle is within Sakuya's homing arc. Now, while we would refer to straight up as 270 degrees in Danmakufu's system, Danmakufu tracks them a bit differently internally. It uses negative values counting backwards from 0 until it gets to 180, so Danmakufu's degree range is -180 to 180 rather than 0 to 360. (This gave me a lot of trouble until I made a script to test it.) That means straight up is -90 degrees. I want Sakuya's homing arc to be 60 degrees, so I check if test_angle is between -60 and -120, 30 degrees offset from -90 in each direction. If it is, we set i to EnumEnemyEnd to end the loop, because that means we've got a suitable target. If it's not, we set enemy_target back to -1 to indicate we don't have a target to home in on yet, and this will continue the loop.

When all this is said and done, enemy_target will either have the ID of the enemy to fire the knife at, or it will be at -1, meaning there is no target. So let's go ahead and add the code for shooting the knife.

Code: [Select]
  task JackTheRipper(x,y,graphic){
    let enemy_target=-1;
    let target_angle=270;
    let test_angle=0;
    ascent(i in EnumEnemyBegin..EnumEnemyEnd) {
      enemy_target=EnumEnemyGetID(i);
      test_angle=atan2(GetEnemyInfo(enemy_target,ENEMY_Y)-GetPlayerY, GetEnemyInfo(enemy_target,ENEMY_X)-GetPlayerX);
      if(test_angle<=-60 && test_angle>=-120){
        i=EnumEnemyEnd;
      }else{
        enemy_target=-1;
      }
    }
    if(enemy_target!=-1){
      target_angle=atan2(GetEnemyInfo(enemy_target,ENEMY_Y)-y, GetEnemyInfo(enemy_target,ENEMY_X)-x);
      CreatePlayerShot01(x,y,15,target_angle,6,1,graphic);
    }else{
      CreatePlayerShot01(x,y,15,270,6,1,graphic);
    }
  }

If enemy_target isn't -1 then we have a target, so we calculate the angle again, but this time it's from the knife to the enemy, so we use x and y instead of GetPlayerX and GetPlayerY. (x and y were the values we passed to JackTheRipper to indicate the knife's starting point, in case you forgot). Then it shoots a knife at that angle. Simple! Sort of. If enemy_target is -1 then we didn't find an enemy in our homing arc, so Sakuya just fires it straight ahead at 270 degrees.

With this task built, we can call it anywhere we want to make a homing knife. So we go back to our @MainLoop and Option() to add this new task.

In @MainLoop:
Code: [Select]
~~~
  if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
    if(count%6 == 0){
      JackTheRipper(GetPlayerX-8,GetPlayerY-8,11);
      JackTheRipper(GetPlayerX+8,GetPlayerY-8,11);
    }
  }else{
    if(count%8 == 0){
      i=-3;
      while(i<=3){
        CreatePlayerShot01(GetPlayerX(), GetPlayerY()-8, 10, 270+(i*5), 2.5, 1, 1);
        i++;
      }
    }
  }
~~~

While focused, Sakuya will shoot two JackTheRipper knives, each originating 8 pixels up and out diagonally from her center.

In Option():
Code: [Select]
~~~
        if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
          if(count%6 == 3){
            JackTheRipper(GetPlayerX-optionxpos-1,GetPlayerY+optionypos-8,12);
          }
        }else{
          if(count%8 == 4){
            i=-2;
            while(i<=2){
              CreatePlayerShot01(GetPlayerX()-optionxpos-1, GetPlayerY()+optionypos-8, 10, 254+(i*8), 2, 1, 2);
              i++;
            }
          }
        }
       
        ~~~

        if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
          if(count%6 == 3){
            JackTheRipper(GetPlayerX+optionxpos,GetPlayerY+optionypos-8,12);
          }
        }else{
          if(count%8 == 4){
            i=-2;
            while(i<=2){
              CreatePlayerShot01(GetPlayerX()+optionxpos, GetPlayerY()+optionypos-8, 10, 286+(i*8), 2, 1, 2);
              i++;
            }
          }
        }
~~~

Each option will fire one JackTheRipper 8 pixels up from its center point, on alternating counts with the ones in MainLoop.

And that should get our homing knives up and running! There's just a few more things to do before we move onto spellcards.

First of all, Sakuya needs a hitbox. I'm sure it'd be nice to play without one, but unfortunately it is required. For this we just add one line to @MainLoop.

Code: [Select]
SetIntersectionCircle(GetPlayerX, GetPlayerY, 2);
Should be obvious right? This puts Sakuya's hitbox on her center point, with a 2 pixel radius. I'm not sure exactly how big Reimu and Marisa's hitboxes are in Danmakufu (tests were inconclusive) but in the Windows games Reimu's hitbox is 3 pixels across, and everyone else's is 5. Therefore a radius of 2 should be about normal (it's either 3 or 5 diameter, not sure if it counts its center pixel). You could make it 1 if you wanted a character with a small hitbox, but Sakuya ain't got one.

The next bit is just for effect's sake; we'll have Sakuya's options reposition when she focuses, coming up front. If you'll recall, we have two variables that track the options' position: optionxpos and optionypos. I'll just have them adjust like so:

In @MainLoop:
Code: [Select]
  if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
    if(count%6 == 0){
      JackTheRipper(GetPlayerX-8,GetPlayerY-8,11);
      JackTheRipper(GetPlayerX+8,GetPlayerY-8,11);
    }
    if(optionxpos>12){optionxpos--;}
    if(optionypos>-12){optionypos--;}
  }else{
    if(count%8 == 0){
      i=-3;
      while(i<=3){
        CreatePlayerShot01(GetPlayerX(), GetPlayerY()-8, 10, 270+(i*5), 2.5, 1, 1);
        i++;
      }
    }
    if(optionxpos<20){optionxpos++;}
    if(optionypos<0){optionypos++;}
  }

See the new pair of if statements involving the variables in question? The way this will work is simple: while focused, the option will move towards 12,-12, and while unfocused it will move towards 20,0.

That should be it! Let's assemble it into the final product. This has everything working except spellcards.

Code: [Select]
#“Œ•?’e–‹•—[Player]
#ScriptVersion[2]
#Menu[Sakuya A]
#Text[Sakuya Izayoi - Illusion Sign

Shot:
Unfocused: Jack the Ludo Bile (spread)
Focused: Jack the Ripper (homing)

Spell Card:
Illusion Sign �uIndiscriminate�v
Illusion Sign �uKiller Doll�v]
#Image[.\sakuya_select.png]
#ReplayName[SakuyaA]

script_player_main{
  let img_sakuya = GetCurrentScriptDirectory()~"sakuya.png";
  let img_cutin = GetCurrentScriptDirectory()~"sakuya_select.png";
  let optionxpos=16;
  let optionypos=0;
  let count=-1;
  let i=0;
 
  task Option(position){
    let objoption=Obj_Create(OBJ_EFFECT);
    Obj_SetAlpha(objoption,200);
    ObjEffect_SetTexture(objoption,img_sakuya);  //uses star orb from spritesheet
    ObjEffect_SetRenderState(objoption,ALPHA);
    ObjEffect_SetPrimitiveType(objoption,PRIMITIVE_TRIANGLEFAN);
    ObjEffect_CreateVertex(objoption,4);         // square object with 4 vertexes
    ObjEffect_SetVertexUV(objoption,0,145,1);    // four coordinates of orb on spritesheet
    ObjEffect_SetVertexUV(objoption,1,159,1);    // object is 15x15
    ObjEffect_SetVertexUV(objoption,2,159,15);
    ObjEffect_SetVertexUV(objoption,3,145,15);
    if(position=="LEFT"){
      while(!Obj_BeDeleted(objoption)){
        ObjEffect_SetVertexXY(objoption,0,GetPlayerX-optionxpos-8,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,1,GetPlayerX-optionxpos+6,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,2,GetPlayerX-optionxpos+6,GetPlayerY+optionypos+7);
        ObjEffect_SetVertexXY(objoption,3,GetPlayerX-optionxpos-8,GetPlayerY+optionypos+7);
        if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
          if(count%6 == 3){
            JackTheRipper(GetPlayerX-optionxpos-1,GetPlayerY+optionypos-8,12);
          }
        }else{
          if(count%8 == 4){
            i=-2;
            while(i<=2){
              CreatePlayerShot01(GetPlayerX()-optionxpos-1, GetPlayerY()+optionypos-8, 10, 254+(i*8), 2, 1, 2);
              i++;
            }
          }
        }
        yield;
      }
    }else{
      while(!Obj_BeDeleted(objoption)){
        ObjEffect_SetVertexXY(objoption,0,GetPlayerX+optionxpos-7,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,1,GetPlayerX+optionxpos+7,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,2,GetPlayerX+optionxpos+7,GetPlayerY+optionypos+7);
        ObjEffect_SetVertexXY(objoption,3,GetPlayerX+optionxpos-7,GetPlayerY+optionypos+7);
        if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
          if(count%6 == 3){
            JackTheRipper(GetPlayerX+optionxpos,GetPlayerY+optionypos-8,12);
          }
        }else{
          if(count%8 == 4){
            i=-2;
            while(i<=2){
              CreatePlayerShot01(GetPlayerX()+optionxpos, GetPlayerY()+optionypos-8, 10, 286+(i*8), 2, 1, 2);
              i++;
            }
          }
        }
        yield;
      }
    }
  }
  task JackTheRipper(x,y,graphic){
    let enemy_target=-1;
    let target_angle=270;
    let test_angle=0;
    ascent(i in EnumEnemyBegin..EnumEnemyEnd) {
      enemy_target=EnumEnemyGetID(i);
      test_angle=atan2(GetEnemyInfo(enemy_target,ENEMY_Y)-GetPlayerY, GetEnemyInfo(enemy_target,ENEMY_X)-GetPlayerX);
      if(test_angle<=-60 && test_angle>=-120){
        i=EnumEnemyEnd;
      }else{
        enemy_target=-1;
      }
    }
    if(enemy_target!=-1){
      target_angle=atan2(GetEnemyInfo(enemy_target,ENEMY_Y)-y, GetEnemyInfo(enemy_target,ENEMY_X)-x);
      CreatePlayerShot01(x,y,15,target_angle,6,1,graphic);
    }else{
      CreatePlayerShot01(x,y,15,270,6,1,graphic);
    }
  }
  @Initialize{
    LoadGraphic(img_sakuya);
    LoadGraphic(img_cutin);
    LoadPlayerShotData(GetCurrentScriptDirectory()~"sakuya_shotdata.txt");
    SetPlayerLifeImage(img_sakuya, 96, 48, 143, 95);
    SetSpeed(3.5, 2.5);
    Option("LEFT");
    Option("RIGHT");
  }
  @MainLoop{
    if((GetKeyState(VK_SHOT)==KEY_PUSH || GetKeyState(VK_SHOT)==KEY_HOLD) && count==-1){
      count = 0;
    }
    if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
      if(count%6 == 0){
        JackTheRipper(GetPlayerX-8,GetPlayerY-8,11);
        JackTheRipper(GetPlayerX+8,GetPlayerY-8,11);
      }
      if(optionxpos>12){optionxpos--;}
      if(optionypos>-12){optionypos--;}
    }else{
      if(count%8 == 0){
        i=-3;
        while(i<=3){
          CreatePlayerShot01(GetPlayerX(), GetPlayerY()-8, 10, 270+(i*5), 2.5, 1, 1);
          i++;
        }
      }
      if(optionxpos<20){optionxpos++;}
      if(optionypos<0){optionypos++;}
    }
    if(count >= 0){
      count++;
    }
    if(count >= 6 && (GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD)){
      count=-1;
    }
    if(count >= 8){
      count=-1;
    }
    SetIntersectionCircle(GetPlayerX, GetPlayerY, 2);
    yield;
  }
  @Missed{
  }
  @SpellCard{
  }
  @DrawLoop{
    SetTexture(img_sakuya);
    if(GetKeyState(VK_LEFT)==KEY_PUSH || GetKeyState(VK_LEFT)==KEY_HOLD){
      SetGraphicRect(49.5, 1.5, 95.5, 47.5); // left movement frame
    }else if(GetKeyState(VK_RIGHT)==KEY_PUSH || GetKeyState(VK_RIGHT)==KEY_HOLD){
      SetGraphicRect(97.5, 1.5, 143.5, 47.5); // right movement frame
    }else{
      SetGraphicRect(1.5, 1.5, 47.5, 47.5); // neutral frame
    }
    DrawGraphic(GetPlayerX(), GetPlayerY());
  }
  @Finalize{
    DeleteGraphic(img_sakuya);
    DeleteGraphic(img_cutin);
  }
}

script_spell Indiscriminate{
  task Spell_Knife(x,y,angle,type){
  }
  @Initialize{
  }
  @MainLoop{
  }
  @Finalize{
  }
}

script_spell KillerDoll{
  task Spell_Knife(x,y,angle,type){
  }
  @Initialize{
  }
  @MainLoop{
  }
  @Finalize{
  }
}

Let's see.



Knives home in on enemy while focused? Check.
Options move while focused? Check.
Sakuya dies when she is killed? Check.

So far so good! Next, Sakuya A's first spellcard, Indiscriminate.
« Last Edit: March 05, 2010, 09:11:34 AM by Stuffman »

Stuffman

  • *
  • We're having a ball!
Re: Player script tutorial
« Reply #2 on: April 26, 2009, 06:34:50 PM »
Okay, let's get started on her unfocused spellcard. This bomb is simple enough, it launches knives in every direction that curve slightly to the left or right as they fly. However this is made somewhat more complex in that the knives will need to be spell objects. Which means vertices.

To begin with, here's the skeleton of the spellcard.

Code: [Select]
script_spell Indiscriminate{
  task Spell_Knife(angle,type){
  }
  task run{
  }
  @Initialize{
  }
  @MainLoop{
  }
  @Finalize{
  }
}

The task Spell_Knife will be used for each knife we fire. The function of the angle is obvious, the type will indicate the color and curve of the knife. Blue ones (type 1) will curve counterclockwise, purple ones (type 2) will go clockwise.

The task run will perform the actual actions of the bomb, so we don't have to fool around with a "count" variable for the @MainLoop. We'll call it at the end of @Initialize.

Now before we go forward, let's go back to script_player_main and make it so that pressing the bomb button will actually call Indiscriminate.

In @MainLoop:
Code: [Select]
~~~
  @SpellCard{
    SetSpeed(1, 1);
    UseSpellCard("Indiscriminate", 0);
    CutIn(KOUMA,"Illusion Sign ?uIndiscriminate?v", img_cutin);
  }
 ~~~
  • First, the SetSpeed counter. You might have noticed that a lot of bombs in Touhou change your speed in some way; in Sakuya A's case, she slows to a crawl while she's firing her bomb knives. We'll put the function that changes her speed back to normal in her spellcard code later.
  • Next is the function for calling a script_spell, UseSpellCard. In this case, the first argument is simply whatever you named the script_spell. The second is a custom argument you can call from inside script_spell using GetArgument, but we won't need that.
  • Finally we call CutIn, which is the function that shows the character's portrait just before their spellcard goes off. There are two styles of cutin, KOUMA (EoSD-esque) and YOUMU (PCB-esque).
Simple enough, right? Now back to script_spell. Let's fill out some of the basics.

Code: [Select]
script_spell Indiscriminate{
  let img_spell = GetCurrentScriptDirectory()~"sakuya_spell.png";
  task Spell_Knife(angle,type){
  }
  task run{
    SetPlayerInvincibility(240);
    ForbidShot(true);
    loop(45){yield;}

    // shootin' will go here

    ForbidShot(false);
    SetSpeed(3.5, 2.5);
    loop(90){yield;}
    End();
  }
  @Initialize{
    LoadGraphic(img_spell);
    run();
  }
  @MainLoop{
    CollectItems();
    yield;
  }
  @Finalize{
    DeleteGraphic(img_spell);
  }
}


img_spell is the image of the spellcard knives I showed earlier. Now, let's go over the new functions in here.
  • SetPlayerInvincibility makes the player invincible for the amount of frames indicated, in this case, Sakuya is invincible for 4 seconds. Remember that the player should remain invincible for a short while after their bomb finishes so that they have a chance to get their bearings as the enemy bullets start to respawn.
  • ForbidShot does exactly what it says. It ignores your shot button while it's true. We do this so Sakuya can't layer her spell knives and normal knives for a silly amount of damage, though some bombs do allow you to fire while they're on.
  • End is the command you use to quit script_spell and thus end the bomb.
  • CollectItems causes all the items on the screen to gather to the player, as if they were at the collection point at the top of the screen.

With that out of the way, I'll explain how this is going to play out. Naturally, @Initialize is called as soon as the script_spell is called. It calls the task "run", which will run through all the functions the bomb performs. "Run" will make the player invincible and unable to shoot, then wait for a few frames (45 to be exact) to begin the next part. There should always be a bit of delay before the bomb goes off to give the cutin time to show. After that, the bulk of the code (yet to be written) will perform the attack action of the code by calling multiple Spell_Knifes, and once it finishes with that it will reset the player's speed and shot, then delay some more to give the knives time to fly offscreen. Once the delay is finished, the bomb ends. Note that @MainLoop is running the whole time alongside "run", collecting any items that come onscreen.

So, let's start working on Spell_Knife so "run" will have something to shoot.

Each instance of Spell_Knife will create a spell object, which are identical to effect objects, much like the options we made earlier, with the exception that they can be given damaging hitboxes. However, this is a lot more complicated because the knives move a lot more and can also go at different angles than simply straight up. That means we're going to have to make code that will constantly place and rotate the vertices to display the knife image correctly. Let's start by just making the knife, which is simple enough compared to what we did before.

Code: [Select]
  task Spell_Knife(angle,type){
    let objknife=Obj_Create(OBJ_SPELL);
    let knifexpos=GetPlayerX;
    let knifeypos=GetPlayerY;
    let knifeangle=angle;
    Obj_SetAlpha(objknife,200);
    ObjEffect_SetTexture(objknife,img_spell);
    ObjEffect_SetRenderState(objknife,ADD);
    ObjEffect_CreateVertex(objknife,4);
    ObjEffect_SetPrimitiveType(objknife,PRIMITIVE_TRIANGLEFAN);
    if(type==1){
      ObjEffect_SetVertexUV(objknife,0,1,1);
      ObjEffect_SetVertexUV(objknife,1,46,1);
      ObjEffect_SetVertexUV(objknife,2,46,46);
      ObjEffect_SetVertexUV(objknife,3,1,46);
    }else{
      ObjEffect_SetVertexUV(objknife,0,49,1);
      ObjEffect_SetVertexUV(objknife,1,94,1);
      ObjEffect_SetVertexUV(objknife,2,94,46);
      ObjEffect_SetVertexUV(objknife,3,49,46);
    }
    while(!Obj_BeDeleted(objknife)){
      yield;
    }
  }

As you can see, it's basically identical to the option code earlier. The two differences are that we are now using ADD render for the glowy effect spellcards have, and that there are two UV vertex sets, one for the blue knife, and the other for the purple knife. Thankfully, we don't need to move those vertices at all once they're set, since the actual image of the knife doesn't change.

Now for the tricky part, making the while loop control the movement and position of the vertices. First, we can't use SetSpeed or SetAngle with an Effect object. We have to make it calculate where to go automatically. knifexpos and knifeypos indicate the centerpoint of the knife. knifeangle indicates the angle the knife is travelling at, which will steadily change since the knife curves.

Let's look at this image so that we have a clear image in our heads of how the knife is set up.



The center point of the knife is the coordinate knifexpos, knifeypos. The image area we've chosen is square, so we can start at 45 degrees and reach each corner in 90 degree increments. By default, knifeangle points at 0 degrees, so we'll have to adjust by +270 degrees since the knife in the image is pointing at that angle. In order to place each vertex, we'll need to measure out from the center point at the given angle using cos() and sin(); cosine and sine.

cos() and sin() are used to calculate the distance on the x and y axis, respectively, to a point at a given angle and radius; in other words, it's used to calculate distance in a circular radius. The formula is fairly simple, the hard part is understanding what exactly they're giving you.

The formula for each is:
x distance = Radius * cos(Angle)
y distance = Radius * sin(Angle)

The radius for the knife will be half of the knife's size from corner to corner. The angle will be the angle of the vertex relative to knifeangle, so the angle for each (with modulus for 360 degrees) will be:

vertex 0: 225+270 = 135
vertex 1: 315+270 = 225
vertex 2: 45+270 = 315
vertex 3: 135+270 = 45

Let's try putting it to use. Here's the code to make the position of the knife move:

Code: [Select]
    while(!Obj_BeDeleted(objknife)){
      if(type==1){
        knifeangle-=1;
      }else{
        knifeangle+=1;
      }
      knifexpos=knifexpos+(8*cos(180+knifeangle));
      knifeypos=knifeypos+(8*sin(180+knifeangle));
      yield;
    }

The adjustments to knifeangle should be obvious enough. It will go up or down by one degree each frame, depending on the knife type. To figure out where to move the knife, we use cos() and sin() with a radius of 8 to give it a speed of 8, and the angle as knifeangle, since that's where the knife is going. We add the results to the current knifexpos and knifeypos to get the new coordinates.

Now for the vertices.

Code: [Select]
    while(!Obj_BeDeleted(objknife)){
      if(type==1){
        knifeangle-=1;
      }else{
        knifeangle+=1;
      }
      knifexpos=knifexpos+(8*cos(180+knifeangle));
      knifeypos=knifeypos+(8*sin(180+knifeangle));
      ObjEffect_SetVertexXY(objknife,0,knifexpos+(45*cos(135+knifeangle)),knifeypos+(45*sin(135+knifeangle)));
      ObjEffect_SetVertexXY(objknife,1,knifexpos+(45*cos(225+knifeangle)),knifeypos+(45*sin(225+knifeangle)));
      ObjEffect_SetVertexXY(objknife,2,knifexpos+(45*cos(315+knifeangle)),knifeypos+(45*sin(315+knifeangle)));
      ObjEffect_SetVertexXY(objknife,3,knifexpos+(45*cos(45+knifeangle)),knifeypos+(45*sin(45+knifeangle)));
      yield;
    }

In here, we're putting the cos() and sin() calculations right inside the SetVertexXY. In the x position argument we put the cos() formula, and in the y position argument we put the sin() formula. I chose 45 for the radius to make the knife 90 pixels from corner to corner. Note that I could make it any size and the image of the knife would stretch to fit. For the angle, we use the angle adjustment of each vertex from knifeangle that we calculated above, plus knifeangle itself to make each vertex turn with the knife. By keeping this in a loop, we assure the vertices will constantly adjust to the new position and angle of the knife.

One more step to make the knife fully functional: the hitbox. Add this before the yield:

Code: [Select]
ObjSpell_SetIntersecrionCircle(objknife,knifexpos,knifeypos,30,10,true);
For this function, we specify the object, the coordinates, the radius of the hitbox (30), the damage per frame (10), and whether or not the hitbox will absorb enemy bullets (yes).

So there we have it, a task that will create a fully functional knife for the bomb. Now we just need the code to spawn them in "run".

In script_spell:
Code: [Select]
~~~
  let spellcount=0;
  let spellangle=0;

 ~~~

  task run{
    ForbidShot(true);
    loop(45){yield;}
    while(spellcount<90){
      if(spellcount%5==0){
        Spell_Knife(spellangle,1);
        Spell_Knife(spellangle+90,2);
        Spell_Knife(spellangle+180,1);
        Spell_Knife(spellangle+270,2);
        spellangle+=40;
        PlaySE(GetCurrentScriptDirectory()~"knife.wav");
      }
      spellcount++;
      yield;
    }
    ForbidShot(false);
    SetSpeed(3.5, 2.5);
    loop(90){yield;}
    End();
  }
  ~~~
spellcount tracks the time the spell has been going, and spellangle tracks the angle the knives are shooting at. Hopefully you can tell what it does on your own by now; for 90 frames, every 5 frames, it will fire a knife of alternating type in four directions, in 40 degree increments. Also, it'll play our sound effect, "knife.wav". It doesn't need to be loaded beforehand, like other resources.

Now, put it all together and what do you get?

Code: [Select]
script_spell Indiscriminate{
  let spellcount=0;
  let spellangle=0;
  let img_spell = GetCurrentScriptDirectory()~"sakuya_spell.png";
  task Spell_Knife(angle,type){
    let objknife=Obj_Create(OBJ_SPELL);
    let knifexpos=GetPlayerX;
    let knifeypos=GetPlayerY;
    let knifeangle=angle;
    Obj_SetAlpha(objknife,200);
    ObjEffect_SetTexture(objknife,img_spell);
    ObjEffect_SetRenderState(objknife,ADD);
    ObjEffect_CreateVertex(objknife,4);
    ObjEffect_SetPrimitiveType(objknife,PRIMITIVE_TRIANGLEFAN);
    if(type==1){
      ObjEffect_SetVertexUV(objknife,0,1,1);
      ObjEffect_SetVertexUV(objknife,1,46,1);
      ObjEffect_SetVertexUV(objknife,2,46,46);
      ObjEffect_SetVertexUV(objknife,3,1,46);
    }else{
      ObjEffect_SetVertexUV(objknife,0,49,1);
      ObjEffect_SetVertexUV(objknife,1,94,1);
      ObjEffect_SetVertexUV(objknife,2,94,46);
      ObjEffect_SetVertexUV(objknife,3,49,46);
    }
    while(!Obj_BeDeleted(objknife)){
      if(type==1){
        knifeangle-=1;
      }else{
        knifeangle+=1;
      }
      knifexpos=knifexpos+(8*cos(180+knifeangle));
      knifeypos=knifeypos+(8*sin(180+knifeangle));
      ObjEffect_SetVertexXY(objknife,0,knifexpos+(45*cos(135+knifeangle)),knifeypos+(45*sin(135+knifeangle)));
      ObjEffect_SetVertexXY(objknife,1,knifexpos+(45*cos(225+knifeangle)),knifeypos+(45*sin(225+knifeangle)));
      ObjEffect_SetVertexXY(objknife,2,knifexpos+(45*cos(315+knifeangle)),knifeypos+(45*sin(315+knifeangle)));
      ObjEffect_SetVertexXY(objknife,3,knifexpos+(45*cos(45+knifeangle)),knifeypos+(45*sin(45+knifeangle)));
      ObjSpell_SetIntersecrionCircle(objknife,knifexpos,knifeypos,30,10,true);
      yield;
    }
  }
  task run{
    SetPlayerInvincibility(240);
    ForbidShot(true);
    loop(45){yield;}
    while(spellcount<90){
      if(spellcount%5==0){
        Spell_Knife(spellangle,1);
        Spell_Knife(spellangle+90,2);
        Spell_Knife(spellangle+180,1);
        Spell_Knife(spellangle+270,2);
        spellangle+=40;
        PlaySE(GetCurrentScriptDirectory()~"knife.wav");
      }
      spellcount++;
      yield;
    }
    ForbidShot(false);
    SetSpeed(3.5, 2.5);
    loop(90){yield;}
    End();
  }
  @Initialize{
    LoadGraphic(img_spell);
    run();
  }
  @MainLoop{
    CollectItems();
    yield;
  }
  @Finalize{
    DeleteGraphic(img_spell);
  }
}



And there we are, one indiscriminating spellcard. Next, we turn Sakuya into a killer doll.
« Last Edit: May 02, 2009, 10:05:16 PM by Stuffman »

Stuffman

  • *
  • We're having a ball!
Re: Player script tutorial
« Reply #3 on: April 26, 2009, 06:35:00 PM »
Alright. Killer Doll is pretty similar to Indiscriminate, actually, so we'll be able to re-use a lot of code. At the same time, a bunch of things are different, and more complicated. To begin with, Killer Doll's knives can rotate independently of their angle. Secondly, they're homing. Third, they stretch out when they fly towards the enemy, with a laser-like appearance.

First of all, we need to go back and add KillerDoll to @SpellCard.

Code: [Select]
  @SpellCard{
    SetSpeed(1, 1);
    if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
      UseSpellCard("KillerDoll", 0);
      CutIn(KOUMA,"Illusion Sign �uKiller Doll�v", img_cutin);
    }else{
      UseSpellCard("Indiscriminate", 0);
      CutIn(KOUMA,"Illusion Sign �uIndiscriminate�v", img_cutin);
    }
  }

Now, @SpellCard will call KillerDoll when focused, and Indiscriminate only when unfocused.

Back to KillerDoll. Instead of starting with the skeleton, let's start with the things from Indiscriminate that we know will be the same.

Code: [Select]
script_spell KillerDoll{
  let spellcount=0;
  let spellangle=0;
  let img_spell = GetCurrentScriptDirectory()~"sakuya_spell.png";
  task Spell_Knife(x,y,angle,type){
    let objknife=Obj_Create(OBJ_SPELL);
    let knifexpos=GetPlayerX;
    let knifeypos=GetPlayerY;
    let knifeangle=angle;
    Obj_SetAlpha(objknife,200);
    ObjEffect_SetTexture(objknife,img_spell);
    ObjEffect_SetRenderState(objknife,ADD);
    ObjEffect_CreateVertex(objknife,4);
    ObjEffect_SetPrimitiveType(objknife,PRIMITIVE_TRIANGLEFAN);
    if(type==1){
      ObjEffect_SetVertexUV(objknife,0,1,1);
      ObjEffect_SetVertexUV(objknife,1,46,1);
      ObjEffect_SetVertexUV(objknife,2,46,46);
      ObjEffect_SetVertexUV(objknife,3,1,46);
    }else{
      ObjEffect_SetVertexUV(objknife,0,49,1);
      ObjEffect_SetVertexUV(objknife,1,94,1);
      ObjEffect_SetVertexUV(objknife,2,94,46);
      ObjEffect_SetVertexUV(objknife,3,49,46);
    }
    while(!Obj_BeDeleted(objknife)){
                                            // everything about the behavior will be different
      yield;
    }
  }
  task run{
    SetPlayerInvincibility(240);
    ForbidShot(true);
    loop(45){yield;}
    while(spellcount<90){
                                            //new knives go here
      spellcount++;
      yield;
    }
    ForbidShot(false);
    SetSpeed(3.5, 2.5);
    loop(90){yield;}                        // will probably need longer delay
    End();
  }
  @Initialize{
    LoadGraphic(img_spell);
    run();
  }
  @MainLoop{
    CollectItems();
    yield;
  }
  @Finalize{
    DeleteGraphic(img_spell);
  }
}
Okay. It still makes a knife with the same basic frame, it still does everything in "run". We need to upgrade the knives for increased flexibility, and then write new code in "run" for firing them.

The first thing Spell_Knife is going to need is a whole bunch of new variables.

Code: [Select]
  task Spell_Knife(angle,type){
    let objknife=Obj_Create(OBJ_SPELL);
    let knifexpos=GetPlayerX;
    let knifeypos=GetPlayerY;
    let knifexpos2=GetPlayerX;
    let knifeypos2=GetPlayerY;
    let knifeangle=angle;
    let knifespin=angle+180;
    let knifelength=0;
    let knifecount=0;

  • knifexpos2/knifeypos2: When the knife stretches, it will no longer be square; that would complicate things because then the angle to the vertexes would no longer be 45 degrees. Instead of messing with that, I'm going to make a second "center" point which will trail behind the first, making a square bottom for the hilt of the knife. At first, knifexpos2/knifeypos2 will be right on top of the original center point, but once it flies forward the second point will travel slower but at the same angle. The top two vertexes will belong to the knifexpos/knifeypos, while the bottom two will come from knifexpos2/knifeypos2, which will allow it to stretch in the middle as the two points seperate.
  • knifespin has been added to accommodate the knife pointing towards an angle other than the one it is travelling at. knifeangle will be used for the direction of movement, while knifespin will be used for the visual direction of the object.
  • knifecount is now necessary because the knife has two steps; first it is released from Sakuya, spinning, then it flies forward at the enemy. This variable will count the number of frames passed.
The knife will pick a target and fly into them once 60 frames have passed, but let's first code what happens before that.

Code: [Select]
    while(!Obj_BeDeleted(objknife)){
      if(knifecount<60){
        knifespin+=4;
        knifexpos=knifexpos+(cos(180+knifeangle));
        knifeypos=knifeypos+(sin(180+knifeangle));
        knifexpos2=knifexpos;
        knifeypos2=knifeypos;
        ObjSpell_SetIntersecrionCircle(objknife,knifexpos,knifeypos,30,1,true);
      }else{
        // homing mode code
      }
      ObjEffect_SetVertexXY(objknife,0,knifexpos+(45*cos(135+knifespin)),knifeypos+(45*sin(135+knifespin)));
      ObjEffect_SetVertexXY(objknife,1,knifexpos+(45*cos(225+knifespin)),knifeypos+(45*sin(225+knifespin)));
      ObjEffect_SetVertexXY(objknife,2,knifexpos2+(45*cos(315+knifespin)),knifeypos2+(45*sin(315+knifespin)));
      ObjEffect_SetVertexXY(objknife,3,knifexpos2+(45*cos(45+knifespin)),knifeypos2+(45*sin(45+knifespin)));
      knifecount++;
      yield;
    }

knifespin has each knife spin at 4 degrees per frame. The movement calculations are the same as before, except no radius has been specified; this defaults the radius to 1, so they only have a speed of 1 at first. The two center points are still together at this point so knifexpos2/knifeypos2 equal knifexpos/knifeypos. The hitbox is the same, but only does 1 damage until they start homing.

The vertex code is the same as before too, but with our new variables in place. knifespin is used to calculate the angle of the vertexes instead of knifeangle, and vertex 2 and 3 use knifexpos2/knifexpos3, as discussed above.



Now to add the more complicated part.

Code: [Select]
    while(!Obj_BeDeleted(objknife)){
      if(knifecount<60){
        knifespin+=4;
        knifexpos=knifexpos+(cos(180+knifeangle));
        knifeypos=knifeypos+(sin(180+knifeangle));
        knifexpos2=knifexpos;
        knifeypos2=knifeypos;
        ObjSpell_SetIntersecrionCircle(objknife,knifexpos,knifeypos,30,1,true);
      }else{
        knifexpos=knifexpos+(20*cos(180+knifeangle));
        knifeypos=knifeypos+(20*sin(180+knifeangle));
        knifexpos2=knifexpos2+(5*cos(180+knifeangle));
        knifeypos2=knifeypos2+(5*sin(180+knifeangle));
        ObjSpell_SetIntersecrionCircle(objknife,knifexpos,knifeypos,30,6,true);
      }
      if(knifecount==60){
        let enemy_target=-1;
        ascent(i in EnumEnemyBegin..EnumEnemyEnd) {
          enemy_target=EnumEnemyGetID(i);
          i=EnumEnemyEnd;
        }
        if(enemy_target!=-1){
          knifeangle=180+atan2(GetEnemyInfo(enemy_target,ENEMY_Y)-knifeypos, GetEnemyInfo(enemy_target,ENEMY_X)-knifexpos);
          knifespin=knifeangle;
        }
        PlaySE(GetCurrentScriptDirectory()~"knife.wav");
      }
      ObjEffect_SetVertexXY(objknife,0,knifexpos+(45*cos(135+knifespin)),knifeypos+(45*sin(135+knifespin)));
      ObjEffect_SetVertexXY(objknife,1,knifexpos+(45*cos(225+knifespin)),knifeypos+(45*sin(225+knifespin)));
      ObjEffect_SetVertexXY(objknife,2,knifexpos2+(45*cos(315+knifespin)),knifeypos2+(45*sin(315+knifespin)));
      ObjEffect_SetVertexXY(objknife,3,knifexpos2+(45*cos(45+knifespin)),knifeypos2+(45*sin(45+knifespin)));
      knifecount++;
      if(knifecount>120){
        Obj_Delete(objknife);
      }
      yield;
    }
When the knifecount is no longer under 60, the knife doesn't spin anymore, and the center points travel at different speeds. knifexpos/knifeypos will travel at 20 speed(!), but knifexpos2/knifeypos2 will drag behind at a speed of 5. Also, the damage is increased, but not by too much, since each knive will sit on the enemy for an extended period of time when it's stretched out.

Does the code under knifecount==60 look familiar? It's mostly the same as the homing code for JackTheRipper. test_angle is no longer necessary since the knife can home in from any angle, and it now uses knifexpos/knifeypos instead of the player's position to calculate. knifespin is also calibrated to the new angle to make it fly straight, instead of rotating. Also, it plays the sound effect when it flies forward, rather than when it's released.

The second centerpoint of the knife will not naturally leave the screen in a reasonable amount of time when it stretches out, so we'll automatically delete the knife after 120 frames.

That should get the knife working. Now, when you make your own player characters with their own spellcards, you'll need to come up with your own solutions to how their various objects might shift their vertices to get the effects you want. Hopefully, this tutorial will give you an idea of how to go about it.

Moving on, let's update our "run" to shoot these fancy homing knives.

Code: [Select]
  task run{
    SetPlayerInvincibility(240);
    ForbidShot(true);
    loop(45){yield;}
    while(spellcount<90){
      if(spellcount%8==0){
        Spell_Knife(spellangle,1);
        Spell_Knife(spellangle+180,1);
spellangle+=20;
      }
      if(spellcount%8==4){
        Spell_Knife(spellangle,2);
        Spell_Knife(spellangle+180,2);
        spellangle+=20;
      }
      spellcount++;
      yield;
    }
    ForbidShot(false);
    SetSpeed(3.5, 2.5);
    loop(180){yield;}
    End();
  }

Again, this is pretty simple, though different from before. Sakuya will fire a pair of knives every 4 frames, of alternating type, and she'll release them in a circle around her.

Also, since the last knife will be onscreen for 120 frames, we'll increase the final delay to 180 before ending the spellcard.

Final result!

Code: [Select]
script_spell KillerDoll{
  let spellcount=0;
  let spellangle=0;
  let img_spell = GetCurrentScriptDirectory()~"sakuya_spell.png";
  task Spell_Knife(angle,type){
    let objknife=Obj_Create(OBJ_SPELL);
    let knifexpos=GetPlayerX;
    let knifeypos=GetPlayerY;
    let knifexpos2=GetPlayerX;
    let knifeypos2=GetPlayerY;
    let knifeangle=angle;
    let knifespin=angle+180;
    let knifecount=0;
    Obj_SetAlpha(objknife,200);
    ObjEffect_SetTexture(objknife,img_spell);
    ObjEffect_SetRenderState(objknife,ADD);
    ObjEffect_CreateVertex(objknife,4);
    ObjEffect_SetPrimitiveType(objknife,PRIMITIVE_TRIANGLEFAN);
    if(type==1){
      ObjEffect_SetVertexUV(objknife,0,1,1);
      ObjEffect_SetVertexUV(objknife,1,46,1);
      ObjEffect_SetVertexUV(objknife,2,46,46);
      ObjEffect_SetVertexUV(objknife,3,1,46);
    }else{
      ObjEffect_SetVertexUV(objknife,0,49,1);
      ObjEffect_SetVertexUV(objknife,1,94,1);
      ObjEffect_SetVertexUV(objknife,2,94,46);
      ObjEffect_SetVertexUV(objknife,3,49,46);
    }
    while(!Obj_BeDeleted(objknife)){
      if(knifecount<60){
        knifespin+=4;
        knifexpos=knifexpos+(cos(180+knifeangle));
        knifeypos=knifeypos+(sin(180+knifeangle));
        knifexpos2=knifexpos;
        knifeypos2=knifeypos;
        ObjSpell_SetIntersecrionCircle(objknife,knifexpos,knifeypos,30,1,true);
      }else{
        knifexpos=knifexpos+(20*cos(180+knifeangle));
        knifeypos=knifeypos+(20*sin(180+knifeangle));
        knifexpos2=knifexpos2+(5*cos(180+knifeangle));
        knifeypos2=knifeypos2+(5*sin(180+knifeangle));
        ObjSpell_SetIntersecrionCircle(objknife,knifexpos,knifeypos,30,6,true);
      }
      if(knifecount==60){
        let enemy_target=-1;
        ascent(i in EnumEnemyBegin..EnumEnemyEnd) {
          enemy_target=EnumEnemyGetID(i);
          i=EnumEnemyEnd;
        }
        if(enemy_target!=-1){
          knifeangle=180+atan2(GetEnemyInfo(enemy_target,ENEMY_Y)-knifeypos, GetEnemyInfo(enemy_target,ENEMY_X)-knifexpos);
          knifespin=knifeangle;
        }
        PlaySE(GetCurrentScriptDirectory()~"knife.wav");
      }
      ObjEffect_SetVertexXY(objknife,0,knifexpos+(45*cos(135+knifespin)),knifeypos+(45*sin(135+knifespin)));
      ObjEffect_SetVertexXY(objknife,1,knifexpos+(45*cos(225+knifespin)),knifeypos+(45*sin(225+knifespin)));
      ObjEffect_SetVertexXY(objknife,2,knifexpos2+(45*cos(315+knifespin)),knifeypos2+(45*sin(315+knifespin)));
      ObjEffect_SetVertexXY(objknife,3,knifexpos2+(45*cos(45+knifespin)),knifeypos2+(45*sin(45+knifespin)));
      knifecount++;
      if(knifecount>120){
        Obj_Delete(objknife);
      }
      yield;
    }
  }
  task run{
    SetPlayerInvincibility(240);
    ForbidShot(true);
    loop(45){yield;}
    while(spellcount<90){
      if(spellcount%8==0){
        Spell_Knife(spellangle,1);
        Spell_Knife(spellangle+180,1);
spellangle+=20;
      }
      if(spellcount%8==4){
        Spell_Knife(spellangle,2);
        Spell_Knife(spellangle+180,2);
        spellangle+=20;
      }
      spellcount++;
      yield;
    }
    ForbidShot(false);
    SetSpeed(3.5, 2.5);
    loop(180){yield;}
    End();
  }
  @Initialize{
    LoadGraphic(img_spell);
    run();
  }
  @MainLoop{
    CollectItems();
    yield;
  }
  @Finalize{
    DeleteGraphic(img_spell);
  }
}



I'll also post the full, complete code. Enjoy your Sakuya.

Code: [Select]
#“Œ•?’e–‹•—[Player]
#ScriptVersion[2]
#Menu[Sakuya A]
#Text[Sakuya Izayoi - Illusion Sign

Shot:
Unfocused: Jack the Ludo Bile (spread)
Focused: Jack the Ripper (homing)

Spell Card:
Illusion Sign �uIndiscriminate�v
Illusion Sign �uKiller Doll�v]
#Image[.\sakuya_select.png]
#ReplayName[SakuyaA]

script_player_main{
  let img_sakuya = GetCurrentScriptDirectory()~"sakuya.png";
  let img_cutin = GetCurrentScriptDirectory()~"sakuya_select.png";
  let optionxpos=16;
  let optionypos=0;
  let count=-1;
  let i=0;
 
  task Option(position){
    let objoption=Obj_Create(OBJ_EFFECT);
    Obj_SetAlpha(objoption,200);
    ObjEffect_SetTexture(objoption,img_sakuya);  //uses star orb from spritesheet
    ObjEffect_SetRenderState(objoption,ALPHA);
    ObjEffect_SetPrimitiveType(objoption,PRIMITIVE_TRIANGLEFAN);
    ObjEffect_CreateVertex(objoption,4);         // square object with 4 vertexes
    ObjEffect_SetVertexUV(objoption,0,145,1);    // four coordinates of orb on spritesheet
    ObjEffect_SetVertexUV(objoption,1,159,1);    // object is 15x15
    ObjEffect_SetVertexUV(objoption,2,159,15);
    ObjEffect_SetVertexUV(objoption,3,145,15);
    if(position=="LEFT"){
      while(!Obj_BeDeleted(objoption)){
        ObjEffect_SetVertexXY(objoption,0,GetPlayerX-optionxpos-8,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,1,GetPlayerX-optionxpos+6,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,2,GetPlayerX-optionxpos+6,GetPlayerY+optionypos+7);
        ObjEffect_SetVertexXY(objoption,3,GetPlayerX-optionxpos-8,GetPlayerY+optionypos+7);
        if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
          if(count%6 == 3){
            JackTheRipper(GetPlayerX-optionxpos-1,GetPlayerY+optionypos-8,12);
          }
        }else{
          if(count%8 == 4){
            i=-2;
            while(i<=2){
              CreatePlayerShot01(GetPlayerX()-optionxpos-1, GetPlayerY()+optionypos-8, 10, 254+(i*8), 2, 1, 2);
              i++;
            }
          }
        }
        yield;
      }
    }else{
      while(!Obj_BeDeleted(objoption)){
        ObjEffect_SetVertexXY(objoption,0,GetPlayerX+optionxpos-7,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,1,GetPlayerX+optionxpos+7,GetPlayerY+optionypos-7);
        ObjEffect_SetVertexXY(objoption,2,GetPlayerX+optionxpos+7,GetPlayerY+optionypos+7);
        ObjEffect_SetVertexXY(objoption,3,GetPlayerX+optionxpos-7,GetPlayerY+optionypos+7);
        if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
          if(count%6 == 3){
            JackTheRipper(GetPlayerX+optionxpos,GetPlayerY+optionypos-8,12);
          }
        }else{
          if(count%8 == 4){
            i=-2;
            while(i<=2){
              CreatePlayerShot01(GetPlayerX()+optionxpos, GetPlayerY()+optionypos-8, 10, 286+(i*8), 2, 1, 2);
              i++;
            }
          }
        }
        yield;
      }
    }
  }
  task JackTheRipper(x,y,graphic){
    let enemy_target=-1;
    let target_angle=270;
    let test_angle=0;
    ascent(i in EnumEnemyBegin..EnumEnemyEnd) {
      enemy_target=EnumEnemyGetID(i);
      test_angle=atan2(GetEnemyInfo(enemy_target,ENEMY_Y)-GetPlayerY, GetEnemyInfo(enemy_target,ENEMY_X)-GetPlayerX);
      if(test_angle<=-60 && test_angle>=-120){
        i=EnumEnemyEnd;
      }else{
        enemy_target=-1;
      }
    }
    if(enemy_target!=-1){
      target_angle=atan2(GetEnemyInfo(enemy_target,ENEMY_Y)-y, GetEnemyInfo(enemy_target,ENEMY_X)-x);
      CreatePlayerShot01(x,y,15,target_angle,6,1,graphic);
    }else{
      CreatePlayerShot01(x,y,15,270,6,1,graphic);
    }
  }
  @Initialize{
    LoadGraphic(img_sakuya);
    LoadGraphic(img_cutin);
    LoadPlayerShotData(GetCurrentScriptDirectory()~"sakuya_shotdata.txt");
    SetPlayerLifeImage(img_sakuya, 96, 48, 143, 95);
    SetSpeed(3.5, 2.5);
    Option("LEFT");
    Option("RIGHT");
  }
  @MainLoop{
    if((GetKeyState(VK_SHOT)==KEY_PUSH || GetKeyState(VK_SHOT)==KEY_HOLD) && count==-1){
      count = 0;
    }
    if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
      if(count%6 == 0){
        JackTheRipper(GetPlayerX-8,GetPlayerY-8,11);
        JackTheRipper(GetPlayerX+8,GetPlayerY-8,11);
      }
      if(optionxpos>12){optionxpos--;}
      if(optionypos>-12){optionypos--;}
    }else{
      if(count%8 == 0){
        i=-3;
        while(i<=3){
          CreatePlayerShot01(GetPlayerX(), GetPlayerY()-8, 10, 270+(i*5), 2.5, 1, 1);
          i++;
        }
      }
      if(optionxpos<20){optionxpos++;}
      if(optionypos<0){optionypos++;}
    }
    if(count >= 0){
      count++;
    }
    if(count >= 6 && (GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD)){
      count=-1;
    }
    if(count >= 8){
      count=-1;
    }
    SetIntersectionCircle(GetPlayerX, GetPlayerY, 2);
    yield;
  }
  @Missed{
  }
  @SpellCard{
    SetSpeed(1, 1);
    if(GetKeyState(VK_SLOWMOVE)==KEY_PUSH || GetKeyState(VK_SLOWMOVE)==KEY_HOLD){
      UseSpellCard("KillerDoll", 0);
      CutIn(KOUMA,"Illusion Sign �uKiller Doll�v", img_cutin);
    }else{
      UseSpellCard("Indiscriminate", 0);
      CutIn(KOUMA,"Illusion Sign �uIndiscriminate�v", img_cutin);
    }
  }
  @DrawLoop{
    SetTexture(img_sakuya);
    if(GetKeyState(VK_LEFT)==KEY_PUSH || GetKeyState(VK_LEFT)==KEY_HOLD){
      SetGraphicRect(49.5, 1.5, 95.5, 47.5); // left movement frame
    }else if(GetKeyState(VK_RIGHT)==KEY_PUSH || GetKeyState(VK_RIGHT)==KEY_HOLD){
      SetGraphicRect(97.5, 1.5, 143.5, 47.5); // right movement frame
    }else{
      SetGraphicRect(1.5, 1.5, 47.5, 47.5); // neutral frame
    }
    DrawGraphic(GetPlayerX(), GetPlayerY());
  }
  @Finalize{
    DeleteGraphic(img_sakuya);
    DeleteGraphic(img_cutin);
  }
}

script_spell Indiscriminate{
  let spellcount=0;
  let spellangle=0;
  let img_spell = GetCurrentScriptDirectory()~"sakuya_spell.png";
  task Spell_Knife(angle,type){
    let objknife=Obj_Create(OBJ_SPELL);
    let knifexpos=GetPlayerX;
    let knifeypos=GetPlayerY;
    let knifeangle=angle;
    Obj_SetAlpha(objknife,200);
    ObjEffect_SetTexture(objknife,img_spell);
    ObjEffect_SetRenderState(objknife,ADD);
    ObjEffect_CreateVertex(objknife,4);
    ObjEffect_SetPrimitiveType(objknife,PRIMITIVE_TRIANGLEFAN);
    if(type==1){
      ObjEffect_SetVertexUV(objknife,0,1,1);
      ObjEffect_SetVertexUV(objknife,1,46,1);
      ObjEffect_SetVertexUV(objknife,2,46,46);
      ObjEffect_SetVertexUV(objknife,3,1,46);
    }else{
      ObjEffect_SetVertexUV(objknife,0,49,1);
      ObjEffect_SetVertexUV(objknife,1,94,1);
      ObjEffect_SetVertexUV(objknife,2,94,46);
      ObjEffect_SetVertexUV(objknife,3,49,46);
    }
    while(!Obj_BeDeleted(objknife)){
      if(type==1){
        knifeangle-=1;
      }else{
        knifeangle+=1;
      }
      knifexpos=knifexpos+(8*cos(180+knifeangle));
      knifeypos=knifeypos+(8*sin(180+knifeangle));
      ObjEffect_SetVertexXY(objknife,0,knifexpos+(45*cos(135+knifeangle)),knifeypos+(45*sin(135+knifeangle)));
      ObjEffect_SetVertexXY(objknife,1,knifexpos+(45*cos(225+knifeangle)),knifeypos+(45*sin(225+knifeangle)));
      ObjEffect_SetVertexXY(objknife,2,knifexpos+(45*cos(315+knifeangle)),knifeypos+(45*sin(315+knifeangle)));
      ObjEffect_SetVertexXY(objknife,3,knifexpos+(45*cos(45+knifeangle)),knifeypos+(45*sin(45+knifeangle)));
      ObjSpell_SetIntersecrionCircle(objknife,knifexpos,knifeypos,30,10,true);
      yield;
    }
  }
  task run{
    ForbidShot(true);
    loop(45){yield;}
    while(spellcount<90){
      if(spellcount%5==0){
        Spell_Knife(spellangle,1);
        Spell_Knife(spellangle+90,2);
        Spell_Knife(spellangle+180,1);
        Spell_Knife(spellangle+270,2);
        spellangle+=40;
        PlaySE(GetCurrentScriptDirectory()~"knife.wav");
      }
      spellcount++;
      yield;
    }
    ForbidShot(false);
    SetSpeed(3.5, 2.5);
    loop(90){yield;}
    End();
  }
  @Initialize{
    SetPlayerInvincibility(240);
    LoadGraphic(img_spell);
    run();
  }
  @MainLoop{
    CollectItems();
    yield;
  }
  @Finalize{
    DeleteGraphic(img_spell);
  }
}

script_spell KillerDoll{
  let spellcount=0;
  let spellangle=0;
  let img_spell = GetCurrentScriptDirectory()~"sakuya_spell.png";
  task Spell_Knife(angle,type){
    let objknife=Obj_Create(OBJ_SPELL);
    let knifexpos=GetPlayerX;
    let knifeypos=GetPlayerY;
    let knifexpos2=GetPlayerX;
    let knifeypos2=GetPlayerY;
    let knifeangle=angle;
    let knifespin=angle+180;
    let knifecount=0;
    Obj_SetAlpha(objknife,200);
    ObjEffect_SetTexture(objknife,img_spell);
    ObjEffect_SetRenderState(objknife,ADD);
    ObjEffect_CreateVertex(objknife,4);
    ObjEffect_SetPrimitiveType(objknife,PRIMITIVE_TRIANGLEFAN);
    if(type==1){
      ObjEffect_SetVertexUV(objknife,0,1,1);
      ObjEffect_SetVertexUV(objknife,1,46,1);
      ObjEffect_SetVertexUV(objknife,2,46,46);
      ObjEffect_SetVertexUV(objknife,3,1,46);
    }else{
      ObjEffect_SetVertexUV(objknife,0,49,1);
      ObjEffect_SetVertexUV(objknife,1,94,1);
      ObjEffect_SetVertexUV(objknife,2,94,46);
      ObjEffect_SetVertexUV(objknife,3,49,46);
    }
    while(!Obj_BeDeleted(objknife)){
      if(knifecount<60){
        knifespin+=4;
        knifexpos=knifexpos+(cos(180+knifeangle));
        knifeypos=knifeypos+(sin(180+knifeangle));
        knifexpos2=knifexpos;
        knifeypos2=knifeypos;
        ObjSpell_SetIntersecrionCircle(objknife,knifexpos,knifeypos,30,1,true);
      }else{
        knifexpos=knifexpos+(20*cos(180+knifeangle));
        knifeypos=knifeypos+(20*sin(180+knifeangle));
        knifexpos2=knifexpos2+(5*cos(180+knifeangle));
        knifeypos2=knifeypos2+(5*sin(180+knifeangle));
        ObjSpell_SetIntersecrionCircle(objknife,knifexpos,knifeypos,30,6,true);
      }
      if(knifecount==60){
        let enemy_target=-1;
        ascent(i in EnumEnemyBegin..EnumEnemyEnd) {
          enemy_target=EnumEnemyGetID(i);
          i=EnumEnemyEnd;
        }
        if(enemy_target!=-1){
          knifeangle=180+atan2(GetEnemyInfo(enemy_target,ENEMY_Y)-knifeypos, GetEnemyInfo(enemy_target,ENEMY_X)-knifexpos);
          knifespin=knifeangle;
        }
        PlaySE(GetCurrentScriptDirectory()~"knife.wav");
      }
      ObjEffect_SetVertexXY(objknife,0,knifexpos+(45*cos(135+knifespin)),knifeypos+(45*sin(135+knifespin)));
      ObjEffect_SetVertexXY(objknife,1,knifexpos+(45*cos(225+knifespin)),knifeypos+(45*sin(225+knifespin)));
      ObjEffect_SetVertexXY(objknife,2,knifexpos2+(45*cos(315+knifespin)),knifeypos2+(45*sin(315+knifespin)));
      ObjEffect_SetVertexXY(objknife,3,knifexpos2+(45*cos(45+knifespin)),knifeypos2+(45*sin(45+knifespin)));
      knifecount++;
      if(knifecount>120){
        Obj_Delete(objknife);
      }
      yield;
    }
  }
  task run{
    SetPlayerInvincibility(240);
    ForbidShot(true);
    loop(45){yield;}
    while(spellcount<90){
      if(spellcount%8==0){
        Spell_Knife(spellangle,1);
        Spell_Knife(spellangle+180,1);
spellangle+=20;
      }
      if(spellcount%8==4){
        Spell_Knife(spellangle,2);
        Spell_Knife(spellangle+180,2);
        spellangle+=20;
      }
      spellcount++;
      yield;
    }
    ForbidShot(false);
    SetSpeed(3.5, 2.5);
    loop(180){yield;}
    End();
  }
  @Initialize{
    LoadGraphic(img_spell);
    run();
  }
  @MainLoop{
    CollectItems();
    yield;
  }
  @Finalize{
    DeleteGraphic(img_spell);
  }
}

And that concludes our tutorial.



Oh wait, no it doesn't.

There's also the issue of balance I'd like to bring up. For best results, I recommend balancing your player using the standards set by the default Reimu and Marisa. They set the standard so that your player character will be well-balanced for any script they are used in.

I've made a pair of scripts that you can use to test the damage your character does, so that you know if you're doing too much or too little. Get them here.

Doing a bit of testing, here are the results for Reimu and Marisa. "Far" means from the bottom of the screen, and "Max" means practically right on top of the enemy (if no far/max value is listed, it does max from the bottom of the screen):

Quote
REIMU
Normal Speed: 4
Focus Speed: 1.5

Reimu A Normal DPS (far/max): ~136/210
Reimu A Focus DPS (far/max): ~150/232
Reimu A Unfocused Bomb damage (far/max): ~150/2000
Reimu A Focused Bomb damage: ~1600

Reimu B Normal DPS (far/max): ~160/305
Reimu B Focus DPS (far/max): ~235/283
Reimu B Unfocused Bomb damage (far/max): ~300/1300
Reimu B Focused Bomb damage (far/max): ~0/2000

MARISA
Normal Speed: 5
Focus Speed: 2
Special: Only 2 bombs, Auto-collection zone ~40% bigger

Marisa A Normal DPS: ~174
Marisa A Focus DPS: ~248
Marisa A Unfocused Bomb damage (far/max): ~400/1800
Marisa A Focused Bomb damage (far/max): ~1000/4800

Marisa B Normal DPS (far/max): ~140/160
Marisa B Focus DPS: ~281
Marisa B Unfocused Bomb damage (far/max): ~400/2700
Marisa B Focused Bomb damage: ~4000

There may be some other special things about them that I didn't pick up on.

Now to test my Sakuya:

Quote
SAKUYA
Normal Speed: 3.5
Focus Speed: 2.5

Sakuya A Normal DPS (far/max): ~124/282
Sakuya A Focus DPS: ~240
Sakuya A Unfocused Bomb damage (far/max): ~600/5000
Sakuya A Focused Bomb damage (far/max): ~1700/4500

Whoops! Looks like I went a little overboard on damage! There's a couple things I can conclude from this to make Sakuya more balanced:
- I might want to lower the damage of the secondary knives in Jack the Ludo Bile to reduce its point-blank damage.
- I should cut the damage of the knives in Indiscriminate to about half.
- I should make the knives in Killer Doll do a miniscule amount of damage, or maybe 0, before they home in, since it racks up a ton of damage if they spin on top of the boss.
- Not necessary, but if I wanted to cut damage a lot I could probably get away with Sakuya having 4 bombs as she does in PCB, since Marisa seems to have only 2, also mirroring her PCB incarnation.

This falls under my final piece of advice; playtest, playtest, playtest. Fine tune your character to make them play and look as smooth as possible. Find bugs and other oddities and crush them under your righteous heel. It is your obligation as a creator of content. That's all folks!
« Last Edit: March 05, 2010, 09:12:04 AM by Stuffman »

Re: Player script tutorial
« Reply #4 on: April 26, 2009, 11:25:24 PM »
sticky this maybe? not that its gonna fall off the page anytime soon, but it would probably be best to sticky it

Stuffman

  • *
  • We're having a ball!
Re: Player script tutorial
« Reply #5 on: April 26, 2009, 11:32:22 PM »
What we'll probably do is have one sticky pointing to all the tutorial threads, so we don't have a ton of them.

Hat

  • Just an unassuming chapeau.
  • I will never be ready.
Re: Player script tutorial
« Reply #6 on: April 27, 2009, 12:30:31 AM »
Or we could lump them all into one thread, and make a mega-tutorial like we've all dreamed of?

... *gazes wistfully into the distance* Aah, to have a complete, comprehensive english tutorial...

Re: Player script tutorial
« Reply #7 on: April 27, 2009, 12:34:34 AM »
Or we could lump them all into one thread, and make a mega-tutorial like we've all dreamed of?

... *gazes wistfully into the distance* Aah, to have a complete, comprehensive english tutorial...
We we make tutorials for each topic then it won't be hard to sort through them and put them together in an organized manner.

Stuffman

  • *
  • We're having a ball!
Re: Player script tutorial
« Reply #8 on: April 27, 2009, 02:51:15 AM »
Next part added. Man this is gonna be wordy. If you think something needs clarification or you catch a typo or something let me know.

Hat

  • Just an unassuming chapeau.
  • I will never be ready.
Re: Player script tutorial
« Reply #9 on: April 27, 2009, 11:47:48 PM »
So long as Stuffman pins all of them, then that's cool too. I was figuring that we'd be able to organize them by merging threads, then using a system of Ctrl-F to leaf through them (like, with a table of contents and references). But eh.

Re: Player script tutorial
« Reply #10 on: May 03, 2009, 12:22:09 AM »
I actually just came in here to ask if you had made anymore progress on this, then noticed that you just edited-in the bombing spellcard section! Next time make a post saying you've updated so we know when you do.

This is looking very nice, I'm learning alot. Thanks for the hard work.

Stuffman

  • *
  • We're having a ball!
Re: Player script tutorial
« Reply #11 on: May 03, 2009, 12:51:05 AM »
Yep, all finished now.

Also need to go adjust damage per test results.

Re: Player script tutorial
« Reply #12 on: May 03, 2009, 12:58:45 AM »
Holy balls another update. Again, thanks for doing this tutorial, it's really helpful.

Garlyle

  • I can't brain today
  • I have the dumb
    • Tormod Plays Games
Re: Player script tutorial
« Reply #13 on: May 03, 2009, 04:03:34 PM »
Yay meaningful testable scripts/damage values!  Thank you for those T_T

Although, I now have another request: I don't remember Nuclear Cheese's drawing tutorial covering Effect Objects.  And of all of the many, many things in Danmakufu, that's the one thing that is still completely beyond my ability to understand for some reason [Yes, even beyond 3D Graphics for stages].

I could really use one, especially for aiding with those poor, miserable little bombs - they're the biggest obstacle between me and player creation.

Re: Player script tutorial
« Reply #14 on: May 03, 2009, 05:29:24 PM »
As a matter of fact, Nuclear Cheese does have an Object Effect tutorial. Whether or not it completely covers what you're looking for: I'm not sure. It's not as long as some of the other tutorials made thus far, but it should be of some assistance to you.

Hat

  • Just an unassuming chapeau.
  • I will never be ready.
Re: Player script tutorial
« Reply #15 on: May 04, 2009, 12:39:44 AM »
Question: how do you define Object Shots created by the player as player shots, not enemy bullets?

Stuffman

  • *
  • We're having a ball!
Re: Player script tutorial
« Reply #16 on: May 04, 2009, 12:59:58 AM »
Any object shots the player creates will belong to the player. You don't need to do anything special.

Hat

  • Just an unassuming chapeau.
  • I will never be ready.
Re: Player script tutorial
« Reply #17 on: May 04, 2009, 04:18:05 AM »
Is that so~

Wonderful! Okay, great, thanks.

Re: Player script tutorial
« Reply #18 on: May 12, 2009, 11:55:38 PM »
Why did you not sticky this?

Fun fact: Copy and pasting this tutorial into Word gives over 50 pages of text!

Quote
Sakuya dies when she is killed? Check.
:V
« Last Edit: May 13, 2009, 12:01:04 AM by Suikama »

Stuffman

  • *
  • We're having a ball!
Re: Player script tutorial
« Reply #19 on: May 13, 2009, 12:09:33 AM »
I will create a single topic that links to all the tutorials and sticky that when I am not feeling lazy.

...I guess I'm not feeling THAT lazy so I suppose I'll do it now

Chronojet ⚙ Dragon

  • The Oddity
  • 今コソ輝ケ、我ガ未来、ソノ可能性!!
Re: Player script tutorial
« Reply #20 on: October 30, 2009, 07:06:04 PM »
So long as Stuffman pins all of them, then that's cool too. I was figuring that we'd be able to organize them by merging threads, then using a system of Ctrl-F to leaf through them (like, with a table of contents and references). But eh.
Someone will print out the tutorials and put them together like a book. What next?

Helepolis

  • Charisma!
  • *
  • O-ojousama!?
Re: Player script tutorial
« Reply #21 on: October 30, 2009, 07:37:08 PM »
Was it really nessecary to bump the thread up for this? :V

Re: Player script tutorial
« Reply #22 on: October 30, 2009, 08:15:38 PM »
He just wants us to know he is now able to use trigonometry. Be nice, Helepolis!

Helepolis

  • Charisma!
  • *
  • O-ojousama!?
Re: Player script tutorial
« Reply #23 on: October 30, 2009, 08:31:20 PM »
I sense more like the failure of reading and being weird. He even gave wrong advice to someone who asked how to record in Dnh and with what program. The dude told to upload replay file.

I mean like seriously. . . what the hell?

Chronojet ⚙ Dragon

  • The Oddity
  • 今コソ輝ケ、我ガ未来、ソノ可能性!!
Re: Player script tutorial
« Reply #24 on: October 31, 2009, 10:12:02 PM »
I sense more like the failure of reading and being weird. He even gave wrong advice to someone who asked how to record in Dnh and with what program. The dude told to upload replay file.

I mean like seriously. . . what the hell?

Let's leave it at I can be weird at times (read: every nanosecond of the day)

Stuffman

  • *
  • We're having a ball!
Re: Player script tutorial
« Reply #25 on: October 31, 2009, 11:02:29 PM »
Was it really nessecary to bump the thread up for this? :V

To be fair, the tutorial thread index does mention that necromancy is permitted on this board.

Cabble

  • Ask me about my Cat.
  • Not unwilling to shank you.
Re: Player script tutorial
« Reply #26 on: November 01, 2009, 03:46:59 AM »
To be fair, the tutorial thread index does mention that necromancy is permitted on this board.
Not to mention that for him necroing isn't uncommon for him.
I had a teacher who used to play radiohead during class once.

ONCE.

Chronojet ⚙ Dragon

  • The Oddity
  • 今コソ輝ケ、我ガ未来、ソノ可能性!!
Re: Player script tutorial
« Reply #27 on: November 01, 2009, 04:26:17 AM »
Not to mention that for him necroing isn't uncommon for him.
I think I'm gonna look that word up...
Edit: O_o What?

Chronojet ⚙ Dragon

  • The Oddity
  • 今コソ輝ケ、我ガ未来、ソノ可能性!!
Re: Player script tutorial
« Reply #28 on: November 26, 2009, 08:58:36 PM »
SetGrazeCircle;

From SakuyaB player character:
Code: [Select]
    @Initialize {
        LoadGraphic(ImgSakuya);
        LoadGraphic(ImgCutIn);
        LoadGraphic(imgEffect);
        LoadPlayerShotData(ShotSakuya);
LoadSE(SE);
LoadSE(SE2);
SetGrazeCircle(60); //<---This.
SetRibirthFrame(9);
        SetPlayerLifeImage(ImgSakuya, 0, 0, 24, 54);
        SetSpeed(3.6, 2.5);
        SetItemCollectLine(128);

        SetTexture(ImgSakuya);

        TMain;
    }
That.
« Last Edit: November 26, 2009, 09:32:50 PM by Always お⑨烏 »

Becky_Botsford

Re: Player script tutorial
« Reply #29 on: July 09, 2010, 01:38:42 PM »
does the txt file have to be ANSI coding?