Enemies are more than just obstacles to be avoided. Good enemies give the player a sense that there is some underlying artificial intelligence (AI). The enemies seem to know when you are near, can chase you around walls, and wander on their own. In this chapter we will create three creatures that will inhabit the world, each with their own unique AI.
The first creature will consist of two parts: the overdue BookPile and the Ghost Librarian that protects it. If the player approaches a BookPile, a Ghost will spawn and chase the player. If the player gets too far away from the Ghost, the Ghost will return to the BookPile that spawned it. If the player attacks the Ghost, it will disappear and respawn from the BookPile. If the player destroys the BookPile, the Ghost it spawned will be destroyed as well.
spr_BookPile
, and load Chapter 4/Sprites/BookPile.gif
with Remove Background checked.snd_GhostMoan
, and load Chapter 4/Sounds/GhostMoan.wav
. Click on OK.obj_BookPile
, and assign spr_BookPile
as the Sprite.scr_BookPile_Create
, and write the following code:myRange = 100; hasSpawned = false;
The first variable sets the value for how close the player needs to be to become active and the second variable is Boolean that will determine if this BookPile has spawned a Ghost or not.
scr_BookPile_Step
, which will be applied to a Step event and contain the following code:if (instance_exists(obj_Player)) { if (distance_to_object(obj_Player) < myRange && hasSpawned == false) { ghost = instance_create(x, y, obj_Ghost); ghost.myBooks = self.id; sound_play(snd_GhostMoan); hasSpawned = true; } }
The first line of the code is incredibly important. Here we are checking to see if the player exists before we do anything else. If the player does exist, we check if the distance to the player object is within the range, and whether this BookPile has spawned a Ghost yet. If the player is within range and hasn't spawned anything, we spawn a Ghost. We will also send the unique ID of this BookPile, using the self
variable, into the ghost so it knows where it came from. Next we play the Ghost moaning sound, making sure that it does not loop. Finally, we indicate that we have spawned a Ghost by changing the hasSpawned
variable to true
.
obj_Player_Attack
event with a new Script, scr_BookPile_Collision
, and write the following code:if (instance_exists(ghost)) { with (ghost) { instance_destroy(); } } instance_destroy();
Once again, we start by checking to see if a Ghost has spawned from this BookPile and is still in existence. If it is, we destroy that Ghost and then remove the BookPile itself. The BookPile is now complete and should look like the following screenshot:
spr_Ghost
and spr_Ghost_Spawn
, and load Chapter 4/Sprites/Ghost.gif
and Chapter 4/Sprites/Ghost_spawn.gif
, respectively.-50
so that the ghost will appear over most objects, but below the player attack object. There is nothing else we need to do, so click on OK.obj_Ghost
, and apply spr_Ghost_Spawn
as the Sprite. This will make the spawn animation the initial sprite, and then we will change it to the regular Ghost through code.scr_Ghost_Create
, as seen in the following code:mySpeed = 2; myRange = 150; myBooks = 0; isDissolving = false; image_speed = 0.3; alarm[0] = 6;
scr_Ghost_Alarm0
, that has the following line of code to change sprites:sprite_index = spr_Ghost;
We are now ready to start implementing some artificial intelligence. The Ghost is going to be the most basic enemy that will chase the player through the room, including passing through walls and other enemies, until the player gets out of range. At that point the Ghost will float back to the BookPile it came from.
scr_Ghost_Step
, and write the following code:if (instance_exists(obj_Player)) { targetDist = distance_to_object(obj_Player) if (targetDist < myRange) { move_towards_point(obj_Player.x, obj_Player.y, mySpeed); } }
After checking to ensure that the player is alive, we create a variable that will hold the distance from the Ghost to the player. The reason we have created a targetDist
variable is that we will be needing this information a few times and this will save us from having to recheck the distance each time we have an if
statement. We then compare the distance to the chase range and if the player is within range, we move towards the player. The move_towards_point
function calculates the direction and applies a velocity to the object in that direction.
Sandbox
up to the near top, so that it is the room immediately after the title screen. Open the Sandbox
room and place a couple of instances of obj_BookPile
around the edges as shown in the following screenshot:scr_Ghost_Step
, within the braces for the player existence check, add the following code:else if (targetDist > myRange && distance_to_point(myBooks.x, myBooks.y) > 4) { move_towards_point(myBooks.x, myBooks.y, mySpeed); }
First we check to see if the player is out of range and that the Ghost isn't near its own BookPile. Here we are using distance_to_point
, so that we are checking the origin of the BookPile rather than the edges of the collision area that distance_to_object
would look for. If this is all true, the Ghost will start moving back to its BookPile.
else if
statement:else { speed = 0; if (isDissolving == false) { myBooks.hasSpawned = false; sprite_index = spr_Ghost_Spawn; image_speed = -1; alarm[1] = 6; isDissolving = true; } }
Here we have a final else
statement that will execute if the player is out of range and the Ghost is near its BookPile. We start by stopping the speed of the Ghost. Then we check to see if it can dissolve. If so, we tell the BookPile that the Ghost can be spawned again, we change the sprite back to the spawn animation, and by setting the image_speed
to -1
it will play that animation in reverse. We also set another alarm, so that we can remove the Ghost from the world and deactivate the dissolve check.
Altogether the entire scr_Ghost_Step
should look like the following code:
if (instance_exists(obj_Player)) { targetDist = distance_to_object(obj_Player) if (targetDist < myRange) { move_towards_point(obj_Player.x, obj_Player.y, mySpeed); } else if (targetDist > myRange && distance_to_point(myBooks.x, myBooks.y) > 4) { move_towards_point(myBooks.x, myBooks.y, mySpeed); } else { speed = 0; if (isDissolving == false) { myBooks.hasSpawned = false; sprite_index = spr_Ghost_Spawn; image_speed = -1; alarm[1] = 6; isDissolving = true; } } }
scr_Ghost_Alarm1
, that is attached to an Alarm 1 event and has one line of code to remove the instance:instance_destroy();
The Ghost is almost complete. It spawns, chases the player, and returns to its BookPile, but what happens if it catches the player? With this Ghost we will want it to smash into the player, cause some damage, and then vanish in a puff of smoke. For this we will need to create a new asset for the dead Ghost.
spr_Ghost_Dead
, and load Chapter 4/Sprites/Ghost_Dead.gif
with Remove Background checked.obj_Ghost_Dead
, and apply the Sprite.scr_Ghost_Dead_AnimEnd
, write the following line of code and attach it to an Animation End event:instance_destroy();
The Animation End event will execute code when the last image of the Sprite is played. In this case, we have a poof of smoke animation that at the end will remove the object from the game.
obj_Ghost
and add an obj_Player event with a new script, scr_Ghost_Collision
, with the following code:health -= 5; myBooks.hasSpawned = false; instance_create(x, y, obj_Ghost_Dead); instance_destroy();
We start by removing five points of health, and then telling the Ghost's BookPile that it can be respawned. Next we create the Ghost death object which will hide the real Ghost when we remove it from the game. If everything is built correctly, it should look like the following screenshot:
One last thing, as the room is meant to be a sandbox for experimenting in and not part of the actual game, we should clean up the room to prepare for the next enemy.
Sandbox
room and remove all instances of the BookPiles.The next enemy we will create is a Brawl that will wander around the room. If the player gets too close to this enemy, the Brawl will become enraged by growing larger and moving faster, though it won't leave its path. Once the player is out of range, it will calm back down and shrink to its original size and speed. The player won't be able to kill this enemy, but the Brawl will damage the player if there is contact.
For the Brawl, we will be utilizing a path and we will need three sprites: one for the normal state, one for the transition of states, and another for the enraged state.
spr_Brawl_Small
, and load Chapter 4/Sprites/Brawl_Small.gif
with Remove Background checked. This is the Sprite for the normal state. Center the origin and click on OK.spr_Brawl_Large
, and load Chapter 4/Sprites/Brawl_Large.gif
with Remove Background checked. We need to center the origin, so that the Brawl will scale properly with this image. The enraged state is twice the size of the normal state.spr_Brawl_Change
and load Chapter 4/Sprites/Brawl_Change.gif
, still with Remove Background checked. Don't forget to center the origin.pth_Brawl_01
.8
.snd_Brawl
, and load Chapter 4/Sounds/Brawl.wav
.obj_Brawl
, and apply spr_Brawl_S
as the default Sprite.scr_Brawl_Create
.mySpeed = 2; canGrow = false; isBig = false; isAttacking = false; image_speed = 0.5; sound_play(snd_Brawl); sound_loop(snd_Brawl); path_start(pth_Brawl_01, mySpeed, 1, true);
The first variable sets the base speed of the Brawl. The next three variables are checks for the transformation and enraged states, and whether it has attacked. Next, we set the animation speed and then we play the Brawl sound, and in this case we want the sound to loop. Finally, we set the Brawl onto the path with a speed of two; when it hits the end of the path it will loop and most importantly, the path is set to absolute, which means it will run based as designed in the Path editor.
scr_Brawl_Step
and we will start by getting the movement working.image_angle = direction; if (isBig == true) { path_speed = mySpeed * 2; } else { path_speed = mySpeed; }
Here we start by rotating the Sprite itself to face in the proper direction. This will work, because we have the Sprite images facing to the right, which is the same as zero degrees. Next, we check to see if the Brawl is big or not. If the Brawl is the enraged version, we set the path speed to be the base speed times two. Otherwise, we set the speed to the default base speed.
if (instance_exists(obj_Player)) { if (distance_to_object(obj_Player) <= 200) { if (canGrow == false) { if (!collision_line(x, y, obj_Player.x, obj_Player.y, obj_Wall, false, true)) { sprite_index = spr_Brawl_Change; alarm[0] = 12; canGrow = true; } } } }
We start by making sure the player exists, and then we check to see if the player is within range. If the player is in range, we check to see if we have become enraged or not. If the Brawl hasn't grown yet, we use the collision_line
function to see if the Brawl can actually see the player or not. This function draws a line between two points, in this case the location of the Brawl and the player positions, and determines if an instance of an object, or a wall crosses that line. If the Brawl can see the player, we change the sprite to the transformation sprite, set an alarm so we can finalize the transformation, and indicate that the Brawl has grown.
scr_Brawl_Alarm0
for an Alarm 0 event with the code that will switch to the enraged sprite and indicate that the Brawl is now full size.sprite_index = spr_Brawl_Large; isBig = true;
scr_Brawl_Step
, add an else
statement on the distance check, which would be before the final brace and add the following code:else { if (canGrow == true) { sprite_index = spr_Brawl_Change; alarm[1] = 12; canGrow = false; } }
If the player is out of range, this else
statement will become active. We check to see if the Brawl is still enraged. If it is, we change the Sprite to the transformation, set a second alarm, and indicate that the Brawl is back to normal.
Here is the full scr_Brawl_Step
script:
image_angle = direction; if (isBig == true) { path_speed = mySpeed * 2; } else { path_speed = mySpeed; } if (instance_exists(obj_Player)) { if (distance_to_object(obj_Player) <= 200) { if (canGrow == false) { if (!collision_line(x, y, obj_Player.x, obj_Player.y, obj_Wall, false, true)) { sprite_index = spr_Brawl_Change; alarm[0] = 12; canGrow = true; } } } else { if (canGrow == true) { sprite_index = spr_Brawl_Change; alarm[1] = 12; canGrow = false; } } }
scr_Brawl_Alarm0
script, name it scr_Brawl_Alarm1
, and adjust the values as shown in the following code. Remember to add this as an Alarm 1 event.sprite_index = spr_Brawl_Small; isBig = false;
scr_Brawl_Collision
, for a obj_Player event with the following code:if (isAttacking == false) { health -= 10; alarm[2] = 60; isAttacking = true; }
If the player collides with the Brawl for the first time, we remove 10 points of health and set an alarm for two seconds that will allow the Brawl to attack again.
scr_Brawl_Alarm2
, that contains the following line of code:isAttacking = false;
The Brawl is now complete and functions as designed. If everything is implemented correctly, the object properties should look like the following screenshot:
obj_Brawl
from the Sandbox
room, so that we can start fresh for the final enemy.The final enemy we will create, the Coach, is going to be the most challenging opponent yet. This enemy will move all around the room, randomly going from trophy to trophy to make sure it is still there. If it sees the player, it will chase them and if it gets close enough, it will have a melee attack. If the player escapes, it will wait for a moment before returning to duty. The Coach has a body, so it will need to go around obstacles and even avoid other coaches. This also means that it can die if the player is able to attack it.
spr_Trophy
, and load Chapter 4/Sprites/Trophy.gif
with Remove Background checked.obj_Trophy
, and apply scr_Trophy
as its Sprite.scr_Trophy_Create
:image_speed = 0; image_index = 0;
Much like the player, we will need four sprites for the four directions this enemy will move in.
spr_Coach_WalkRight
, and load Chapter 4/Sprites/Coach_WalkRight.gif
with Remove Background checked.spr_Coach_LWalkLeft
, spr_Coach_WalkDown
, and spr_Coach_WalkUp
sprites.obj_Coach
, and apply spr_Coach_WalkRight
as its Sprite.We are going to be dynamically creating paths for this enemy, so that it can navigate to the trophies on its own. We also want it to avoid obstacles and other enemies. This isn't too difficult to achieve, but it is going to require a lot of setup on initialization.
scr_Coach_Create
, apply it to a Create event, and then we will start with some basic variables:mySpeed = 4; isChasing = false; isWaiting = false; isAvoiding = false; isAttacking = false; image_speed = 0.3;
Once again we start by setting the speed of the object. Then we have four variables representing the various states we will need to check, all set to false
. We also set the animation speed for the sprite.
Next we need to set up the pathing system which will utilize some of GameMaker's motion planning functions. The basic concept here is that we create a grid that covers the area we want to be able to move the enemy in. We then locate all the objects we want the enemy to avoid, such as walls, and mark those areas of the grid as forbidden. We can then assign a start and goal location in the free area and create a path between them while avoiding obstacles.
scr_Coach_Create
, add the following code to the end of the script:myPath = path_add(); myPathGrid= mp_grid_create(0, 0, room_width/32, room_height/32, 32, 32); mp_grid_add_instances(myPathGrid, obj_Wall, false);
The first thing needed is an empty path that we can use for all future paths. Next we create a grid that will set the dimensions of the pathing map. The mp_grid_create
attribute has parameters for where it's located in the world, how many grids in width and height, and the size of each grid cell. In this case, we start in the grid in the upper-left corner and cover the entire room in 32 pixel increments. Dividing the room dimensions by 32 means that this will work in any size room without having to adjust the code. Finally, we take all instances of the wall found in the room and add it to the grid as areas where pathing is not allowed.
nextLocation = irandom(instance_number(obj_Trophy)-1); target = instance_find(obj_Trophy, nextLocation); currentLocation = nextLocation;
We start by getting a rounded random number that is based on the amount of trophies in the room. Notice that we subtracted one from the number of trophies. We need to do this because in the following line of code, we are searching for a specific instance using the instance_find
function. This function is pulling from an array and the first item in an array always starts with a zero. Lastly, we have created a second variable for when we want to change destinations.
mp_grid_path(myPathGrid, myPath, x, y, target.x, target.y, false); path_start(myPath, mySpeed, 0, true);
Here we select the grid we created and the empty path, and have a new path created that goes from the Coach's position to the targeted location and will not go on diagonals. Then we set the Coach into motion and this time, when it hits the end of the path, it will come to a stop. The final value in the path_start
function sets the path to absolute, which we want in this case as the path is created dynamically.
Here is the entire scr_Coach_Create
script:
mySpeed = 4; isChasing = false; isWaiting = false; isAvoiding = false; isAttacking = false; image_speed = 0.3; myPath = path_add(); myPathGrid= mp_grid_create(0, 0, room_width/32, room_height/32, 32, 32); mp_grid_add_instances(myPathGrid, obj_Wall, false); nextLocation = irandom(instance_number(obj_Trophy)-1); target = instance_find(obj_Trophy, nextLocation); currentLocation = nextLocation; mp_grid_path(myPathGrid, myPath, x, y, target.x, target.y, false); path_start(myPath, mySpeed, 0, true);
obj_Coach
in the corners and three instances of obj_Trophy
as seen in the following screenshot:scr_Coach_Step
, apply it to a Step event and write the following code:if (direction > 45 && direction <= 135) { sprite_index = spr_Coach_WalkUp; } else if (direction > 135 && direction <= 225) { sprite_index = spr_Coach_WalkLeft; } else if (direction > 225 && direction <= 315) { sprite_index = spr_Coach_WalkDown; } else { sprite_index = spr_Coach_WalkRight; }
Here we are changing the Sprite based on the direction of the instance as it moves. We can do this here, because we are not allowing diagonal movement on the path.
targetDist = distance_to_object(obj_Player); if (targetDist < 150 && targetDist > 16) { canSee = collision_line(x, y, obj_Player.x, obj_Player.y, obj_Wall, false, false) if (canSee == noone) { path_end(); mp_potential_step(obj_Player.x, obj_Player.y, 4, all); isChasing = true; } }
Once again we are using a variable to hold the value of how far away the player is to save us some coding time and minimize function calls. If the player is within range and not within striking distance, we do a sightline check. The collision_line
function returns the ID of any wall instance that the line crosses. If it does not intersect with any wall instances, it will return a special variable called noone
. If the player is in sight, we end the path the Coach is following, and start moving towards the player. The mp_potential_step
function will make an object move in the desired direction while avoiding obstacles, and in this case we are avoiding all instances. Finally we indicate that the Coach is chasing the player.
else
statement to the sightline check with the following code:else if (canSee != noone && isChasing == true) { alarm[0] = 60; isWaiting = true; isChasing = false; }
This else
statement states that if the player cannot be seen and the Coach is chasing, it will set an alarm for finding a new destination, tell it to wait, and the chase is over.
scr_Coach_Alarm0
, and apply it to an Alarm 0 event. Write the following code in the script:while (nextLocation == currentLocation) { nextLocation = irandom(instance_number(obj_Trophy)-1); } target = instance_find(obj_Trophy, nextLocation); currentLocation = nextLocation; mp_grid_path(myPathGrid, myPath, x, y, target.x, target.y, false); path_start(myPath, mySpeed, 1, false); isWaiting = false;
We start with a while
loop checking to see if the next location is the same as the old location. This will ensure that the Coach always moves to another trophy. Just as we did in the initial setup, we select a new target and set the current location variable. We also create a Path and start moving on it, which means the Coach is no longer waiting.
spr_Coach_Attack
, with Chapter 4/Sprites/Coach_Attack.gif
loaded and Remove Background checked.-16
, Y: 24
and adjust the Bounding Box values to Left: 0
, Right: 24
, Top: 0
, and Bottom: 4
.obj_Coach_Attack
, apply the Sprite to it, and set Depth to -100
.scr_Coach_Attack_Create
, with code to control the animation speed, set an alarm to remove the instance, and a variable that we can turn on.image_speed = 0.3; alarm[0] = 6; isHit = false;
scr_Coach_Attack_Alarm0
, that removes the instance.instance_destroy();
scr_Coach_Attack_Collision
with the following code:if (isHit == false) { health -= 15; isHit = true; }
If this is the first collision, we remove a point of health and then deactivate this check.
scr_Coach_Step
and add the attack code as an else if
statement, after the last brace:else if (targetDist <= 16) { if (isAttacking == false) { swing = instance_create(x, y, obj_Coach_Attack); swing.image_angle = direction; alarm[1] = 90; isAttacking = true; } }
If the Coach is near the player and has not attacked yet, we create an instance of the Coach Attack. We then rotate the attack Sprite to face the same direction as the Coach. An alarm is set for three seconds to allow for a breather before this code can be run again.
scr_Coach_Alarm1
and turn off the attack.isAttacking = false;
The Coach is now only doing half of its job, chasing the player. We still need to add in the regular patrol duties. Currently, if the Coach doesn't see the player and it gets to the end of the path, it stops and does nothing again. It should only wait a few seconds and then move on to the next trophy.
scr_Coach_Step
and add an else
statement to the very end of the script with this code:else { if (isWaiting == false) { if (distance_to_object(target) <= 8) { alarm[0] = 60; path_end(); isWaiting = true; } } }
This else
statement means that the player is out of range. We then check to see if the Coach is waiting or not. If it isn't waiting, but is within eight pixels of its targeted trophy, we set the alarm for choosing a new destination for two seconds, end the path to stop movement, and state that we are now waiting.
if (isAvoiding == true) { mp_potential_step (target.x, target.y, 4, all); }
The first thing we need to do is do a variable check to see if the Coach needs to avoid something. If it does, we use the mp_potential_step
function which will move an instance towards a specified goal while attempting to avoid certain objects, or in this case, all instances.
if (distance_to_object(obj_Coach) <= 32 && isAvoiding == false) { path_end(); isAvoiding = true; } else if (distance_to_object(obj_Coach) > 32 && isAvoiding == true) { mp_grid_path(myPathGrid, myPath, x, y, target.x, target.y, false); path_start(myPath, mySpeed, 1, true); isAvoiding = false; }
First we check to see if an instance of the Coach is nearby and it hasn't tried to avoid it. If that is true then we take the Coach off of its path and start to avoid. We follow this with an else if
statement checking to see if we are far enough away from another Coach that we were trying to avoid. If so, we set a new path to the destination, start moving on it, and end the avoidance.
scr_Coach_Step
Script, write the following:if (place_meeting(x, y, obj_Coach)) { x = xprevious; y = yprevious; mp_potential_step(target.x, target.y, 4, all); }
This will check to see if two instances of the Coach are colliding with each other. If they are, we set the x
and y
coordinates to the special variables xprevious
and yprevious
, which represent the position of the instance in the previous step. Once they have taken a step back, we can then attempt to move around them again.
The Coach is now complete. To check to see if you have all the code for scr_Coach_Step
written correctly, here it is in its completed form:
if (direction > 45 && direction <= 135) { sprite_index = spr_Coach_WalkUp; } else if (direction > 135 && direction <= 225) { sprite_index = spr_Coach_WalkLeft; } else if (direction > 225 && direction <= 315) { sprite_index = spr_Coach_WalkDown; } else { sprite_index = spr_Coach_WalkRight; } targetDist = distance_to_object(obj_Player); if (targetDist < 150 && targetDist > 16) { canSee = collision_line(x, y, obj_Player.x, obj_Player.y, obj_Wall, false, false) if (canSee == noone) { path_end(); mp_potential_step(obj_Player.x, obj_Player.y, 4, all); isChasing = true; } else if (canSee != noone && isChasing == true) { alarm[0] = 60; isWaiting = true; isChasing = false; } } else if (targetDist <= 16) { if (isAttacking == false) { swing = instance_create(x, y, obj_Coach_Attack); swing.image_angle = direction; alarm[1] = 90; isAttacking = true; } } else { if (isWaiting == false) { if (distance_to_object(target) <= 8) { alarm[0] = 60; path_end(); isWaiting = true; } if (isAvoiding == true) { mp_potential_step(target.x, target.y, 4, all); } if (distance_to_object(obj_Coach) <= 32 && isAvoiding == false) { path_end(); isAvoiding = true; } else if (distance_to_object(obj_Coach) > 32 && isAvoiding == true) { mp_grid_path(myPathGrid, myPath, x, y, target.x, target.y, false); path_start(myPath, mySpeed, 1, true); isAvoiding = false; } } } if (place_meeting(x, y, obj_Coach)) { x = xprevious; y = yprevious; mp_potential_step(target.x, target.y, 4, all); }
3.133.133.233