Slight issues with patrolling enemy script - SOLVED!

Started by Akril15, Sun 17/03/2024 02:44:20

Previous topic - Next topic

Akril15

I've got a room where the player has to avoid a roaming enemy (which is usually offscreen) by staying in a "safe" region. If the player steps into an unsafe region, a timer starts, and if that timer reaches a certain value, the enemy makes a beeline for the player, and the player dies if the enemy reaches them, while the enemy keeps moving in a straight line until it's completely offscreen if the player dodges it.

CrimsonWizard provided me with a script that mostly works, however, it still has a couple of issues:

1) The enemy doesn't always attack when player is in the unsafe region; it just remains offscreen (I can sometimes "wake it up" by moving the player character into the safe region (region 5), then out again.
2) When the enemy is moving towards the player, sometimes it disappears before reaching the edge of the screen.

This is what my room's RepExec looks like:
Code: ags
function room_RepExec()
{
  
if (Region.GetAtRoomXY(player.x, player.y)==region[5] && AreCharactersColliding(KITE, EGO)==1) caught_by_kite(); //death script

if (Region.GetAtRoomXY(player.x, player.y)==region[5]) {
oDanger.Visible=true; //obj currently being used for testing
exposed_timer++;
}
else {
oDanger.Visible=false;
exposed_timer=0;
}

if (exposed_timer>100 && (cKite.x>1180 || cKite.y>750 || cKite.x<-220 || cKite.y<0)) { //kite swoops for player, continuing straight if player moves

int kite_dx = player.x - cKite.x; // x distance from enemy to player
int kite_dy = player.y - cKite.y; // y distance from enemy to player
float factor = IntToFloat(kite_dy) / IntToFloat(kite_dx); // the relation between dy and dx

   if (kite_dx > 0)
    kite_dx += Room.Width; // increase dx to some far away point to be certain
else
    kite_dx -= Room.Width;
kite_dy = FloatToInt(IntToFloat(kite_dx) * factor); // calc new dy, based on the new dx
int targetx = cKite.x + kite_dx;
int targety = cKite.y + kite_dy;
cKite.Walk(targetx, targety, eNoBlock, eAnywhere);
exposed_timer=0;
}

if (exposed_timer>100) exposed_timer=0;

if (circling==true && Region.GetAtRoomXY(player.x, player.y)!=region[5]) { //"patrolling" script - happens while char is in safe zone
  
  kite_circling++;
  
  if (kite_circling==200) {

  if (bird_r==false) bottom_to_right(); //moves enemy from (810, 900) to (1187, 207)
  else if (bird_l==false) top_to_left(); //(161, 100) to (-130, 381) 
  kite_circling=0;
  
  }
  
} //circling=true

}

FYI: My game's resolution is 1067x600.

Khris

#1
I'd change the target calculation first:

Code: ags
  float kite_dx = IntToFloat(player.x - cKite.x); // x distance from enemy to player
  float kite_dy = IntToFloat(player.y - cKite.y); // y distance from enemy to player
  float f = 1500.0 / Maths.Sqrt(kite_dx * kite_dx + kite_dy * kite_dy); // factor to extend vector to a length of 1500
  cKite.Walk(cKite.x + FloatToInt(kite_dx * f), cKite.y + FloatToInt(kite_dy * f), eNoBlock, eAnywhere);

This essentially eliminates the possibility of a division by zero error.

The next step is basic debugging: add Display() commands to your code to double check what actually runs and doesn't run, and when.

Akril15

Which part of the script should those lines replace? I'm guessing that the first two lines replace the int kite_dx and int kite_dy lines and the walk command replaces the existing one, but does the third line replace the float factor = IntToFloat(kite_dy) line, or is it simply added?

I tried doing this and got a "Cannot convert float to int" error at the if (kite_dx > 0) line.

Khris

#3
They replace lines 17-28 from your snippet. The error occurs because the existing code declares the variables as ints.
Btw, if the kite stops short of leaving the screen, just increase 1500.0 to something like 1800.0 or 2000.0

