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

Replying to that and some new stuff here.

First, and apology to anyone watching this thread, or reading and hoping for more.

My eyes were too big with my stomach for this one, ie I overpromised and underdelivered in terms of providing a guide. Because... reasons.

On a longer timeline I do want a good guide to exist, and I want to help, because I think that making use of the crank should be easy. Or fun. Or straightforward. Or learnable. Or something. I guess I'm not sure.

@sgeos On one level I agree the crank is simple, but the human imagination part of the equation is the challenging part.

Imagine two classrooms of children:

To room 1, you give a powered-off Playdate to a group of children and have them come up with ideas on what to do with the crank. You don't "teach" them anything about game design, programming, etc. As their minder, you write down 100 of their ideas, and then give them marshmallows.

The room 2, you show a working Playdate and provide computers with the Pulp interface. No restraints on teaching game and tech. The kids split into groups, and with some level of adult coaching, each of them gets a game piece created. This sort of scenario happens in STEAM ed, I'm guessing.

In room 1, you'd have a bunch of really creative ideas. 100 of them would be wild ideas only a kid can have, let's say 50 are feasible. But you'd have no working software, and no direct route to working software in the classroom settings because of the uniqueness of the ideas. After class, if the teacher did develop the concepts into working prototypes, the games would likely be very laborious to "finish" because of their uniqueness.
Worth noting, for developers to whom nothing is impossible: A good developer/teacher could totally take one of the ideas to a working prototype in the classroom setting, but they'd likely do so by 1) picking a feasible idea from the bigger list, 2) editing down the idea & convincing the kid to accept a limited version, and 3) other developer coping strategies. I'm not talking about that, that's the sort of thing that would happen in room 2.

So yeah, In room 2, you'd have more practical ideas, or ones limited by solutions becoming obvious in the typical process of software development. But you'd end up with working software, and the "editing down" process of applying software development logic may end up with a piece of a game that could possibly be completed. If the kids started with 100 ideas, you'd also have a record of the room/groups editing down to what's possible in the time given.

Personally, I'm more of a room 1 type of kid. Hard to keep on task, but I can really ideate. Fences are merely things to leap from. When my ideas hit the reality of software development, things don't go well. I end up babysitting little problems and making no big progress.

This is my problem though, linked how I like to work (getting budget and working with professionals is how I work at work), perhaps it's a personal pathology. But I still think the world needs "playful" programming/prototyping environments. And I think it's a valid idea that some people would want to avoid the hard parts of programming and make a thing... the thing they wanted to make... not someone else's idea or something from a pre-baked demo.

But I do think it's ambitious (impossible) to provide a guidebook for room 1 kids, because there are so many things one can do with a rich input device like the crank.

So I'm re-evaluating the scope of this thread. And I'm open to others ideas here (and in the larger forum).

My next post I'm going to jot down a few "room 1" ideas. Though I think we may want a new space for those thoughts. Honestly, the more I think of the classroom experiment the more I actually want to do it, despite not being an educator.

Still in agreement on that, but I want to develop the thought.

There is not a whole lot to the crank, but there is a lot you can do with it.

To follow that thought and some of the thoughts from my last post, here are some crank ideas I just had...

  1. Rotation of the crank gives your character movement ability - Once you've run out of speed and d-pad no longer moves the character, player must crank to recharge. Upon entering "foot-chase" sequences, player must crank quickly, while making basic navigation choices with the dpad that behave like quick time events
  2. Use crank to synchronize breath - Player character must catch breath (to survive injuries or overexertion) or meditate (to level up) at times, in which the player must synchronize breathing based on prompts. The crank would need to be rocked to and fro based on the prompted respiration (onscreen and audio).
  3. Altered beasty stuff At special altars, cranking will change to beast form - before being killed, crank ## times to enable transformation. Altar glows while cranking before transformation. When in beast mode, various crank positions will change your gate - 180 for "bounding" / max movement, 90 for "cowering" max defense, 0 for "pouncing" max attack. Player can select a certain level between those states though.
  4. Crank wields dungeon torch. RA controls aim of torch when lit. When fuel dwindling, single crank to increase fuel. Multiple cranks to temporarily increase fuel for extra illumination. Dock crank to douse torch.

