This commit is contained in:
2025-11-15 09:07:39 +08:00
parent 2b5cfc1538
commit 9529ecd0d5
17 changed files with 1234 additions and 28 deletions
+55
View File
@@ -0,0 +1,55 @@
#ifndef GAME_H
#define GAME_H
#include <Arduino.h>
#include "ui.h" // for DisplayList and colors
namespace esp32 {
namespace game {
using namespace esp32::ui;
// 棋盘尺寸(经典 10x20)
static const int COLS = 10;
static const int ROWS = 20;
enum class State : uint8_t { Playing = 0, GameOver = 1 };
struct Piece {
uint8_t type; // 0..6
int8_t x; // 左上角/参考点
int8_t y;
// 仅使用一个朝向(简化:不提供旋转)
int8_t blocks[4][2]; // 4 个方块偏移
uint16_t color;
};
struct Tetris {
uint8_t board[ROWS][COLS]; // 0=空,其它=颜色索引或标记
uint16_t colors[8]; // 颜色表(index->RGB565)
Piece current;
uint32_t lastDropMs = 0;
uint32_t dropInterval = 700; // ms
State state = State::Playing;
uint32_t score = 0;
uint32_t rng = 1;
};
void init(Tetris& g);
void reset(Tetris& g);
void update(Tetris& g, uint32_t nowMs);
// 输入
void moveLeft(Tetris& g);
void moveRight(Tetris& g);
void hardDrop(Tetris& g);
// UI 构建
void buildScene(const Tetris& g, DisplayList& out,
int16_t screenW, int16_t screenH,
int16_t cell, int16_t originX, int16_t originY);
} // namespace game
} // namespace esp32
#endif // GAME_H
+30
View File
@@ -0,0 +1,30 @@
#ifndef TIMER_H
#define TIMER_H
#include <Arduino.h>
#include "ui.h"
namespace esp32 {
namespace timer {
struct ClockState {
uint32_t startMs = 0; // 起始时间
uint32_t elapsedMs = 0; // 当前经过时间
static const int MAX_LAPS = 12;
uint16_t lapCount = 0;
float lapAngles[MAX_LAPS]; // 存储圈记录的角度(秒针位置)
uint32_t lapMs[MAX_LAPS]; // 每次记录的时间戳 (ms)
bool running = true;
};
void init(ClockState& s);
void reset(ClockState& s);
void update(ClockState& s, uint32_t nowMs);
void addLap(ClockState& s);
void buildScene(const ClockState& s, esp32::ui::DisplayList& out,
int16_t screenW, int16_t screenH);
} // namespace timer
} // namespace esp32
#endif // TIMER_H
+191
View File
@@ -0,0 +1,191 @@
#ifndef UI_H
#define UI_H
#include <Arduino.h>
#include <Adafruit_ST7735.h>
#include <Adafruit_GFX.h>
#include <LittleFS.h>
#include <U8g2_for_Adafruit_GFX.h>
#include "utils.h"
namespace esp32 {
namespace ui {
// 简易图标:RGB565 像素数组,内存持有
struct Icon {
uint16_t width = 0;
uint16_t height = 0;
uint16_t* pixels = nullptr; // width*height 大小的 RGB565 数组
};
// 显示操作类型
enum class OpType : uint8_t { FillRect = 0, DrawText = 1, DrawIcon = 2, Line = 3, Circle = 4 };
// 显示项(Display List 条目)
struct DisplayItem {
OpType op;
uint16_t key; // 用于跨帧识别(简化版,当前全量重放)
int16_t x;
int16_t y;
int16_t w; // FillRect 使用 或 Line/Circle 半径/终点 x
int16_t h; // FillRect 使用 或 Line 终点 y
uint16_t color; // FillRect/DrawText 使用
const char* text; // 指向 textBuf 或字符串字面量
const Icon* icon; // DrawIcon 使用
char textBuf[64]; // 小型文本缓冲,存放动态生成的字符串(例如时间、lap)
};
// 固定容量的显示列表
class DisplayList {
public:
static const int MAX_ITEMS = 256;
DisplayList() : _count(0) {}
void clear() { _count = 0; }
int size() const { return _count; }
const DisplayItem& at(int i) const { return _items[i]; }
bool addFillRect(uint16_t key, int16_t x, int16_t y, int16_t w, int16_t h, uint16_t color) {
if (_count >= MAX_ITEMS) return false;
auto& it = _items[_count++];
it.op = OpType::FillRect;
it.key = key;
it.x = x; it.y = y; it.w = w; it.h = h; it.color = color;
it.text = nullptr; it.icon = nullptr;
return true;
}
bool addText(uint16_t key, int16_t x, int16_t y, uint16_t color, const char* text, bool centered = false) {
if (_count >= MAX_ITEMS) return false;
auto& it = _items[_count++];
it.op = OpType::DrawText;
it.key = key;
it.x = x; it.y = y;
it.w = centered ? 1 : 0; // w=1 表示需要居中
it.h = 0;
it.color = color;
// 复制文本到缓冲,保证作用域结束后仍可用
if (text) {
strncpy(it.textBuf, text, sizeof(it.textBuf) - 1);
it.textBuf[sizeof(it.textBuf) - 1] = '\0';
it.text = it.textBuf;
} else {
it.textBuf[0] = '\0';
it.text = it.textBuf;
}
it.icon = nullptr;
return true;
}
bool addIcon(uint16_t key, int16_t x, int16_t y, const Icon* icon) {
if (_count >= MAX_ITEMS) return false;
auto& it = _items[_count++];
it.op = OpType::DrawIcon;
it.key = key;
it.x = x; it.y = y; it.w = 0; it.h = 0; it.color = 0;
it.text = nullptr; it.icon = icon;
return true;
}
bool addLine(uint16_t key, int16_t x1, int16_t y1, int16_t x2, int16_t y2, uint16_t color) {
if (_count >= MAX_ITEMS) return false;
auto& it = _items[_count++];
it.op = OpType::Line;
it.key = key;
it.x = x1; it.y = y1; it.w = x2; it.h = y2; it.color = color;
it.text = nullptr; it.icon = nullptr;
return true;
}
bool addCircle(uint16_t key, int16_t cx, int16_t cy, int16_t radius, uint16_t color) {
if (_count >= MAX_ITEMS) return false;
auto& it = _items[_count++];
it.op = OpType::Circle;
it.key = key;
it.x = cx; it.y = cy; it.w = radius; it.h = 0; it.color = color;
it.text = nullptr; it.icon = nullptr;
return true;
}
private:
int _count;
DisplayItem _items[MAX_ITEMS];
};
// Tween(补间动画)
struct Tween {
float* target = nullptr;
float from = 0.0f;
float to = 0.0f;
uint32_t startMs = 0;
uint32_t durationMs = 0;
float x1 = 0.42f, y1 = 0.0f, x2 = 0.58f, y2 = 1.0f; // 贝塞尔参数
bool active = false;
};
// UI 框架主体
class UI {
public:
UI() : _tft(nullptr), _u8g2(nullptr), _builder(nullptr), _builderCtx(nullptr) {}
void begin(Adafruit_ST7735* tft, U8G2_FOR_ADAFRUIT_GFX* u8g2 = nullptr) {
_tft = tft;
_u8g2 = u8g2;
}
// builder: 负责把状态转为显示列表
using BuilderFn = void (*)(void* ctx, DisplayList& out);
void setScene(BuilderFn builder, void* ctx) {
_builder = builder;
_builderCtx = ctx;
}
// 每帧调用:更新动画 + 生成显示列表 + 渲染
void tick(uint32_t nowMs);
// 添加一个 float 补间(从当前值到目标值)
int addTweenFloat(float* target, float to, uint32_t durationMs,
float x1 = 0.42f, float y1 = 0.0f, float x2 = 0.58f, float y2 = 1.0f);
bool hasActiveTweens() const;
bool isTweenActive(int id) const;
// 图标加载:文件格式 (little-endian): [2字节宽][2字节高][像素: width*height*2]
static bool loadIconFromFS(const char* path, Icon& out);
static void freeIcon(Icon& icon);
// 启用内存帧缓冲(双缓冲),第一次调用会分配两块 16 位色的画布
bool enableBackBuffer(uint16_t width, uint16_t height);
bool isBackBufferEnabled() const { return _useBackBuffer; }
private:
void _updateTweens(uint32_t nowMs);
void _render(const DisplayList& list);
void _drawIcon(int16_t x, int16_t y, const Icon& icon);
void _renderToCanvas(const DisplayList& list); // 使用内存画布绘制
void _flushCanvas(); // 将当前画布推送到屏幕
void _swapCanvas(); // 交换前后缓冲
Adafruit_ST7735* _tft;
U8G2_FOR_ADAFRUIT_GFX* _u8g2;
BuilderFn _builder;
void* _builderCtx;
DisplayList _list;
static const int MAX_TWEENS = 8;
Tween _tweens[MAX_TWEENS];
// 双缓冲相关
bool _useBackBuffer = false;
GFXcanvas16* _canvasA = nullptr; // 当前绘制目标
GFXcanvas16* _canvasB = nullptr; // 上一帧
GFXcanvas16* _canvasCurrent = nullptr;
GFXcanvas16* _canvasPrev = nullptr;
uint16_t _bufW = 0;
uint16_t _bufH = 0;
};
} // namespace ui
} // namespace esp32
#endif // UI_H