Akril15

Okay --- I replaced all those lines and temporarily removed the circling section (lines 34-46) to see how the new code worked.

Now the kite doesn't head straight towards the player -- it just moves across the screen about 100 pixels to the left of the player, with no way of intersecting the player unless the player tries to catch it. After several seconds, the game crashed with the error "Sqrt: cannot perform square root of negative number" at the float f = 1500.0 / Maths.Sqrt(kite_dx * kite_dx + kite_dy); line.

Khris

Right, I forgot to square the y value. Here's the fixed line:

Code: ags
  float f = 1500.0 / Maths.Sqrt(kite_dx * kite_dx + kite_dy * kite_dy);

Akril15

I used that new line, but the kite still doesn't head straight for the player. When the value was set to 1500, it moved about 300 pixels onscreen, then disappeared and never showed up again. When I increased the value to 1800 and 2000, the kite moved about 400 pixels onscreen, then froze in place and never moved again.

Khris

#7
There's another typo in there :P
They y coordinate of the walk command is supposed to use kite_dy instead of kite_dx.

Another issue is that if the kite moves too far outside the screen, it won't move far enough next time.
I already made a test game to troubleshoot this, so give me a few minutes to implement this properly.

Edit:
Here's my script, seems to work fine:

Code: ags
// room script file

enum KiteState {
  eKiteCircling, eKiteSwoopingIn, eKiteSwoopingOut
};

KiteState kite_state = eKiteCircling;
int exposed_timer;
int kite_circling_timer;

bool bird_l, bird_r;

void caught_by_kite() {
  Display("Caught by kite");
  QuitGame(0);
}

void bottom_to_right() {
}

void top_to_left() {
}

bool kite_is_outside_screen() {
  ViewFrame* vf = Game.GetViewFrame(cKite.NormalView, cKite.Loop, cKite.Frame);
  int hw = Game.SpriteWidth[vf.Graphic] / 2;
  int h = Game.SpriteHeight[vf.Graphic];
  if (cKite.x < -hw || cKite.x > Game.Camera.Width + hw) return true;
  return cKite.y < 0 || cKite.y > Game.Camera.Height + h;
}

function room_RepExec() {

  // is player in danger
  bool danger_zone = Region.GetAtRoomXY(player.x, player.y) == region[5];
  
  if (danger_zone && cEgo.IsCollidingWithChar(cKite)) caught_by_kite(); //death script

  oDanger.Visible = danger_zone; // obj currently being used for testing
  exposed_timer = (exposed_timer + 1) * danger_zone; // increase / reset timer

  if (exposed_timer > 100) {
    exposed_timer = 0;
    
    // kite swoops for player, continuing straight if player moves
    if (kite_state == eKiteCircling) { 
      float kite_dx = IntToFloat(player.x - cKite.x); // x distance from enemy to player
      float kite_dy = IntToFloat(player.y - cKite.y); // y distance from enemy to player
      float f = 5000.0 / Maths.Sqrt(kite_dx * kite_dx + kite_dy * kite_dy); // factor to extend vector to a length of 1500
      int targetx = cKite.x + FloatToInt(kite_dx * f);
      int targety = cKite.y + FloatToInt(kite_dy * f);
      cKite.Walk(targetx, targety, eNoBlock, eAnywhere);
      kite_state = eKiteSwoopingIn;
    }
  }
  
  // this makes the kite stop as soon as it has left the screen
  if (kite_state == eKiteSwoopingIn && !kite_is_outside_screen()) kite_state = eKiteSwoopingOut;
  if (kite_state == eKiteSwoopingOut && kite_is_outside_screen()) {
    cKite.StopMoving();
    kite_state = eKiteCircling;
  }

  if (kite_state == eKiteCircling && !danger_zone) { //"patrolling" script - happens while char is in safe zone

    kite_circling_timer++;

    if (kite_circling_timer == 200) {

      if (bird_r == false) bottom_to_right(); //moves enemy from (810, 900) to (1187, 207)
      else if (bird_l == false) top_to_left(); //(161, 100) to (-130, 381) 
      kite_circling_timer = 0;

    }

  } //circling=true

}

