GameMaker [Solved] Need help with a wall orbiting enemy

M

McFlyGold

Guest
I've been trying to create an enemy that travels along the walls of a room. Something similar to a "Zoomer" from Metroid. I was trying to do so with a state machine, but it hasn't been working out the way I've planned it to. So with a search on these forums, I found an almost perfect solution for my needs. https://forum.yoyogames.com/index.php?threads/need-example-wall-hugging-entity.2399/

The post with the solution is was made by @jo-thijs midway down the page. Here's the code for those not looking to download any files:

Create event:
Code:
spd = 1;
dir = 0;

fpixel = 1;

image_speed = 0.0625;
Step event:
Code:
var gridW = 16;
var gridH = 16;

var ldx = 0;
var rdx = 16;
var tdy = 0;
var bdy = 16;

var t = abs(spd);
while t > 0 {
    var d;
    switch (dir + (1 - sign(spd)) * 90) % 360 {
    case 0:
        d = min(t, floor((x - ldx) / gridW + 1) * gridW - (x - ldx), floor((x + rdx) / gridW + 1) * gridW - (x + rdx));
        break;
    case 90:
        d = min(t, (y - tdy) - ceil((y - tdy) / gridH - 1) * gridH, (y + bdy) - ceil((y + bdy) / gridH - 1) * gridH);
        break;
    case 180:
        d = min(t, (x - ldx) - ceil((x - ldx) / gridW - 1) * gridW, (x + rdx) - ceil((x + rdx) / gridW - 1) * gridW);
        break;
    case 270:
        d = min(t, floor((y - tdy) / gridH + 1) * gridH - (y - tdy), floor((y + bdy) / gridH + 1) * gridH - (y + bdy));
        break;
    }
    t -= d;
  
    var b = true;
    if place_meeting(floor(x + lengthdir_x(sign(spd), dir)), floor(y + lengthdir_y(sign(spd), dir)), obj_parent_wall) {
        dir = (dir + (2 - sign(spd)) * 90) mod 360;
        b = false;
    } else if fpixel <= 0 && !place_meeting(floor(x + dcos(dir - 90)), floor(y - dsin(dir - 90)), obj_parent_wall) {
        dir = (dir + (2 + sign(spd)) * 90) mod 360;
        fpixel = 1;
    }
    if b {
        x += lengthdir_x(d * sign(spd), dir);
        y += lengthdir_y(d * sign(spd), dir);
        fpixel -= d;
    }
}
Draw event:
Code:
draw_sprite_ext(sprite_index, -1, x + 8 + lengthdir_x(sqrt(2) * 8, dir + 135), y + 8 + lengthdir_y(sqrt(2) * 8, dir + 135), image_xscale, image_yscale, dir, image_blend, image_alpha);
I'm not even going to pretend that I understand everything that's going on, because I don't. The fact of the matter is that it works almost perfectly as I need it to. The only thing wrong with it is that I'd like to make it so that while the instance is on a wall, and the wall is destroyed, it continues moving in the direction it was moving until it hits another wall and then continues following along that wall. (I hope that makes sense.)

Right now, if the wall it's on is destroyed, it stays in place and jitters around until the wall is replaced. Then it continues on. But I'd like it so that if it's moving right and the wall is broken, it keeps moving right until it hits a new wall and then continues moving along.

Thanks for taking the time to read this. I truly do appreciate it!

Edit: I should add that in my project, I changed the "gridW" and "gridH" to 96 because that's the size of the walls that I'm working with. I also changed the "rdx" and "bdy" to the width and height of my enemy sprite's bounding box size. The code works great at that size too. :)
 
Last edited by a moderator:
M

McFlyGold

Guest
Just giving this a slight bump in case anyone might have any suggestions for me.
 

Phil Strahl

Member
Wow, now that's some unfriendly code (in terms of legibility)! And quite costly too, square root in the draw event, oh my! So it's no surprise you're not fully understand what's going on, neither do I. ;)

The course of action would be to find the line(s) where the wallhugger checks for the existence of walls, which happens somewhere in the if block on the bottom:
Code:
   var b = true;
   if place_meeting(floor(x + lengthdir_x(sign(spd), dir)), floor(y + lengthdir_y(sign(spd), dir)), obj_parent_wall) {
       dir = (dir + (2 - sign(spd)) * 90) mod 360;
       b = false;
   } else if fpixel <= 0 && !place_meeting(floor(x + dcos(dir - 90)), floor(y - dsin(dir - 90)), obj_parent_wall) {
       dir = (dir + (2 + sign(spd)) * 90) mod 360;
       fpixel = 1;
   }
   if b {
       x += lengthdir_x(d * sign(spd), dir);
       y += lengthdir_y(d * sign(spd), dir);
       fpixel -= d;
   }
