Want something a little unique for your next tabletop role-play game? How about an electronic D20 with custom graphics for critical hits and misses? Today I'll show you how to build your own with an Arduino and a few simple parts.

Don't worry if you have never used an Arduino before, we have a getting started guide.

Build Plan

This is a simple project. An Arduino will drive an OLED display, and a button will roll the die. Custom graphics will show for critical hit or critical miss rolls. You can easily modify the code to be a D8, D10, or D12.

What You Need

arduino_d20_what_you_need
  • 1 x Arduino
  • 1 x 0.96" I2C OLED display
  • 1 x Push Button
  • 1 x 10k ? Resistor
  • 1 x Breadboard
  • Assorted hook up wires
  • Full code here, if you don't want to follow all the way through the written instructions.

Those are the core parts you need to build your own D20. You may wish to install it into a case (discussed below) and solder the circuit into a more permanent state. Here are the additional parts you will need to do that:

  • 4 x M2 x 10mm (0.4 inch) bolts
  • 4 x M2 nuts
  • 4 x 7mm (0.28 inch) washers
  • 9V battery snap (or suitable alternative)
  • Assorted heat shrink tubing

These OLED displays are very cool. They can usually be purchased in white, blue, yellow, or a mixture of the three. I have purchased one in blue, to match my case. Make sure you get an I2C model instead of SPI.

Almost any Arduino will be suitable. I have chosen a Nano, as they are small enough to fit into the case. Check out our buying guide for more information on Arduino models.

The Circuit

Here's the circuit you need:

arduino d20 circuit

Connect VCC and GND on the OLED display to the Arduino +5V and ground. Connect analog 4 on the Arduino to the pin labelled SDA. Connect analog 5 to the SCL pin. These pins contain the circuitry needed to drive the display using the I2C bus. The exact pins will vary by model, but A4 and A5 are used on the Nano and Uno. Check the Wire library documentation for your model if you’re not using an Uno or Nano.

Connect the battery to ground and the VIN pin. This stands for voltage in, and accepts a variety of different DC voltages -- but check your specific model first, and it can sometimes vary slightly.

Connect the button to digital pin 2. Notice how the 10k ? resistor is connected to ground. This is very important! This is known as a pull down resistor, and it prevents the Arduino detecting spurious data or interference as a button press. It also serves to protect the board. If this resistor was not used, +5V would go straight into ground. This is known as a dead short and is an easy way to kill an Arduino.

If you are soldering this circuit, protect your connections with heat shrink tubing:

arduino d20 heatshrink

Make sure you don't heat it up too much, and only do so once you are sure the circuit works. You may also wish to twist your cables into pairs. This keeps them neat and helps protect them from undue stress:

arduino d20 twisted cables

Button Test

Now that you have built the circuit, upload this test code (make sure to select the correct board and port from the Tools > Board and Tools > Port menus):

            const int buttonPin = 2;     // the number of the button pin

void setup() {
  pinMode(buttonPin, INPUT); // setup button     
  Serial.begin(9600); // setup serial
}

void loop(){
    if(digitalRead(buttonPin) == HIGH) {
        Serial.print("It Works");
        delay(250); 
    }
}
    

Once uploaded, keep the Arduino connected via USB and open the serial monitor (Top Right > Serial Monitor). You should see the words It Works appear every time you press the button.

If nothing happens, go and double check your circuit.

OLED Setup

arduino oled test

You need to install two libraries to drive the display. Download the Adafruit_SSD1306 and Adafruit-GFX [No Longer Available] libraries from Github, and save them into your library folder. If you are not sure where you library folders are, go read my retro gaming tutorial, where I configure this same display in more detail.

Restart your Arduino IDE and upload a test sketch from the File > Examples menu. Select Adafruit SSD1306 and then ssd1306_128x64_i2c. Upload this code (it will take a while), and you should see lots of shapes and patterns on the display:

arduino oled test

If nothing happens, double check your connections. If, after checking, it still will not work, you will need to modify the sample code.

