How to: Ship AI movement basics.

As said in my previous post I’ve been working on space games hobby wise for quite some time. This has lead to certain patterns of doing things that I have used for the past 15 years. Surprisingly these are the patterns I came up with very early in my journey. What I did for the next games was simply iterated to make them better, but still kept the core ideas.

One of the oldest tricks in my book is how to enemy ship AI movement easy, fast and cheap. This might be too basic to some, then you can leave a comment and I’ll go into detail on some fancyer code next time. But for all the rest…

Mikk’s Interactable Guide to how to do Ship/other vehicle movement AI:

This is a Interactable How To Tutorial. Scroll down to the end of the page to try what the various bits of code feel like in game.

I mainly work in Game Maker: Studio, so this will all be in GML. But GML should be very easily readable to most devs working on most languages. Just think of it as pseudocode if you’re working in C# for instance. All you’d have to do to use this somewhere else is recreate the few GM internal functions I use. I’ll add notes on what they do so this is easier.

Let’s start with the basics.

We have the player. We have an enemy. We want the enemy to attack the player. Obvious first timer first step is to make the enemy move towards the player. Probably also point towards the player when we are moving there, since that makes it look like it makes sense.

So the code we would constantly run inside the enemy ship (step event in GM:S, Update or similar for you non GM folk) would be:
direction = point_direction(x,y,o_player.x,o_player.y);
speed = 6;

Test this in Playable Test 1. (Scroll way down the page)

Set the movement direction to be towards the player. Then add an arbitrary speed so we start moving there. The enemy moves towards us and attacks. Makes sense.

For non GM folk: Internally in GM every frame the speed and direction get turned into a x,y movement vector which gets added to the enemy x,y coordinates. So we move speed amount of pixels in direction every frame automatically.

If you test this in the playable html5 at the end of this post you can see this makes for a very bad enemy AI though. It feels mechanical, in a bad way. Difficult, again in a bad way. If the AI speed is anywhere near decent it’s close to impossible to escape. The AI always heads straight towards you. It will not miss. It gets even worse though if we add more enemies with the same AI.

My favourite two lines of code.

There are two lines of code underneath pretty much all of my space ship AI. There are 10 completely different AI movement types in Voidship which all stem from using these two lines in different ways.

The previous way I demonstrated had one giant problem. The AI is boringly mechanical and never misses. We can solve this by accelerating towards the player instead of directly moving towards it. This boils down to some vector math, where we take the current x,y movement vector stemming from direction and speed and we add a new vector stemming from direction and acceleration to it n times a second/once a frame.

In GM:S luckily all of that boils down to the command motion_add(direction,acceleration);

This leads to us not directly ever touching speed and direction. We just slowly use vector addition to change it. Meaning the direction will lag behind the actual direction from enemy to player and the speed will slowly ramp up in a nice natural way. We have nothing to stop the speed from going too far though so we also have to add a speed limiter. The new code would be:

motion_add(point_direction(x,y,o_player.x,o_player.y),0.3);
if (speed>4) {speed=4;}

The 0.3 is how much we want to accelerate per frame. The 6 is our maximum speed of pixels per frame we want to limit the ship to.

Test this in Playable Test 2.

For non GM folk: As previously in GM the engine still automatically moves the enemy every frame according to a x,y movement vector which gets added to the enemy x,y coordinates. Just now we are no longer driving that vector with speed and direction. We are driving that vector by adding a x,y acceleration vector stemming from the acceleration and direction we used in motion_add();.

If you test the demo out you can see this is already a much more enjoyable enemy. It twirls like an enemy fighter around you. Dives in. Can sometimes miss you if you move fast enough at the right time etc.

What if we would add more enemies though. I’ll make them smaller so we can get a nice swarm going. Everything is still better than our first option, but the enemies clump together ontop of each other. Which is boring and unnatural.

Test this in Playable Test 3.