I wish it had some comments. e.g. as what b is supposed to mean etc. My hunch is that once there's no wall object, no condition in this if-block is met, so I would test this, e.g. by inserting a show_debug_message there:

var b = true;
if place_meeting(floor(x + lengthdir_x(sign(spd), dir)), floor(y + lengthdir_y(sign(spd), dir)), obj_parent_wall) {
dir = (dir + (2 - sign(spd)) * 90) mod 360;
b = false;
} else if fpixel <= 0 && !place_meeting(floor(x + dcos(dir - 90)), floor(y - dsin(dir - 90)), obj_parent_wall) {
dir = (dir + (2 + sign(spd)) * 90) mod 360;
fpixel = 1;
}
else
{
show_debug_message("No condition met")
}

if b {
x += lengthdir_x(d * sign(spd), dir);
y += lengthdir_y(d * sign(spd), dir);
fpixel -= d;
}

If that turns out to be the culprit, you can add some logic there that just keeps the instance going at the last spd and dir or however the instance is moved. I hope it's as simple as that.
 
Last edited:

jo-thijs

Member
I've been trying to create an enemy that travels along the walls of a room. Something similar to a "Zoomer" from Metroid. I was trying to do so with a state machine, but it hasn't been working out the way I've planned it to. So with a search on these forums, I found an almost perfect solution for my needs. https://forum.yoyogames.com/index.php?threads/need-example-wall-hugging-entity.2399/

The post with the solution is was made by @jo-thijs midway down the page. Here's the code for those not looking to download any files:

Create event:
Code:
spd = 1;
dir = 0;

fpixel = 1;

image_speed = 0.0625;
Step event:
Code:
var gridW = 16;
var gridH = 16;

var ldx = 0;
var rdx = 16;
var tdy = 0;
var bdy = 16;

var t = abs(spd);
while t > 0 {
var d;
switch (dir + (1 - sign(spd)) * 90) % 360 {
case 0:
d = min(t, floor((x - ldx) / gridW + 1) * gridW - (x - ldx), floor((x + rdx) / gridW + 1) * gridW - (x + rdx));
break;
case 90:
d = min(t, (y - tdy) - ceil((y - tdy) / gridH - 1) * gridH, (y + bdy) - ceil((y + bdy) / gridH - 1) * gridH);
break;
case 180:
d = min(t, (x - ldx) - ceil((x - ldx) / gridW - 1) * gridW, (x + rdx) - ceil((x + rdx) / gridW - 1) * gridW);
break;
case 270:
d = min(t, floor((y - tdy) / gridH + 1) * gridH - (y - tdy), floor((y + bdy) / gridH + 1) * gridH - (y + bdy));
break;
}
t -= d;

var b = true;
if place_meeting(floor(x + lengthdir_x(sign(spd), dir)), floor(y + lengthdir_y(sign(spd), dir)), obj_parent_wall) {
dir = (dir + (2 - sign(spd)) * 90) mod 360;
b = false;
} else if fpixel <= 0 && !place_meeting(floor(x + dcos(dir - 90)), floor(y - dsin(dir - 90)), obj_parent_wall) {
dir = (dir + (2 + sign(spd)) * 90) mod 360;
fpixel = 1;
}
if b {
x += lengthdir_x(d * sign(spd), dir);
y += lengthdir_y(d * sign(spd), dir);
fpixel -= d;
}
}
Draw event:
Code:
draw_sprite_ext(sprite_index, -1, x + 8 + lengthdir_x(sqrt(2) * 8, dir + 135), y + 8 + lengthdir_y(sqrt(2) * 8, dir + 135), image_xscale, image_yscale, dir, image_blend, image_alpha);
I'm not even going to pretend that I understand everything that's going on, because I don't.
Then allow me to explain:
Create event:
Code:
spd = 1;
dir = 0;

fpixel = 1;

image_speed = 0.0625;
In this event, some variables are initialized.

The variable "spd" is a number representing the amount of pixels the instance (wall hugger) will move per step.
It accepts negative numbers, which indicate moving backwards.
It replaces the behavior of the built-in variable "speed".
This is necessary, because speed makes a single jump and could overshoot edges.

The variable "dir" is the direction (counter clockwise in degrees) towards which the instance is oriented.
It always corresponds to the rotation of the sprite.
If "spd" is positive, it corresponds to the moving direction of the instance as well.
It replaces the behavior of the built-in variables "direction" and "image_angle".
This was necessary because GameMaker hitboxes were janky when using image_angle.

The variable "fpixel" indicates fractional progress on movement within a single pixel.
It indicates how much the instance has yet to move to reach the next pixel.
A value of 0.25 would mean the instance still needs to move a quarter of a pixel to completely reach the next pixel.
It may also be a negative value, indicating a pixel has been passed.
It only serves to solve a bug in the program.