Change this line (at the start of the setup function):

            display.begin(SSD1306_SWITCHCAPVCC, 0x3D);
    

To this:

            display.begin(SSD1306_SWITCHCAPVCC, 0x3C);
    

This tells the library specific details about the display you are using. You should now be all set to the continue with the build.

The Case

If you are building this on a breadboard, or do not wish to box it up, then you can skip this step.

arduino d20 case

I designed and 3D printed this box. Get the files on Thingiverse. Don't worry if you don't have a 3D printer -- online services 3D Hubs and Shapeways provide online printing services.

You could easily make this box out of wood, or by purchasing a plastic project box.

The lid is a simple push fit design, and contains a few cutouts for the hardware:

arduino d20 case

The Code

Now that everything is ready, it's time for the code. Here's how it will work in Pseudocode:

            if button is pressed
    generate random number
        if random number is 20
            show graphic
        else if random number is 1
            show graphic
        else
            show number
    

In order for this to work properly, a random number needs to be generated -- this is the roll of the die. Arduino has a random number generator called random, but shouldn't use it. While it is good enough for basic random tasks, it's just not random enough for an electronic die. The reasons why are somewhat complicated, but you can read more if you are interested at boallen.com.

Download the TrueRandom library by sirleech on Github. Add this to your library folder and restart the IDE.

Now create a new file and setup your initial code (or just grab the finished code from GitHub):

            #include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <TrueRandom.h> 

Adafruit_SSD1306 display(4);

void setup() {
    display.begin(SSD1306_SWITCHCAPVCC, 0x3C); // setup the OLED
    pinMode(buttonPin, INPUT); // setup button  
}

void loop() {
 
}
    

This code configures the OLED, and includes all the libraries you need to communicate with it, along with your new random number library. Now add this to the main loop:

            if(digitalRead(buttonPin) == HIGH) {
    delay(15);
    if(digitalRead(buttonPin) == HIGH) {
          display.fillScreen(BLACK); // erase the whole display
          display.setTextColor(WHITE);
          display.setTextSize(2);
          display.setCursor(0, 0);
          display.println(TrueRandom.random(1, 21)); // print random number
          display.display(); // write to display
          delay(100);
    }
}
    

This is quite basic at the minute, but it is a working D20. Whenever the button is pressed, a random number between one and 20 is shown on the screen:

arduino d20 first code

This works well, but it's a bit boring. Let's make it better. Create two new methods, drawDie and eraseDie:

            void drawDie() {
    display.drawRect(32, 0, 64, 64, WHITE);
}
    

These will draw a die in the middle of the screen. You may wish to make this more complicated, perhaps by drawing a D20, or a D12 and so on, but it's simpler to draw a basic six-sided die. Here's the basic usage:

            drawDie();
    

Next, modify your main loop to draw the random number, only bigger and in the middle. Change the text size and cursor to this:

            display.setTextColor(WHITE);
display.setCursor(57, 21);
    

It looks much better now:

arduino d20 single character

The only problem is with numbers larger than nine:

arduino d20 dual character

The fix for this is simple. Any numbers less than 10 will have the cursor set to a different position than those numbers 10 or larger. Replace this line:

            display.setCursor(57, 21);
    

With this:

            int roll = TrueRandom.random(1, 21); // store the random number
if (roll < 10) {
    // single character number
    display.setCursor(57, 21);
 }
else {
    // dual character number
    display.setCursor(47, 21);  
}
    

Here's what that looks like now:

arduino d20 fixed dual character

All that's left now is for the images when you roll a critical hit or miss. There are a few steps involved, but it's a simple enough process.

Find a suitable image you want to use (the simpler the better as the display is single color only). Here are the images I used:

arduino d20 artwork

Any image you wish to use will need to be converted to a HEX array. This is a representation of the image in code form. There are many tools available to do this, and some are written specifically for OLED displays. The easiest way is to use the PicturetoC_Hex online tool. Here are the settings needed:

arduino image to hex

