Thanks! It's not quite there, but I'm getting closer.
They're following the player now, but they can't navigate around solid objects. (And the sprites are a little wobbly for some reason)
Anything else I should try?
I was going to be nice and give a breakdown of the Balloon Fight code, because I haven't played Balloon Fight except once on the NES Megamix games. I took a look at the code, identified a handful of variables, then decided to postpone it for a later date because the coding style is a little too different from what I'm used to. It's fun code to glance through, though. So I'll just go over a couple of the basics.
Screen Wrapping
This is pretty basic, and you said you already have it, but I'll go over it briefly. The simplest way is to use the Outside Room event, although that has issues when moving diagonally or changing directions at the moment of wrapping. It also relies on internal mechanics, so I would personally say don't use the built-in events. The most common alternative is to just check when x is equal to 0 or equal to room_width, then set the coordinates respectively. A better method is to check if x is less than 0 or greater than room_width, then either add or subtract room_width. A "better yet" method, and I put it in quotes because it does cause a minuscule speed difference, is to just add the room_width to x every step and then get the modulo.
Code:
x = (x + room_width) mod room_width;
This method does the same thing as adding or subtracting room_width, but removes the conditionals. If you don't add room_width first, then the range of coordinates generated by GameMaker is -room_width to +room_width. If you add room_width first, the range of coordinates is 0 to +room_width, which should be consistent across all platforms.
Target Finding
As samspade already touched upon, the formula
target.x-x can be used to find the vector
toward the target. Conversely, the formula
x-target.x can be used to find the vector
away from the target. You can use
abs(target.x-x) to find the distance to the target, regardless of checking toward or away from the target. Subsequently, by using
sign(target.x-x), you can find the direction toward the target. Furthermore, using
sign(x-target.x), you can find the direction away from the target. In this case, -1 is to the left and +1 is to the right. Sometimes target.x will be equal to x, resulting in a vector of 0. A lot of people use the direction toward the target to set image_xscale, so if the vector is 0, the sprite is going to disappear. You can prevent this by prioritizing either direction by adding |1 to the formula.
Code:
image_xscale = sign(target.x - x | 1);
What the |1 does is set the parity of the result to odd. This means the result can never be 0, because 0 is not an odd number. This will prioritize facing right, which sometimes isn't ideal, so many people just simply ignore the vector if it is 0.
Target Finding With Screen Wrapping
The biggest issue with using target.x-x to find the vector toward the target is the direction of the vector can only be +1 if the target is to the right and -1 if the target is to the left. When you have screen wrapping, you might want the direction of the vector to be -1 if the target is too far to the right. One way to think of the solution is, since you can wrap instance positions around the screen, you can also wrap the distances between those instance positions around the screen. Just combine the two points above to determine which vector - to the left or to the right - is shorter and prioritize that one.
Code:
var A = (target.x - x);
var B = (x - target.x);
if (A + room_width) mod room_width) < (B + room_width) mod room_width
image_xscale = sign(A);
else
image_xscale = sign(B);