The built-in variable "image_speed" was used to slow down the animation of the instance.
GameMaker:studio 2 has added functionality that makes this superfluous however.

Step event:
Code:
var gridW = 16;
var gridH = 16;

var ldx = 0;
var rdx = 16;
var tdy = 0;
var bdy = 16;
This piece of code further initializes some variables that are supposed to stay constant and can thus be declared locally.

The variables "gridW" and "gridH" represent the width and height in pixels respectively of the tiles (e.g. walls) of the game.
With "tiles", I don't mean GameMaker:Studio 2's tile system, but the general concept of tiles in game design.

The variables "ldx", "rdx", "tdy" and "bdy" indicate left, right, top and bottom offset coordinates of the bounding box (I believe d stands for distance).
"ldx" is the horizontal offset, the x coordinate of the instance minus the x coordinate of its bounding box's left edge.
"rdx" is the complementary horizontal offset, the x coordinate of the instance's bounding box's right edge minus the x coordinate of the instance itself.
This means that "ldx+rdx" equals the bounding box width.
"tdy" is the vertical offset, the y coordinate of the instance minus the y coordinate of its bounding box's top edge.
"bdy" is the complementary vertical offset, the y coordinate of the instance's bounding box's bottom edge minus the y coordinate of the instance itself.
This means that "tdy+bdy" equals the bounding box height.

Code:
var t = abs(spd);
while t > 0 {
    ...
}
This loop performs the movement of the instance.
Its path is split up in multiple line segments.
Each iteration of the while loop calculates the next line segment of the movement path and performs a movement along that line segment.
The next line segment always starts at the current position of the instance and moves in the direction the instance is moving towards (taking the sign of "spd" into account).
The line segment ends at a point where an "event" occurs.
With "event" I don't mean a GameMaker event, but an artificial event.
The "events" we consider are bounding box edges crossing tiles.
The reason for this is as follows:
Suppose the instance is moving to the right.
When the right bounding box edge enters a new tile, that new tile may contain a wall.
In this case, we will need to respond by rotating the instance to move upwards along the wall.
When the left bounding box edge enters a new tile, there may be no floor below the instance anymore.
In this case, we will need to respond by rotating the instance to move downwards along the floor.
So, the "events" are the moments at which we will potentially have to change direction and start a new line segment entirely.

The variable "t" indicates how much distance the instance still has to move in the current step.
This gets updated after each iteration of the while-loop.
I believe the t stands for time.

Code:
    var d;
    switch (dir + (1 - sign(spd)) * 90) % 360 {
    case 0:
        d = min(t, floor((x - ldx) / gridW + 1) * gridW - (x - ldx), floor((x + rdx) / gridW + 1) * gridW - (x + rdx));
        break;
    case 90:
        d = min(t, (y - tdy) - ceil((y - tdy) / gridH - 1) * gridH, (y + bdy) - ceil((y + bdy) / gridH - 1) * gridH);
        break;
    case 180:
        d = min(t, (x - ldx) - ceil((x - ldx) / gridW - 1) * gridW, (x + rdx) - ceil((x + rdx) / gridW - 1) * gridW);
        break;
    case 270:
        d = min(t, floor((y - tdy) / gridH + 1) * gridH - (y - tdy), floor((y + bdy) / gridH + 1) * gridH - (y + bdy));
        break;
    }
This piece of code calculates the length of the next line segment, which is then stored in the variable "d".

The switch-expression:
Code:
(dir + (1 - sign(spd)) * 90) % 360
calculates the actual moving direction of the instance.
If "spd" is positive, it is just "dir".
Otherwise, it is 180 degrees more, but we need to take modulo 360 to keep the number in the range of 0-360.

If this results in 0, we are moving to the right.
If it results in 90, we are moving upwards.
If it is 180, we are moving to the left.
If it is 270, we are moving downwards.
Other values cannot be obtained.

In case 0 (movinf to the right), we calculate "d" as follows:
Code:
d = min(t, floor((x - ldx) / gridW + 1) * gridW - (x - ldx), floor((x + rdx) / gridW + 1) * gridW - (x + rdx));
This is the minimum of 3 expressions (the distance to whichever "event" occurs first among 3 "events"):
1)
Code:
t
The step ends.
"t" is the remaining distance we can move.

2)
Code:
floor((x - ldx) / gridW + 1) * gridW - (x - ldx)
The left bounding box edge crosses a tile.
The current position of the left bounding box edge is:
Code:
x - ldx
and the right bounding box edge of the first tile to the right of the instance is:
Code:
floor((x - ldx) / gridW + 1) * gridW
So the difference between the two is the distance to this event.

