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

It seems very much like we need a better learning curve for using the crank in Pulp. Some people can code using the crank quite elegantly, and the rest of us are like "whaaaaat!?". Which is a compliment, but also a real question.

I'm a dummy—a designer dilettante who prefers to learn through doing. There are others like me, who never studied computer science, and learn except through doing.

I'm creating this thread as the dumping ground for crank related information. I'll aim to clarify things in the thread.

So please opine here if you...

  • ...have knowledge or code to share
  • ...need information or code starting points
  • ...want to check if something is possible
  • ...think something needs a feature requests
  • ...think something needs documentation

Not to say you shouldn't start your own thread. Please keep doing that—it will be far better for you getting to your solution. I'll troll through those posts and curate the learnings here.

Also: Example Game File
I'm going to maintain a game file that DOES all the cranky things, and can be used for code starting points or for learning. Hopefully I can keep up, and everyone else should feel free to post new versions of it. Full blessing on that, I claim the name. Wind-Up City ©2022 everyone who posts. We'll lawyer up eventually. Anyhooooooo... I'll post a response with my starting game file and initial question.

I may tag some people in who I suspect would have some easy answers or can point people in the right direction. I appreciate your input!

2 Likes

Okay, here's what I thought a good starting point would be. I have a super early version of Wind-Up City. Each room will be a different common use case for the crank.

The first room will be a direct rotation control of the turbine blade. It's working, but it needs pretty* exact tuning of the crank to work. You need to rotate exactly to 0°, 90°, for the turbine to spin.

*rounding is the only way I could make this kinda sorta work

Here's the code. Note the comments at the beginning are aspiration goals for how the room should work, when complete.

// this room will use crank rotation to directly rotate the turbine blades
// crank          °   range           blade(marked)  frame
// top            0   >-45 to <=45    up             0
// right/back    90   >135 to <=225   right          2
// bottom       180   >225 to <=315   down           4
// left/fore    270   >315 to <=45    left           6
on isCranking do
	// say "crank works!" //use this as a basic test to see if crank is moving
	crankDegrees = round event.aa // this captures and rounds the crank angle
	
	if crankDegrees==0 then
		tell 18,6 to
			frame 0
		end
	end
	
	if crankDegrees==90 then
		tell 18,6 to
			frame 2
		end
	end
	
	if crankDegrees==180 then
		tell 18,6 to
			frame 4
		end
	end
	
	if crankDegrees==270 then
		tell 18,6 to
			frame 6
		end
	end
	
end

Questions at this point:

What does .aa mean?

  1. I think of it as absolute angle.
  2. Is that perhaps documented somewhere?
  3. Does Pulp have a parent codebase we should be referencing?

How can I get the rotation to work properly?

  1. Rounding was a hack, but I could use math plus rounding. That'd be cool to know how to do, but isn't it just just a bigger hack.
  2. How are we supposed to define a range of values without compound conditions*?
  3. My only though is that it could be a bunch of nested if/elseif behavior. That does my head in just thinking about it, it's ridiculously hacky.

*Compound expressions are the && (and) and || (or) most code languages use to allow if statements to consider multiple things. However, PulpScript doesn't support them.

Wind-Up City.json.zip (4.8 KB)

This is my first game upload here, let me know if there's any trouble!

Okay, here's a new version. It has a "Math Hack" area in the code where I get the cardinal directions to work well. I reduce the rotation number, then round again.

ezgif-2-f72a0ad420

// hacky math method
on isCranking do
	// say "crank works!" //use this as a basic test to see if crank is moving
	crankDegrees = round event.aa // this captures and rounds the crank angle
	crankDegreesRaw = crankDegrees
	crankDegrees /= 90 
	crankDegrees = round crankDegrees
	
	if crankDegrees==0 then
		call "up"
	end
	
	if crankDegrees==4 then
		call "up"
	end
	
	if crankDegrees==1 then
		call "right"
	end
	
	if crankDegrees==2 then
		call "down"
	end
	
	if crankDegrees==3 then
		call "left"
	end
end

I have no idea if this is a good way to do this. Someone throw me a bone.

Also, is there no way to iterate repetitive code?

My functions for up/right/down/left all ask for room tiles to go a specific frame. It's supa repetitive...

on up do
	
	// displary rotations front view turbine
	tell 18,6 to // hub
		frame 0
	end
	
	tell 18,4 to // verticals
		frame 0
	end
	
	tell 18,5 to
		frame 0
	end

//...and so on, 10 tiles per function, 235 lines of code

Wind-Up City (2).json.zip (6.5 KB)

Pulp docs are a bit lacking, IMO.

It is the absolute angle, you can find stuff here:

In every event, its aa and ra members will be set to the current absolute angle and the amount of change since the last frame of the crank in degrees. Also, its frame member will be set to the number of frames that have elapsed since starting the game.

It looks to me like you're trying to reset the frame on a number of tiles?
You might use

emit "resetFrames"

And then in the code for each of those tiles have:

on resetFrames do
  frame 0
end

Does that help?

PulpScript is interesting in that it's reduced to make things simpler, but sometimes it forces you into different ways of doing things than you might have otherwise.

Good luck!

As long as emit is used sparingly, it should be fine, but it's worth mentioning that the docs refer to emit as being an expensive call to make:

