Bringing enemies to life

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.

Summoning the Ghost Librarian

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.

  1. Let's start with the BookPile. Create a new Sprite, spr_BookPile, and load Chapter 4/Sprites/BookPile.gif with Remove Background checked.
  2. Center the origin and click on OK.
  3. We will also want a scary noise to alert the player of the danger. Create a new Sound, snd_GhostMoan, and load Chapter 4/Sounds/GhostMoan.wav. Click on OK.
  4. Create a new Object, obj_BookPile, and assign spr_BookPile as the Sprite.
  5. We don't want the player to be able to walk through the BookPile, so check the box for Solid.
  6. We will need to initialize some variables, so create a new Script, 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.

  7. Add a Create event and apply this script.
  8. Next we need a new Script, 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.

  9. The only element remaining is to add an 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:

    Summoning the Ghost Librarian
  10. Now we need to build the Ghost. For this we will need to bring in two sprites, one for the spawning and one for the chase. Create sprites with Remove Background checked for spr_Ghost and spr_Ghost_Spawn, and load Chapter 4/Sprites/Ghost.gif and Chapter 4/Sprites/Ghost_spawn.gif, respectively.
  11. In both the sprites, center the origin.
  12. Set the Depth: field to -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.
  13. Create a new Object, 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.
  14. We have several variables that we need to initialize in a new Script, scr_Ghost_Create, as seen in the following code:
    mySpeed = 2;
    myRange = 150;
    myBooks = 0;
    isDissolving = false;
    image_speed = 0.3; 
       alarm[0] = 6;
  15. We set variables for the movement speed, the range the Ghost will track within, who spawned the Ghost, (which we will change through the BookPile),and one for whether the Ghost has returned to the BookPile. Notice that the range of the Ghost is larger than the range of the BookPile. This will ensure that the Ghost starts chasing the player immediately. We then set the animation speed and set an alarm for six steps which we will use to change sprites.
  16. Add an Alarm0 event and then apply a new script, 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.

  1. We will start with chasing the player. Create a new script, 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.

  2. Add a Step event and apply this script. We will be continuing to add code to this script, but it will function properly already.
  3. Let's take a moment to test everything we have done so far. First, in the Resource tree, move 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:
    Summoning the Ghost Librarian
  4. Run the game. If you get too close to a BookPile, a single Ghost will spawn from it and it will slowly chase the player. If the player gets too far away from the Ghost, the Ghost will continue moving in the direction it was last heading in and will eventually go offscreen.
  5. Let's get the Ghost to return to its BookPile. In 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.

  6. Let's run the game again. As before, the Ghost will chase the player, and if the player gets too far away, the Ghost will return to its BookPile.
  7. There is an issue with the fact that the Ghost ends up ping-ponging over the top of the BookPile. This is due to the Ghost having velocity-based speed and not having any code telling it to stop. We can fix this by adding this code after the last 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;
            }
        }
    }
  8. One last Script is needed, 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.

  9. Create a new Sprite, spr_Ghost_Dead, and load Chapter 4/Sprites/Ghost_Dead.gif with Remove Background checked.
  10. Center the origin and click on OK.
  11. Create a new Object, obj_Ghost_Dead, and apply the Sprite.
  12. In a new Script, 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.

  13. All we need now is to reopen 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:

    Summoning the Ghost Librarian
  14. Run the game. The Ghost should now function exactly as designed. It will spawn and chase the player. If it catches the player it will cause damage and disappear. If the player gets away, the Ghost will return to its BookPile and dissolve out of existence. Great work!

    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.

  15. Open the Sandbox room and remove all instances of the BookPiles.

Building a wandering Brawl

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.

  1. Create a new Sprite, 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.
  2. Create another new Sprite, 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.
  3. We also need to undergo transition between these two states, so let's create a new Sprite, spr_Brawl_Change and load Chapter 4/Sprites/Brawl_Change.gif, still with Remove Background checked. Don't forget to center the origin.
  4. Next we need a path for the Brawl to follow. Create a new Path and name it pth_Brawl_01.
  5. We want the Brawl to move smoothly, so check Smooth Curve under Connection Kind and change the Precision to 8.
  6. To see what we can do with paths, let's make the Path in the shape of a figure eight as can be seen in the following screenshot:
    Building a wandering Brawl
  7. Let's also create a new Sound, snd_Brawl, and load Chapter 4/Sounds/Brawl.wav.
  8. Create a new Object, obj_Brawl, and apply spr_Brawl_S as the default Sprite.
  9. We'll start with initializing some variables in a Create event script, 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.

  10. We can now start working on the AI of the Brawl. Create a new script for a Step event named 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.

  11. Place an instance of the Brawl anywhere in the room and run the game. The Brawl should move around the figure eight and properly face in the proper direction.
  12. Next, we will add in the first transformation, becoming enraged. Right after the previous line of code, add:
    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.

  13. Let's create a script 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;
  14. Run the game to make sure that the code is working. The Brawl should remain small until it can clearly see the player, in which case it will then transform into the large, enraged Brawl.
  15. We have the Brawl growing larger, now we need to calm it down and have it shrink. Back in 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;
            }
        }
    }
  16. Duplicate the 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;
  17. Run the game and confirm that the Brawl grows larger and faster when the player is near and in sight, and returns to normal when out of range.
  18. The only thing we have left is the attack. Create a new Script, 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.

  19. To wrap up the Brawl, all we need is the final Alarm 2 event with a new Script, 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:

    Building a wandering Brawl
  20. Remove any instances of obj_Brawl from the Sandbox room, so that we can start fresh for the final enemy.