3)
Code:
floor((x + rdx) / gridW + 1) * gridW - (x + rdx)
The right bounding box edge crosses a tile.
This is analogous to the previous event.

The other moving direction cases (90, 180 and 270) work analogously.

Code:
 t -= d;
In this piece of code, we subtract the distance we're about to walk from the distance we have still to walk in this step.

Code:
    var b = true;
    if place_meeting(floor(x + lengthdir_x(sign(spd), dir)), floor(y + lengthdir_y(sign(spd), dir)), obj_parent_wall) {
        dir = (dir + (2 - sign(spd)) * 90) mod 360;
        b = false;
    } else if fpixel <= 0 && !place_meeting(floor(x + dcos(dir - 90)), floor(y - dsin(dir - 90)), obj_parent_wall) {
        dir = (dir + (2 + sign(spd)) * 90) mod 360;
        fpixel = 1;
    }
    if b {
        x += lengthdir_x(d * sign(spd), dir);
        y += lengthdir_y(d * sign(spd), dir);
        fpixel -= d;
    }
In this piece of code, we check for collisions with tiles to determin if we have to rotate as a rection to an "event".

The variable "b" holds whether the instance has not hit a wall (the b stands for boolean).
It's an artifact that could easily be removed by replacing the code with:
Code:
    if place_meeting(floor(x + lengthdir_x(sign(spd), dir)), floor(y + lengthdir_y(sign(spd), dir)), obj_parent_wall) {
        dir = (dir + (2 - sign(spd)) * 90) mod 360;
    } else {
        if fpixel <= 0 && !place_meeting(floor(x + dcos(dir - 90)), floor(y - dsin(dir - 90)), obj_parent_wall) {
            dir = (dir + (2 + sign(spd)) * 90) mod 360;
            fpixel = 1;
        }
        x += lengthdir_x(d * sign(spd), dir);
        y += lengthdir_y(d * sign(spd), dir);
        fpixel -= d;
    }
I'm not sure why I left "b" in there, but it might have been to easily add on additional collision checks.
It might have also been because I initially needed it and forgot to remove it afterwards.

The first if-statement checks if the instance just hit a wall
and if so, rotates so it can start crawling the wall.

The second if-statement checks if the instance is no longer on top of a wall (ran over a corner)
and if so, rotates so it turns around the corner.

Because GameMaker's collision system is integer based as opposed to floating point number based,
the following check in the second if-statement:
Code:
fpixel <= 0
prevents early turning around corner when "abs(spd) < 1",
which would otherwise result in the instance getting stuck on corners.

To understand this, consider moving to the left on a floor with "spd == 0.25", approaching the corner of a pit.
GameMaker rounds coordinates down, so when you're still for 0.75 pixels on floor, GameMaker won't detect this.
As a result, the instance will rotate to scale down the pit.
The instance then scales it down by 0.25 pixels.
However, this is not enough for GameMaker to detect that the instance is already on the wall it's scaling down.
As a result, the instance rotates once again, now returning back from where it came from with its head on the ground.
The instance moves 0.25 pixels back (to the right).
Because the instance is not on a wall anymore, it rotates again, now with its head on the pit wall.
The instance moves 0.25 pixels upwards.
Because the instance is not on a wall still, it rotates again.
It is now back in the same exact state it started at, so it just loops this process of being stuck on the corner.

"fpixel" solves this by requiring the instance to have moved at least 1 entire pixel since the previous event before deciding to rotate again around a corner.
This ensures the rounding error of GameMaker in collision checking is bridged over.

The code that is executed when "b" is true, just simply performs movement along the calculated line segment and updates "fpixel".

Draw event:
Code:
draw_sprite_ext(sprite_index, -1, x + 8 + lengthdir_x(sqrt(2) * 8, dir + 135), y + 8 + lengthdir_y(sqrt(2) * 8, dir + 135), image_xscale, image_yscale, dir, image_blend, image_alpha);
This draws the sprite based on the variables in the create event as opposed to the built-in variables.
(x+8,y+8) is the center of the sprite.
sqrt(2) * 8 is the size of half the diagonal of the sprite.

The fact of the matter is that it works almost perfectly as I need it to. The only thing wrong with it is that I'd like to make it so that while the instance is on a wall, and the wall is destroyed, it continues moving in the direction it was moving until it hits another wall and then continues following along that wall. (I hope that makes sense.)

Right now, if the wall it's on is destroyed, it stays in place and jitters around until the wall is replaced. Then it continues on. But I'd like it so that if it's moving right and the wall is broken, it keeps moving right until it hits a new wall and then continues moving along.

Thanks for taking the time to read this. I truly do appreciate it!
You will need to add some checks in the "no ground below instance event" to distinguish between crossing a corner and having the ground below it destroyed.

