Bringing games played on 2-dimensional screens into 3-dimensional space.
When games are played on flat 2-dimensional screen, it greatly limits the player’s interaction with the game. It simply kills some of the exciting aspects and new possibilities within the game that we might be able to see in higher dimensions. In this project, we wanted to give some games new life and new perspectives by implementing it in 3-D and truly, we were able to demonstrate this phenomenon.
The hardware needed for this project consisted of four key systems:
the Cube Control Hardware,
Port Expansion, and
LED Cube Construction
Constructing the 8x8x8 LED cube was initially a daunting task because it was immediately apparent that there were a great number of soldering required and there were so many ways the cube could go wrong (ex. shorting the LEDs). However, with careful planning and patience, the construction process turned out very successful and we ended up with a very nice looking cube.
The first step in the building process was to build the wooden templates that can hold 8×8 LEDs in place. We drilled each of the 64 slots that can fit the LED bulbs and put duct-tape on one side of the template so that the stickiness can better hold the LED in place when it gets slotted in. This made soldering very easy and organized. Each LEDs were also tested before slotting them into the template to make sure that there are no defects from the manufacturer.
For each layer, 8 LEDs (one column) were slotted in at a time and the ground pins (the cathode side) of the column were bent down flat onto the board all facing up, in one direction. These pins were all soldered together for a shared GND. Next, each of the voltage pins (the anode side) were bent to the right (perpendicular to the direction of the cathode pins) so that it levitates about half centimeter above the plane where the ground pins dwell (to prevent short-circuit between the voltage and ground). The bending process was done consistently with a use of a flat-head screw-driver (we held the screw-driver at an angle and bent the pin at exactly the right height). For each subsequent columns of LEDs, these voltage pins (anodes) were soldered with the levitating anode pins of the already established LED column to the right. After each column of LEDs have been soldered down, we tested each of the LEDs to ensure that none of them are burnt out during the construction process.
After all 8 LED sheets were constructed, we tested each of the sheets again to make sure there are no burnouts. When we were confident of each sheet’s functionality, we began to slot the constructed sheets vertically into the designated adapters (holders) sticking out on the provided PCB. This required a great amount of patience. After all of the sheets were put in place, we had all the constructed sheets resting vertically on the PCB. The final piece that bridged the cube for the 3 dimensional interaction was the inter-sheet cathode connections. 8 stripped wires were used to connect all 8 sheet’s cathode pins. This was done at each level from top-to-bottom.
Finally, these top-to-bottom layers were each connected to each of the 8 designated pins corresponding the layer number. We were finally able to manipulate the heck out of this 3-D structure with all the decoders and latches.
Cube Control Hardware
The kit we purchased came with hardware used to interface with the cube. The main features of this set were 8 Octal D-Type Latches (74HC573), a Darlington Transistor Array (ULN2803), and a custom PCB on which to mount the LED sheets.
Eight octal latches can control a total of 64 bits. Each layer of our cube had 64 LEDs. This meant that we could drive one layer of the cube, choosing exactly which LEDs to turn on, just by writing to these latches.
The PCB also had an interface by which the 8 inputs of each latch were all connected to an 8 bit input. The enables for the latches also had an 8 bit input. We call the former interface the Data In port and the latter the Row Select port. By setting high a single bit on the Row Select port, we can set the corresponding latch to mirror the Data In port. Doing this eight times lets us write to all eight latches.
The Darlington Transistor Array was used to select which layer of the cube was being driven. The 64 latch output are wired up to the 64 columns of the LED cube. To create the illusion of all eight layers of this cube being lit, at once, we quickly iterate through each layer, lighting each layer one-eighth of the time. The cathodes of each LED are connected in their row. Each of 8 rows in a layer are connected together. In this way, the collective cathode of a layer is wired into one collector output of the transistor array. This means that when the base input corresponding to this layer is driven high, the layer’s cathode terminal will be pulled to ground, causing any LED which has an anode (latch output) that is high to light. This meant that the 8 base inputs of this transistor formed another port which we named the Sheet Select.
Together, these ports allowed for quick and total control over the 512 LEDs, using only three 8-bit input ports.
One big challenge in our design, was allowing a PIC32 with about 16 usable pins to drive 24 cube control bits, while also using 5 pins to sample controller inputs. Our solution to this used a Microchip 16-bit Port Expander (MCP23S17), and also two 3-to-8 decoders (74LS238).
The first expansion was made easy through the use of Sean Carroll’s PIC32 Port Expander Library. This made it very easy to use 4 SPI pins as 16 output pins. 8 of these port expander pins were connected directly to the Data In port of the cube control circuit. The other 8 were used to control both the 8-bit Row Select and the 8-bit Sheet Select. When we realized that only one bit of the Row Select or Sheet Select would ever have to be high at any given time, we decided to try using 3 bits and a 3-to-8 decoder to achieve this functionality using less port expander pins. We wired up the Row Select and Sheet Select to the outputs of two 3-to-8 decoders and 6 pins from the port expander to the inputs. This let us fully control the cube, however, we were unable to turn off the Row and Sheet Selects, which led to some undesired behavior. We fixed this by using another port expander pin for the enables of each decoder. This way, by switching the enable, we could, for example, turn off the Sheet Select completely, while writing the next layer into the latches.
In this way, we only used 4 pins to control our Cube, and we could easily find 5 pins to use for the controller setup.
In order to interface with the game, we created a conroller with 6 buttons. These buttons were named “up”, “down”, “left”, “right”, “a”, and “b”, based on their utility in our game. In order to poll all six buttons, we used two output lines (output from the PIC), and three input lines (inputs to the PIC) to poll each button. Three buttons are connected to an output line. When they are pressed and that output line is high, they pull an input line high. Using this, we are able to set one output line high, then read the three input lines as the state of three different buttons, then set the other ouptut line high and read the three inputs as the other three buttons. Notably, in addition to setting an output line high, we actually change the other output line to an input temporarily. This ensures that the three buttons that we aren’t trying to sample are floating, and allows buttons from both lines to be pushed without tainting the readings. We also have a 100 kΩ pulldown resistor attached to each input line.
The software we created for this project included the following:
Cubeio (Cube Output Library),
Plane (A 3D game), and
The Main “Game Engine”.
In order to easily draw images on the cube, our group created a library called cubeio. This library provides functions such as writing the next layer to the screen, editting a buffer from which that layer is written, and other utilities to clear or flash the screen.
Cubeio has one function, displayNext which is a main driver of the cube. 480 times a second, this function is called. When this function is called, the main use of the cube control circuit is observed. First the Sheet Select is turned off so that intermediary parts of the write aren’t seen. Next, we iterate through each row, by setting the Row Select, then the Data In. We do this 8 times to write all 8 latches. Then we set Sheet Select to the next row.
The values to write to Data In are obtained from an array. This array is obtained with the call of the function initCubeio To change the state of the screen, one could use the functions provided by Cubeio, or just write to the array itself.
These files created the game that we played with the system. It was modular in design, having the functions game_init, game_update, buttonPressed, and buttonReleased. as the only interface to the main files. The update function was called 30 times a second, and updated the state of the enemies and any bullets that had been fired, as well as checking for collisions and spawning new enemies when ready. The button functions facilitated the movement of the player’s ship as well as allowing for soft resets of the game.
The game consisted of the player, controlling a “Plane” or 3x3x1 ship, firing at enemies that began to come from the far side of the cube towards the player. If the player can successfully line up a shot and shoot the enemy down, they get a point. Each time a point is scored, an LED on the border of the cube lights up. This was a nice way to visualize one’s success in the game. The LEDs light up around the edge in order. When a full ring around the cube is formed, the player wins.
The player’s ship can take damage any time an enemy hits part of it. That part will be destroyed. For example, if the top 3 parts of the ship are destroyed, the ship now appears as a 2x3x1 shape. As one takes more damage, it is easier to avoid future enemies, making the game more forgiving. If an enemy hits the center part (which is the part which fires bullets) then a random other part of the ship is destroyed. If all parts are destroyed, the player loses.
The enemies spawn once every 1-2 seconds and start at a random spot on the far side of the cube. They begin to move towards the player, slowly at first. If they reach the player’s side and do not hit the player, they restart their journey, but begin moving faster. As more and more enemies spawn, those enemies left to speed up become more of an issue. If the player is diligent and destroys the enemies before they start moving quickly, they have an easier time. However, if they waste time, they will soon find themselves in a blizzard of enemies.
The main code is a very simple protothreads setup. There are three threads which perform basic functions for the game.
An update thread updates the game about 30 times per second.
A display thread calls the cubeio function displayNext about 480 times a second.
A “key” or controller polling thread samples the controller about 30 times a second, and calls the game’s buttonPressed and buttonReleased functions accordingly.
Results & Conclusions
In the end, our system performed very well and our team is very happy with the results. We were able to achieve an impressive 60 three-dimensional fps (which corresponds to 480 two-dimensional fps). The controller and the game update functions were also able to operate very well at 30 times per second to provide a very smooth gaming experience. Due to these impressive specs, when the plane (the player) shoots at the incoming enemies, it had a pretty impressive visual effects. It really looks like something is being fired in 3 dimensional space.
One phenomenon that we unintentionally created was that of “shadows” in the cube. This occured because we were using a 3-to-8 decoder, so so whenever we cleared the Sheet Select, it would be reset to 0 for a very short instant, making the bottom row display. This meant that LEDs on the bottom row would light up VERY faintly if any LEDs above them were lit. This created an illusion of shadows on the ground under the fliying objects in our game. It was such a nice feature, we decided to keep it.
While we did achieve most of what we set out to do, we had a few ideas that we considered implementing in the future.
One such idea was to implement a system of writing letters to the cube. Using the same font file that the tft screen libraries use, we could have implemented a simple function to draw characters into the led buffer. This would have made it possible to better communicate with players, for example, reading out their score at the end of a game, or the name of the game at the beginning. Overall, this feature would have been useful to have in the cubeio library.
Another feature we could have implemented was brightness, blink rate, and other features of the LEDs. Expanding the library, we could have made it possible for users to have more control over the expressiveness of the cube. Allowing such things as the blink rate of an LED to be set and forgotten would greatly simplify any game’s code, and provide a more robust system.