Dat crank tho! A place to share crank know-how

A Crank Based Crane Game

Someone asked me if it would be possible to control the player using the crank. After a bit of tinkering, it sure is!
chrome_2022-04-09_11-38-02
The process for moving the player is actually relatively simple. The ropes to the left and right of the player are just tiles that aren't collideable, so the player can move through them freely using left and right. However, the white space above and below the player are set to be collideable, so the player can't move through them using the arrows.

To move the player using the crank we just check every update to see if the crank has turned. If it has, we move the player up or down, based on the change in rotation:

on loop do
	// if the crank has turned, move the player
	if event.ra>0 then
		player_y += 1
	elseif event.ra<0 then
		player_y -= 1
	end
	
	// if the player is out of bounds, lock them back in  bounds
	if player_y<=0 then
		player_y = 1
	elseif player_y>=15 then
		player_y = 14
	end
	
	// if the y has changed, move the player to the new position
	if prev_py!=player_y then
		goto event.px,player_y
	end
end

Notice, we only need to handle changes in the y axis, because that's the only axis the crank moves in. We also add a short script to the player to update the previous position whenever they move.

on update do
	// update the previous position
	prev_px = event.px
	prev_py = player_y
end

Adding the Dangly Rope

It's a bit more work to add the rope left behind by the player sprite as they move up and down. The key here is to keep track of where the player moves from, and where they move to, and notify those tiles accordingly. Then, they can just swap as the player moves over them! I did this by adding the following code to the players update event, before the update to the previous position.

	// if we moved down, tell the old tile we left it
    // this is only done when moving downwards,
    // so we don't leave ropes while moving up.
	if prev_py<player_y then
		tell prev_px,prev_py to
			call "playerMovedFrom"
		end
	end
	
	// tell the new tile we entered it
	tell event.px,player_y to
		call "playerMovedTo"
	end

Then, all you have to do is add a handler to the tiles that you plan to move across to process these events! In my case, I have the empty tile swap to a rope when its left, and the rope swap to an empty tile when entered. I also have the top rope swap to a solid tile when exited so that the player can't move up and into it.
You can import this project from here.
As always have a great time with your Playdate!

1 Like

In the loop event, you can calculate the current crank rotation speed as they described using ra. You can then average this with the current running average, and swap the sprite to the correct one based off of that.

on loop do
    // as they described calculate the rpm
    rpm = event.ra
    rpm *= 10
    rpm /= 3
    // average with the current rpm
    avg_rpm += rpm
    avg_rpm /= 2
    // no just tell the sprite to update its animation
end

Rather than averaging, it might be a good idea to use some kind of interpolation to smooth the transition to your likings.

1 Like