Change this line:
Code:
    } else if fpixel <= 0 && !place_meeting(floor(x + dcos(dir - 90)), floor(y - dsin(dir - 90)), obj_parent_wall) {
to:
Code:
    } else if fpixel <= 0
           && !place_meeting(floor(x + dcos(dir - 90)), floor(y - dsin(dir - 90)), obj_parent_wall)
           && place_meeting(floor(x + dcos(dir - 90) + dcos(dir) * sign(spd)), floor(y - dsin(dir - 90) - dsin(dir) * sign(spd)), obj_parent_wall) {
Edit: I should add that in my project, I changed the "gridW" and "gridH" to 96 because that's the size of the walls that I'm working with. I also changed the "rdx" and "bdy" to the width and height of my enemy sprite's bounding box size. The code works great at that size too. :)
That's correct usage of those variables.

Just giving this a slight bump in case anyone might have any suggestions for me.
You're actually expected to wait at least 48 hours before bumping your thread.
It's no problem that you did, but keep it in mind for in the future.

And quite costly too, square root in the draw event, oh my!
How so is having a square root in a draw event costly?
It's one of the fastest operations in modern computer architecture available and it only needs to be evaluated a couple of times per step.
A good compiler would also recognize that the square root is taken over a constant and just substitute the expression with the result at compile time (when squeezing performance).

The course of action would be to find the line(s) where the wallhugger checks for the existence of walls, which happens somewhere in the if block on the bottom:
Code:
var b = true;
if place_meeting(floor(x + lengthdir_x(sign(spd), dir)), floor(y + lengthdir_y(sign(spd), dir)), obj_parent_wall) {
dir = (dir + (2 - sign(spd)) * 90) mod 360;
b = false;
} else if fpixel <= 0 && !place_meeting(floor(x + dcos(dir - 90)), floor(y - dsin(dir - 90)), obj_parent_wall) {
dir = (dir + (2 + sign(spd)) * 90) mod 360;
fpixel = 1;
}
if b {
x += lengthdir_x(d * sign(spd), dir);
y += lengthdir_y(d * sign(spd), dir);
fpixel -= d;
}
I wish it had some comments. eg. as vat b is supposed to mean etc. My hunch is that once there's no wall object, no condition in this if-block is met, so I would test this, e.g. by inserting a show_debug_message there:

var b = true;
if place_meeting(floor(x + lengthdir_x(sign(spd), dir)), floor(y + lengthdir_y(sign(spd), dir)), obj_parent_wall) {
dir = (dir + (2 - sign(spd)) * 90) mod 360;
b = false;
} else if fpixel <= 0 && !place_meeting(floor(x + dcos(dir - 90)), floor(y - dsin(dir - 90)), obj_parent_wall) {
dir = (dir + (2 + sign(spd)) * 90) mod 360;
fpixel = 1;
}
else
{
show_debug_message("No condition met")
}

if b {
x += lengthdir_x(d * sign(spd), dir);
y += lengthdir_y(d * sign(spd), dir);
fpixel -= d;
}

If that turns out to be the culprit, you can add some logic there that just keeps the instance going in at the last spd and dir. I hope it's as simple as that.
Very close!
 

Phil Strahl

Member
Wow, now *that's* some solid documentation! Thumbs up!!

How so is having a square root in a draw event costly?
It's one of the fastest operations in modern computer architecture available and it only needs to be evaluated a couple of times per step.
A good compiler would also recognize that the square root is taken over a constant and just substitute the expression with the result at compile time (when squeezing performance).
You're absolutely right! I just saw sqrt() and looked not much further. My assumption that it was costly is based on my experience with older languages that took quite a hit when square roots and trigonometric functions where involved. I did a quick test and GMS2 handles them very efficiently it seems.
 
M

McFlyGold

Guest
@jo-thijs Firstly I just wanted to thank you for taking the time to go through your code and explain what it all means. I really do appreciate it! I got a chance to test the changes before leaving for work, but it seems as though it's not working. Now the object moves through the air, but if it's on an isolated block, it doesn't stick to it. If I replace the last "fpixel" line with the old one, it sticks. But the new one does not. Thanks again for taking the time to help me! Again, I really do appreciate it! :)
 

jo-thijs

Member
@jo-thijs Firstly I just wanted to thank you for taking the time to go through your code and explain what it all means. I really do appreciate it! I got a chance to test the changes before leaving for work, but it seems as though it's not working. Now the object moves through the air, but if it's on an isolated block, it doesn't stick to it. If I replace the last "fpixel" line with the old one, it sticks. But the new one does not. Thanks again for taking the time to help me! Again, I really do appreciate it! :)
You're right! I messed up the signs...
It should have been:
Code:
    } else if fpixel <= 0
           && !place_meeting(floor(x + dcos(dir - 90)), floor(y - dsin(dir - 90)), obj_parent_wall)
           && place_meeting(floor(x + dcos(dir - 90) - dcos(dir) * sign(spd)), floor(y - dsin(dir - 90) + dsin(dir) * sign(spd)), obj_parent_wall) {
checking if the instance was on a wall 1 pixel back, instead of 1 pixel further.
 
M

McFlyGold

Guest
You're right! I messed up the signs...
It should have been:
Code:
    } else if fpixel <= 0
           && !place_meeting(floor(x + dcos(dir - 90)), floor(y - dsin(dir - 90)), obj_parent_wall)
           && place_meeting(floor(x + dcos(dir - 90) - dcos(dir) * sign(spd)), floor(y - dsin(dir - 90) + dsin(dir) * sign(spd)), obj_parent_wall) {
checking if the instance was on a wall 1 pixel back, instead of 1 pixel further.
Thanks for the reply. I'll check it out tonight when I get home from work. I'll let you know how it turns out. Thanks again!
 
M

McFlyGold

Guest
@jo-thijs So I've finally had a chance to test it out and it works as long as I don't put the speed above 1.5. Anything above that and it misbehaves. I was going to describe the issues, but I thought it'd be better to just record a short video and show you.


The game is set to 60fps and the resolution is 1080p if that helps in any way. :)
Edit: The 1.6 ball just bounces back and forth along the top wall. I just cut the video because it took too long to wait for the bounce. ;)
 
