There’s a lot more I’d like to do with it, but at this point it’s fun to use and if someone needs to keep a tally of something and have only a Playdate at hand, this is absolutely their best bet. I can feel the urge to turn this into a forever project taking hold, so I had to get a complete version out into the world. It was a fun challenge, but I have other things to learn and I don’t foresee myself spending enough time with it to take it to the places I imagine.
Look, it’s got an itch.io page and everything. If anyone happens to try it out, I hope you find it enjoyably useful and welcome feedback and bug reports via itch.io or e-mail at TallyPal at this domain dot com.
At first I had thought I would put together a whole system for managing tallies, like I had for the iOS version, but after going a little ways down that path, I figured having that level of complexity was probably asking more of the average person who’s using their Playdate to count stuff. Instead, I opted for a fixed number of tallies, which can still be renamed, similar to old cartridge-based game save slots. This makes data management much simpler for both myself and the user.
Speaking of saving time, I also didn’t have to create the menus and scene transitions. Instead I used Noble Engine and simply altered some of the premade code from that. This got some of the fundamental features up and running quickly, allowing me to get started on the unique features.
Look at those lovely button glyphs. Much better than the word “Crank” all typed out, and “glyph” is a fun word to say to boot.
I was able to replace my text-based control guide with the Playdate system font button glyphs even though I’m using a custom font by assigning the system font as the italicized variant using the technique described here.
Sound effects are also in. I grabbed a lot of small mechanical sounds such as ratchets, switches, seatbelts and padlocks from Freesound. There is nothing like implementing sound effects to really refresh one’s impression of an interactive element. If you can get something feeling pretty good without sound effects (and it should feel good even without sound effects), and then add them in, it’s revelatory. Even placeholder effects add so much, and no surprise, it’s engaging a whole other sense! The process of selecting and implementing sounds should be another post should I think of it.
At this point, I have what might be considered a minimum viable product, . With a bit of polish and bug fixes, the Playdate’s first piece of productivity software (that I know of) should be ready soon, and when it is you will find it here for sideloading. Probably no updates next week as I’ll be out of town starting my new job with Schell Games. Very exciting! Although I would be surprised if they plan to publish any Playdate titles, so my foray into lo-fi development may be on pause for some time.
As anyone who’s done so can attest, giving the user the power to enter text opens up a whole can of worms. Fortunately, the Playdate SDK includes a text entry keyboard, so I don’t have to start from scratch. It is also based on rotating tumblers so it fits Tally Pal’s theme (even if that means that “keyboard” is kind of a misnomer).
I spent a fair amount of time on this due to my tendency to try to break things while playtesting. Partly I think it’s my QA tendencies, but perhaps it’s also a way of procrastinating. After all, I can put off committing to an approach if I keep picking at the edge cases. Text entry in particular provides such a huge possibility space that trying to find out ways to break it is a pretty compelling exploration. It does give me robust results, though. Good thing I have such a small scope for this project.
The keyboard in action. The “_” after “Apples” blinks to indicate that more text could be added.
For example, while adding in text entry doesn’t seem like a great deal of work, I’ve already established that I have to use the same font to display the title that the keyboard uses, or else some of the glyphs won’t match up (in a longer-term project I’d simply make a new font). I’ve also added a blinking cursor to the text entry field, a check to have the text scroll out from under the keyboard display if it gets too long, and feedback for indicating when the character limit is reached.
A critical thing to establish with any text entry is defining what exactly a user can enter. Are there any characters that don’t render properly, or wouldn’t make sense in context? How many characters should they be limited to entering, if at all? One trick I like to employ is to enter a row of “W”s to get the maximum possible width. That informed what my character limit should be. It’s unfortunate to have to limit titles to just 10 characters (in the future I may have it calculate the width of the text and cut off anything that would go off screen to be replaced by an ellipsis). Localization will not factor into this project, but if it did, translating into German generally gives the longest words in terms of characters. And those are just the considerations apart from whether the content of the text itself is appropriate, which could be a whole blog entry and more on its own.
On the opposite end of things: what happens when nothing is entered? In this case, it would produce an error whenever the title is loaded, so we can’t have that. My initial thought was to prevent the user from entering anything invisible, i.e. nothing at all or only space characters. Here’s the solution I use to validate that the string the user has entered is something visible, while still giving them the freedom to use spaces freely, presuming there are other characters in the string:
-- If the first character is a space
if string.sub(playdate.keyboard.text, 1, 1) == " " then
-- check that at least one other character in the string is not a space
for i = 2, (string.len(playdate.keyboard.text)), 1 do
if string.sub(playdate.keyboard.text, i, i) ~= " " then
titleValid = true
end
end
-- or that the string is not empty
elseif playdate.keyboard.text ~= "" then
titleValid = true
end
(I don’t know if I’ll ever get used to typing “~=” to mean “not equal to” in Lua. I keep reading it in my head as “sorta equal to”.) In the end, I decided to allow space characters. If it were, for example, an entry field for one’s handle in an online game, I would disallow it, but here it doesn’t really have a downside apart from looking odd, and I suppose the user should have the freedom to do that.
It’s compromise time indeed as I try to put a bow on Tally Pal. I hope to have a solid, if not too extravagant, project done before I start at my new job next week. This is a good, challenging exercise, but I look forward to being back in the more complex but familiar world of VR development.
After a few false starts and lots of iteration, I’ve got the process of using the crank to alter the count functioning and feeling the way I intended. First was replacing the functionality that used playdate.getCrankTicks(ticksPerRevolution), with one that used playdate.getCrankChange(), so that instead of being event-driven, it simply tells me how much the angle of the crank has changed each frame (the delta). This change was easy enough to make, but having the numbers smoothly driven by the crank felt like advancing a tape on a reel, while what I wanted was the chunkiness of a ratchet, so of course I had to make things difficult for myself impart a feeling of mechanical operation and friction.
In order to get the numbers to feel like they were “clicking” into place instead of being smoothly translated, I added a multiplier for how far an angle delta would move the affected digit, which decreases as the digit approaches the halfway point. If the delta this frame would move the digit past the halfway point, it adds a little extra so that it jumps ahead (as well as playing a sound effect).
The end effect is indeed more ratchety, getting progressively “harder” until it slips past, which makes it easy to tell when the count updates. The Playdate’s crank moves smoothly and not like the clicky (I’m sure there’s a better word for it) knob on the side of a mechanical tally counter, but the effect is still fairly convincing, especially with the addition of sound (which I’ll need to detail in another post).
Another thing I had to account for was what happens when the crank advances so far in a single frame that two or more digits should go by. In order to make the count increase the proper amount, I check the difference between the current offset and the predicted offset, find how many times the height of the number (100 in this case) it encompasses and change the count accordingly. I considered adding characters to the font that looked like a blur of numbers when this occurs, but even at the Playdate’s default frame rate of 30 FPS, the rapidly changing numbers appear close enough to a blur that I’m satisfied.
Here’s what it looks like in Lua:
function updateVerticalOffsetFromCrank(amount, digit)
local changeToCount = 0
local countMultiplier = 1
if digit == 2 then
countMultiplier = 100 -- adjust for hundreds place
end
local slipAmount = 25 -- slipAmount is how far the tumbler jumps after the count is updated, to emphasize the count changing and feel mechanical
-- change the count if moving the digit this far down would put it more than halfway outside the window
if yOffsets[digit] + amount > (fontHeight/2) then
-- Account for rotating past multiple numbers in one frame
changeToCount = -countMultiplier * ((math.floor((yOffsets[digit] + amount)/fontHeight)) + 1)
yOffsets[digit] = ((yOffsets[digit] + amount) % (fontHeight/2)) - slipAmount
crankMultiplier = crankMultiplierDefault -- reset multiplier
-- change the count if moving this digit this far up would put it more than halfway outside the window
elseif (yOffsets[digit] + amount < -(fontHeight/2)) then
-- Account for rotating past multiple numbers in one frame
changeToCount = countMultiplier * ( ((math.floor((yOffsets[digit] + amount)/fontHeight)) * -1 )) -- + 1
yOffsets[digit] = slipAmount + ((yOffsets[digit] + amount) % -(fontHeight/2))
crankMultiplier = crankMultiplierDefault -- reset multiplier
else
-- Reduce ratio of cranking to tumbler movement as it approaches the halfway point to build up a feeling of tension.
if amount ~= 0 and crankMultiplier < -1 then
crankMultiplier = crankMultiplier - (crankMultiplier / 3)
end
-- not increasing count, just move the tumbler in the appropriate direction
yOffsets[digit] = yOffsets[digit] + amount
end
updateTally(changeToCount, countMultiplier)
end
One last feature I implemented was having the A Button + Crank drive the ones digits and B Button + Crank drive the hundreds digits. This way, it’s easy to very quickly and precisely use them in tandem to set the count to any amount.
A digit on the cusp of incrementing. I know it’s not much to look at in a still screenshot, but at least you can rest assured it’s not edited!
Having completed the main ways that the user would interact with it, Tally Pal is pretty close to the minimum viable product at this point, but I still want to do some polish and write some posts about adding audio and the framework for creating/saving/editing tallies.
One afternoon each week I volunteer in my kid’s classroom teaching math. They are working on standard model multiplication and occasionally trip over number places. It seems so intuitive to me, but of course it’s something that must be learned. Happily this has given me some experience with trying to code a decimal system display.
One of their worksheets require placing all the tiles of the digits 0-9 into places in some mathematical functions, like a sort of arithmetic Sudoku and one ah-ha moment I often see is the realization that the “0” can be placed to the left of other digits without changing the amount. Computers are of course not native base-10 counters either, so in order to emulate a mechanical display, they need a little help understanding place values as well.
Here’s the bit of script I landed on to ensure that the display always shows 4 digits, regardless of the count.
local countString = tostring(count)
-- generate leading 0's
if count < 1000 then
countString = "0" .. countString
end
if count < 100 then
countString = "0" .. countString
end
if count < 10 then
countString = "0" .. countString
end
That takes care of ensuring there’s always 4 digits even when the count is under 1,000, but rather than simply have the display update instantly when changing it, I plan to use script to animate them moving like the tumblers on a real tally counter. To accomplish this, I’ll need to be able to move each digit place independently, instead of drawing the entire string at once.
I can extract each character as a substring (since it is a string read left-to-right thousands are index 1, and ones are index 4), then I just need to tell it to draw those characters individually. After a long time working with visual scripting and editors, my hand is itching to drag elements around on the screen to place them, but of course it doesn’t work that way. I have to instruct the Playdate exactly what to draw where every frame. Still an interesting challenge, though!
To draw them, and to draw any digits on the “tumbler” that are incoming from underneath (when increasing) or above (when decreasing), I at first tried merely adding or subtracting 1 from that place digit, but of course Lua views them as integers instead of single digits, so I needed to describe using decimal place values, i.e. that counting above 9 or below 0 loops the digit around again and affects the next highest place value as well. Since the font is precisely 100 pixels wide, I can position it horizontally on the screen by subtracting 1 from the index and multiplying that by 100 (thousands at index 1 draw at x=0, and ones at index 4 draw at x=300).
if playdate.buttonIsPressed("down") then
-- Iterate through the number places from right to left
for i = 4, 1, -1 do
if isDigitChanging then
-- if it's counting down past zero, make the oncoming number a 9
if (string.sub (countString, i, i)) - 1 < 0 then
numberFont:drawText("0",((i-1)* 100), 100 + verticalOffsets[i])
numberFont:drawText("9",((i-1)* 100), verticalOffsets[i])
-- otherwise make the oncoming number 1 less and indicate this is the last digit to change
else
numberFont:drawText((string.sub (countString, i, i)),((i-1)* 100), 100 + verticalOffsets[i])
numberFont:drawText((string.sub (countString, i, i)) - 1,((i-1)* 100), verticalOffsets[i])
isDigitChanging = false
end
else
-- if this digit should not change, just draw it
numberFont:drawText((string.sub (countString, i, i)),((i-1)* 100),100)
end
updateVerticalOffset( i )
end
end
The array of vertical offsets are what drives the animation. On my mechanical counter, presing the plunger a little moves the tumbler a little, and past a certain point, the tumbler snaps into the new position. On the Playdate, the buttons have no travel, so I can’t replicate this feeling exactly, but having the action of changing the count divided into button pressed/held/released events can still imitate a certain amount of mechanical “chunkiness”.
So instead, when the button is pressed and held, it will move the character in the appropriate direction up to a maximum amount (accelerating using Euler’s number, which is a satisfying go-to constant for such things). Releasing it instantly changes the value and centers it in the window again. If I had curves at my disposal I might add a slight bounce to it, but I think coding up a curve is a little more than I’m willing to tackle at the moment, and this happens quickly enough to feel pretty good.
function updateVerticalOffset(i)
local multiplier = 2.71828
if verticalOffsets[i] * multiplier >= verticalOffsetMax then
verticalOffsets[i] = verticalOffsetMax
else
verticalOffsets[i] = verticalOffsets[i] * multiplier
end
end
The result when incrementing the count using a button press. I also wish I knew how to make animated GIFs from the Playdate simulator, but I think you get the idea.
After that, I just need to hide the vertically offset characters, so that it appears one is viewing these tumblers through a window. I will admit this is probably no the most optimal way to do it, but I simply drew a big white rectangle over the top and bottom of the count display. In a commercial project, I’d probably get some help with a way to display this stuff in a more performant manner, but it should be sufficient for my purposes.
Now my next challenge will be to get a similar animated behavior when using the crank to change the numbers!