Akril15

Thank you! Your script works great except for a couple of minor details, which I think may have to do with the sizes of the enemy and player sprites (the enemy sprite is 500x500 pixels, while the player character's is just 100x100).

Sometimes the enemy isn't centered on the character, and just the outer edge of its sprite touches (and kills) the player. Other times it passes right through the character, and the death script doesn't always trigger when this happens. I've tried changing the order of the IsCollidingWithChar characters and using AreThingsColliding instead, but that didn't do much.

Khris

Yeah, I was debating whether to address this but didn't.

The thing is, you're already calculating the distance between the enemy and the player anyway. So you can use that instead to determine whether the player got caught.
Maybe the position of their respective feet isn't the best choice for the collision check?

However if that's the case, shifting the sprite down using like cKite.z = -200; might do the trick, although this requires adjusting the y coordinates in my off-screen check.

Code: ags
bool kite_is_outside_screen() {
  ViewFrame* vf = Game.GetViewFrame(cKite.NormalView, cKite.Loop, cKite.Frame);
  int hw = Game.SpriteWidth[vf.Graphic] / 2;
  int h = Game.SpriteHeight[vf.Graphic];
  if (cKite.x < -hw || cKite.x > Game.Camera.Width + hw) return true;
  return cKite.y < cKite.z || cKite.y > Game.Camera.Height + h + cKite.z; // fixed to include z coordinate
}

Akril15

Setting the kite's z coordinate to -100 seems to be almost perfect. Now the only issue is that it has a habit of coming at the player in a way that "breaks" its circling pattern -- i.e., if it comes at the player from the lower right and exits the screen at the upper right, if the player stays out in the open it should come at the player from the left side of the screen, but sometimes it will re-enter the screen from the right.

I'm guessing that this has to do with the way I've set up the script for the circling, as the bird doesn't actually move in a continuous path and just gets relocated from one side of the screen to the other. I tried replacing the bird_to_left and bird_to_right lines with some lines telling the bird to walk to various coordinates based on where it was (if (cKite.x>900 && cKite.y>300) cKite.Walk(500, -300, eNoBlock, eAnywhere); else if ...), but couldn't quite get it to work.

Khris

If you look at lines 60 and 61 in the second to last snippet, that's what runs once each time the kite has left the screen. You can reposition it there instead.
I'm not sure how exactly this is supposed to work though; you said "comes at the player from the lower right and exits the screen at the upper right" but I guess you meant "upper left"?

If it left the screen at the upper left, from where should it enter the screen the next time it attacks?

Akril15

Quote from: Khris on Fri 22/03/2024 08:05:58If you look at lines 60 and 61 in the second to last snippet, that's what runs once each time the kite has left the screen. You can reposition it there instead.
I'm not sure how exactly this is supposed to work though; you said "comes at the player from the lower right and exits the screen at the upper right" but I guess you meant "upper left"?
Yes, I should have said "upper left," sorry about that.

I tried replacing cKite.StopMoving(); and kite_state = eKiteCircling; with bottom_to_right(); and top_to_left();, and it might be a bit more accurate, but the kite now keeps freezing offscreen and never reappearing.

Khris

Sorry, I didn't mean for you to remove these crucial lines. What I meant is you can add lines in there to run additional code.

Akril15

Okay, I see. Unfortunately, the kite is still coming at the character from offscreen spots in its roaming pattern where it shouldn't be. I even tried making some additions to the script that make the kite move from the right to the top and the left to the bottom as well, but I'm still seeing the same issue.

Khris

#15
Just to be clear: bird, enemy and cKite are the same character? As in, the bottom_to_right function is supposed to reposition cKite?

Also, what exactly is cKite supposed to do after a failed attack? Visibly move to a specific spot (or spots, depending on its position)?

What exactly was this line from your original code supposed to do?
Code: ags
  if (bird_r==false) bottom_to_right(); //moves enemy from (810, 900) to (1187, 207)

If I understand you correctly:
If after a failed attack the kite ends up off-screen at the top, their next attack is supposed to come from the left?
And if they ended up on the bottom, from the right?

If so, all you need to do is (this replaces the existing block):
Code: ags
  if (kite_state == eKiteSwoopingOut && kite_is_outside_screen()) {
    cKite.StopMoving();
    if (cKite.y < cKite.z) {
      cKite.x = -200;
      cKite.y = 300;
    }
    else if (cKite.y > Game.Camera.Height) {
      cKite.x = 1250;
      cKite.y = 300;
    }
    kite_state = eKiteCircling;
  }

Akril15

Quote from: Khris on Tue 26/03/2024 20:50:49Just to be clear: bird, enemy and cKite are the same character? As in, the bottom_to_right function is supposed to reposition cKite?
Yes, they are.

QuoteAlso, what exactly is cKite supposed to do after a failed attack? Visibly move to a specific spot (or spots, depending on its position)?
The kite is supposed to continue offscreen on the same path it took in its attempt to reach the player.

QuoteWhat exactly was this line from your original code supposed to do?
Code: ags
  if (bird_r==false) bottom_to_right(); //moves enemy from (810, 900) to (1187, 207)

If I understand you correctly:
If after a failed attack the kite ends up off-screen at the top, their next attack is supposed to come from the left?
And if they ended up on the bottom, from the right?

If so, all you need to do is (this replaces the existing block):
Code: ags
  if (kite_state == eKiteSwoopingOut && kite_is_outside_screen()) {
    cKite.StopMoving();
    if (cKite.y < cKite.z) {
      cKite.x = -200;
      cKite.y = 300;
    }
    else if (cKite.y > Game.Camera.Height) {
      cKite.x = 1250;
      cKite.y = 300;
    }
    kite_state = eKiteCircling;
  }
Yes, that is what it's supposed to do. Unfortunately, even with the new code you provided, the same "pattern-breaking" problem keeps happening. I really appreciate all the help you've provided, but I'm starting to think that having the kite completely offscreen except when it's attacking might be the simplest solution here.

Khris

Quote from: Akril15 on Thu 28/03/2024 11:36:24but I'm starting to think that having the kite completely offscreen except when it's attacking might be the simplest solution here.

I assumed that's already the case though? My code is based on that very idea: the kite is never visible until it attacks, then it leaves the screen again. Is that not what's happening for you?

Also I'm still not entirely sure what the desired pattern actually is. How is it breaking?
It's not even that hard to implement a more complex behavior, but I'm still fumbling in the dark here.

Akril15

Quote from: Khris on Thu 28/03/2024 15:17:00
Quote from: Akril15 on Thu 28/03/2024 11:36:24but I'm starting to think that having the kite completely offscreen except when it's attacking might be the simplest solution here.

I assumed that's already the case though? My code is based on that very idea: the kite is never visible until it attacks, then it leaves the screen again. Is that not what's happening for you?
The top_to_right and bottom_to_right codes make the kite move across the top left and bottom right corners of the screen (to give the illusion that the kite is moving in a wide circle that's almost entirely offscreen) while the player is in a safe zone. It's meant to be onscreen then.

QuoteAlso I'm still not entirely sure what the desired pattern actually is. How is it breaking?
It's not even that hard to implement a more complex behavior, but I'm still fumbling in the dark here.
For example: if the kite comes onscreen at 3:00, misses the player and goes offscreen at 9:00, since it's mean to look like it's moving in a counterclockwise circle, if the player moves into a safe zone, the kite should then run the bottom_to_right script. Instead, the top_to_left script runs, so it looks like the kite teleported to a different spot offscreen instead of appearing to move in a circle. I attached a picture that I hope makes things clearer.

Akril15

Any tips on making the script work while incorporating the on-screen passes shown in the graphic?

SMF spam blocked by CleanTalk