Last edited by a moderator:

jo-thijs

Member
@jo-thijs So I've finally had a chance to test it out and it works as long as I don't put the speed above 1.5. Anything above that and it misbehaves. I was going to describe the issues, but I thought it'd be better to just record a short video and show you.


The game is set to 60fps and the resolution is 1080p if that helps in any way. :)
Edit: The 1.6 ball just bounces back and forth along the top wall. I just cut the video because it took too long to wait for the bounce. ;)
Hm, I'm not immediately sure what causes that.
However, I just reviewed my code and found some things that lead to imprecisions in coordinates, which can lead to bugs.
I fixed those and uploade the GMZ file of the updated project here:
http://www.mediafire.com/file/1kugzgc3okv2s6m/GMC_zoomer_example_2.gmz/file

Here's the new code for obj_zoomer:
Create event:
Code:
///Initialize variables

//  spd (short for speed)
//- indicates the amount of pixels this instance moves per step
//- it may also be a negative value, indicating moving backwards
//- this var replaces the built-in variable "speed"
//  to avoid teleporting too far and overshooting edges
//- this variable may be any (sufficiently small) real number
spd = 2.5;
//  dir (short for direction)
//- indicates the orientation of this instance
//- corresponds to the moving direction if "spd > 0"
//- is opposite of the moving direction if "spd < 0"
//- replaces the built-in variable "image_angle"
//  to avoid performing ransformations on the collision box
//  and obtaining 1-pixel-off results
//- diections are counter clockwise angles in degrees
//  with respect to the positive x-axis
//- this variable may be 0, 90, 180 or 270
dir = 0;

// animation speed
image_speed = 0.0625;
Step event:
Code:
/// Perform movement

// <<<< Initialize constant >>>>

//  gridW (short for grid width)
//- the width of a wall tile
//- may be any strictly positive natural number
var gridW = 16;
//  gridH (short for grid width)
//- the width of a wall tile
//- may be any strictly positive natural number
var gridH = 16;

//  ldx (short for left distance on x-axis)
//- indicates the horizontal distance between the x-coordinate of this instance
//  and the x-coordinate of the left edge of te hitbox of this instance
//  with respect to the wall tiles
//- in other words, the x-coordinate of this instance relative to its hitbox
//- may be any integer
var ldx = 0;
//  rdx (short for right distance on x-axis)
//- indicates the horizontal distance between the x-coordinate of this instance
//  and the x-coordinate of the right edge of te hitbox of this instance
//  with respect to the wall tiles
//- in other words, the width of the hitbox of this instance minus "ldx"
//- may be any integer
var rdx = 16;
//  tdy (short for top distance on y-axis)
//- indicates the vertical distance between the y-coordinate of this instance
//  and the y-coordinate of the top edge of te hitbox of this instance
//  with respect to the wall tiles
//- in other words, the y-coordinate of this instance relative to its hitbox
//- may be any integer
var tdy = 0;
//  bdy (short for bottom distance on y-axis)
//- indicates the vertical distance between the y-coordinate of this instance
//  and the y-coordinate of the bottom edge of te hitbox of this instance
//  with respect to the wall tiles
//- in other words, the height of the hitbox of this instance minus "tdy"
//- may be any integer
var bdy = 16;

