• Hey Guest! Ever feel like entering a Game Jam, but the time limit is always too much pressure? We get it... You lead a hectic life and dedicating 3 whole days to make a game just doesn't work for you! So, why not enter the GMC SLOW JAM? Take your time! Kick back and make your game over 4 months! Interested? Then just click here!

SOLVED Weird stutter with grid-based movement.

SalTheThief

Member
Hey all,

I've been having this weird but subtle stutter with my grid based movement. I used a tutorial from @GMWolf (found here:
). I don't believe it to be animation based, but I could be wrong on that count. I've slowed my animation speed down a lot because it's harder to see when things are moving at full speed.



It seems as if the character is ever-so-slightly being warped back a bit. I believe this is caused by the walk_anim_length variable because if I change it back to 0.5 or even 0.4 I don't notice the stutter. However, when I change walk_anim_length to 0.1, the stutter becomes more apparent:


That's as far as I've been able to figure it out. Here is my relevant code:

GML:
//States for state machine
enum states {
    idle,
    walking,
    locked,
    running
}
state = states.idle;

//Movement based variables
x_pos = x div tile_width;
y_pos = y div tile_height;

x_from = x_pos;
y_from = y_pos;

x_to = x_pos;
y_to = y_pos;

walk_anim_length = 0.1;
walk_anim_time = 0;
inputMagnitude = 0;
timeWalk = 0;
moveFinished = true;
timeToMove = 0.6;
moveBuffer = 0;
facing = -1;

//Animation
image_speed = 0;
anim_speed = 0;
x_frame = 1;
y_frame = 0;


//Collision
var tile_layer = layer_get_id("Walls");
tile_map = layer_tilemap_get_id(tile_layer);

//Dialogue
portrait_index = 3;
voice = sndVoice1;
_name = "Player";
radius = 16;
active_textbox = noone;
can_move = true;
spotted = noone;
sprinting = false;

GML:
//Controls
    input_interact = keyboard_check_pressed(ord("E"));
    moveLeft =    keyboard_check(vk_left)  
    moveRight = keyboard_check(vk_right)
    moveUp =    keyboard_check(vk_up)      
    moveDown =    keyboard_check(vk_down)  
//Check for player direction
    inputDirection = point_direction(0,0,moveRight-moveLeft,moveDown-moveUp);
//Check for player input
    inputMagnitude = (moveRight - moveLeft != 0) || (moveDown - moveUp != 0);
   
    if(inputMagnitude != 0)
    {
        //moveBuffer exists so that a player can change direction without moving
        moveBuffer += 0.1;
    }
//Check for dialogue. If in a conversation, do not allow movement.
if (!instance_exists(objTextBox) && can_move == true)
{
    //Do not allow player to change directions if they are currently moving
    if(inputMagnitude != 0 && moveFinished == true)
    {
        switch(inputDirection)
        {
            case 0:
                    facing = directions.right;
                    if(moveBuffer > timeToMove) move(directions.right);
                    break;
            case 90:
                    facing = directions.up;
                    if(moveBuffer > timeToMove) move(directions.up);
                    break;      
            case 180:
                    facing = directions.left;
                    if(moveBuffer > timeToMove) move(directions.left);
                    break;
            case 270:
                    facing = directions.down;
                    if(moveBuffer > timeToMove) move(directions.down);
                    break;
        }
    }
}
//Code to apply movement to next tile
if (state == states.walking) {
    moveFinished = false;
    walk_anim_time += delta_time / 1000000;
    timeWalk = walk_anim_time / walk_anim_length;
//Reset variables so the code can be re-used to move to next tile
    if (timeWalk >= 1)
    {
        walk_anim_time = 0;
        timeWalk = 1;
        state = states.idle;
        moveFinished = true;
    }
    //Move from old tile to new tile
     var _x = lerp(x_from, x_to, timeWalk);
     var _y = lerp(y_from, y_to, timeWalk);
    //Only move in increments of 32.
    x = _x * tile_width;
    y = _y * tile_height;
   
}

//Set facing to -1 if player has finished moving and there is no input
if (state == states.idle && inputMagnitude == 0)
{
    facing = -1;
    //Reset moveBuffer so that a player can again change directions without moving
    moveBuffer = 0;
}

