sylvie_ln

The Sweet Kitty Motions (platformer push/carry movement)

Oct 19th, 2025 (edited)
3,481
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. function move_init() {
  2.   moveable = false;  // can be pushed or carried
  3.   can_push = false;  // can push things
  4.   can_carry = false; // can carry things
  5.   carry_dist = {l:0, r:0} // how far the object has been carried left or right
  6.   subpixel=[0,0];
  7.   collision_at = function(xp,yp) {
  8.     // can override for custom collision behaviour
  9.     return !place_free(xp,yp);
  10.   }
  11. }
  12.  
  13. // call this in begin step, it should run every frame before calls to move
  14. function move_bstep() {
  15.   if moveable {
  16.     carry_dist = {l:0,r:0}
  17.   }
  18. }
  19.  
  20. // internal function for resetting positions after a push fails
  21. function _move_reset(reset) {
  22.   if is_array(reset) {
  23.     for(var i=0; i<array_length(reset); ++i) {
  24.       var data = reset[i];
  25.       if instance_exists(data.id) {
  26.         data.id.x = data.x;
  27.         data.id.y = data.y;
  28.       }
  29.     }
  30.   }
  31. }
  32.  
  33. function move(amount, axis) {
  34.   // split the amount into pixel movement (target) and subpixel movement
  35.   // we use ceil(n - 1/2) instead of round(n) so that round(#.5) always rounds down
  36.   // ensures subpixel range is precisely -0.5 <= subpixel < 0.5
  37.   // this is important for vertical carrying, if i remember right
  38.   var target = ceil(subpixel[axis] + amount - 1/2);
  39.   subpixel[axis] += amount - target;
  40.  
  41.   // shorthands:
  42.   var xa = 1-axis;       // true if we're moving on the x-axis
  43.   var ya = axis;         // true if we're moving on the y-axis
  44.   var s = sign(target);  // direction of movement
  45.  
  46.   // temporaries:
  47.   var souls = undefined; // holds other pushable/carriable objects we collide with
  48.   var reset = undefined; // holds reset info, if we need to take back a sequence of moves
  49.   var dist = 0;          // total pixel distance we have moved
  50.  
  51.   // now move towards the target, one pixel at a time
  52.   while target != 0 {
  53.  
  54.     // before moving, try to push objects out of the way
  55.     if (xa and can_push)                              // pushing horizontally
  56.     or ((ya and s < 0) and can_carry)                 // or carrying an object on top of us
  57.     or ((ya and s > 0) and can_push and !moveable) {  // or pushing an object below us
  58.       // clear the reset data
  59.       reset = [];
  60.       // we temporarily toggle our moveableness off
  61.       // it's to avoid infinite loops (the thing we push tries to push us, etc.)
  62.       var moveable_restore = moveable;
  63.       moveable = false;
  64.  
  65.       // gather all souls in our path and push them
  66.       souls = instance_place_array(x+xa*s,y+ya*s,oSoul);
  67.       for(var i=0; i<array_length(souls); ++i) {
  68.         var soul = souls[i];
  69.         // if we can push the soul, try to push it
  70.         if soul.moveable {
  71.           // add the soul to the reset data, in case we need to revert the push
  72.           array_push(reset,{id: soul.id, x:soul.x, y:soul.y});
  73.           // now do the actual push
  74.           // note that this might recursively push or carry other things!
  75.           with soul {
  76.             move(s,axis);
  77.           }
  78.         }
  79.       }
  80.  
  81.       // restore our original moveable status
  82.       moveable = moveable_restore;
  83.     }
  84.  
  85.     // now, check for a collision one step ahead
  86.     if collision_at(x+xa*s,y+ya*s) {
  87.       // we hit something, abort!
  88.  
  89.       // this first part is optional
  90.       // you can do a collision callback for any souls you collided with
  91.       souls = instance_place_array(x+xa*s,y+ya*s,oSoul);
  92.       for(var i=0; i<array_length(souls); ++i) {
  93.         var soul = souls[i];
  94.         with soul {
  95.           with other {
  96.             // callback goes here
  97.             // "self" is the soul that move was called on
  98.             // "other" is the soul we collided with
  99.             // on_collide(xa,ya,s);
  100.           }
  101.         }
  102.       }
  103.  
  104.       // this part is not optional
  105.       // if we pushed things, but then were unable to move ourselves,
  106.       // that means the push failed at some point
  107.       // reset the positions of everything we pushed
  108.       if can_push {
  109.         _move_reset(reset);
  110.       }
  111.  
  112.       // this part comes down to personal preference
  113.       // when you hit a wall there are various things you can do with subpixels
  114.       // set to 0, set to bounds of subpixel range, do nothing etc.
  115.       subpixel[axis] = 0
  116.  
  117.       // returning true indicates movement was successful
  118.       // returning false indicates we hit something
  119.       // you could customize this to return more complex data if you need it
  120.       return false;  
  121.     }
  122.  
  123.     // there is nothing in our way, so we're going to move
  124.     // before moving, determine what needs to be carried
  125.     // we only carry objects that are riding on top of us
  126.     if can_carry {
  127.       souls = instance_place_array(x,y-1,oSoul);
  128.     }
  129.  
  130.     // finally, we actually move one pixel
  131.     x += xa*s;
  132.     y += ya*s;
  133.     target -= s;
  134.     dist++;
  135.  
  136.     // now that we've moved, carry things along with us
  137.     if can_carry {
  138.       // again we temporarily toggle our moveableness off
  139.       // to avoid infinite carry loops
  140.       var moveable_restore = moveable;
  141.       moveable = false;
  142.  
  143.       // finally, carrying time
  144.       for(var i=0; i<array_length(souls); ++i) {
  145.         var soul = souls[i];
  146.         if soul.moveable {
  147.           with soul {
  148.             if xa { // if we're carrying to the left or right
  149.               // we keep track of the maximum distance, per-step, the object has been carried
  150.               // separate left and right distances are tracked, the count is reset in begin step
  151.               // we only do the carry if it will increase the maximum distance in that direction
  152.               // why? this is to avoid the "double carry problem":
  153.               // imagine a soul is standing on top of two souls, both moving left
  154.               // the soul on top gets carried 2px for every 1px the bottom ones move, this shouldn't happen
  155.               // i don't have a mathematical proof this carry rule is correct or anything
  156.               // but i think it worked well in my experiments
  157.               // anyways
  158.               if s < 0 {
  159.                 if dist > carry_dist.l {
  160.                   carry_dist.l++;
  161.                   move(s,axis);
  162.                 }
  163.               } else {
  164.                 if dist > carry_dist.r {
  165.                   carry_dist.r++;
  166.                   move(s,axis);
  167.                 }
  168.               }
  169.             } else if ya {
  170.               // only triggers if we're carrying downwards (an upwards carry is a push)
  171.               move(s,axis);
  172.             }
  173.           }
  174.         }
  175.       }
  176.      
  177.       // restore moveable status
  178.       moveable = moveable_restore;
  179.     }
  180.   }
  181.  
  182.   // we're out of the while loop which means we moved the full target distance without hitting anything
  183.   return true;
  184. }
Advertisement