// <<<< Main loop >>>>

//  tries
//- indicates the amount of times this instance is still allowed
//  to rotate in succession without moving in the meanwhile
//  in this step
//- this prevents an infinite loop when this instance gets stuck in a tight spot
//- this variable may be aany integer in the range [0,5],
//  but must be initialized to 4 or 5
var tries = 5;

// Set maximal precision for comparisons
var eps = math_get_epsilon();
math_set_epsilon(0);

//  t (short for time remaining)
//- indicates the amount of pixels this instance still has to move this step
var t = abs(spd);
// As long as this instance still has to move
while t > 0 {
    
    // <<<< Perform movement >>>>
    
    /*\
    | | The movement of this instance consists of several line segments.
    | | Points at the path where the direction of this instance may change are called "events".
    | | Events occur where one of the edges of this hitbox of this instance with respect to wall tiles
    | | enters a new wall tile (a collision may happen or the instance may have just crosed a corner)
    | | or when the current step ends.
    | | The first next line segment of the path starts at the current position of the instance
    | | and ends at the first next occuring event.
    \*/
    
    //  d (short for distance)
    //- indicates the length of the next line segment
    var d;
    // make a case distinction over the moving direction (dir, dir + 180 or dir - 180)
    switch (dir + (1 - sign(spd)) * 90) % 360 {
    case 0: // case moving right
        // let d be the shortest distance of 3 line segments:
        // 1) till the step ends: t
        // 2) till the left edge enters a new tile: floor((x - ldx) / gridW + 1) * gridW - (x - ldx)
        // 3) till the right edge enters a new tile: floor((x + rdx) / gridW + 1) * gridW - (x + rdx)
        d = min(t, floor((x - ldx) / gridW + 1) * gridW - (x - ldx), floor((x + rdx) / gridW + 1) * gridW - (x + rdx));
        break;
    case 90: // case moving up
        // let d be the shortest distance of 3 line segments:
        // 1) till the step ends: t
        // 2) till the top edge enters a new tile: (y - tdy) - ceil((y - tdy) / gridH - 1) * gridH
        // 3) till the bottom edge enters a new tile: (y + bdy) - ceil((y + bdy) / gridH - 1) * gridH
        d = min(t, (y - tdy) - ceil((y - tdy) / gridH - 1) * gridH, (y + bdy) - ceil((y + bdy) / gridH - 1) * gridH);
        break;
    case 180: // case moving left
        // let d be the shortest distance of 3 line segments:
        // 1) till the step ends: t
        // 2) till the left edge enters a new tile: (x - ldx) - ceil((x - ldx) / gridW - 1) * gridW
        // 3) till the right edge enters a new tile: (x + rdx) - ceil((x + rdx) / gridW - 1) * gridW
        d = min(t, (x - ldx) - ceil((x - ldx) / gridW - 1) * gridW, (x + rdx) - ceil((x + rdx) / gridW - 1) * gridW);
        break;
    case 270: // case moving down
        // let d be the shortest distance of 3 line segments:
        // 1) till the step ends: t
        // 2) till the top edge enters a new tile: floor((y - tdy) / gridH + 1) * gridH - (y - tdy)
        // 3) till the bottom edge enters a new tile: floor((y + bdy) / gridH + 1) * gridH - (y + bdy)
        d = min(t, floor((y - tdy) / gridH + 1) * gridH - (y - tdy), floor((y + bdy) / gridH + 1) * gridH - (y + bdy));
        break;
    }
    
    // <<<< Collision checks >>>>
    
    // if the instance just hit a wall
    if x == round(x) && y == round(y)
    && place_meeting(round(x + lengthdir_x(sign(spd), dir)), round(y + lengthdir_y(sign(spd), dir)), obj_parent_wall) {
        // rotate to scale the wall
        dir = (dir + (2 - sign(spd)) * 90) mod 360;
        // fix small imprecisions in coordinates (fixes bugs)
        x = round(x);
        y = round(y);
        // we just rotated without moving, so update the "tries" counter
        tries -= 1;
        // if we've been rotating for too long without moving
        if tries == 0 {
            // we detected an infinite loop!
            // end the loop prematurely
            t = 0;
        }
    } else {
        //  dx (short for difference in x-coordinates)
        //- the amount of pixels on the x-axis we move along the next line segment
        var dx = lengthdir_x(d * sign(spd), dir);
        //  dy (short for difference in y-coordinates)
        //- the amount of pixels on the y-axis we move along the next line segment
        var dy = lengthdir_y(d * sign(spd), dir);
        // if we don't move at all (this check prevents a bug where lengthdir results get rounded and near-infinite loops are created)
        if dx == 0 && dy == 0 {
            // fix small imprecisions in coordinates
            x = round(x);
            y = round(y);
        } else {
            // update the coordinates of this instance
            x += dx;
            y += dy;
        }
        // update the remaining distance "t" to move in this step
        t -= d;
        // we just moved, so we may reset the "tries" counter
        tries = 5;
        
        // if we cross a corner
        if x == round(x) && y == round(y)
        && !place_meeting(round(x + dcos(dir - 90)), round(y - dsin(dir - 90)), obj_parent_wall)
        && place_meeting(round(x + dcos(dir - 90) - lengthdir_x(sign(spd), dir)), round(y - dsin(dir - 90) - lengthdir_y(sign(spd), dir)), obj_parent_wall) {
            // rotate along with the coner
            dir = (dir + (2 + sign(spd)) * 90) mod 360;
        }
    }
}