//Code used for room transitions to make sure that the player is facing the correct way when entering a new room
var transInst = instance_place(x, y, objTransition);
if(transInst != noone && facing == transInst.playerFacingBefore){
    with(objGame){
        if(!doTransition) {
            spawnRoom = transInst.targetRoom;
            spawnX = transInst.targetX;
            spawnY = transInst.targetY;
            spawnPlayerFacing = transInst.playerFacingAfter;
            doTransition = true;
        }
    }
}

//Textbox code. Used to interact with NPCs and world objects.
if(input_interact){
   
    if(active_textbox == noone){
   
    var inst = collision_rectangle(x-radius, y-radius, x+radius, y+radius, parNPC, false, false);
   
    if(inst != noone){
        with(inst){
            var tbox = CreateTextbox(text, speakers, next_line, scripts);
            can_move = false;
        }
        active_textbox = tbox;
        }
    }
}

GML:
/// @desc
var anim_length = 4;
var frame_size = 64;
var offset = 6;

switch(facing){
    case directions.right:    if(sprinting == false) y_frame = 7;
                            if        (x_from < x_to && state == states.walking && sprinting == false)        anim_speed = 8;
                            else if (x_from < x_to && state == states.walking && sprinting == true)            y_frame = 11;
                            break;
    case directions.left:    if(sprinting == false) y_frame = 6;
                            if        (x_from > x_to && state == states.walking && sprinting == false)        anim_speed = 8;
                            else if (x_from > x_to && state == states.walking && sprinting == true)            y_frame = 10;
                            break;
    case directions.up:        if(sprinting == false) y_frame = 5;
                            if        (y_from > y_to && state == states.walking && sprinting == false)        anim_speed = 8;
                            else if (y_from > y_to && state == states.walking && sprinting == true)            y_frame = 9;
                            break;
    case directions.down:    if(sprinting == false) y_frame = 4;
                            if        (y_from < y_to && state == states.walking && sprinting == false)        anim_speed = 8;
                            else if (y_from < y_to && state == states.walking && sprinting == true)            y_frame = 8;
                            break;
    case -1:                                                                                                 x_frame = 0;  
}

x_frame += anim_speed/60;
if(x_frame >= anim_length) x_frame = 1;
//if(x_frame + (anim_speed/60) < anim_length) { x_frame += anim_speed/60;}
//else                        { x_frame = 1;}

//Draw shadow
draw_sprite(sprShadow, 0, x+8, y+16)

//Draw base
draw_sprite_part(global.sprBase, 0, floor(x_frame)*frame_size, (floor(y_frame)*frame_size) + (y_frame * offset), frame_size, frame_size, x-16, y-16);

//Draw pants
draw_sprite_part(global.sprBot, 0, floor(x_frame)*frame_size, (floor(y_frame)*frame_size) + (y_frame * offset), frame_size, frame_size, x-16, y-16);

//Draw top
draw_sprite_part(global.sprTop, 0, floor(x_frame)*frame_size, (floor(y_frame)*frame_size) + (y_frame * offset), frame_size, frame_size, x-16, y-16);

//Draw accessory
draw_sprite_part(global.sprAcc, 0, floor(x_frame)*frame_size, (floor(y_frame)*frame_size) + (y_frame * offset), frame_size, frame_size, x-16, y-16)

//Draw hat/hair
draw_sprite_part(global.sprHat, 0, floor(x_frame)*frame_size, (floor(y_frame)*frame_size) + (y_frame * offset), frame_size, frame_size, x-16, y-16);

GML:
function move(inputDir)
{
    var dir = inputDir;
    var components = global.components[dir]
    var dx = components[0];
    var dy = components[1];
   
    if (state == states.idle) {
       
        if !(tilemap_get(tile_map, x_pos + dx, y_pos + dy)){
       
        x_from = x_pos;
        y_from = y_pos;
   
        x_to = x_pos + dx;
        y_to = y_pos + dy;
   
        x_pos = x_to;
        y_pos = y_to;
   
        state = states.walking;
        }
    }
}
 
Last edited:

GMWolf

aka fel666
What I think is happening here is that when we reset out walk_anim_time variable, we see it as a stutter as the player moves less than in the previous or next frame.


I think a good fix would be to only reset the walk_anim_time variable to 0, and clamp timeWalk to 1 if we want to stop moving.
Otherwise, we can substract walk_anim_length from walk_anim_time.

The idea is to not stop at an idle state, but continue on walking carrying over our walk_anim_length value.