Upload your image, and set the code format to HEX:0x. Set Used for to Black/White for all draw image function. Leave all the other options as the defaults. You can resize the image here if you need to. Press Get C String and you should see the image data appear:

arduino d20 image data

You will need this generated data in a minute. Create two functions called drawExplosion and drawSkull (or a suitable name for your version). Here's the code:

            void drawExplosion() {
    // store image in EEPROM
    static const unsigned char PROGMEM imExp[] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xfc,0x00,0x00,0x00,0x00,0x00,0x78,0x7f,0xff,0xc0,0x00,0x00,0x00,0x00,0xfe,0xff,0xff,0xf0,0x00,0x00,0x00,0x3f,0xff,0xff,0xff,0xfb,0x00,0x00,0x00,0x7f,0xff,0xff,0xff,0xff,0xc0,0x00,0x00,0x7f,0xff,0xff,0xff,0xff,0xff,0x00,0x01,0xff,0xff,0xff,0xff,0xff,0xff,0x80,0x03,0xff,0xff,0xff,0xff,0xff,0xff,0x80,0x03,0xff,0xff,0xff,0xff,0xff,0xff,0x80,0x03,0xff,0xff,0xff,0xff,0xff,0xff,0xc0,0x03,0xff,0xff,0xff,0xff,0xff,0xff,0xf0,0x07,0xff,0xff,0xff,0xff,0xff,0xff,0xf0,0x07,0xff,0xff,0xff,0xff,0xff,0xff,0xf0,0x07,0xff,0xff,0xff,0xff,0xff,0xff,0xe0,0x07,0xff,0xff,0xff,0xff,0xff,0xff,0xc0,0x0f,0xff,0xff,0xff,0xff,0xff,0xff,0xe0,0x1f,0xff,0xff,0xff,0xff,0xff,0xff,0xe0,0x1f,0xff,0xff,0xff,0xff,0xff,0xff,0xe0,0x0f,0xff,0xff,0xff,0xff,0xff,0xff,0xf0,0x03,0xff,0xff,0xff,0xff,0xff,0xff,0xf0,0x03,0xff,0xff,0xff,0xff,0xff,0xff,0xf0,0x03,0xff,0xff,0xff,0xff,0xff,0xff,0xe0,0x01,0xff,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0x0f,0xff,0xff,0xff,0xff,0xfe,0x00,0x00,0x07,0xff,0xff,0xf9,0xff,0xd8,0x00,0x00,0x00,0x3f,0xff,0xf0,0x0f,0x00,0x00,0x00,0x00,0x1f,0x1f,0xf0,0x00,0x00,0x00,0x00,0x00,0x00,0x0f,0xe0,0x00,0x00,0x00,0x00,0x00,0x00,0x0f,0xe0,0x00,0x00,0x00,0x00,0x00,0x00,0x0f,0xe0,0x00,0x00,0x00,0x00,0x00,0x00,0x0f,0xf0,0x00,0x00,0x00,0x00,0x00,0x00,0x0f,0xf0,0x00,0x00,0x00,0x00,0x00,0x00,0x0f,0xf0,0x00,0x00,0x00,0x00,0x00,0x00,0x3f,0xf8,0x00,0x00,0x00,0x00,0x00,0x00,0x7f,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x7f,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x7f,0xff,0x00,0x00,0x00,0x00,0x00,0x00,0x7f,0xfe,0x00,0x00,0x00,0x00,0x00,0x00,0x7f,0xfc,0x00,0x00,0x00,0x00,0x00,0x00,0x0f,0xf0,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0xf0,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0xe0,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0xe0,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0xe0,0x00,0x00,0x00,0x00,0x00,0x00,0x07,0xf0,0x00,0x00,0x00,0x00,0x00,0x00,0x0f,0xf0,0x00,0x00,0x00,0x00,0x00,0x00,0x0f,0xf8,0x00,0x00,0x00,0x00,0x00,0x00,0x0f,0xfc,0x00,0x00,0x00,0x00,0x00,0x00,0x1f,0xff,0x00,0x00,0x00,0x00,0x00,0x0f,0xff,0xff,0xff,0x00,0x00,0x00,0x07,0xff,0xff,0xff,0xff,0xf0,0x00,0x00,0x0f,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0x1f,0xff,0xff,0xff,0xff,0xff,0x00,0x00,0x1f,0xff,0xff,0xff,0xff,0xfc,0x00,0x00,0x01,0xbf,0xff,0xff,0xff,0x30,0x00,0x00,0x00,0x13,0xf7,0xb8,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
    };
    display.drawBitmap(0, 0, imExp, 64, 62, 1); // draw mushroom cloud
}

