Enemy Pathfinding(mp_grid) / State Machine Problems

Discussion in 'Programming' started by madorca, Apr 16, 2019.

  1. madorca

    madorca Member

    Joined:
    Jan 10, 2019
    Posts:
    4
    Hello everyone,

    I know this question asked many times before and yes I checked old posts and tried different solutions and approaches as much as I could but I'm still learning :/ (I started to GMS 4 months ago, pretty new).

    I will try to explain my problem.
    I'm trying to make a topdown shooter (a really bad imitation of nuclear throne).
    The player movement, collision, shooting etc works without a problem.
    But the enemy..

    The enemy uses state machine.
    I followed FriendlyCosmonaut's tutorial for that.
    (It has idle, wander, alert and attack state. It's using collision circle to check if the player is near or not and behave accordingly).
    But they were hugging the walls while chasing me. They needed some AI. So I searched for that and found that it is possible using grids / pathfinding.

    I decided to go for mp_grid. I somehow managed to implement the grid system and did the pathfinding for the enemy. So far so good.

    The problems that I'm facing are;
    1 - when the enemy enters it's "alert state" and starts to chase me, it creates the path towards me but its sprite is facing always to the right side.

    2 - In order to calculate the path, im using an alarm instead of the step event. While the enemy is chasing me, there isn't any problem changing its states, the problem starts when I exit its range. Then the enemy enters its "idle state" and start to slide along the path / till the end.

    I really appreciate if you can help me with this.
    Thank you^^

    Here are my codes.

    Code:
    ///// @description Enums
    mypath = path_add();
    alarm[1] = 0;
    
    depth = 2;
    counter = 0;
    spd        = .5;
    
    my_dir = irandom_range(0,359);
    moveX = lengthdir_x(spd, my_dir);
    moveY = lengthdir_y(spd, my_dir);
    
    //Enemy Health
    max_hp = 3;
    hp = max_hp;
    
    ////Enemy Hurt
    flash = 0;
    
    //Bullet cooldown
    bullet_cooldown = room_speed*2;
    alarm[0] = bullet_cooldown;
    
    //Enemy states
    enum estates
    {
        enemyidle,
        enemywander,
        enemyalert,
        enemyattack,
        enemystandattack,
        enemydie,
        enemyknockback,
    
    }
    
    state = estates.enemyidle;
    
    states_array[estates.enemyidle] = enemy_state_idle;
    states_array[estates.enemywander] = enemy_state_wander;
    states_array[estates.enemyalert] = enemy_state_alert;
    states_array[estates.enemyattack] = enemy_state_attack;
    states_array[estates.enemystandattack] = enemy_state_stand_attack;
    states_array[estates.enemydie] = enemy_state_die;
    

    Code:
    /// @description Create Grid
    var cell_width = 48;
    var cell_height = 48;
    
    var hcells = room_width div cell_width;
    var vcells = room_height div cell_height;
    
    global.grid = mp_grid_create(0, 0, hcells, vcells, cell_width, cell_height);
    
    //Add walls
    mp_grid_add_instances(global.grid, o_collision, false);
    

    Code:
    /// @description update path
    var tx, ty;
    tx = o_player.x
    ty = o_player.y
    
    if (mp_grid_path(global.grid, mypath, x, y, tx, ty, 1))
    {
        path_start(mypath, 2, path_action_stop, false);
        path_set_kind(mypath, 1); //Smooth path (kind = 1)
        path_set_precision(mypath, 8); //Even smoother path (precision = 8)
        alarm[1] = 30;
    }

    Code:
    //Behaviour
    alarm[1] = 0;
    
    counter += 1;
       
    //Transition Triggers
    if(counter >= room_speed * 3)
    {
        var change = choose (0, 1);
        switch(change)
        {
            case 0: state = estates.enemywander;
            case 1: counter = 0;
            break;
        }
    }
    if instance_exists(o_player) and (collision_circle(x, y, 250, o_player, false, false))
    {
        state = estates.enemyalert;
    }
    
    //Die state
    if (hp <= 0)
    {
        state = estates.enemydie;
    }
       
    //sprite
    sprite_index = s_Enemy_idle;
    
    

    Code:
    alarm[1] = 0;
    
    ////collision
    //enemy_collision();
    
    //Behaviour
    counter += 1;
    x += moveX;
    y += moveY;
    
    //Transition Triggers
    if(counter >= room_speed *3)
    {
        var change = choose(0 , 1);
        switch(change)
        {
            case 0: state = estates.enemyidle;
            case 1:
            my_dir = irandom_range(0, 359);
            moveX = lengthdir_x(spd, my_dir);
            moveY = lengthdir_y(spd, my_dir);
            counter = 0;
        }
    }
    if instance_exists(o_player) and (collision_circle(x, y, 250, o_player, false, false))
    {
        state = estates.enemyalert;
    }
    
    //Die state
    if (hp <= 0)
    {
        state = estates.enemydie;
    }
    
    //Sprite
    sprite_index = s_Enemy_wander;
    
    if(moveX != 0) image_xscale = sign(moveX);
    

    Code:
    if instance_exists(o_player)
    {
        alarm [1] = 1;
    
        ////collision
        //enemy_collision();
    
        //Transition Triggers
        if(!collision_circle(x,y, 250, o_player, false, false))
        {
            state = estates.enemyidle;
        }
        if(collision_circle(x, y, 180, o_player, false, false))
        {
            state= estates.enemyattack;
        }
       
        //Die state
        if (hp <= 0)
        {
            state = estates.enemydie;
        }
    
        //Sprite
        sprite_index = s_Enemy_alert
    }
    

    Code:
    if instance_exists(o_player)
    {
        alarm[1] = 1;
       
        ////collision
        //enemy_collision();
    
        //Enemy Shoot
        if alarm[0] <= 0
        {
            var bullet = instance_create_layer(x, y, "projectiles", oBullet_enemy);
            with (bullet)
            {
               direction = point_direction(x, y, o_player.x, o_player.y);
               speed = 3;
            }
            alarm[0] = room_speed * 2; // Enemy will shoot
        }
    
        //Transition Triggers
        if(image_index > image_number-1)
        {
            state = estates.enemyalert;
        }
    
            if(!collision_circle(x, y, 250, o_player, false, false))
            {
                state = estates.enemyidle;
            }
       
            if(collision_circle(x, y, 100, o_player, false, false))
            {
                state = estates.enemystandattack;
            }
    
        //Die state
        if (hp <= 0)
        {
            state = estates.enemydie;
        }
    
        //Sprite
        sprite_index = s_Enemy_attack;
    }
    
    
     
  2. vdweller

    vdweller Member

    Joined:
    Jun 24, 2016
    Posts:
    96
    Hi madorca,

    A useful way of compartmentalizing enemy states is to have their step event code run through a switch statement.

    Create event:
    Code:
    _phase=0; //idle
    Step event:
    Code:
    switch _phase {
        case 0: //idle
            (do stuff)
            break;
    
        case 1: //alert
            (do stuff)
            break;
    
        case 2: //follow player
            (do stuff)
            break;
    
        case 3: //attack player
            (do stuff)
            break;
    }
    
    This helps ensure that at any given time the enemy is in one and only one state, whereas in using alarms it is riskier (an alarm starts countdown for state 0, then for whatever reason enemy enters state 1, then the alarm for state 0 is finally triggered and messes things up and so on). Obviously, in each switch case, when certain conditions are met, you can use something like
    Code:
    if (condition1) then {
        _phase=2;
        exit;
    }
    if (condition2) then {
        _phase=3;
        exit;
    }
    
    So next step the wanted phase begins execution immediately.
    I used this method for all Gleaner Heights enemies and bosses and it works great!

    To help with question 1, I have found that one way to align a sprite to a direction is to use path_get_x() and path_get_y() using a position just a tiny bit down the path. Example:
    Code:
    image_angle=point_direction(path_get_x(path_index,path_position),path_get_y(path_index,path_position),path_get_x(path_index,path_position+0.01),path_get_y(path_index,path_position+0.01));
    Not sure if I remember this correctly but x,xprevious etc don't quite work as intended when an instance follows a path. For question 2, you obviously have to call path_end() when the enemy changes its state, ore else they will keep sliding along the path from the previous state.
     
    Last edited: Apr 17, 2019 at 8:20 PM
  3. madorca

    madorca Member

    Joined:
    Jan 10, 2019
    Posts:
    4
    Thank you for your response vdweller!

    I tried to implement the switch statements.
    It works, the enemy changing its states.

    But maybe there was a misunderstanding, I was using the Alarm for the pathfinding of the enemy.
    I'm still using it (In the beginning of the states), I don't know is there any other way, because using the pathfinding code in the step event slows down the game.

    I used the "image angle" code you posted but the enemy sprite is now rotating while following the player. (the origin is the middle center)
    it has to face right or face left depending of my position / or where it's moving.

    And for the other question, I don't know exactly where should I use the "path_end".
    I'm at work atm, I can't copy the code but I tried something like this but the enemy was still sliding along the path in it's idle "state".

    Code:
    if (condition2) then {
        _phase=3;
        path_end();
        exit;
    }
    I'm a little bit confused :(
    Thanks again^^
     
    Last edited: Apr 18, 2019 at 10:55 AM
  4. vdweller

    vdweller Member

    Joined:
    Jun 24, 2016
    Posts:
    96
    Haha I'm at work too, will take a look later!
     
  5. vdweller

    vdweller Member

    Joined:
    Jun 24, 2016
    Posts:
    96
    The thing with alarms is that they can be a bit harder to control. For example, say you have a global variable about the game being paused:

    global.game_paused=0;

    And you have some game logic in a step event. Now you can "pause" execution of the step event by writing, in the beginning of the event,

    if (global.game_paused) then exit;

    And, sure enough, when the game is paused, nothing in the step event is running.
    However if there is an alarm somewhere ticking, it will keep ticking! I'm not saying that using alarms is wrong, it's just that they might end up needing additional code to control them.

    Now for your sprite facing issue: I thought you had a top-down sprite which can rotate 0-360 degrees, but apparently you have a sprite that can either look "left" or "right", is that correct? I'm asking to understand how we're going to go about this.

    About ending the path: I started a dummy project only with this Step Event code:
    Code:
    switch _phase {
        case 0: //idle
            if (keyboard_check_pressed(vk_space)) then {
                with object0 path_start(path0,1,path_action_restart,1);
                _phase=1;
                exit;
            }
            break;
       
        case 1: //follow path
            if (keyboard_check_pressed(vk_space)) then {
                with object0 path_end();
                _phase=0;
                exit;
            }   
            break;
    }
    
    Sure enough, path movement stops.

    It is, indeed, unnecessary to have a path being calculated each step. A way to go is to have instance variable "timers". Example:
    Create Event:
    Code:
    _time_path=0;
    
    Step event, inside the Switch statement:
    Code:
    case 2: //follow player
        _time_path+=1;
        if (_time_path>=60) then {
            _time_path=0;
            mp_grid_path(etc etc)
        }
        break;
    
    The benefit of this approach is that this "custom" timer obeys code execution halting when the Step Event is skipped (like in the pausing example above). From what I can see in your code, the alarm event triggers, and then you set the alarm again, which will keep triggering regardless of the instance's state.
     
    Last edited: Apr 19, 2019 at 4:24 PM
  6. madorca

    madorca Member

    Joined:
    Jan 10, 2019
    Posts:
    4
    First of all thank you so much @vdweller for your time and interest^^

    That being said, yes that's correct, the enemy sprite can only look "left" or "right".

    for setting the timer in the step event, I have to test it when I go back home because I'm on a easter holiday for a couple of days^^
    I will let you know about the process.
    I think I got the idea but I'm not sure if I can manage to do this,we will see :)

    Thank you anyways^^
     
  7. vdweller

    vdweller Member

    Joined:
    Jun 24, 2016
    Posts:
    96
    Alright man let us know about it when you get back and we'll check about the sprite thing too. My baby daughter will be born on Tuesday (probably) so I may not be available for a few days but even then I'm sure that some of the good folks around here will be able to help!

    Enjoy your easter holidays!
     

Share This Page

  1. This site uses cookies to help personalise content, tailor your experience and to keep you logged in if you register.
    By continuing to use this site, you are consenting to our use of cookies.
    Dismiss Notice