This is where my favourite combination of two lines of code comes in:

motion_add(point_direction(x,y,o_player.x,o_player.y),0.2);
motion_add(random(360),0.1);

What we are doing now is both moving the enemy in our nice accelerating way towards the player, but also giving them a slight random nudge in a random direction. These random nudges every frame add up. This means that no two enemies are ever going to have the exact same trajectory. If we keep the random acceleration small enough they won’t veer of trajectory too much, just enough that it looks interesting and natural.

Test this in Playable Test 4.

What happens is something which visually looks quite a lot like a flocking swarm. But we never actually orient ourselves according to the other enemies. It comes completely naturally. This is basically (with a few omissions) the AI for all fighters in voidship.

Simple beginnings.

This is a pretty good start and basically how all ships worked in my space game tests back in 2002. Although I had no idea of speed and direction, nor x,y vectors and had just discovered while playing some orbit cannon example game that if I moved the planet the cannon ball orbits then it looks like a fighter darting after its prey. The code I used back then was:

gravity_direction = point_direction(x,y,o_player.x,o_player.y);
gravity = 0.3;

It’s functionally identical to motion_add, but used GM’s inbuilt gravity feature.

Let’s improve on this a bit further.

Moving to about 2004-2006 and some first awkward teenage years of mine. I started noticing that it’s really not nice that the enemy ships literally dart into my ship like ramming frigates from homeworld cataclysm. Unless I’m actually making ramming frigates this won’t do (totally made those for Voidship btw).

So let’s add a simple if statement to only accelerate towards the player if they are far away. This means that if the enemies hit us not exactly directly on or we move they wont dart into us. They’ll take a wider arc and fly around.

Let’s also introduce friction (inbuilt GM:S variable which gets subtracted from speed every frame) to add some tighter curves and more natural movement. Not real space natural mind you, but natural for the environments humans and most players are used to.

if (point_distance(x,y,o_player.x,o_player.y) > 100)

{

motion_add(point_direction(x,y,o_player.x,o_player.y),0.3);
motion_add(random(360),0.1);

}

if (speed>6) {speed=6;}

friction = 0.1;

Test this in Playable Test 5.

If you look at the demo you can see this is pretty solid natural movement code. Both for big enemies and also fighters.

How do I turn this into a whole game though?

This is the most basic movement type I use. It’s fluid and natural and looks cool in big groups.

You can build on this and modify it into much more things though. For instance what if instead of only checking if the player is far enough to accelerate towards them we add other distance checking if clause’s to it. For instance we could if the player is between the distances of 100 to 200 pixels instead do: motion_add(point_direction(x,y,o_player.x,o_player.y)-90,0.3);

What this would do is create an enemy which accelerates towards the player. Then once they get near they start orbiting the player. Of course in motion since the player can also move and the enemy has inertia this can look a lot more complex and cooler than what I am writing.

We could also add timers which periodically reverse the direction we are acceleration towards. Meaning we get an enemy that strafes in for attack runs every 10 seconds and then retreats.

We could also take into account where your bullets are in releation to the enemy:

motion_add(point_direction(x,y,nearest_bullet.x,nearest_bullet.y)-90,0.3);

nearest_bullet in this case is pseudocode. But we can find it by using instance_nearest();

This would create an enemy which moves in and attacks you, but then strafes when shot at.

There are a lot more options to choose from. But pretty much all of them for me stem from creative uses of motion_add();

If people want I can go into more detail on what this can be built into in a later post.

Interactable HTML5 example

Keep in mind this example only demonstrates the basic code. The speed, acceleration, friction etc variables are all untweaked. Tweaking these to perfection will lead to much cooler gameplay.

(Also, just because people are asking. This is a demo made from scratch code wise in 60 minutes which I timed. This is not how Voidship plays. We are talking 60 minutes of dev time vs over a year here.)

 

Leave a Reply

Your email address will not be published. Required fields are marked *