void drawSkull() {
    // store image in EEPROM
    static const unsigned char PROGMEM imSku[] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0xe0,0x00,0x00,0x00,0x00,0x30,0x00,0x00,0xf0,0x00,0x00,0x00,0x00,0x78,0x00,0x07,0xf0,0x00,0x00,0x00,0x00,0xfc,0x00,0x07,0xf8,0x00,0x00,0x00,0x00,0xfe,0x00,0x07,0xf8,0x00,0x00,0x00,0x01,0xfe,0x00,0x07,0xfc,0x00,0x00,0x00,0x01,0xfe,0x00,0x07,0xfe,0x00,0x3f,0xc0,0x03,0xfe,0x00,0x01,0xff,0x81,0xff,0xfc,0x07,0xec,0x00,0x00,0x3f,0xc7,0xff,0xff,0x1f,0xc0,0x00,0x00,0x0f,0xcf,0xff,0xff,0xdf,0x00,0x00,0x00,0x07,0xbf,0xff,0xff,0xee,0x00,0x00,0x00,0x01,0x7f,0xff,0xff,0xf0,0x00,0x00,0x00,0x00,0xff,0xff,0xff,0xf8,0x00,0x00,0x00,0x01,0xff,0xff,0xff,0xf8,0x00,0x00,0x00,0x03,0xff,0xff,0xff,0xfc,0x00,0x00,0x00,0x07,0xff,0xff,0xff,0xfe,0x00,0x00,0x00,0x0f,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x0f,0xff,0xff,0xff,0xff,0x00,0x00,0x00,0x1f,0xff,0xff,0xff,0xff,0x80,0x00,0x00,0x1f,0xff,0xff,0xff,0xff,0x80,0x00,0x00,0x1f,0xff,0xff,0xff,0xff,0x80,0x00,0x00,0x1f,0xff,0xff,0xff,0xff,0x80,0x00,0x00,0x1f,0xff,0xff,0xff,0xff,0x80,0x00,0x00,0x1f,0xff,0xff,0xff,0xff,0x80,0x00,0x00,0x1e,0x3f,0xff,0x3f,0xc7,0x80,0x00,0x00,0x1e,0x0c,0x0f,0x00,0x07,0x80,0x00,0x00,0x1e,0x00,0x0f,0x00,0x0f,0x80,0x00,0x00,0x1e,0x00,0x19,0x80,0x0f,0x00,0x00,0x00,0x0f,0x00,0x19,0x80,0x0f,0x00,0x00,0x00,0x0d,0x00,0x30,0xc0,0x1f,0x00,0x00,0x00,0x05,0x80,0x70,0xc0,0x1e,0x00,0x00,0x00,0x05,0xf0,0xe0,0xe0,0x36,0x00,0x00,0x00,0x01,0xff,0xe0,0x7f,0xf0,0x00,0x00,0x00,0x03,0xff,0xc4,0x7f,0xf0,0x00,0x00,0x00,0x03,0xff,0xcc,0x7f,0xf0,0x00,0x00,0x00,0x03,0xff,0xcc,0x7f,0xf0,0x00,0x00,0x00,0x03,0xff,0x9e,0x7f,0xf0,0x00,0x00,0x00,0x00,0xff,0xfe,0x7f,0xc0,0x00,0x00,0x00,0x00,0x01,0xff,0xf8,0x1c,0x00,0x00,0x00,0x03,0xe0,0x3f,0x01,0xbf,0x00,0x00,0x00,0x07,0xa6,0x40,0x09,0x9f,0x80,0x00,0x00,0x1f,0x27,0x5a,0x39,0x9f,0xf8,0x00,0x01,0xff,0x27,0xdb,0x39,0x0f,0xfc,0x00,0x03,0xfe,0x31,0x7f,0x39,0x07,0xfc,0x00,0x03,0xfc,0x10,0x1a,0x02,0x03,0xf8,0x00,0x03,0xf8,0x10,0x00,0x02,0x01,0xf0,0x00,0x01,0xf8,0x10,0x00,0x02,0x01,0xe0,0x00,0x00,0x78,0x10,0x00,0x02,0x00,0xe0,0x00,0x00,0x70,0x30,0x00,0x02,0x00,0x00,0x00,0x00,0x30,0x20,0x00,0x03,0x00,0x00,0x00,0x00,0x00,0x64,0x00,0x1b,0x00,0x00,0x00,0x00,0x00,0x73,0x55,0x63,0x00,0x00,0x00,0x00,0x00,0xf9,0x55,0x4f,0x00,0x00,0x00,0x00,0x00,0x7f,0x14,0x1f,0x00,0x00,0x00,0x00,0x00,0x1f,0xe0,0xfe,0x00,0x00,0x00,0x00,0x00,0x0f,0xff,0xfc,0x00,0x00,0x00,0x00,0x00,0x07,0xff,0xf0,0x00,0x00,0x00,0x00,0x00,0x03,0xff,0xc0,0x00,0x00,0x00,0x00,0x00,0x00,0x38,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
    };
    display.drawBitmap(0, 0, imSku, 60, 64, 1); // draw skull cloud
}
    