Based on these, I see a few crank behaviors that would be needed as kit:

  1. Crank-to-charge to a certain speed/rotation that has a natural feel.
  2. Crank direction for rocking* | Crank synchronization. This could be angle or speed**, maybe other "feel" factors, checked against a preset crank sequence, with a set tolerance.
  3. Listen for ## rotation count | While cranking events | Crank used for setting a variable
  4. Crank used for aiming | Listen for single crank vs ## cranks

*Not sure if we have a native crank direction at this point
**Not sure if we have a native crank speed at this point

This makes me consider of the difference between gameplay and scenario.

Perhaps use of the crank could be described in an abstract or general concept rather than anything to do with the scenario.

When using the absolute angle, the crank is a circle. Anything that can be mapped to a circle can be mapped to the crank. You could roll an analog stick instead, but the crank is a far better input device. Note that anything cyclical can be mapped to a circle.

When using the relative change in angle, the crank is the speed of movement along a one-dimensional line. Any linear relation can also be mapped to the crank. The D-pan can be used for movement in two directions, but there is no speed component. An analog stick can also be used for movement in two directions, and it does offer a speed component. The crank shines when you have a linear relation that you want to restrict to one dimension.

Finally, there is a tactile component to the crank. If turning a crank makes your particular game more fun, then it is clearly the right input device for the job. The Playdate has so few buttons that I think it generally makes sense to use the crank for something.


That all sounds reasonable but it's quite abstract.

Do you think you'd be able to create a script snippet to synthesize a speed value from aa/ra?

It could count same-direction crank revolutions over a time period and update a variable.

I can make a room in Wind-Up City that makes use of it.

The relative angle is the speed if you calibrate it correctly. (Velocity is the derivative of speed, if you want to be speeding up.) Here is an example Lua function that converts the relative angle to a final speed for a specific range of motion. Note that this calculation can be inlined. It does not need to live in a function.

function relative_angle_to_speed(relative_angle, speed, range_of_motion)
  return relative_angle * speed / range_of_motion

Say you want the speed to be 2 (pixels or game world units or whatever) for every 90 degrees the crank is turned. In that case, you would call the function as follows.

local ra, ara = playdate.getCrankChange() -- degrees
local base_speed = 2 -- pixels or game world units or whatever
local range_of_motion = 90 -- degrees

local speed = relative_angle_to_speed(ra, base_speed, range_of_motion)

If you want to do anything else, you will need to figure out what your specification is and translate it into code. It is easy enough to add a counter or use the accelerated change return value (ara in the code above). Having said that, you need a clear idea of what your specification is before it can be coded, even if values get tweaked along the way.

Disclaimer: I tested the Lua in an online editor, so I am not 100% sure the Playdate SDK call is correct. The rest of the code is verified as working.

EDIT: It could be fun to have powerups/enemy effects that modify base_speed/range_of_motion. Also, note that these values are split out to make the problem easier to think about, but the terms are inversely equivalent.

calibration_constant = base_speed / range_of_motion
speed = relative_angle * calibration_constant

-- therefore
base_speed = range_of_motion * calibration_constant
range_of_motion = base_speed  / calibration_constant

-- base_speed*2 or range_of_motion/2 to double the final speed

You could precalculate the calibration_constant and apply powerups to it, but it might just be easier to think in terms of speed up and speed down modifiers.

local ra, ara = playdate.getCrankChange() -- degrees
local speed_modifier = 2 -- unitless, double speed
local base_speed = 2 -- pixels or game world units or whatever
local range_of_motion = 90 -- degrees

local speed = relative_angle_to_speed(ra, speed_modifier * base_speed, range_of_motion)
1 Like

A further note on rotation speed of the crank:

In Pulp, the relative angle event.ra is "the amount of change since the last frame of the crank in degrees", which might sound kind of abstract, but that is just a measure of rotational speed and it's easy to convert it to the possibly more familiar revolutions per minute (rpm) by changing the units.

event.ra has units of degrees/frame.

Pulp has a frame rate of 20 frames per second (fps), so multiply by 20 to get degrees/second.

Multiply again by 60 to get degrees/minute.

One revolution is 360 degrees, so finally divide by 360 to get revolutions/minute.

To put that in Pulpscript:

