Orkn's Pulp Devlog

Crank Controls

In Patterns of the Wind the crank controls the intensity of the burner on the hot air balloon. Somewhat like a throttle, when the crank is pointing straight up that's at maximum burn, and when pointing straight down the burner is off. If the crank is perpendicular to the console (in front or behind, it doesn't matter) the burner is at half strength and this roughly corresponds to the position needed to maintain the balloon's current height. Actually this "neutral" position varies with the height of the balloon, but that's getting ahead of myself! Most notably the angle of the crank does not directly correspond to the height of the balloon. To achieve this I'm not simply mapping the angle of the crank as you might expect but performing a more complex translation of crank input.

In Pulp whenever the crank is rotated the crank event is called on the player. The event attribute event.aa is an angle between 0 and 360 degrees matching the "absolute angle" of the crank's position in that frame. (There's also event.ra or "relative angle" that gives you the change in crank position since the last frame, but that's not relevant to how I'm handling crank input in this case.) Whenever the crank is rotated I transform the absolute angle of the crank into a value between -90 and +90 degrees i.e. -90 represents the crank pointing straight down and +90 represents the crank pointing straight up, and there is no distinction between the crank being in front of or behind the console. I assign this value to a variable I call aa, which despite the name isn't the same as event.aa but I know what I mean! Here’s a simplified version of the crank event showing that (I’ve stripped out some additional code for handling the burner sound effects):

on crank do
  // aa ends up in range -90 <= aa <= 90
  aa = event.aa
  if aa>180 then
    aa -= 180
  else
    _ = 180
    _ -= aa
    aa = _
  end
  aa -= 90
end

Regardless of whether the crank is rotated or not on any particular frame the game is always processing crank input. This makes sense for the behaviour I want where for example holding the crank stationary in a pointing up position should continue to apply the burner at full strength. Each frame then the game starts with aa and another variable height, the current height of the balloon, and has to calculate dh, the difference in height that the balloon should move by on that frame.

The first thing I do is convert aa into the height dependent ea, the "effective angle" of the crank where 0 is the neutral position that maintains the current height. Any value below zero will cause the balloon to fall and any value above zero will cause it to rise. This neutral position can vary by 30 degrees above or below perpendicular so that you need to hold a higher burn to maintain a higher height.

Next I apply a five frame input buffer i.e. the rest of the frame's calculations are actually using the ea value from five frames previous and the just calculated ea value will be used five frames in the future. This introduces a small delay to the player's input intended to simulate the actual delay between firing up a burner and the air above being heated. I've never actually flown a hot air balloon, but I have driven a narrowboat on a canal and it's a lot like that!

potw-crank-controls

Determining dh then varies depending on whether the balloon is rising or falling. When rising I apply a height variable scale factor calculated by converting ea from degrees to radians with the radians function and then applying the cosine function. The effect is to increasingly dampen the balloon reaching max height, so if you put the burner on full burn the balloon won't just shoot to max height linearly and stop but will slow down as it rises. When falling I instead apply a linear scale factor that simply reduces the max fall speed i.e. the balloon can rise more quickly than it falls, simulating a balloon drifting back down to Earth as the air inside cools. In both cases another linear scale factor is applied at the end to create a dh value that matches the height scale I am using.

This value for dh is derived from a single frame of input, but actually I want there to be a kind of input hysteresis - the movement of the balloon should take into account multiple frames of input and not change erratically every frame. To do this I create a circular buffer of dh values for the last 20 frames and average across them. This moving average value of dh is what is then finally applied to change the height of the balloon!

That's quite a wordy explanation, but in summary the crank acts like a throttle and the balloon has a delayed and smooth response to the player's input. This makes for nuanced controls with a learning curve that rewards player skill and it leads to the balloon appearing to rise and fall more naturally, as hopefully can be seen in the gif above.

If you've read this far and are thinking "that's a lot for pulp, what's the performance like?" then the next post is for you!