If you wish to use the images I have used, then go ahead and copy the code. If you want to use your own images you generated earlier, copy the byte code into the imSku and imExp arrays as required.

Here's what those images look like on the display:

arduino oled images

The most important part of that code is this line:

            static const unsigned char PROGMEM imSku[]
    

This tells the Arduino to store your images in the EEPROM (what is EEPROM?) instead of its RAM (quick guide to RAM). The reason for this is simple; the Arduino has limited RAM, and using it all up to store images may not leave any remaining for your code to execute

Modify your main if statement to show these new graphics when a one or 20 is rolled. Note the lines of code to show the number rolled alongside the images as well:

            if(roll == 20) {
    drawExplosion();
    display.setCursor(80, 21);
    display.println("20"); 
}
else if(roll == 1) {
    display.setCursor(24, 21);
    display.println("1"); 
    drawSkull();  
}
else if (roll < 10) {
    // single character number
    display.setCursor(57, 21);
    display.println(roll); // write the roll
    drawDie(); // draw the outline
}
else {
    // dual character number
    display.setCursor(47, 21); 
    display.println(roll); // write the roll 
    drawDie(); // draw the outline
}
    

And here's what those new rolls look like:

arduino d20 critical images

That's all for the code side (go grab the code from GitHub if you skipped all that). You could easily modify this to be a D12, D8, and so on.

Final Assembly

Now that everything else is finished, it's time to box everything up. Bolt the display on, making sure not to over-tighten the bolts. This is possibly the most difficult part. I cracked a display doing so, so you may wish to use some plastic washers. I cut some squares out of Plasticard:

Arduino D20 Spacers

The small nuts and bolts can tricky to connect. Tip: Use a small piece of Blu-Tack on the end of a screwdriver to initially seat the nuts:

Arduino D20 Nut

Screw the button on, connect the battery and close the lid. Be careful not to trap any wires, or bundle them up too tightly, possibly causing a short. Depending on the length of your trailing leads, you may need to protect exposed connections with some insulation (a serial box works well):

Arduino D20 Inside Cardboard

Here's what it looks like inside:

Arduino D20 Inside

And here's the finished product:

Arduino D20 Skull

You should now be the proud owner of an electronic D20!

What modifications did you make? Did you change the images? Let us know in the comments, we would love to see what you did!