Juego Breakout táctil Arduino: Arcade clásico en pantalla TFT 2,4″
Componentes y suministros
| Componente | Cantidad | Notas |
|---|---|---|
| Arduino UNO | 1 | |
| AZ-Delivery 2.4 TFT LCD Touch Display Shield para Arduino | 1 | Driver ILI9341, comunicación paralela 8-bit, 240×320 px |
Aplicaciones y servicios en línea
- Arduino IDE
Sobre el proyecto
Pequeña versión del clásico juego Breakout para Arduino UNO y pantalla TFT LCD (240×320 px), driver ILI9341 con comunicación paralela de 8 bits.
El juego
Este Breakout tiene múltiples pantallas con filas y columnas de ladrillos configurables, hasta 8 filas, cada dos filas un color distinto, que pueden programarse activas/inactivas con distintos patrones. Con una sola bola, usando el panel táctil, el jugador debe derribar el máximo de ladrillos posible usando paredes y/o la paleta inferior para rebotar la bola contra los ladrillos y eliminarlos. Si la paleta falla el rebote, se pierde una vida.
Cada fila de ladrillos da distinta puntuación:
uint8_t pointsForRow[] = {7, 7, 5, 5, 3, 3, 1, 1};
Cada nivel configura tamaño de paleta y bola. La velocidad de la bola aumenta en cada golpe; se puede configurar velocidad inicial por pantalla. Según el punto de la paleta que golpea la bola, cambia también la velocidad horizontal.
Puedes definir nuevas pantallas con distintos patrones de pared:
Cómo jugar
Sujeta el dispositivo con las manos y usa los pulgares sobre la pantalla para mover la paleta a izquierda o derecha.
Definir una nueva pantalla
Esta estructura define una nueva pantalla:
typedef struct game_type {
int ballsize;
int playerwidth;
int playerheight;
int exponent;
int top;
int rows;
int columns;
int brickGap;
int lives;
int wall[GAMES_NUMBER];
int initVelx;
int initVely;
} game_type;
Y añade la nueva pantalla al conjunto:
game_type games[GAMES_NUMBER] =
// ballsize, playerwidth, playerheight, exponent, top, rows, columns, brickGap, lives, wall[8], initVelx, initVely
{
{ 10, 60, 8, 6, 40 , 8, 8, 3, 3, {0x18, 0x66, 0xFF, 0xDB, 0xFF, 0x7E, 0x24, 0x3C} , 28, -28},
// ... más niveles
};
Patrón de pared
El patrón de pared es un array de 8×8 bits.
Ejemplo:
{0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA}
Corresponde a:
1,0,1,0,1,0,1,0
1,0,1,0,1,0,1,0
1,0,1,0,1,0,1,0
1,0,1,0,1,0,1,0
1,0,1,0,1,0,1,0
1,0,1,0,1,0,1,0
1,0,1,0,1,0,1,0
1,0,1,0,1,0,1,0
Produce este muro (espejado):
Hardware
Este proyecto usa shield AZ-Delivery 2.4" TFT LCD con touch resistivo 4 hilos y lector SD integrado.
Más sobre este shield en mi artículo "Road testing an AZ-Delivery 2,4 TFT LCD Touch Display".
Montaje del shield
Solo enchufa el shield sobre el Arduino.
Librerías
#include <Adafruit_GFX.h> // Librería gráfica base
#include <Adafruit_TFTLCD.h>
#include <TouchScreen.h>
Calibrar la pantalla táctil
Debes calibrar el display para que la posición táctil sea correcta. La librería MCUFriend_kbv incluye ejemplo "TouchScreen_Calibr_native". Envía resultados al puerto serie. Abre Monitor Serie del Arduino IDE para copiar el código generado.
Sigue instrucciones en pantalla: pulsa y mantén los marcadores de posición (blancos). Al terminar, la calibración se muestra en pantalla y por puerto serie.
Para este proyecto necesitas los datos de calibración PORTRAIT.
TouchScreen.h GFX Calibration
Making all control and bus pins INPUT_PULLUP
Typical 30k Analog pullup with corresponding pin
would read low when digital is written LOW
e.g. reads ~25 for 300R X direction
e.g. reads ~30 for 500R Y direction
Testing : (A2, D8) = 26
Testing : (A3, D9) = 28
ID = 0x9341
cx=153 cy=103 cz=534 X, Y, Pressure
cx=150 cy=475 cz=406 X, Y, Pressure
cx=155 cy=868 cz=231 X, Y, Pressure
cx=517 cy=103 cz=561 X, Y, Pressure
cx=535 cy=855 cz=364 X, Y, Pressure
cx=884 cy=88 cz=650 X, Y, Pressure
cx=908 cy=478 cz=557 X, Y, Pressure
cx=902 cy=864 cz=488 X, Y, Pressure
*** COPY-PASTE from Serial Terminal:
const int XP=8,XM=A2,YP=A3,YM=9; //240x320 ID=0x9341
const int TS_LEFT=118,TS_RT=931,TS_TOP=72,TS_BOT=887;
PORTRAIT CALIBRATION 240 x 320
x = map(p.x, LEFT=118, RT=931, 0, 240)
y = map(p.y, TOP=72, BOT=887, 0, 320)
LANDSCAPE CALIBRATION 320 x 240
x = map(p.y, LEFT=72, RT=887, 0, 320)
y = map(p.x, TOP=931, BOT=118, 0, 240)
Animación
Mover imagen por pantalla en el tiempo: posición += velocidad × dt.
pos += vel * dt;
Evitar aritmética de punto flotante
Resolución ILI9341 es 240×320 → necesitamos dos enteros de 9 bits para referenciar un pixel. Usando enteros de 16 bits nos quedan 6 bits para parte decimal.
nnnn nnnn nndd dddd
Llamamos 6 el exponente binario. Esos 6 bits dan parte decimal 0.000 a 0.63. Así usamos aritmética entera evitando float.
Para obtener parte entera: desplazamiento aritmético a la derecha.
number >> exponent
state.ballx += state.velx;
state.bally += state.vely;
// check ball collisions and exit
checkBallCollisions(game, &state, state.ballx >> game->exponent, state.bally >> game->exponent);
checkBallExit(game, &state, state.ballx >> game->exponent, state.bally >> game->exponent);
Modo Demo
Descomenta la directiva y la paleta seguirá a la bola (ver vídeos demo):
#define DEMO_MODE
¡Disfrútalo!
¡Crea nuevos patrones y niveles y compártelos!
Código
- Arduino Breakout (Arduino)
Arduino Breakout (Arduino)
/* Arduino Touch TFT Breakout
Classic breakout game
Parts needed:
Ardunio UNO
AZ-Delivery 2.4 TFT LCD Touch Display Arduino Shield or compatible
This example code is in the public domain.
Modified 07 11 2020
By Enrique Albertos */
// #define DEMO_MODE
#include <Adafruit_GFX.h> // Core graphics library
#include <Adafruit_TFTLCD.h>
#include <TouchScreen.h>
#define BLACK 0x0000
#define BLUE 0x001F
#define RED 0xF800
#define GREEN 0x07E0
#define CYAN 0x07FF
#define MAGENTA 0xF81F
#define YELLOW 0xFFE0
#define WHITE 0xFFFF
#define PRIMARY_COLOR 0x4A11
#define PRIMARY_LIGHT_COLOR 0x7A17
#define PRIMARY_DARK_COLOR 0x4016
#define PRIMARY_TEXT_COLOR 0x7FFF
#define LCD_CS A3
#define LCD_CD A2
#define LCD_WR A1
#define LCD_RD A0
#define LCD_RESET A4
Adafruit_TFTLCD tft(LCD_CS, LCD_CD, LCD_WR, LCD_RD, LCD_RESET);
#define LOWFLASH (defined(__AVR_ATmega328P__) && defined(MCUFRIEND_KBV_H_))
#define MINPRESSURE 40
#define MAXPRESSURE 1000
const int16_t XP = 8, XM = A2, YP = A3, YM = 9; //240x320 ID=0x9341
const int16_t TS_LEFT = 122, TS_RT = 929, TS_TOP = 77, TS_BOT = 884;
const TouchScreen ts = TouchScreen(XP, YP, XM, YM, 300);
#define SCORE_SIZE 30
char scoreFormat[] = "%04d";
typedef struct gameSize_type {
int16_t x, y, width, height;
} gameSize_type;
gameSize_type gameSize;
uint16_t backgroundColor = BLACK;
int level;
const uint8_t BIT_MASK[] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80};
uint8_t pointsForRow[] = {7, 7, 5, 5, 3, 3, 1, 1};
#define GAMES_NUMBER 16
typedef struct game_type {
int ballsize;
int playerwidth;
int playerheight;
int exponent;
int top;
int rows;
int columns;
int brickGap;
int lives;
int wall[GAMES_NUMBER];
int initVelx;
int initVely;
} game_type;
game_type games[GAMES_NUMBER] =
// ballsize, playerwidth, playerheight, exponent, top, rows, columns, brickGap, lives, wall[8], initVelx, initVely
{
{ 10, 60, 8, 6, 40 , 8, 8, 3, 3, {0x18, 0x66, 0xFF, 0xDB, 0xFF, 0x7E, 0x24, 0x3C} , 28, -28},
{ 10, 50, 8, 6, 40 , 8, 8, 3, 3, {0xFF, 0x99, 0xFF, 0xE7, 0xBD, 0xDB, 0xE7, 0xFF} , 28, -28},
{ 10, 50, 8, 6, 40 , 8, 8, 3, 3, {0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55} , 28, -28},
{ 8, 50, 8, 6, 40 , 8, 8, 3, 3, {0xFF, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xC3, 0xFF} , 34, -34},
{ 10, 40, 8, 6, 40 , 8, 8, 3, 3, {0xFF, 0xAA, 0xAA, 0xFF, 0xFF, 0xAA, 0xAA, 0xFF} , 28, -28},
{ 10, 40, 8, 6, 40 , 8, 8, 3, 3, {0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA} , 28, -28},
{ 12, 64, 8, 6, 60 , 4, 2, 3, 4, {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} , 20, -20},
{ 12, 60, 8, 6, 60 , 5, 3, 3, 4, {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} , 22, -22},
{ 10, 56, 8, 6, 30 , 6, 4, 3, 4, {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} , 24, -24},
{ 10, 52, 8, 6, 30 , 7, 5, 3, 4, {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} , 26, -26},
{ 8, 48, 8, 6, 30 , 8, 6, 3, 3, {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} , 28, -28},
{ 8, 44, 8, 6, 30 , 8, 7, 3, 3, {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} , 30, -30},
{ 8, 40, 8, 6, 30 , 8, 8, 3, 3, {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} , 32, -32},
{ 8, 36, 8, 6, 40 , 8, 8, 3, 3, {0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} , 34, -34},
{ 8, 36, 8, 6, 40 , 8, 8, 3, 3, {0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA} , 34, -34}
};
game_type* game;
typedef struct game_state_type {
uint16_t ballx;
uint16_t bally;
uint16_t ballxold;
uint16_t ballyold;
int velx;
int vely;
int playerx;
int playerxold;
int wallState[8];
int score;
int remainingLives;
int top;
int bottom;
int walltop;
int wallbottom;
int brickheight;
int brickwidth;
} game_state_type;
state;
//////////////////////////////////////////////////////////////
// ARDUINO SETUP
//////////////////////////////////////////////////////////////
void setup() {
initTft(tft);
gameSize = {0, 0, tft.width(), tft.height()};
newGame(&games[0], &state, tft);
}
//////////////////////////////////////////////////////////////
// ARDUINO LOOP
//////////////////////////////////////////////////////////////
int selection = -1;
void loop(void) {
selection = readUiSelection(game, &state, selection);
drawPlayer(game, &state);
state.playerxold = state.playerx;
if (abs(state.vely) > ((1 << game->exponent) - 1)) {
state.vely = ((1 << game->exponent) - 1) * ((state.vely > 0) - (state.vely < 0));
}
if (abs(state.velx) > ((1 << game->exponent) - 1)) {
state.velx = ((1 << game->exponent) - 1) * ((state.velx > 0) - (state.velx < 0));
}
state.ballx += state.velx;
state.bally += state.vely;
checkBallCollisions(game, &state, state.ballx >> game->exponent, state.bally >> game->exponent);
checkBallExit(game, &state, state.ballx >> game->exponent, state.bally >> game->exponent);
drawBall(state.ballx >> game->exponent, state.bally >> game->exponent,
state.ballxold >> game->exponent, state.ballyold >> game->exponent,
game->ballsize);
state.ballxold = state.ballx;
state.ballyold = state.bally;
state.velx = (20 + (state.score >> 3)) * ((state.velx > 0) - (state.velx < 0));
state.vely = (20 + (state.score >> 3)) * ((state.vely > 0) - (state.vely < 0));
if (noBricks(game, &state) && level < GAMES_NUMBER) {
level++;
newGame(&games[level], &state, tft);
} else if (state.remainingLives <= 0) {
gameOverTouchToStart();
state.score = 0;
level = 0;
newGame(game, &state, tft);
}
}
void newGame(game_type* newGame, game_state_type* state, Adafruit_TFTLCD& tft) {
game = newGame;
setupState(game, state, tft);
clearDialog(gameSize);
updateLives(game->lives, state->remainingLives);
updateScore(state->score);
setupWall(game, state);
touchToStart();
clearDialog(gameSize);
updateLives(game->lives, state->remainingLives);
updateScore(state->score);
setupWall(game, state);
}
void setupStateSizes(game_type* game, game_state_type* state, Adafruit_TFTLCD& tft) {
state->bottom = tft.height() - 30;
state->brickwidth = tft.width() / game->columns;
state->brickheight = tft.height() / 24;
}
void setupState(game_type* game, game_state_type* state, Adafruit_TFTLCD& tft) {
setupStateSizes(game, state, tft);
for (int i = 0; i < game->rows; i++) {
state->wallState[i] = 0;
}
state->playerx = tft.width() / 2 - game->playerwidth / 2;
state->remainingLives = game->lives;
state->bally = state->bottom << game->exponent;
state->ballyold = state->bottom << game->exponent;
state->velx = game->initVelx;
state->vely = game->initVely;
}
void updateLives(int lives, int remainingLives) {
for (int i = 0; i < lives; i++) {
tft.fillCircle((1 + i) * 15, 15, 5, BLACK);
}
for (int i = 0; i < remainingLives; i++) {
tft.fillCircle((1 + i) * 15, 15, 5, YELLOW);
}
}
void setupWall(game_type* game, game_state_type* state) {
int colors[] = {RED, RED, BLUE, BLUE, YELLOW, YELLOW, GREEN, GREEN};
state->walltop = game->top + 40;
state->wallbottom = state->walltop + game->rows * state->brickheight;
for (int i = 0; i < game->rows; i++) {
for (int j = 0; j < game->columns; j++) {
if (isBrickIn(game->wall, j, i)) {
setBrick(state->wallState, j, i);
drawBrick(state, j, i, colors[i]);
}
}
}
}
void drawBrick(game_state_type* state, int xBrick, int yBrickRow, uint16_t backgroundColor) {
tft.fillRect((state->brickwidth * xBrick) + game->brickGap,
state->walltop + (state->brickheight * yBrickRow) + game->brickGap,
state->brickwidth - game->brickGap * 2,
state->brickheight - game->brickGap * 2,
backgroundColor);
}
boolean noBricks(game_type* game, game_state_type* state) {
for (int i = 0; i < game->rows; i++) {
if (state->wallState[i]) return false;
}
return true;
}
void drawPlayer(game_type* game, game_state_type* state) {
tft.fillRect(state->playerx, state->bottom, game->playerwidth, game->playerheight, YELLOW);
if (state->playerx != state->playerxold) {
if (state->playerx < state->playerxold) {
tft.fillRect(state->playerx + game->playerwidth, state->bottom,
abs(state->playerx - state->playerxold), game->playerheight, backgroundColor);
} else {
tft.fillRect(state->playerxold, state->bottom,
abs(state->playerx - state->playerxold), game->playerheight, backgroundColor);
}
}
}
void drawBall(int x, int y, int xold, int yold, int ballsize) {
if (xold <= x && yold <= y) {
tft.fillRect(xold, yold, ballsize, y - yold, BLACK);
tft.fillRect(xold, yold, x - xold, ballsize, BLACK);
} else if (xold >= x && yold >= y) {
tft.fillRect(x + ballsize, yold, xold - x, ballsize, BLACK);
tft.fillRect(xold, y + ballsize, ballsize, yold - y, BLACK);
} else if (xold <= x && yold >= y) {
tft.fillRect(xold, yold, x - xold, ballsize, BLACK);
tft.fillRect(xold, y + ballsize, ballsize, yold - y, BLACK);
} else if (xold >= x && yold <= y) {
tft.fillRect(xold, yold, ballsize, y - yold, BLACK);
tft.fillRect(x + ballsize, yold, xold - x, ballsize, BLACK);
}
tft.fillRect(x, y, ballsize, ballsize, YELLOW);
}
void touchToStart() {
drawBoxedString(0, 200, " BREAKOUT", 3, YELLOW, BLACK);
drawBoxedString(0, 240, " TOUCH TO START", 2, RED, BLACK);
while (waitForTouch() < 0) {}
}
void gameOverTouchToStart() {
drawBoxedString(0, 180, " GAME OVER", 3, YELLOW, BLACK);
drawBoxedString(0, 220, " TOUCH TO START", 2, RED, BLACK);
while (waitForTouch() < 0) {}
}
void updateScore(int score) {
char buffer[5];
snprintf(buffer, sizeof(buffer), scoreFormat, score);
drawBoxedString(tft.width() - 50, 6, buffer, 2, YELLOW, PRIMARY_DARK_COLOR);
}
void checkBrickCollision(game_type* game, game_state_type* state, uint16_t x, uint16_t y) {
int x1 = x + game->ballsize;
int y1 = y + game->ballsize;
int collissions = 0;
collissions += checkCornerCollision(game, state, x, y);
collissions += checkCornerCollision(game, state, x1, y1);
collissions += checkCornerCollision(game, state, x, y1);
collissions += checkCornerCollision(game, state, x1, y);
if (collissions > 0) {
state->vely = (-1 * state->vely);
if ((((x % state->brickwidth) == 0) && (state->velx < 0)) ||
((((x + game->ballsize) % state->brickwidth) == 0) && (state->velx > 0))) {
state->velx = (-1 * state->velx);
}
}
}
int checkCornerCollision(game_type* game, game_state_type* state, uint16_t x, uint16_t y) {
if ((y > state->walltop) && (y < state->wallbottom)) {
int yBrickRow = (y - state->walltop) / state->brickheight;
int xBrickColumn = (x / state->brickwidth);
if (isBrickIn(state->wallState, xBrickColumn, yBrickRow)) {
hitBrick(state, xBrickColumn, yBrickRow);
return 1;
}
}
return 0;
}
void hitBrick(game_state_type* state, int xBrick, int yBrickRow) {
state->score += pointsForRow[yBrickRow];
drawBrick(state, xBrick, yBrickRow, WHITE);
delay(16);
drawBrick(state, xBrick, yBrickRow, BLUE);
delay(8);
drawBrick(state, xBrick, yBrickRow, backgroundColor);
unsetBrick(state->wallState, xBrick, yBrickRow);
updateScore(state->score);
}
void checkBorderCollision(game_type* game, game_state_type* state, uint16_t x, uint16_t y) {
if (x + game->ballsize >= tft.width()) state->velx = -abs(state->velx);
if (x <= 0) state->velx = abs(state->velx);
if (y <= SCORE_SIZE) state->vely = abs(state->vely);
if (((y + game->ballsize) >= state->bottom) &&
((y + game->ballsize) <= (state->bottom + game->playerheight)) &&
(x >= state->playerx) && (x <= (state->playerx + game->playerwidth))) {
if (x > (state->playerx + game->playerwidth - 6)) state->velx = state->velx - 1;
else if (x < state->playerx + 6) state->velx = state->velx + 1;
state->vely = -abs(state->vely);
}
}
void checkBallCollisions(game_type* game, game_state_type* state, uint16_t x, uint16_t y) {
checkBrickCollision(game, state, x, y);
checkBorderCollision(game, state, x, y);
}
void checkBallExit(game_type* game, game_state_type* state, uint16_t x, uint16_t y) {
if (((y + game->ballsize) >= tft.height())) {
state->remainingLives--;
updateLives(game->lives, state->remainingLives);
delay(500);
state->vely = -abs(state->vely);
}
}
void setBrick(int wall[], uint8_t x, uint8_t y) {
wall[y] = wall[y] | BIT_MASK[x];
}
void unsetBrick(int wall[], uint8_t x, uint8_t y) {
wall[y] = wall[y] & ~BIT_MASK[x];
}
boolean isBrickIn(int wall[], uint8_t x, uint8_t y) {
return wall[y] & BIT_MASK[x];
}
//////////////////////////////////////////////////////////////
// TFT SETUP
//////////////////////////////////////////////////////////////
void initTft(Adafruit_TFTLCD& tft) {
tft.reset();
uint16_t ID = tft.readID();
tft.begin(ID);
tft.setRotation(0);
}
//////////////////////////////////////////////////////////////
// Screen Painting methods
//////////////////////////////////////////////////////////////
void drawBoxedString(const uint16_t x, const uint16_t y, const char* string,
const uint16_t fontsize, const uint16_t foreColor,
const uint16_t backgroundColor) {
tft.setTextSize(fontsize);
int16_t x1, y1;
uint16_t w, h;
tft.getTextBounds(string, x, y, &x1, &y1, &w, &h);
tft.fillRect(x, y, w, h, backgroundColor);
tft.setCursor(x, y);
tft.setTextColor(foreColor);
tft.print(string);
}
void clearDialog(gameSize_type gameSize) {
tft.fillRect(gameSize.x, gameSize.y, gameSize.width, gameSize.height, backgroundColor);
tft.fillRect(gameSize.x, gameSize.y, gameSize.width, SCORE_SIZE, PRIMARY_DARK_COLOR);
}
//////////////////////////////////////////////////////////////
// READ UI SELECTION
//////////////////////////////////////////////////////////////
int readUiSelection(game_type* game, game_state_type* state, const int16_t lastSelected) {
int16_t xpos, ypos;
TSPoint tp = ts.getPoint();
pinMode(XM, OUTPUT);
pinMode(YP, OUTPUT);
if (tp.z > MINPRESSURE && tp.z < MAXPRESSURE) {
xpos = map(tp.x, TS_RT, TS_LEFT, 0, tft.width());
ypos = map(tp.y, TS_BOT, TS_TOP, 0, tft.height());
if (xpos > tft.width() / 2) state->playerx += 2;
else state->playerx -= 2;
if (state->playerx >= tft.width() - game->playerwidth) state->playerx = tft.width() - game->playerwidth;
if (state->playerx < 0) state->playerx = 0;
return 1;
}
#ifdef DEMO_MODE
state->playerx = (state->ballx >> game->exponent) - game->playerwidth / 2;
if (state->playerx >= tft.width() - game->playerwidth) state->playerx = tft.width() - game->playerwidth;
if (state->playerx < 0) state->playerx = 0;
#endif
return -1;
}
int waitForTouch() {
int16_t xpos, ypos;
TSPoint tp = ts.getPoint();
pinMode(XM, OUTPUT);
pinMode(YP, OUTPUT);
if (tp.z > MINPRESSURE && tp.z < MAXPRESSURE) return 1;
return -1;
}
Esquemáticos
Proceso de manufactura
- Construye un Juego Giroscópico en Tiempo Real con Arduino Nano y Sensor MPU-6050
- Construya el clásico juego Pong en Arduino UNO con pantalla OLED: Tutorial paso a paso
- Construye un Piano Táctil Capacitivo Portátil con Arduino – Guía Paso a Paso
- Mando USB para juegos DIY con Arduino – Construye tu propio gamepad de alto rendimiento
- Construye una consola Pong 24×16 LED con Arduino Nano y MAX7219
- Construye un juego «Operación» animatrónico con minifigura LEGO gigante y Arduino
- Adaptadores USB-BLE MIDI Inalámbricos: Conectividad Sin Cables para Músicos
- Pixel Chaser: Juego LED Interactivo de Un Toque con Arduino Nano
- Dominando reconocimiento y síntesis de voz en Arduino Due
- Automatiza el Juego del Dinosaurio de Chrome con Arduino y Python
- Interruptor Táctil Capacitivo LED con Arduino UNO – Proyecto DIY Fácil