Untitled Grayscale RPG Dev Log

Thanks :smiley:

I think at least a few games could render that fast. I just looked into your game “Kong”:
Looks like it runs at 70fps while only using ~50% CPU. With only a slight focus on performance I’d bet it could easily render at 156 Hz.

Even if the sprites are on the FX chip it’s not too much of an issue. For this game I have my own sprite routines which sacrifice code size and flexibility (limited draw modes, height must be multiple of 8) for speed. The FX versions maintain the minimal 18 cycles/byte cadence which is competitive with the prog versions in the Sprites lib. Drawing lots of tiny sprites has a penalty due to the seek overhead so I keep the dialog font in prog for that reason and text rendering has its own specialized method as well.

OK … that’s interesting. I am trying to recall whether Kong has lots of little sprites or less, big ones.

Is your game using tile sheets to render the landscape? If so, this would be similar to Prince of Persia and probably give me some confidence that it might be achievable.

Yes, 8x4 map chunks of 16x16 tiles, and sprites are also 16x16.

EDIT: The demo in the ArduboyG thread is just over 50% cpu rendering only the tiles, though they are in prog, not FX:

How do you uh . . . use/make spritesheets? I think a spritesheet for all my map tiles could prove quite handy.

A straightforward way is to arrange your tiles vertically into one image (example from Arduventure).

Convert your image to the Sprites format, which encodes the dimensions of a single tile at its header.

const uint8_t TILESHEET[] PROGMEM =
   // image data here

Note that if the height of your tiles is not a multiple of 8, you will need to align them vertically to 8 pixels in the tilesheet image.

Then use the frame parameter of the drawing methods in the Sprites library to select the tile from the tilesheet when drawing.

1 Like


You might want to look at this tutorial > Make Your Own Arduboy Game: Part 8 - Starting DinoSmasher

Part 9 in particular shows how to use the concepts the @brow1067 introduced above.

1 Like

You may wish to try disecting this:

Note in particular TileSheet.h and the images from which it’s generated.

(Also, AB Sprite Editor can generate sprite frames in the necessary format.)

1 Like

The display only runs at somewhere around 60fps though right? What’s the advantage for having such an overdriven tick rate?

Most games use the default bootup, which has Arduboy2Core::bootOLED set the display to these settings:

  1. Fosc is set by a configurable oscillator to one of 16 settings (0-15, slowest to fastest).
  2. Fosc is divided by a clock divider, and the divided clock is used to clock the row drivers.
  3. Driving each row takes 50 + precharge phase 1 + precharge phase 2 cycles.
  4. The number of rows to drive each frame is the mux ratio.

So the frame rate is calculated as:

FPS = Fosc / Divider / (50 + Precharge cycles) / Mux Ratio

The thing that’s less known is the value of Fosc for each oscillator setting. The Thumby community, which came up with the trick used to achieve frame sync without a vsync signal, have experimentally determined that for a setting of 15, Fosc=530kHz is a safe minimum value to assume that all SSD1306s will achieve.

My Arduboys seem to achieve about 570 kHz, which is the value my sim assumes. I’ve also put them through many battery drain tests rendering grayscale the entire time to ensure Fosc doesn’t begin to droop too low before brownout.

This game and ArduboyG reduce the precharge phase 2 to two cycles to speed up the row drive time:

My Arduboys tend to render at 165-170 Hz. However, if the value of Fosc were as low as 530 kHz, the frame rate would be

FPS = 530kHz / 1 / (50+1+2) / 64 = 156.25 Hz

So 156 Hz is the greatest allowable “safe” rate. I want to render as fast as possible to both avoid strobing and maximize effective brightness.

EDIT: If you only need one shade of gray (three shades total), you could try setting the precharge phase 2 back to 15 cycles (to try to maintain brightness) and rendering at

FPS = 530kHz/(50+1+15)/64 = 125 Hz

to give you more rendering time. With three levels the strobing is much less of a concern and you can afford higher frame times.


Great write-up; very useful.

Just a note~

A recent change in OLED module supplier broke this. These units have much slower refresh rates (amongst other slight differences); it’s a risk of using things out-of-spec


Yes :frowning: Hopefully that doesn’t happen with Arduboy units. If it does I think only three shades gray will be visually acceptable and there will probably need to be some sort of calibration value stored in EEPROM.

The Arduboy having 64 rows vs the Thumby’s 40 already means frame times are much longer by necessity.


Note: the recordings are now taken from the sim instead of my windows port. The Arduboy version renders at 128x63 because the 64th row is used for the grayscale park row in the actual game. This is visible as a black line at the bottom of the recordings but isn’t noticeable on an actual Arduboy (unless you take the time to count the pixel height).