@lurgypai maybe you can help me diagnose what's going wrong. I'm trying to work up to being able to use your averaging function. (I have it in, but haven't applied it to the blade speed yet).

The problem I'm having is with getting the blades to slow down naturally. I want the speed to decrement 1 fps per second. With that in place, spinning the blades up should feel natural.

Here's my problem area, in line 30 of SPEED room:

// this is not working properly
// it's supposed to decrement the turbineSpd variable 1 per second
on goNeutral do
	while turbineSpd>0 do
		turbineSpd--
		wait 1 then
		end
	end
end

What happens is that it doesn't decrement smoothly. The waiting doesn't run, so it goes from 10 to 0 immediately.

Json attached. Thanks for any help you can provide.

WuC Speed.json.zip (7.5 KB)

My understanding (and I could be wrong) of the wait function is waits the duration before executing its body, but doesn't function as a sleep. It probably schedules whatever you put after then to occur in one second, but doesn't actually delay code execution by one second.

You'll have to add an event thats called over and over, after each wait occurs. something like

on doWait do
    if turbineSpd > 0 then
        wait 1 then
            turbineSpd--
            tell event.tx, event.ty to
                call "doWait"
            end
        end
    end
end

There might be cleaner ways, and this code is untested.

Thanks, tried that. It decrements by one and then stops.

I'm wondering if Pulp has the ability to do anything like sleep (or a gradual decrement, etc). I hear people having issues with call and emit all the time. Is Pulp unfinished, or just incompletely documented?

If it calls itself it should loop? I'll try my own implementation when I have a moment.

Pulp is neither unfinished, nor do I think its incompletely documented. Rather I think its a very limited tool, and this kind of timed feature system is stretching the limits of its capabilities.

Alternatively, you can add a counter to the loop event in the game.

on loop do
    tick++
    if tick == 30 then // runs at 30 ticks per second iirc
        tick = 0
        turbineSpd--
    end
end

And just wrap this with conditionals based on when you want it to occur.

I don't mean to denigrate Pulp itself, I have a bunch of great ideas started, and the efficiency is quite nice. The documentation though, is quizzically minimal. At first the documentation was nicely spartan—hey I can scan and search this single page easily. And then I realize that it seems to be written for people who are either Lua literate or have a lot of other code background.

Eg, you mention ticks. It's mentioned in the docs page but never explained. Like many other things (hence this thread covering aa and ra earlier). I read past the ticks example several times thinking it was just an on-the-fly custom variable the docs writer used.

ticks aren't a Lua thing, but a general game programming thing representing a unit of time. In our case, pulp calls its loop event 30 times per second. Each update can be referred to as a tick. So if 12 updates have occured, we can say 12 ticks have passed.

Pulp does its best to be beginner friendly, but unfortunately its difficult to know what degree of knowledge your users will be operating under. So, for the scripting side, they decided to assume a general understanding of programming/game programming concepts. The Pulp side of the pulp docs might help some, but probably not a ton.

(edit)
Note in my code, the tick variable is an on the fly variable I'm using, to quantify the amount of updates that have passed, in contrast to the more general usage of the term "game ticks" to refer to updates.

1 Like

WuC_speed_crankWrkg

Okay, with huge thanks to @lurgypai I have a working SPEED room. It... works okay. The crank does control the turbine speed.

However, I can imagine many folks could look at the code being able to improve on the methodology.

Known problems

  • For the gradual blade slowdown, decelerating by swapping animation tiles with different fps isn't great, it causes a visual flash to the 0th frame during each swap. I wish this way was viable through some sort of synchronization (or direct fps control of a tile), as I do think it's very beginner friendly.
  • @lurgypai mentioned a better way to smooth the input using interpolation instead of averaging. I have my doubts the current method will feel natural.

WuC-Speed_pdx_wrkg.zip (36.9 KB)
WuC Speed_working.json.zip (7.3 KB)

You could probably set the fps of the animation to 0, and manually move through the animation based on the current speed.

This is likely the tell event.tx, event.ty to line not doing what you think it is doing - the actual pattern of a function recursively calling itself after waiting is sound. The best thing to do for a problem like this is to add some logging with log "event.tx = {event.tx}; event.ty = {event.ty}" (we're using string formatting to log those values). Log lines will appear in the browser console which you can probably open in your browser with ctrl + shift + i. I suspect you will find these aren't the coordinates of the tile you were expecting!

Being able to debug like this is very useful for spotting unexpected errors or even just making sure you understand what your code is doing line-by-line.

JITLootbox_pdx

@lurgypai I started to adapt your Jim-in-the-Box mechanic for loot boxes. Any suggestions on how I can go about the following?

  • Only allowing cranking of a specific chest when the user is directly to the right of a chest (allowing multiple chests per room).
  • Clearing charge when the user steps away from a chest (or each chest has its own charge)
  • Allowing the chest to be placed in pulp without further coding or secondary tile placement—meaning the charging tile and pop location auto-index to the placed location

Special blank could work tiles, but ideally these loot boxes could be ready to drop without that.

I imagine this mechanic would be useful for beginners laying out a dungeon crawler with a minor crank interaction. Chests could hold keys, health potion or poison potions for a few easy ones.

JITLootbox_pdx.zip (30.9 KB)
JITLootbox.json.zip (3.4 KB)

I'm trying another way... but your way makes sense.

Here's what I'm trying. I think it might be easier for beginning users, and this general method could be good for animation synchronization.

  1. Get the frame from one of the tiles that is spinning (they should match). Store it in variable currentFrame
  2. Adjust the value number if needed (++, etc)
  3. Do any tile swaps based on the turbineSpd variable changing*
  4. Using frame to set the new tile to the correct synchronized frame.

*this is still the slowing spinning blade example, but imagine these animation swaps being monster smiling instead of frowning (while dancing), a fluttering flame growing larger, a waving flag changing colors based on allegiance)

Does this make sense? I've got it roughly coded but not working in the json attached.

Having a lot of trouble with emit and call as usual. Thanks for anything you see.

WuC Speed - animation synchronization.json.zip (7.8 KB)

My method is doomed to not work.

from docs: frame Sets the frame of the current instance of a tile to frameIndex . Only affects tiles with an fps set to 0 .

Booooo!

However, I did find out that a straight swap of two tiles with the same amount of frames and same fps will stay sychronized.