math_set_epsilon(eps);
Draw event:
Code:
///Draw this instance

draw_sprite_ext(
        sprite_index, -1,
        x + 8 + lengthdir_x(sqrt(2) * 8, dir + 135), // rotate x around the center of the bounding box (with diagonal length sqrt(2) * 8)
        y + 8 + lengthdir_y(sqrt(2) * 8, dir + 135), // rotate y around the center of the bounding box (with diagonal length sqrt(2) * 8)
        image_xscale, image_yscale,
        dir, image_blend, image_alpha);
Does that do the trick?
 
M

McFlyGold

Guest
Hm, I'm not immediately sure what causes that.
However, I just reviewed my code and found some things that lead to imprecisions in coordinates, which can lead to bugs.
I fixed those and uploade the GMZ file of the updated project here:
http://www.mediafire.com/file/1kugzgc3okv2s6m/GMC_zoomer_example_2.gmz/file
.......................
Does that do the trick?
@jo-thijs
It seems like I'm still having issues with increasing the speed. I recorded another video showcasing what's happening. Originally I wasn't rotating the instances because I didn't need to, so I was just using "draw_self()" instead of your
"draw_sprite_ext" code, but I decided to try it out to see what it looked like and it made things look even stranger. I tried to capture the relevant information to show that as far as I know, my sprites shouldn't be causing the issues. Again I just want to thank you for trying to help me with this!!!

 

jo-thijs

Member
@jo-thijs
It seems like I'm still having issues with increasing the speed. I recorded another video showcasing what's happening. Originally I wasn't rotating the instances because I didn't need to, so I was just using "draw_self()" instead of your
"draw_sprite_ext" code, but I decided to try it out to see what it looked like and it made things look even stranger. I tried to capture the relevant information to show that as far as I know, my sprites shouldn't be causing the issues. Again I just want to thank you for trying to help me with this!!!

In your case, you should be using draw_self() instead indeed.

If you put the origin of your sprites in the center, you'll need to set ldx, rdx, tdy and bdy to 22 however.
 
M

McFlyGold

Guest
In your case, you should be using draw_self() instead indeed.

If you put the origin of your sprites in the center, you'll need to set ldx, rdx, tdy and bdy to 22 however.
Wow... so the first problem was probably my fault as well! I totally missed that I wasn't setting the hitboxes in the code correctly! I'm so sorry about that! I changed them and they're sticking perfectly, even at various speeds!
I tried to keep your "draw_sprite_ext" code just to test, but there's still an offset when it's on the right and bottom of the block. But otherwise, it works perfectly! Thank you again and again for the help!!! :D
 

jo-thijs

Member
Wow... so the first problem was probably my fault as well! I totally missed that I wasn't setting the hitboxes in the code correctly! I'm so sorry about that! I changed them and they're sticking perfectly, even at various speeds!
I tried to keep your "draw_sprite_ext" code just to test, but there's still an offset when it's on the right and bottom of the block. But otherwise, it works perfectly! Thank you again and again for the help!!! :D
You're welcome!

The draw event I posted was designed specifically for the zoomer sprite.
It doesn't apply to your sprite.
You can simply delete the draw event.
 
O

OccultOne

Guest
This is a bit of an older post, but would anyone happen to know how to convert this to support slopes? We have 1x1 and 2x1 slopes in the game I'm working on, I'm kind of at my wits end trying to get a decent Wall Orbit enemy.
 

TheouAegis

Member
} else if fpixel <= 0 && !place_meeting(floor(x + dcos(dir - 90)), floor(y - dsin(dir - 90)), obj_parent_wall) { dir = (dir + (2 + sign(spd)) * 90) mod 360; fpixel = 1; }
That line is causing the spinning. As soon as you change dir, check if there is still a block at the new dir. If there isn't, set dir back to the old value
 
4

404_not_found

Guest
oh my god, I have been working at programming a zoomer for weeks only now to realize the code necessary to make this kind of thing work is wayyyy over my head.šŸ™ƒ
 
Top