How to... ice and conveyor belt tiles for sliding puzzles

You know the sort - when you step onto ice you keep sliding in that direction until you hit a wall or reach less slippy ground, or a conveyor belt carries you similarly in its direction of travel. Tried-and-tested puzzles in top-down tile-based games, the kind Pulp was made for! As it happens, Pulp makes these nice and simple to implement too. I thought I would make a quick tutorial as these can be added directly to a standard Pulp setup to expand your gameplay options!

Let's start with an ice tile. Here is what we are aiming for:

pulp_ice_slide_demo

Notice how the direction keys are only pressed once when moving onto the (badly drawn) ice, and then the player keeps sliding in that direction until they hit a wall or land on the regular ground.

Naively you might think "ok, this is a kind of floor, I should create a world tile". Certainly you could do this, but Pulp provides four kinds of tile, each with their own behaviour, and world tiles are not best suited for what we want to do! As a refresher:

  • Player tiles cannot be placed in rooms. Let's not worry about them!
  • World tiles are the basic building blocks of rooms. They can be solid or non-solid. They cannot be scripted - their default, and only, behaviour is to do nothing!
  • Sprite tiles are always solid and can be scripted. Their default behaviour is to "say" something. Bumping into a sprite tile calls its interact event.
  • Items tiles are always non-solid and can be scripted. Their default behaviour is to increment a counter and swap to the default non-solid world tile. Moving onto an item tile calls its collect event.

We want to create an ice tile. That is, a non-solid tile with some scripted behaviour to continue moving (or "slide") the player in their direction of travel. We could use a world tile and implement sliding in the player script, maybe in the update event handler, but that would mean checking what tile we are on every time we move and potentially some messy code if we are doing that for lots of different tiles. What we really want is an item tile! This will keep all of our sliding code neatly contained in the tile it relates to.

Let's create ourselves a new ice item and place it in a room. If we do nothing else it is going to have the default item behaviour - walking over it will add one to the ices variable and swap the tile with "white" (if that's your default non-solid world tile). Not very helpful! Let's override that default behaviour and begin scripting:

on collect do
  // Our code will go here
end

Remember that collect is called by Pulp on an item tile when the player moves onto it. Try this out and now you'll see the default behaviour no longer happens! The ice tile now acts like a non-solid world tile and the player can just move over it.

Now let's add the sliding behaviour. When the player moves onto the ice tile we need to know what direction they are moving, and from that work out which tile they should slide into next.

Pulp already makes available what direction the player last moved in the collect event through event.dx and event.dy - these tell us if their movement was forwards (+1), backwards (-1), or none (0) on the x and y axes respectively.

Pulp also makes available the player's coordinates as event.px and event.py. All we need to do is add the player's direction to their current coordinates to get the coordinates of the tile into which they should slide and call goto to move them there:

on collect do
  x = event.px
  y = event.py
  x += event.dx
  y += event.dy

  goto x,y
end

And that's it!

...well, not quite. Try it out with a single ice tile in the middle of an empty room and it just about works, but it looks like the player teleports instantly to the end rather than sliding. We can fix that by adding a wait before calling the goto:

on collect do
  x = event.px
  y = event.py
  x += event.dx
  y += event.dy

  wait 0.2 then
    goto x,y
  end
end

That tells Pulp to wait 0.2 seconds before calling the goto, which is a nice-ish sort of delay. But hang on, what if the player tries moving again after moving onto the ice but before the wait ends? Really we want to stop the player from doing anything until they stop sliding, so lets add an ignore and listen to tell Pulp we don't want any player input while we are sliding:

on collect do
  x = event.px
  y = event.py
  x += event.dx
  y += event.dy

  ignore
  wait 0.2 then
    listen
    goto x,y
  end
end

Phew! That's not our only problem though. Try placing two or more ice tiles in a row and sliding across them - you only slide over the first! This is because the collect event is only being triggered when the player manually moves onto an ice tile, not when they are sent there with goto. We can easily fix this by making sure to call collect on the target tile when we slide onto it:

on collect do
  x = event.px
  y = event.py
  x += event.dx
  y += event.dy

  ignore
  wait 0.2 then
    listen
    goto x,y
    tell x,y to
      call "collect"
    end
  end
end

Now you can slide over multiple ice tiles in a row, which is great! Each tile handles its own sliding onto the next, which is nice and neat.

There is still one more problem though - if you slide into a solid tile, like a wall or sprite, the player moves onto it rather than being stopped! That's because we are always calling goto without checking if we really should be able to keep sliding in that direction. To fix that, lets wrap our sliding code inside an if block that checks the tile is not solid before moving the player there. This is done easily enough with Pulp's solid function:

on collect do
  x = event.px
  y = event.py
  x += event.dx
  y += event.dy
  
  tileSolid = solid x,y
  if tileSolid==0 then
    ignore
    wait 0.2 then
      listen
      goto x,y
      tell x,y to
        call "collect"
      end
    end
  end
end

And that, really, is it! Ice sliding puzzles, simply implemented and with the code neatly contained within the ice tile itself.

You might still find you run into bugs if you have ice tiles placed on the very edge of the screen - I'll leave that as an exercise for the reader to solve!

As an aside, if you want to add another tile that the player slides on like ice, you can just use mimic to copy the behaviour:

on collect do
  mimic "ice"
end

Now what about conveyor belts? Now we are aiming for something like this:

pulp_conveyor_belt_demo

If anything, conveyor belts are actually simpler than ice, just with a little more duplication!

Lets take the upwards moving conveyor belt to begin with. We want a non-solid tile with some scripted behaviour to move the player upwards. That's almost exactly the same as the ice tile, apart from now we don't care what direction the player was initially moving in - we are always going to move them up! We can replace this bit:

  x = event.px
  y = event.py
  x += event.dx
  y += event.dy

With this:

  x = event.px
  y = event.py
  y--

Remember the origin in Pulp is in the top-left of the screen, so moving upwards is moving in the negative y-direction.

Otherwise the code is identical to ice:

on collect do
  x = event.px
  y = event.py
  y--
  
  tileSolid = solid x,y
  if tileSolid==0 then
    ignore
    wait 0.2 then
      listen
      goto x,y
      tell x,y to
        call "collect"
      end
    end
  end
end

That's the upwards moving conveyor belt sorted - for the other three directions, just change y-- to y++, x++ or x-- for downwards, right and left moving conveyor belts respectively!

The same caveat around conveyor belts trying to move the player off-screen causing a bug still applies. You also might see what happens if you place two opposing conveyor belts facing each other - the player will get stuck in an infinite loop! You probably don't want this to happen, but whether you cover that situation in code or just avoid level design where that is possible is up to you.

Hopefully this will be helpful to someone, especially if you're just starting out making games with Pulp. Let me know if you make anything fun!

5 Likes

Great write up!
Really helpful, particularly towards the beginning with how to decide if this should be a world or item tile. I feel like the name "world" tends to make that sound like the right answer, but item is definitely the way to go here.

That's so cool! Thank you for sharing your detailed documentation.