You should use call with tell instead of emit if you already know exactly which tile(s) you want to handle the event. emit should be used sparingly as it has to check 378 objects (25x15 room tiles + game + current room + player), all of which could handle the event. The runtime overhead adds up quick.

1 Like

Let me start a new thread on iterative functions, I'll loop you in. Since this is only incidentally related to the crank. My bad.

Cool, absolute angle. Learning happening.

I'm going to think of ra as "recently-rotated angle" then (though I assume it's "rotation" or some such). I need to understand the whole ra thing much better, so I will add a new post to this thread later, after I've hit a wall. I will be updating Wind-Up City with a room dedicated to ra.

In the meantime, if you have any knowledge to share, here are the areas I expect to struggle with:

  1. Knowing when ra triggers an event. Does ra behave as if it has a debounce that makes a judgement on when the crank stops/starts? Slow turning of the crank seem like it might complicate that. Or maybe it works well (I'll noodle with it). This concept of event-i-ness (not a literal event, but this sort of behavior that resets ra values as the crank stops and starts) seems baked into the frame concept, which the next two points deal with.
  2. Struggling with the concept frame in general, which is something ra uses. Frame itself is not clearly described in the docs, except as a variable or function (?) and a few other uses, like telling a tile to display a certain frame. More on frame in next one.
  1. Seems like understand frame is going to be key to using ra. But that's a foreign concept to me. So what does a frame mean in reference to the crank?
  2. Do other things that don't obviously have frames have frames?

Here's my recent update. This works for 8 directions of rotation. I'm still using hacky math, but imho it's okay. In future I'll comment it so people can use it.

ezgif-5-853309ac03

Wind-Up Position 8 working!.json.zip (9.4 KB)

aa = absolute angle
ra = relative angle

Absolute angle is in relation to the physical crank on the playdate.

Relative angle is the number of degrees the crank has moved since the last frame of the game.

I think you have some confusion over frames and events.

In pulp, games run at 20 frames per second. If you think of the game logic as one big loop, that loop repeats every one-twentieth of a second. Hence the game's loop event is called every one-twentieth of a second.

From any event you can call event.frames and this will return the number of frames that have passed since the game was launched. So after exactly 1 second, event.frames will return 20.

frame is also an unrelated pulpscript function. In this context it is related to the animation of tiles. I would say don't worry about this for now!

Events are code blocks that run under certain conditions. Everything in pulscript is event driven. For example the game's loop event is run every frame, while a sprite's interact event is run when the player interacts with that sprite (naturally!)

The cited documentation refers to the event variable. This variable is available in any event. The documentation tells you what members that variable will have available depending on what event is being handled.

aa and ra are available in any event with event.aa and event.ra. This means you can check the crank from any event, which is useful as who knows what you are using the crank for!

Similarly frames is always available with event.frames and returns the number of frames that have passed since the game was launched. This is unrelated to the crank. Don't worry about it!

If you want to do something based on the actual physical position of the crank on the playdate, use event.aa.

If you are instead interested in whether the crank is being rotated, how fast and/or in what direction, use event.ra (and probably some maths).

2 Likes

The C API gives the bare minimum functions to work with the crank. These are the fundamental ways of utilizing the crank as an input device.

  • isCrankDocked()
  • getCrankAngle()
  • getCrankChange()

First of all, you can check if the crank is docked or not. You might want a game where the player avatar pulls out a tool and interacts with the environment in a special way when the crank is extended. Likewise, the avatar may put this tool away when the crank is stowed.

Assuming the crank is extended, there are two fundamental ways to conceptualize it, depending on your use case- absolute position and relative movement. For example, if the crank is used to control the direction of a turret, then absolute position is likely the better conceptual model. If rotating the crank is used to fire a gun, then you care about the relative movement required to fire the next shot, not the position of the crank.

It is also worth noting that absolute position can be used to calculate relative movement, and vice versa.

relative_movement = current_absolute_position - previous_absolute_position
current_absolute_position = previous_absolute_position + relative_movement
-- current and previous absolute position are zero immediately after the crank is extended

My guess is that the hardware only reports the current absolute position, and the SDK uses that to calculate the relative movement. Strictly speaking, you only need one or the other. It is however, useful to have access to both values because both conceptual models are very common use cases.

Pulp offers some convenience functions, like getCrankTicks(ticksPerRevolution). In the gunshot example above, if I want to fire 12 bullets per revolution then I could use getCrankTicks(12). Ticks can be manually calculated from relative movement using either of the following equivalent calculations.

ticks = relative_movement / (360.0 / ticksPerRevolution)
ticks = relative_movement * ticksPerRevolution / 360.0

The Pulp playdate.getCrankChange() also supplies an acceleratedChange value that would need to be manually calculated in C. I would need investigate the details, but calculation sounds relatively simple.

Crank input in Pulp is also tied into the callback and input handler systems. Default sound output can be controlled with setCrankSoundsDisabled(disable). Presumably you can manually play our own sounds if you need to. Crank indicators can be used to make a more polished product.

There is not a whole lot to the crank. Figure out what you want to achieve and determine the better conceptual model (absolute or relative). Next, figure out how you want to read values (directly, callback, or input handler). Finally, consider crank indicators, custom sounds, and any other information that users need to operate your product.

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.

2 Likes

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
end

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
	end

	// 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
		end
	end
end

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"
		end
	end

And inside this crank tile:

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

Together, the full effect looks like this!
chrome_2022-04-09_11-34-03

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

2 Likes

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)