sylvie_ln icon

The Sweet Kitty Motions (platformer push/carry movement)

sylvie_ln | PRO | 10/19/25 05:16:30 PM UTC (Edited) | 0 ⭐ | 15589 👁️ | Never ⏰ | []
Game Maker |

0 B

|

None

|

0 👍

/

0 👎

function move_init() {
  moveable = false;  // can be pushed or carried
  can_push = false;  // can push things
  can_carry = false; // can carry things
  carry_dist = {l:0, r:0} // how far the object has been carried left or right
  subpixel=[0,0]; 
  collision_at = function(xp,yp) {
    // can override for custom collision behaviour
    return !place_free(xp,yp);
  }
}
 
// call this in begin step, it should run every frame before calls to move
function move_bstep() {
  if moveable {
    carry_dist = {l:0,r:0}
  }
}
 
// internal function for resetting positions after a push fails
function _move_reset(reset) {
  if is_array(reset) {
    for(var i=0; i<array_length(reset); ++i) {
      var data = reset[i]; 
      if instance_exists(data.id) {
        data.id.x = data.x;
        data.id.y = data.y;
      }
    }
  }
}
 
function move(amount, axis) {
  // split the amount into pixel movement (target) and subpixel movement
  // we use ceil(n - 1/2) instead of round(n) so that round(#.5) always rounds down
  // ensures subpixel range is precisely -0.5 <= subpixel < 0.5
  // this is important for vertical carrying, if i remember right
  var target = ceil(subpixel[axis] + amount - 1/2);
  subpixel[axis] += amount - target;
 
  // shorthands:
  var xa = 1-axis;       // true if we're moving on the x-axis
  var ya = axis;         // true if we're moving on the y-axis
  var s = sign(target);  // direction of movement
 
  // temporaries:
  var souls = undefined; // holds other pushable/carriable objects we collide with
  var reset = undefined; // holds reset info, if we need to take back a sequence of moves
  var dist = 0;          // total pixel distance we have moved 
 
  // now move towards the target, one pixel at a time
  while target != 0 {
 
    // before moving, try to push objects out of the way
    if (xa and can_push)                              // pushing horizontally
    or ((ya and s < 0) and can_carry)                 // or carrying an object on top of us
    or ((ya and s > 0) and can_push and !moveable) {  // or pushing an object below us
      // clear the reset data
      reset = [];
      // we temporarily toggle our moveableness off
      // it's to avoid infinite loops (the thing we push tries to push us, etc.)
      var moveable_restore = moveable;
      moveable = false;
 
      // gather all souls in our path and push them
      souls = instance_place_array(x+xa*s,y+ya*s,oSoul);
      for(var i=0; i<array_length(souls); ++i) {
        var soul = souls[i];
        // if we can push the soul, try to push it
        if soul.moveable {
          // add the soul to the reset data, in case we need to revert the push
          array_push(reset,{id: soul.id, x:soul.x, y:soul.y});
          // now do the actual push
          // note that this might recursively push or carry other things!
          with soul {
            move(s,axis); 
          }
        }
      }
 
      // restore our original moveable status
      moveable = moveable_restore;
    }
 
    // now, check for a collision one step ahead
    if collision_at(x+xa*s,y+ya*s) {
      // we hit something, abort!
 
      // this first part is optional
      // you can do a collision callback for any souls you collided with
      souls = instance_place_array(x+xa*s,y+ya*s,oSoul);
      for(var i=0; i<array_length(souls); ++i) {
        var soul = souls[i];
        with soul {
          with other {
            // callback goes here
            // "self" is the soul that move was called on
            // "other" is the soul we collided with
            // on_collide(xa,ya,s); 
          }
        }
      }
 
      // this part is not optional
      // if we pushed things, but then were unable to move ourselves,
      // that means the push failed at some point
      // reset the positions of everything we pushed
      if can_push {
        _move_reset(reset); 
      }
 
      // this part comes down to personal preference
      // when you hit a wall there are various things you can do with subpixels
      // set to 0, set to bounds of subpixel range, do nothing etc.
      subpixel[axis] = 0
 
      // returning true indicates movement was successful
      // returning false indicates we hit something
      // you could customize this to return more complex data if you need it
      return false;  
    }
 
    // there is nothing in our way, so we're going to move
    // before moving, determine what needs to be carried
    // we only carry objects that are riding on top of us
    if can_carry {
      souls = instance_place_array(x,y-1,oSoul);
    }
 
    // finally, we actually move one pixel
    x += xa*s;
    y += ya*s;
    target -= s;
    dist++;
 
    // now that we've moved, carry things along with us
    if can_carry {
      // again we temporarily toggle our moveableness off 
      // to avoid infinite carry loops
      var moveable_restore = moveable;
      moveable = false;
 
      // finally, carrying time
      for(var i=0; i<array_length(souls); ++i) {
        var soul = souls[i];
        if soul.moveable {
          with soul {
            if xa { // if we're carrying to the left or right
              // we keep track of the maximum distance, per-step, the object has been carried
              // separate left and right distances are tracked, the count is reset in begin step
              // we only do the carry if it will increase the maximum distance in that direction
              // why? this is to avoid the "double carry problem":
              // imagine a soul is standing on top of two souls, both moving left
              // the soul on top gets carried 2px for every 1px the bottom ones move, this shouldn't happen
              // i don't have a mathematical proof this carry rule is correct or anything
              // but i think it worked well in my experiments
              // anyways
              if s < 0 {
                if dist > carry_dist.l {
                  carry_dist.l++;
                  move(s,axis);
                }
              } else {
                if dist > carry_dist.r {
                  carry_dist.r++;
                  move(s,axis);
                }
              }
            } else if ya { 
              // only triggers if we're carrying downwards (an upwards carry is a push)
              move(s,axis); 
            }
          }
        }
      }
      
      // restore moveable status
      moveable = moveable_restore;
    }
  }
 
  // we're out of the while loop which means we moved the full target distance without hitting anything
  return true;
}

Comments

  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎

    
        
  •  icon
    01/01/70 12:00:00 AM UTC
    Plain Text |

    0 B

    |

    👍

    /

    👎