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)
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:
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.
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:
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!