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!

7 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.

I am currently working on a crate pushing puzzle game and I was wondering how you would be able to get the crate to react to the items. I currently have the crate as a sprite and when I push the crate onto the ice it deletes it. Is there a way for the sprite to collect the item the same way the player does?

Remember that a room in Pulp is just a grid of tiles, with one and only one tile at each location on the grid. If you swap the tile at a location then it is simply replaced, with no record of what tile was there previously. Conceptually you might think of ice as being the floor and a crate being an object that gets pushed "onto" the ice, but as far as Pulp is concerned there is only one tile at any location in the room - it's ice or crate, not both.

The player is special. They don't get swapped in to the room but exist outside of or "above" it. The player can be at a location in the room and the tile "beneath" them is still there. Only the player works like this!

To answer your question then, no, a sprite can't "collect" an item in the same way as the player does. That doesn't mean you can't get your crate pushing puzzles working!

There are a few different of approaches to making "objects" in a room that move around "above" the scenery like the player does, whether they are crates, enemies or whatever else:

  • Swap the tile, but keep a record of the tile that has been swapped in a variable so it can be swapped back when the object moves again. This is relatively simple to implement, but if you want more than one of the same object you will have to duplicate the tile and make sure the variables being used are uniquely named. For example, if you want three crates on screen at the same time you will need to use three different crate sprites.
  • Create tiles that represent the "combination" of an object and scenery. For example, if you push a crate onto ice, you don't swap the ice tile for the crate tile, but swap it for a "crate on ice" tile. With this approach you don't need to track anything in variables, but you can easily end up with lots of tiles for lots of combiations of objects and scenery.
  • Don't swap an object into the room, but draw it in the player's draw event. This gives you the most freedom, but you have to handle all of the behaviour manually in scripts, which can be a lot of work, with unique variables for everything being tracked.

With the interaction of your crate object and ice tiles that you want it's a bit more complex as you also need to implement that sliding behaviour for the crate.

For making a crate slide over ice though I would maybe try something starting like this:

The crate sprite script:

on interact do
  // Get the coordinates of the tile the crate is being pushed into
  x = event.x
  y = event.y
  x += event.dx
  y += event.dy

  tile_solid = solid x,y
  if tile_solid==1 then
    // Trying to push the crate into a solid tile, so the crate doesn't move
    done
  end

  // Call an event on the tile the crate is being pushed into which will decide what happens
  tell x,y to
    call "receiveCrate"
  end

  swap "white"
end

The ice item tile:

on receiveCrate do
  // Play will swap the ice tile for the crate on ice tile for the duration of the crate on ice tile's animation
  play "crate on ice" then
    x = event.x
    y = event.y
    x += event.dx
    y += event.dy

    tile_solid = solid x,y
    if tile_solid==1 then
      // Crate can't slide into a solid tile, so stays where it is as a crate on ice tile
      done
    end

    // Call the same receiveCrate event on the next tile the crate is sliding onto
    tell x,y to
      call "receiveCrate"
    end

    // Back to being an ice tile
    swap "ice"
  end
end

That's untested, but hopefully gives you some ideas!

Thanks for the help! The does give me an idea on where to start to solve the issue! I will let you know if I get something working or if I need additional help!

when I pasted your bug, I noticed a bug where there tile you finish on turns into black, so here's how I fixed it

on collect do
	x = event.px
	y = event.py
	x += event.dx
	y += event.dy
	
	tileSolid = solid x,y
	// remeber next tile for swapping
	tilename = name x,y
	if tileSolid==0 then
		ignore
		wait 0.1 then
			listen
			goto x,y
			tell x,y to
				call "collect"
			end
			
			// stops end tile turning black
		  tell x,y to
		    swap tilename
		  end
			
		end
	end
end

The tile the player lands on will have collect called on it. If it's an item tile with the default collect behaviour and the "Fill" tile in the game settings has been changed to black, that would explain what you are seeing. Rather than swapping every tile you slide over with itself you probably just need to define a collect event handler on that item tile you are landing on!