Movement mechanics turned out to be fairly involved. The world map is large and sometimes has complex collision structure so I wanted the movement to respond smoothly and not frustrate the player.

There are four bits of collision info per tile type, one per quarter tile.

When the player runs into a solid object near a corner, the collision resolution can nudge him to the side instead of canceling the movement entirely.

Diagonal movement is supported.

For diagonal movement, the collision is also resolved to slide along an edge instead of canceling the whole movement.

Diagonal movement is coded to allow passing through tight diagonal gaps where collision corners meet.
Screenshot 2023-03-10 1616132

Sprites other than the player are also collision objects. Most pause along their path when the player approaches them to allow more easily selecting them for interaction.


Some sprites don’t stop for the player, and can instead push the player around. The player can be crushed when pushed into another solid object.


Looks wonderful!
I’m a little concerned if the need to navigate level design diagonally will be fun… or a chore!?.. This was also done in Team ARG’s Virus / zombie game; On a small d-pad it doesn’t feel great.

1 Like

Are you referring to the d-pads of the official Arduboy or homemade ones? The official Arduboy is comfortable enough for me, but admittedly I haven’t been considering the ergonomics of other button types.

Currently some areas (such as the one shown above) do require diagonal movement to access. Taking yet more inspiration from @filmote’s dev process I think near completion there will be some sort of beta period for bug reporting and where game design choices can be molded to community preferences.

Homemade Arduboys using the usual tactile push buttons suck for d-pads. I’ve played Team Arg’s Virus-LQP (I forgot the number after the LQP) on my DIY Arduboy and it’s really uncomfortable. But anyway, I guess it’s just a hardware concern, nothing you can really do to fix it for those devices.

The Arduboy Mini will have different buttons, although I struggled with the OG d-pad!

Look awesome! New dev here, just wondering are those display/cpu monitor graphs part of the arduino program? Or is there another emulator that you’re running it on?

1 Like

Welcome :smiley:

While making this game I found debugging and tweaking performance to be so difficult on hardware that I decided to make a tool to assist dev with software emulation.


oh cool! bookmarked =]

Update: New audio system to replace ATMlib2.

I’ve been working on a new synth library designed from the start for high performance and FX streaming – I’ll release it shortly.

I’m very happy with the result so far. In my use case, the new library frees about 90 bytes of RAM and 1300 bytes of prog, and allowed increasing the channel count from 2 to 4 while still providing a large reduction in CPU usage. Frame times now have a healthy margin at all times with music enabled.

One thing I wanted from ATMlib[2] and added to the new lib is a master volume control. I plan to use this for fade out transitions when changing songs, so the overworld music doesn’t abruptly stop when entering a battle.

Right now I am using placeholder music from NES games. Eventually I’ll need to figure out new music or seek collaboration.

Brief explanation of what the lib does to save CPU from the lib's readme

There have primarily been two approaches to audio synthesis on the Arduboy.

The first types uses a timer that toggles a sound pin at the timer frequency, meaning the timer frequency is constantly adjusted to play different notes. This approach is extremely cheap on CPU usage as the work done in the ISR is minimal and interrupts only hit at the transitions of the square wave, but it is limited to full-amplitude square wave synthesis with one or two channels. This approach is used in ArduboyTones (one timer) and ArduboyPlaytune (two timers, one for each sound pin).

The second type uses timer4 to produce PWM analog values on the sound pins, with the PWM duty cycle controlled by another timer. In principle, this allows full control over the audio waveform. Existing solutions like ATMlib and ATMlib2 use a constant sample rate with a high frequency control timer. Each interrupt remixes the channels and sets the duty cycle. This produces a high quality synthesis and allows for many effects like vibrato and tremelo and precise volume control, but requires high CPU usage due to the continual remixing of the channels at a high rate.

SynthU attempts to allow for the benefits of the PWM approach while avoiding most of the performance downside. The high CPU usage in the PWM approach is due to the mixing interrupt triggering once per sample. If the waveform actually changes each sample, as it does for a noise channel, this is unavoidable. But if the mixing only involves square waves, there will be long sample sequences where the duty cycle doesn’t change because no wave transitions occur in any channel. Thus, the mixing ISR in SynthU dynamically schedules the next interrupt to occur on the sample following the next transition in any channel. The resulting waveform stays the same, and all effects (vibrato, tremelo, frequency/volume sweeps, etc) are still possible. However, noise channels are currently not implemented because their usage would defeat this optimization (this may be revisited later).