/*
FlappyCat
author: minbbb
https://github.com/minbbb/FlappyCat
适配 Arduino Mega 2560:
- TVout 初始化使用默认的 3 参数形式,库会自动匹配 Mega 的 11(SYNC) / 12(VIDEO)
- 按钮仍使用 D2,内部上拉,按下接 GND
*/
#include <EEPROM.h>
#include <TVout.h>
#include <TVoutfonts/fontALL.h>
#include "cat.h"
#include "cloud.h"
#include "cloud2.h"
#include "catfly.h"
#define CONTROL_PIN 2 // 控制按钮引脚,按下接 GND
#define X_CLOUD 13 // 第一朵云初始 X 坐标
#define Y_CLOUD 0 // 第一朵云初始 Y 坐标
#define X_CLOUD2 96 // 第二朵云初始 X 坐标
#define Y_CLOUD2 79 // 第二朵云初始 Y 坐标
#define ACCEL_DOWN 0.1 // 下落加速度
#define ACCEL_UP 0.4 // 起跳加速度
#define HEIGHT_BLOCK_DOWN 9 // 下方管道块高度
#define WIDTH_BLOCK_DOWN 23 // 下方管道块宽度
#define WIDTH_BLOCK_UP 17 // 上方管道块宽度
#define HEIGHT_HOLE 40 // 上下管道之间的高度间距
#define WIDTH_HOLE 80 // 左右管道之间的水平间距
#define COUNT_BLOCK 2 // 同时绘制的管道数量
TVout TV; // TVout 对象
int xBlock[COUNT_BLOCK]; // 管道的 X 坐标
int yBlock[COUNT_BLOCK]; // 管道的 Y 坐标
int xCat; // 猫的 X 坐标
int yCat; // 猫的 Y 坐标
float aCat; // 猫的加速度
int score; // 当前分数
void setup() {
pinMode(CONTROL_PIN, INPUT_PULLUP); // 按钮使用内部上拉
// 使用标准 3 参数初始化,TVout 库会自动识别 Mega 并默认使用 11/12 引脚
TV.begin(NTSC, 128, 96);
TV.select_font(font8x8); // 选择 8x8 字体
randomSeed(analogRead(0)); // 用悬空模拟口生成随机种子
preview(); // 进入预览/等待开始界面
}
void loop() {
TV.clear_screen(); // 清屏
for (int i = 0; i <= COUNT_BLOCK - 1; i++) {
xBlock[i] = 0;
yBlock[i] = 0;
}
xBlock[0] = 128; // 第一根管道从屏幕右侧进入
// 初始化管道位置和高度
for (int i = 0; i < COUNT_BLOCK; i++) {
yBlock[i] = random(2 + HEIGHT_BLOCK_DOWN, 94 - HEIGHT_BLOCK_DOWN - HEIGHT_HOLE);
if (i >= 1) {
xBlock[i] = xBlock[i - 1] + WIDTH_HOLE;
}
}
startDemo(); // 等待按键开始
startGame(); // 开始游戏
}
// 预览界面:云朵飘动、猫移动、显示标题和最高分,按按钮开始
void preview() {
int c = 2;
for (int i = 0; i <= 24;) {
TV.bitmap(X_CLOUD, c + Y_CLOUD, cloud);
TV.bitmap(X_CLOUD2, c + Y_CLOUD2, cloud2);
for (int ii = 0; ii <= 6; ii++) {
if (i >= 0)
TV.bitmap(i, 21, cat);
else {
TV.bitmap(i, 19, cat, 79, 56);
}
delay(50);
i += 1;
}
if (i > 24)
break;
TV.draw_rect(X_CLOUD, c + Y_CLOUD, pgm_read_byte_near(cloud), pgm_read_byte_near(cloud + 1), 0, 0);
TV.draw_rect(X_CLOUD2, c + Y_CLOUD2, pgm_read_byte_near(cloud2), pgm_read_byte_near(cloud2 + 1), 0, 0);
c -= 2;
TV.bitmap(X_CLOUD, c + Y_CLOUD, cloud);
TV.bitmap(X_CLOUD2, c + Y_CLOUD2, cloud2);
for (int ii = 0; ii <= 6; ii++) {
if (i >= 0)
TV.bitmap(i, 21, cat);
else {
TV.bitmap(i, 19, cat, 79, 56);
}
delay(50);
i += 1;
}
if (i > 24)
break;
TV.draw_rect(X_CLOUD, c + Y_CLOUD, pgm_read_byte_near(cloud), pgm_read_byte_near(cloud + 1), 0, 0);
TV.draw_rect(X_CLOUD2, c + Y_CLOUD2, pgm_read_byte_near(cloud2), pgm_read_byte_near(cloud2 + 1), 0, 0);
c += 2;
TV.bitmap(X_CLOUD, c + Y_CLOUD, cloud);
TV.bitmap(X_CLOUD2, c + Y_CLOUD2, cloud2);
for (int ii = 0; ii <= 6; ii++) {
if (i >= 0)
TV.bitmap(i, 21, cat);
else {
TV.bitmap(i, 19, cat, 79, 56);
}
delay(50);
i += 1;
}
if (i > 24)
break;
TV.draw_rect(X_CLOUD, c + Y_CLOUD, pgm_read_byte_near(cloud), pgm_read_byte_near(cloud + 1), 0, 0);
TV.draw_rect(X_CLOUD2, c + Y_CLOUD2, pgm_read_byte_near(cloud2), pgm_read_byte_near(cloud2 + 1), 0, 0);
c += 2;
TV.bitmap(X_CLOUD, c + Y_CLOUD, cloud);
TV.bitmap(X_CLOUD2, c + Y_CLOUD2, cloud2);
for (int ii = 0; ii <= 6; ii++) {
if (i >= 0)
TV.bitmap(i, 21, cat);
else {
TV.bitmap(i, 19, cat, 79, 56);
}
delay(50);
i += 1;
}
if (i > 24)
break;
TV.draw_rect(X_CLOUD, c + Y_CLOUD, pgm_read_byte_near(cloud), pgm_read_byte_near(cloud + 1), 0, 0);
TV.draw_rect(X_CLOUD2, c + Y_CLOUD2, pgm_read_byte_near(cloud2), pgm_read_byte_near(cloud2 + 1), 0, 0);
c -= 2;
}
TV.print(45, 3, "FlappyCat"); // 显示标题
TV.select_font(font4x6); // 切换小字体
TV.print(7, 85, "Best:"); // 显示最高分标签
String stringVar = String(EEPROM.read(0), DEC);
char charBufVar[50];
stringVar.toCharArray(charBufVar, 50);
TV.print(27, 85, charBufVar); // 显示 EEPROM 中保存的最高分
TV.select_font(font8x8);
// 预览背景音乐/音效
unsigned int music[] = {500, 600, 700, 400, 200, 300, 600, 200, 250, 400};
unsigned int musicDelay[] = {100, 200, 100, 50 , 150, 100, 150, 100, 100, 400};
unsigned int previewtime = millis();
for (unsigned int t = 300, st = 3, pt = 0, mus = 0;;) {
pt = millis() - previewtime;
if (pt / t >= 1) {
mus++;
if (mus >= sizeof(music) / sizeof(unsigned int)) {
mus = 0;
}
TV.tone(music[mus], musicDelay[mus]);
previewtime = millis();
TV.draw_rect(X_CLOUD, c + Y_CLOUD, 18, 13, 0, 0);
TV.draw_rect(X_CLOUD2, c + Y_CLOUD2, 18, 13, 0, 0);
st++;
if (st >= 4)
st = 0;
switch (st) {
case 0: {
c = 2;
break;
}
case 1: {
c = 0;
break;
}
case 2: {
c = 2;
break;
}
case 3: {
c = 4;
break;
}
}
TV.bitmap(X_CLOUD, c + Y_CLOUD, cloud);
TV.bitmap(X_CLOUD2, c + Y_CLOUD2, cloud2);
}
if (digitalRead(CONTROL_PIN) == LOW) { // 按下按钮,退出预览
return;
}
}
}
// 演示/等待开始:猫上下弹跳,显示“Press any key”
void startDemo() {
int checkButton = 0;
xCat = 25;
yCat = 29; // 起始下落位置
aCat = 0;
score = 0;
TV.select_font(font4x6);
TV.print(65, 40, "Press any key");
TV.print(7, 85, "Best:");
String stringVar = String(EEPROM.read(0), DEC);
char charBufVar[50];
stringVar.toCharArray(charBufVar, 50);
TV.print(27, 85, charBufVar);
TV.select_font(font8x8);
TV.draw_line(0, 0, 128, 0, 1); // 上边界
TV.draw_line(0, 95, 128, 95, 1); // 下边界
for (;;) {
flyCat(); // 更新猫的位置并检测碰撞
if (yCat >= 47) { // 到达弹跳位置就跳一下
jump();
}
if (digitalRead(CONTROL_PIN) == HIGH) {
checkButton = 1; // 检测到按钮释放过
}
if (digitalRead(CONTROL_PIN) == LOW && checkButton == 1) {
TV.draw_rect(65, 40, 52, 6, 0, 0); // 清除提示文字
return; // 开始游戏
}
}
}
// 猫的飞行/下落更新,返回 1 表示碰撞或坠地
int flyCat() {
TV.delay_frame(1); // 控制帧率
TV.draw_rect(xCat, yCat, 20, 14, 0, 0); // 擦除上一帧猫
yCat = yCat - aCat; // 根据加速度更新 Y 坐标
if (yCat < 1) { // 顶部边界
yCat = 1;
aCat = 0;
}
// 检测是否坠地
if (yCat > 94 - pgm_read_byte_near(catFly + 1)) {
yCat = 94 - pgm_read_byte_near(catFly + 1);
TV.bitmap(xCat, yCat, catFly);
TV.tone(300, 100);
delay(100);
TV.tone(200, 100);
delay(100);
return 1;
}
// 检测是否撞到管道
for (int i = 0; i < COUNT_BLOCK; i++) {
if ((xCat + pgm_read_byte_near(catFly) >= xBlock[i] && xCat <= xBlock[i] + WIDTH_BLOCK_DOWN) &&
(yCat <= yBlock[i] || yCat + pgm_read_byte_near(catFly + 1) >= yBlock[i] + HEIGHT_HOLE)) {
TV.bitmap(xCat, yCat, catFly);
TV.tone(300, 100);
delay(100);
TV.tone(200, 100);
delay(100);
return 1;
}
}
TV.bitmap(xCat, yCat, catFly); // 绘制猫
aCat = aCat - ACCEL_DOWN; // 持续受重力影响
return 0;
}
// 起跳:给猫一个向上的加速度
void jump() {
aCat += ACCEL_UP;
}
// 游戏主循环
void startGame() {
for (;;) {
TV.print(4, 4, score); // 显示分数
moveBlock(); // 移动并绘制管道
if (flyCat()) { // 如果碰撞或坠地
TV.draw_rect(45, 3, 67, 11, 0, 1);
TV.print(47, 5, "You Lose");
TV.print(4, 4, score);
if (EEPROM.read(0) < score) { // 更新最高分
EEPROM.write(0, score); // 注意:这里只写入一个字节,分数超过 255 会溢出
}
delay(300);
for (;;) {
if (digitalRead(CONTROL_PIN) == LOW) {
return; // 按下按钮回到开始
}
}
}
if (digitalRead(CONTROL_PIN) == LOW) {
jump(); // 按下按钮起跳
}
}
}
// 移动、绘制管道,并处理得分
void moveBlock() {
// 计算管道坐标
for (int i = 0; i < COUNT_BLOCK; i++) {
xBlock[i] = xBlock[i] - 1;
if (xBlock[i] <= -WIDTH_BLOCK_DOWN) {
switch (i) {
case 0: {
xBlock[i] = xBlock[1] + WIDTH_HOLE;
break;
}
case 1: {
xBlock[i] = xBlock[0] + WIDTH_HOLE;
break;
}
}
yBlock[i] = random(2 + HEIGHT_BLOCK_DOWN, 94 - HEIGHT_BLOCK_DOWN - HEIGHT_HOLE);
}
}
// 绘制管道
for (int i = 0; i < COUNT_BLOCK; i++) {
if (xBlock[i] <= 127 - WIDTH_BLOCK_DOWN && xBlock[i] >= 0) {
TV.draw_rect(xBlock[i] + ((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2)), 0, WIDTH_BLOCK_UP, yBlock[i] - HEIGHT_BLOCK_DOWN, 0, 1);
TV.draw_rect(xBlock[i] + ((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2)), yBlock[i] + HEIGHT_HOLE + HEIGHT_BLOCK_DOWN, WIDTH_BLOCK_UP, 95 - yBlock[i] - HEIGHT_HOLE - HEIGHT_BLOCK_DOWN, 0, 1);
TV.draw_rect(xBlock[i], yBlock[i] - HEIGHT_BLOCK_DOWN, WIDTH_BLOCK_DOWN, HEIGHT_BLOCK_DOWN, 0, 1);
TV.draw_rect(xBlock[i], yBlock[i] + HEIGHT_HOLE, WIDTH_BLOCK_DOWN, HEIGHT_BLOCK_DOWN, 0, 1);
continue;
}
// 管道在右侧刚进入屏幕
if (xBlock[i] <= 127 && xBlock[i] >= 127 - WIDTH_BLOCK_DOWN) {
if (xBlock[i] <= 127 - ((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2)) && xBlock[i] >= 127 - (((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2)) + WIDTH_BLOCK_UP)) {
TV.draw_rect(xBlock[i] + ((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2)), 0, 127 - xBlock[i] - ((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2)), yBlock[i] - HEIGHT_BLOCK_DOWN, 0, 1);
TV.draw_rect(xBlock[i] + ((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2)), yBlock[i] + HEIGHT_HOLE + HEIGHT_BLOCK_DOWN, 127 - xBlock[i] - ((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2)), 95 - yBlock[i] - HEIGHT_HOLE - HEIGHT_BLOCK_DOWN, 0, 1);
} else {
if (xBlock[i] <= 127 - ((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2))) {
TV.draw_rect(xBlock[i] + ((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2)), 0, WIDTH_BLOCK_UP, yBlock[i] - HEIGHT_BLOCK_DOWN, 0, 1);
TV.draw_rect(xBlock[i] + ((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2)), yBlock[i] + HEIGHT_HOLE + HEIGHT_BLOCK_DOWN, WIDTH_BLOCK_UP, 95 - yBlock[i] - HEIGHT_HOLE - HEIGHT_BLOCK_DOWN, 0, 1);
}
}
TV.draw_rect(xBlock[i], yBlock[i] - HEIGHT_BLOCK_DOWN, 127 - xBlock[i], HEIGHT_BLOCK_DOWN, 0, 1);
TV.draw_rect(xBlock[i], yBlock[i] + HEIGHT_HOLE, 127 - xBlock[i], HEIGHT_BLOCK_DOWN, 0, 1);
continue;
}
// 管道在左侧即将离开屏幕
if (xBlock[i] >= -WIDTH_BLOCK_DOWN && xBlock[i] <= 0) {
if (xBlock[i] <= -((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2)) && xBlock[i] >= -(((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2)) + WIDTH_BLOCK_UP) ) {
TV.draw_rect(0, 0, WIDTH_BLOCK_UP + (xBlock[i] + ((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2))), yBlock[i] - HEIGHT_BLOCK_DOWN, 0, 1);
TV.draw_rect(0, yBlock[i] + HEIGHT_HOLE + HEIGHT_BLOCK_DOWN, WIDTH_BLOCK_UP + (xBlock[i] + ((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2))), 95 - yBlock[i] - HEIGHT_HOLE - HEIGHT_BLOCK_DOWN, 0, 1);
} else {
if (xBlock[i] >= -(WIDTH_BLOCK_DOWN - ((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2)))) {
TV.draw_rect(xBlock[i] + ((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2)), 0, WIDTH_BLOCK_UP, yBlock[i] - HEIGHT_BLOCK_DOWN, 0, 1);
TV.draw_rect(xBlock[i] + ((WIDTH_BLOCK_DOWN / 2) - (WIDTH_BLOCK_UP / 2)), yBlock[i] + HEIGHT_HOLE + HEIGHT_BLOCK_DOWN, WIDTH_BLOCK_UP, 95 - yBlock[i] - HEIGHT_HOLE - HEIGHT_BLOCK_DOWN, 0, 1);
}
}
TV.draw_rect(0, yBlock[i] - HEIGHT_BLOCK_DOWN, WIDTH_BLOCK_DOWN + xBlock[i], HEIGHT_BLOCK_DOWN, 0, 1);
TV.draw_rect(0, yBlock[i] + HEIGHT_HOLE, WIDTH_BLOCK_DOWN + xBlock[i], HEIGHT_BLOCK_DOWN, 0, 1);
continue;
}
}
// 得分检测:猫通过管道时加一分
for (int i = 0; i < COUNT_BLOCK; i++) {
if (xCat + pgm_read_byte_near(catFly) - (WIDTH_BLOCK_DOWN / 2) == xBlock[i]) {
score++;
TV.tone(500, 100);
}
}
// 绘制上下边界线
TV.draw_line(0, 0, 128, 0, 1);
TV.draw_line(0, 95, 128, 95, 1);
}