Code:
if (state == states.walking) {
    moveFinished = false;
    walk_anim_time += delta_time / 1000000;
    timeWalk = walk_anim_time / walk_anim_length;
//Reset variables so the code can be re-used to move to next tile
    if (timeWalk >= 1)
    {
        if (inputMagnitude == 0)
       {
          walk_anim_time = 0;
          timeWalk = 1;
          state = states.idle;
          moveFinished = true;
        }
        else
        {
          walk_anim_time -= walk_anim_time;
          //Check you inputs and call move in the direction you want to keep moving in.
        }
    }
    //Move from old tile to new tile
     var _x = lerp(x_from, x_to, timeWalk);
     var _y = lerp(y_from, y_to, timeWalk);
    //Only move in increments of 32.
    x = _x * tile_width;
    y = _y * tile_height;
    
}
I wrote this on my phone in bed, so it might not be exactly right.
Hopefully this helps you figure it out.
 

SalTheThief

Member
Thanks for your reply!

What do you mean when you say//Check you inputs and call move in the direction you want to keep moving in.?

I've tried a few things but they all result in the player looping back to the beginning position of the tile until the key is released.

 

GMWolf

aka fel666
I mean do something similar to here:
Code:
switch(inputDirection)
        {
            case 0:
                    facing = directions.right;
                    if(moveBuffer > timeToMove) move(directions.right);
                    break;
            case 90:
                    facing = directions.up;
                    if(moveBuffer > timeToMove) move(directions.up);
                    break;       
            case 180:
                    facing = directions.left;
                    if(moveBuffer > timeToMove) move(directions.left);
                    break;
            case 270:
                    facing = directions.down;
                    if(moveBuffer > timeToMove) move(directions.down);
                    break;
        }
 

SalTheThief

Member
Forgive my persistence, but I can't seem to get that working either.

Here's the code I have. It still results in the above video where a tile move is never completed as long as the key is held down. If I let go of the key, it completes the move however.

GML:
if (timeWalk >= 1)
    {
       if (inputMagnitude == 0)
       {
        walk_anim_time = 0;
        timeWalk = 1;
        state = states.idle;
        moveFinished = true;
       }
       else
        {
        walk_anim_time -= walk_anim_length;
        switch(facing)
        {
            case directions.right:    move(directions.right); break;
            case directions.up:        move(directions.up); break;
            case directions.left:    move(directions.left); break;
            case directions.down:    move(directions.down); break;
        }
        }
    }
    //Move from old tile to new tile
     var _x = lerp(x_from, x_to, timeWalk);
     var _y = lerp(y_from, y_to, timeWalk);
    //Only move in increments of 32.
    x = _x * tile_width;
    y = _y * tile_height;
    
}
 

GMWolf

aka fel666
Ah of course, move checkes whether state is idle or not.
A quick fix may be to set state to idle in that case:
GML:
if (timeWalk >= 1)
    {
       if (inputMagnitude == 0)
       {
        walk_anim_time = 0;
        timeWalk = 1;
        state = states.idle;
        moveFinished = true;
       }
       else
        {
        walk_anim_time -= walk_anim_length;
        state = states.idle; // Here so move() will trigger correctly.
        switch(facing)
        {
            case directions.right:    move(directions.right); break;
            case directions.up:        move(directions.up); break;
            case directions.left:    move(directions.left); break;
            case directions.down:    move(directions.down); break;
        }
        }
    }
    //Move from old tile to new tile
     var _x = lerp(x_from, x_to, timeWalk);
     var _y = lerp(y_from, y_to, timeWalk);
    //Only move in increments of 32.
    x = _x * tile_width;
    y = _y * tile_height;
 
}
something to that effect. Not super clean, but definitely cleanable.

Another thing: you currently switch on switch(facing) but i think this will mean you cannot change directions untill you stop. I think it should be switch(inputDirection)
 

GMWolf

aka fel666
Arg of course!
Now that we also setup the next grid step, we also need to reset our Interpolation value.


When you do walk_anim_time -= walk_anim_length, also do timeWalk -= 1;

That's *should* fix it.

If the weird stuttering you had originally is still there... I'm stumped 😅

Also, I apologize for not trying these solutions out myself, in sure it would have been a lot less back and forth if I did.
 

SalTheThief

Member
BEAUTIFUL! It works exactly as it should. No stuttering to be found!

If the weird stuttering you had originally is still there... I'm stumped 😅
I have nothing but appreciation for you. You helped me figure out what I had no chance of doing on my own. Thank you so much!
 
Top