Creating the Coach

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.

  1. As this enemy is guarding something, we will start by creating the trophy. Create a new Sprite, spr_Trophy, and load Chapter 4/Sprites/Trophy.gif with Remove Background checked.
  2. Create a new Object, obj_Trophy, and apply scr_Trophy as its Sprite.
  3. As this is an animated sprite, we will want to add a Create event and have it not animate by writing the following code in a new Script, scr_Trophy_Create:
    image_speed = 0;
    image_index = 0;
  4. This is all we need for now for the trophy, so click on OK.

    Much like the player, we will need four sprites for the four directions this enemy will move in.

  5. Create a new Sprite, spr_Coach_WalkRight, and load Chapter 4/Sprites/Coach_WalkRight.gif with Remove Background checked.
  6. Center the origin, click on Modify Mask, and check Full image under Bounding Box.
  7. Repeat this process for spr_Coach_LWalkLeft, spr_Coach_WalkDown, and spr_Coach_WalkUp sprites.
  8. Create a new Object, 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.

  9. Create a new Script, 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.

  10. Still in 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.

  11. Now, we need to find a destination for the Coach to go. Continue adding the following code at the end of the script:
    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.

  12. All we have to do now is make a path and use it. Add the following code at the end of the script:
    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); 
  13. Open up Sandbox, and place two instances of obj_Coach in the corners and three instances of obj_Trophy as seen in the following screenshot:
    Creating the Coach
  14. Run the game. You should see the coaches randomly select a trophy and move towards it. Try and restart it a few times to see the different paths each Coach takes.
  15. With the basic setup complete, we can move on to the AI. We will start by switching the sprites based on the direction of movement. Create a new Script, 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.

  16. Next, we will get the Coach to watch for the player, and if spotted, they will leave their path in pursuit. Add the following code after the Sprite change code:
    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.

  17. This works well for starting the chase, but what if the player escapes? Let's have the Coach wait for a moment and then go back to patrolling. Add an 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.

  18. We have set an alarm, so let's create a new Script, 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.

  19. We have one last element we need to add to the chase sequence, the attack. If the Coach gets close enough to the player, it should melee attack the player. For this we need to first create a new Sprite, spr_Coach_Attack, with Chapter 4/Sprites/Coach_Attack.gif loaded and Remove Background checked.
  20. Just like the player's attack, set Origin to X: -16, Y: 24 and adjust the Bounding Box values to Left: 0, Right: 24, Top: 0, and Bottom: 4.
  21. Create a new Object, obj_Coach_Attack, apply the Sprite to it, and set Depth to -100.
  22. Add a Create event and apply a new Script, 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;
  23. Add an Alarm 0 event with a new Script, scr_Coach_Attack_Alarm0, that removes the instance.
    instance_destroy();
  24. Finally, add an obj_Player event, and apply a new Script, 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.

  25. We are done with the attack. Now to have it activated in the Coach, reopen 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.

  26. We need an Alarm 1 event to reset the attack, so create a new script, scr_Coach_Alarm1 and turn off the attack.
    isAttacking = false;
  27. Run the game. The Coach will now chase the player, and if it gets close enough to the player it will attack.

    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.

  28. Reopen 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.

  29. Run the game and you should see the coaches, when not chasing the player, stopping near a trophy, pausing for a moment, and then moving to another trophy.
  30. There is an issue, however, if both coaches go to the same trophy. Sometimes they will both overlap each other. Let's fix that by adding the following code after the distance check for the trophy:
    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.

  31. Now, we need to set up the condition for when avoidance should occur. Immediately after the last code is inserted, write the following:
     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.

  32. There is still one little issue remaining, which you can see if you run the game for a while. Sometimes two coaches will get too close together and they both stop. This is because they are trying to avoid each other, but are actually touching and can't let go. At the very end of the 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);
    }
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.133.233