rpm = event.ra  // degrees/frame
rpm *= 20       // degrees/second
rpm *= 60       // degrees/minute
rpm /= 360      // revolutions/minute

Or the slightly optimised:

rpm = event.ra
rpm *= 10
rpm /= 3

I don't have an example of why you'd want to do that specifically, but maybe it might help thinking about what relative angle is?

With the Lua SDK, playdate.getCrankChange() is only slightly more complex in that it returns the amount of change in degrees since it was last called rather than since the last frame, and your frame rate may be variable (the default is 30 fps).

Hey there!

I was invited to share some of what I've done for one of my current projects. The game itself uses the Lua side of the SDK, but I translated the core idea into Pulp for beginners to take a look at :smiley:

Jim in the Box

The core idea is having a jack in the box accumulate as the crank is turned, until eventually he pops out. This is really easy to accomplish in code, just increase a variable by the change in rotation until it reaches a predetermined threshold:

on loop do
	// accumulate the change in rotation
	if event.ra>0 then
		charge += event.ra

	// if we haven't popped out yet, and the charge is high enough
	if hasJimPopped==0 then
		if charge>1000 then
            // do something fun here
			hasJimPopped = 1

In my case, I have this hooked up to a tell notifying a tile to swap sprites.
Now, beyond this its fun to have a visual response to the cranking. So, I updated the above script to also notify a crank tile whenever a rotation occurs. The crank tile has an animation with the speed set to 0 fps, and its frames are updated according to the current crank rotation. The code to notify the crank is

	// if we've rotated at all then notify the crank it needs to update
	if event.aa!=prev_aa then
		tell 13,6 to // this position is whatever tile you want to notify
			call "crankChange"

And inside this crank tile:

on crankChange do
	// these first to set the rotation to the top
	if event.aa>315 then
		frame 0
	if event.aa>0 then
		if event.aa<=45 then
			frame 0
	// set the rotation to the rotated forward
	if event.aa>45 then
		if event.aa<=135 then
			frame 1
	// set the rotation to down
	if event.aa>135 then
		if event.aa<=225 then
			frame 2
	// set the rotation to rotated back
	if event.aa>225 then
		if event.aa<=315 then
			frame 3
	// update previous rotation
	prev_aa = event.aa

Together, the full effect looks like this!

The full project can be tested out by importing the json project from here.
Let me know if you have any questions!


Okay, I created a separate Wind-Up City game just for speed, because multiple rooms were getting crufty.

WuC Speed Start

@orkn @sgeos Thanks for your input on Speed. Can you help me get from where I am to a working model where turbine speed is set by crank speed. Reminder: this is all in PulpScript. If you don't use it, then helping me reason through the logic would still be helpful. Am I rounding .ra? Am I developing a function to count total rotations?

@orkn I think your Pulpscript examples are within reach of being able to be applied, but I don't know what scenario (eg events and functions) can be used to trigger the rpm value being applied to the variable. I'm also unsure how to have an event listening all the time (instead of allowing the user to press A or B to apply the speed to the turbines).

Here's how I thought it could work in my head. But this is very general.

"This room will set turbine rotation speed based on crank input: The crank speed, averaged over a few seconds, sets a speed—limited to a max speed—for the animation rotation. The player can keep cranking at whatever speed and the blades will correlate. With no input, the blades then slow down. Docking will cause a full stop. Animation is achieved by swapping the animated tiles to "...spd0-spd10" (so 11 increments of speed). There are a few buttons to synthesize the speed rotation, stopping, or neutral.

If you recommend better methods and practices that will be more useful for newbs, go ahead and make whatever changes you think necessary. Comments much appreciated!

WuC Speed.json.zip (7.1 KB)

(I'll probably have some improvements to this if you need time to think about it. I can probably develop the neutral function, and have turbineSpd actually set the fps of the turbine animation tiles. (by tile swapping unless there's an actual way to set fps)

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!
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
	// 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
	// if the y has changed, move the player to the new position
	if prev_py!=player_y then
		goto event.px,player_y

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

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"
	// tell the new tile we entered it
	tell event.px,player_y to
		call "playerMovedTo"

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

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
		wait 1 then

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
            tell event.tx, event.ty to
                call "doWait"

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
    if tick == 30 then // runs at 30 ticks per second iirc
        tick = 0

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.

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


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.