3D Attacks for Enemies

aL BeasTie

Well-known member
3D Attacks For Enemies by Bloodbane

I'm going to share another script functions. This time the ones which are related to enemy's 3D attack.

About 3D attacks

Before going to script itself, I'll start with 3D attack definition. 3D attack is attack which can hit heroes standing in different z coordinate. It could be simple ramming attack or whole screen attack. If you have played many brawler games, you should have seen many examples of those although not all enemies perform it.

I would like to ramble more about 3D attack types but I'll skip it and just go with certain type which is related to script function discussed here. The type is narrow and targetted type which means it only hit narrow area (technically just as big as the attackbox) but targetted (meaning it doesn't move aimlessly). There are 3 kinds of them which are supported here:

1. Ram/charge (ex: Big Ben's flamethrower dash)
2. Jumpattack (ex: Mona & Lisa SoR1 Jumpkick)
3. Targetted shooting (ex: RobotY's Missile)

All of them are about moving to target and are supported by one function named 'target'.
I'm not going to explain how to set offset, attackbox, range and rangez including how to use animationscript. Learn those yourselves if you haven't.

Targetting

Like said before, these attacks are targetted which means we need to target 1st before performing them. To find the target we use a function named 'findtarget()'.

Code:
void self = getlocalvar("self");
void target = findtarget(self);



'findtarget(self)' means find another entity whom this 'self' entity is hostile to. Since this is for enemies, the target is player. If there are 2 or more player, the closest one will be chosen. If there are none, 'target' will be empty.

What are we going to do with the targetted player assuming it's not empty? See next.

Moving Philosophy

After target is attained, the next thing to do is to move the enemy toward player. How does enemy move? there are 2 ways to do it, one is with 'move' command and 2nd is with 'velocity setting'. The 1st method is not preferred cause it's cumbersome. It requires multiple frames for moving and using script to replicate 'move' brings problem with walls and platforms. It's solvable though but it's best to avoid this method and use 2nd method instead.

2nd method moves entity smoothly with velocity setting and it moves just like using 'jumpframe'. It also require less frame since one frame is enough. If more frames are used, it's usually for graphical effect.

To move entity with velocity setting, we could do this:


Code:
void self = getlocalvar("self");
changeentityproperty(self, "velocity", 4, 2);

4 is x velocity while 2 is z velocity. Just by changing the property above, entity will move with that speed. With 2 velocity set at same time, entity will move diagonally downward right.

Now, how do we make enemy move toward player? simple just set proper velocity buuuut how much velocity do we need? We have to be careful here cause setting improper speed might make enemy misses player completely while moving. In order to get proper value we need to find the distance between enemy and player. Since they are seperated in x and z, we are going to attain distance for x and z.

Code:
 void self = getlocalvar("self");
    float x = getentityproperty(self, "x"); // Get enemy's x coordinate
    float z = getentityproperty(self, "z"); // Get enemy's z coordinate
    void target = findtarget(self); // Find target

    if( target != NULL()){ // Is there a target?
      float Tx = getentityproperty(target, "x"); // Get target's x coordinate
      float Tz = getentityproperty(target, "z"); // Get target's z coordinate
      float Disx = Tx - x; // Get x distance
      float Disz = Tz - z; // Get z distance
    }

   

Note the 'if' above. We need to check if player is available or not cause we can't get any info from it if it's not or empty.

After we get Disx and Disz, we could calculate proper speed. Let's name x velocity with Vx and z velocity with Vz.
The idea here is enemy moves with Vx in x axis to move Disx pixels while at same time enemy moves with Vz in z axis to move Disz pixels. Do you get it? by moving like that, enemy will eventually reach player's position cause the former moves Disx pixels in x axis and Disz in z axis. Both distance must be covered at same time otherwise enemy will miss.

So in math formula we could write them like this:


Code:
Disx = Vx * t
Disz = Vz * t

t = required time for enemy to reach player
Since we don't really need to know t, we could merge both formulas into:


Code:
Disx/Vx = Disz/Vz or Vx/Disx = Vz/Disz

Actually with graphic and little trigonometry, getting above formula is more straightforward but I believe not all ppl here understand trigonometry so I use above logic instead.

Getting Velocity

Both x velocity and z velocity or Vx and Vz in formula above are unknown but don't worry cause it means we can set both enemy's velocity although only one which will be used. Confused? I've said it before that proper speed is required so although one velocity is defined, the other will be calculated instead. That's the trick to get proper speed.

1st assumption: Vx is dominant one and Vz will be the calculated one. Dominant means we can define its value.
Since Vz is unknown while Vx is defined say Vx = Velx, we could get its value from formula above:

Code:
 Vz = Disz * Velx/Disx


2nd assumption: Vz is dominant one and Vx will be the calculated one.
Since Vx is unknown while Vz is defined say Vz = Velz, we could get its value from formula above:


Code:
 Vx = Disx * Velz/Disz

With those, both values of Vx and Vz are attained.

Code Implementation

Both assumption above can easily be implemented with script but there are couple things to deal with:

1. Both Disx and Disz MIGHT be negative value. Both are attained from subtracting player's coordinate with enemy's. If player stands behind and away from enemy, both values will be negative.

To solve this, simply turn them to positive if their value is negative, like this:


Code:
       if(Disx < 0){ // Negative Disx?
        Disx = -Disx; // Turn it to positive
      }
      if(Disz < 0){ // Negative Disz?
        Disz = -Disz; // Turn it to positive
      }


Combine this with above code that is.

2. Velocity is not relative to entity meaning setting positive velocity will move entity to right for x velocity or down for z velocity. Entity would move the opposite way with negative values. If player stand behind and away from enemy, negative value would be required.

To solve this, we must combine fact above with the formula like this:

1st assumption:

Code:
     if(Disx < 0){ // Negative Disx?
        Disx = -Disx; // Turn it to positive
      }
      if(Disz < 0){ // Negative Disz?
        Disz = -Disz; // Turn it to positive
      }
      Vz = (Tz - z) * Velx/Disx; // Calculate Vz
      if(Tx < x){ // Player is behind enemy?
        Vx = -Velx; // Turn Vx to negative
      }




2nd assumption:

Code:
      if(Disx < 0){ // Negative Disx?
        Disx = -Disx; // Turn it to positive
      }
      if(Disz < 0){ // Negative Disz?
        Disz = -Disz; // Turn it to positive
      }
      Vx = (Tx - x) * Velz/Disz; // Calculate Vx
      if(Tz < z){ // Player is behind enemy?
        Vz = -Velz; // Turn Vz to negative
      }


3. How to implement both above assumption? When 1st one is used and when 2nd one is used?

Actually we could set any rule here but I prefer to use Disx and Disz comparison rule. The reason is when one of them is bigger, the respective velocity would be bigger than other's velocity which mean former velocity is more dominant. That rule is implemented like this:


Code:
      if(Disx < 0){ // Negative Disx?
        Disx = -Disx; // Turn it to positive
      }
      if(Disz < 0){ // Negative Disz?
        Disz = -Disz; // Turn it to positive
      }
      if(Disz < Disx){ // Disx bigger than Disz?
        Vz = (Tz - z) * Velx/Disx; // Calculate Vz
        if(Tx < x){ // Player is behind enemy?
          Vx = -Velx; // Turn Vx to negative
        }
      } else { // Disz bigger than Disx
        Vx = (Tx - x) * Velz/Disz; // Calculate Vx
        if(Tz < z){ // Player is behind enemy?
          Vz = -Velz; // Turn Vz to negative
        }
      }



4. What if there are no players to target at all?

The answer is simple, let's set certain value for Vx and Vz. We could just set both to 0 which means no moving at all. However, usually enemy is in moving in x axis animation so he/she would look ugly if he/she's not moving so to solve it, simply apply defined Vx to him/her. Don't forget enemy's facing direction!


Code:
    void self = getlocalvar("self");
    int dir = getentityproperty(self, "direction"); // Get enemy's facing direction
    void target = findtarget(self); // Find target

    if( target != NULL()){ // Is there a target?
     ...
    } else { // No target at all!
      Vz = 0; // 0 velocity
      if(dir==0){ // Facing left?
        Vx = -Velx; // Negative velocity
      } else { Vx = Velx}; // Positive velocity
    }


5. Other enemy using same function ended having same target or even worse altering target. This usually happen if there are 2 same enemies attacking with same attack at almost same time.

I don't get why this could happen but this issue can be fixed by using local value with unique identifier like this:


Code:
    void self = getlocalvar("self");
    setlocalvar("T"+self, findtarget(self)); // Find target and put it in local variable

    if( getlocalvar("T"+self) != NULL()){ // Is there a target?

6. Although velocity is adjusted, enemy won't turn back if player is standing behind while targetting.
To solve this, simply flip facing direction to where player is like this:


Code:
      if(Disx < 0){ // Negative Disx?
        Disx = -Disx; // Turn it to positive
        changeentityproperty(self, "direction", 0); // Face left
      } else {
        changeentityproperty(self, "direction", 1); // Face right
      }



Well the code become complex but don't worry all of this have great effect .

Last, this target function is meant to support other function. That means the function who perform the actual move is not this function. This function only calculate proper velocity values. To transfer this values, we use local variable. And the whole function becomes like this:

Code:
void target(float Velx, float Velz)
{// Targetting opponent before leaping or dashing
    void self = getlocalvar("self");
    int dir = getentityproperty(self, "direction"); // Get enemy's facing direction
    float x = getentityproperty(self, "x"); // Get enemy's x coordinate
    float z = getentityproperty(self, "z"); // Get enemy's z coordinate

    setlocalvar("T"+self, findtarget(self)); // Find target and put it in local variable

    if( getlocalvar("T"+self) != NULL()){ // Is there a target?
      void target = getlocalvar("T"+self);
      float Tx = getentityproperty(target, "x"); // Get target's x coordinate
      float Tz = getentityproperty(target, "z"); // Get target's z coordinate
      float Disx = Tx - x; // Get x distance
      float Disz = Tz - z; // Get z distance

      if(Disx < 0){ // Negative Disx?
        Disx = -Disx; // Turn it to positive
        changeentityproperty(self, "direction", 0); // Face left
      } else {
        changeentityproperty(self, "direction", 1); // Face right
      }

      if(Disz < 0){ // Negative Disz?
        Disz = -Disz; // Turn it to positive
      }

      if(Disz < Disx) // Disx bigger than Disz?
      {
        if(Tx < x){ // Player is behind enemy?
          setlocalvar("x"+self, -Velx); // Turn Vx to negative
        } else { setlocalvar("x"+self, Velx); } // Use defined Vx

        setlocalvar("z"+self, Velx*(Tz-z)/Disx); // Calculate Vz then store value in local variable
      } else { // Disz bigger than Disx!
        if(Tz < z){ // Player is behind enemy?
          setlocalvar("z"+self, -Velz); // Turn Vz to negative
        } else { setlocalvar("z"+self, Velz); } // Use defined Vz

        setlocalvar("x"+self, Velz*(Tx-x)/Disz); // Calculate Vx then store value in local variable
      }

    } else { // No target at all!
      setlocalvar("z"+self, 0); // 0 velocity
      if(dir==0){ // Facing left?
        setlocalvar("x"+self, -Velx); // Negative velocity
      } else { setlocalvar("x"+self, Velx); } // Positive velocity
    }
}


The input of this function is Velx for x velocity and Velz for z velocity. Like said before, 2 possible inputs but only one is used.

The output of this function is local variable named '"x"+self' which contains x velocity and '"z"+self' which contains z velocity.

Velocity Usage

Need aspirin? OK OK just kidding

There are 3 functions which can use the outputs above i.e:
1. dash() for targetted ram/charge
2. leap(float Vely) for semi targetted jump attack
3. shoot2(void Shot, float dx, float dy, float dz) for targetted shooting

Each functions affect enemy differently.
Since they use outputs from 'target' function, they must be declared AFTER that function.

1.Targetted Charge/Ram

In Streets of Rage series, there's a fat enemy who spew flame from his mouth while running toward player. That is one example of this type of targetted attack. The function used here moves enemy with certain speed toward player. It doesn't matter what actually they are doing, they could be ramming, sliding or shoulder charging.

Here's the function:


Code:
void dash()
{// Dash with previously attained speed!
    void self = getlocalvar("self");
    float Vx = getlocalvar("x"+self); // Attain x velocity
    float Vz = getlocalvar("z"+self); // Attain z velocity
    if( Vx!=NULL() && Vz!=NULL() ){ // Are both velocity available?
      changeentityproperty(self, "velocity", Vx, Vz); //Move towards target!
    }
}



As you can see, both velocity are attained from localvariable made by 'target' function before. There's a velocity check to ensure both velocity are available before dashing. Changeentityproperty sets the velocity which moves the enemy. Don't worry about facing direction cause 'target' has solved it.

Due to nature of script, enemy will keep moving whether he/she hit player or not. Not hitting means player moves away. That's why it's recommended to stop the movement after he/she's done.

2.Semi Targetted Jumpattack

In Streets of Rage 1, there are twin female bosses who can perform jumpkick right to player. That is one example of this type of targetted attack. The function here toss enemy with certain speed toward player. Aside from moving in x and z, it also toss enemy upwards. Like previous function, it doesn't matter what actually they are doing, they could be jumppunching, jumpkicking or elbow strike.

Here's the function:


Code:
void leap(float Vely)
{// Leap with previously attained speed!
    void self = getlocalvar("self");
    float Vx = getlocalvar("x"+self); // Attain x velocity
    float Vz = getlocalvar("z"+self); // Attain z velocity
    if( Vx!=NULL() && Vz!=NULL() ){ // Are both velocity available?
      tossentity(self, Vely, Vx, Vz); //Leap towards target!
    }
}



Everything is same as previous function except that it's using tossentity. Tossentity works just like jumpframe. Since attained velocity only for x and z, this function accept other velocity for y axis. However, this function only toss meaning after certain time, tossing apex will be reached then enemy will fall down. While jumping, x and z velocity remains so don't worry about it. After enemy lands, all movements are stopped so no need to stop it with script.

BTW I did name this as semi target cause the real Targetted Jumpattack toss enemy exactly right to where player is. This function doesn't support that precision cause like dash(), enemy will continue moving after hitting player.

3.Targetted Shooting

Last boss of Streets of Rage 3, RobotY can shoot 2 missiles and both are targetted. That's an example of this type of attack. This function shoots a projectile then alter its speed with targetted speed toward player. It doesn't matter how the projectile looks like, it could be knife, missile, firebolt or boomerang.

Here's the function:


Code:
void shoot2(void Shot, float dx, float dy, float dz)
{ // Shooting targetted projectile
   void self = getlocalvar("self");
   int Direction = getentityproperty(self, "direction"); // Get enemy's facing direction
   int x = getentityproperty(self, "x"); // Get enemy's x coordinate
   int y = getentityproperty(self, "a"); // Get enemy's a coordinate
   int z = getentityproperty(self, "z"); // Get enemy's z coordinate
   float Vx = getlocalvar("x"+self); // Attain x velocity
   float Vz = getlocalvar("z"+self); // Attain z velocity
   void vShot; // Projectile entity

   if (Direction == 0){ //Is entity facing left?                  
      dx = -dx; //Reverse X direction to match facing
   }

   vShot = projectile(Shot, x+dx, z+dz, y+dy, Direction, 0, 0, 0); // Shoot the projectile!

   if( Vx!=NULL() && Vz!=NULL() ){ // Are both velocity available?
     changeentityproperty(vShot, "velocity", Vx, Vz); //Move projectile towards target!
     if (Direction == 0){ //Is enemy facing left?                  
       changeentityproperty(vShot, "speed", -Vx); // Change speed so it moves left
     } else {changeentityproperty(vShot, "speed", Vx);} // Change speed so it moves right
   }
}



The function differs alot with previous ones cause this function shoots projectile and alters its speed. 'Projectile' function shoots entity named Shot which is inputted. This projectile function also requires starting position for shot projectile which is procurred by attaining x,z and a coordinate above. Position adjustment is inputted to allow scripter to give position shift to the shot projectile.

vShot gets the shot projectile. After that, Vx and Vz is attained like before to alter projectile's speed with changeentityproperty. However since projectile's flying speed is based on its speed instead of velocity, speed change is required. Since speed isn't relative, we need to get enemy's facing direction 1st before changing the speed with Vx. Negative speed(-Vx) for moving left or positive speed(Vx) for right.

Continued in next post
 
Review

As you can see, targetting is seperated from the action in this method. This allows flexibility in which scripted can use same 'target' function for different attacks. However, there is another important reason behind this seperation.
Take a look at this sample animation:


Code:
 anim attack1
        range   80 250
        rangez  -45 45
   delay   1
   offset   50 105
   bbox   30 20 70 80
   frame   data/chars/enemy/charge01.gif
   delay   10
        @cmd    target 5 2.5
   frame   data/chars/enemy/charge01.gif
   frame   data/chars/enemy/charge02.gif
        delay   5
        blast   75 30 50 80 25 1
   frame   data/chars/enemy/charge03.gif
        delay   65
        @cmd    dash
   frame   data/chars/enemy/charge03.gif
        delay   10
        @cmd    stop
   frame   data/chars/enemy/charge04.gif
        attack  0
   frame   data/chars/enemy/charge05.gif

In this attack, enemy target player with speed of 5 or 2.5 at 2nd frame then dash at 5th frame. Enemy stops at 6th frame.
From 'targetting' frame to 'dashing' frame there's 25 centisecond delay.
Now look at this one:


Code:
 anim attack1
        range   80 250
        rangez  -45 45
   delay   1
   offset   50 105
   bbox   30 20 70 80
   frame   data/chars/enemy/charge01.gif
   delay   10
   frame   data/chars/enemy/charge01.gif
   frame   data/chars/enemy/charge02.gif
        delay   5
        blast   75 30 50 80 25 1
   frame   data/chars/enemy/charge03.gif
        delay   65
        @cmd    target 5 2.5
        @cmd    dash
   frame   data/chars/enemy/charge03.gif
        delay   10
        @cmd    stop
   frame   data/chars/enemy/charge04.gif
        attack  0
   frame   data/chars/enemy/charge05.gif


  The animation is same except that targetting and dashing is done at same frame. In this case, there's practically no delay between targetting and dashing.

What's the point of this? in 1st attack, player can dodge or move away within 25 centiseconds delay but in 2nd one, there's practically no chance to dodge. IOW 2nd attack is more effective than the 1st one. This is assuming player is not blocking or in invicible state.

So does that mean we should avoid giving delays and always declare 'target' and action functions at same frame? No, no and no! This is what's called with intelligence control. Smart enemies have more effective attacks than stupid ones. With this setting, 3D attack effectiveness can be controlled and thus their intelligence. That's the important reason behind this seperation.

Conclusion

I hope you all can understand all above explanation. No need to hurry, reread if you need to. Take all time you need to grasp it.

Anyways, those are just some example of 3D attacks, there are more kinds of them in brawlers games. Most 3D attack in Crime Buster v2.2 are from above type. The rest are other kind of 3D attacks. One of them is shown below. Sweep shoot ala Capcom!

I could share how to make it but I'll do it later cause this thread is already long.
Finally, I'm hoping other mods would be using 3D attacks for enemies too. No need to do it like my mod though.

11111.png


I've just remember something about targetted shooting above. For semi targetted leap and targetted dash, it's accurate but for targetted shooting, it's accuracy depends on distance between fired projectile and shooter. That means if projectile is fired quite far from shooter, it might miss player although it's targetted.

This could be fixed by inserting the distance in the formula above.

-----------
originally posted by Bloodbane
 
Back
Top Bottom