浅写了一个,项目地址在这里
基本框架
采用了类似于Arduino
的框架Arduiduidui。“主进程”负责绘制,另外有时钟负责“多线程”式的状态机更新、外部中断响应按键。
main()
user/main.c
1 2 3 4 5 6 7 8 9
| int main() { setup(); while (1) { loop(); } return 0; }
|
当然了,实际的main()里还有串口通信计算fps的代码、NVIC Group初始化。
1 2 3 4 5 6 7 8 9 10 11 12
| int main() { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); Serial_Init(); Timer_Init(); setup(); while (1){ loop(); ++fps; } return 0; }
|
setup() 与 loop()
setup()
和loop()
都在user/arduiduidui.c
里。
1 2 3 4 5 6 7 8 9 10 11
| void setup() { srand(2333); OLED_Init(); Key_Init(); timer_init(); game_start(); }
|
loop()
loop()
只负责绘制,这样可以保证显示稳定且流畅。
1 2 3 4 5
| void loop() { draw(); OLED_Refresh(); }
|
游戏机制
绘制机制
draw()
函数的实现非常简单,只需要依次调用需要绘制的对象的绘制函数即可
1 2 3 4 5 6 7 8
| void draw() { ground_draw(); cloud_draw(); blocks_draw(); dino_draw(); score_draw(); }
|
以前我会纠结于“哎呀呀,更细节的东西应该怎样写,才能保证我写出来的东西能保证我的框架比较合理”,现在我终于是懂得了:有啥画啥。
绘制地面
1 2 3 4 5 6 7 8 9 10
| void ground_draw() { uint8_t i; OLED_Line(0, ground_y, 127, ground_y, WHITE); for (i = 0; i <= dino_alive; ++i) { OLED_Square(12 * i - 8, ground_y + 4, 12 * i, ground_y + 4 + 8, 1, WHITE); } }
|
绘制云
1 2 3 4 5 6 7 8 9
| void cloud_draw() { uint8_t i; for (i = 0; i < cloud_amo; ++i) { OLED_FilletMatrix(clouds[i].x, clouds[i].y, clouds[i].x + clouds[i].w, clouds[i].y + clouds[i].h, clouds[i].r, 0, WHITE); } OLED_Circle(8, 8, 8, 0, WHITE); }
|
绘制石
1 2 3 4 5 6 7 8
| void blocks_draw() { uint8_t i; for (i = 0; i < block_amo; ++i) { OLED_Square(blocks[i].x - block_w / 2, ground_y - blocks[i].y - block_h, blocks[i].x + block_w / 2, ground_y - blocks[i].y, 0, WHITE); } }
|
绘制鸟
皇帝的新bird
绘制小恐龙
1 2 3 4 5 6 7 8 9
| void dino_draw() { OLED_Square(dino_left, ground_y - dino_y - dino_h / (dino_lie + 1), dino_left + dino_w, ground_y - dino_y, 1, WHITE); if (!dino_alive) { OLED_ShowString(0, 24, " -- Game Over -- ", BLACK); } }
|
绘制分数
1 2 3 4
| void score_draw() { OLED_ShowNum(87, 0, score, 5, BLACK); }
|
状态更新机制
小恐龙状态更新
借助很好用的CTweeny库
,实现小恐龙平滑的跳跃效果并不是一件困难的事。
小恐龙采用相对坐标,当dino_y
为0时,dino的底部与地面接触。
- 如果不在跳跃状态,dino_y保持为0.
- 如果在跳跃状态,通过ctweeny库计算当前小恐龙的高度
小恐龙的横坐标是写死的,我的设定是dino_left
为24
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| void dino_update() { uint8_t i; if (dino_jump) { dino_y = ctweeny_step(&dino_jump_ctw, 1); if (dino_y == dino_jump_max_height) { ctweeny_backward(&dino_jump_ctw); } if (dino_y == 0 && ctweeny_direction(&dino_jump_ctw) == backward) { dino_jump = 0; } } for (i = 0; i < block_amo; ++i) { if (blocks[i].x < dino_left + dino_w && blocks[i].x > dino_left && dino_y < blocks[i].y + block_h) { dino_alive--; block_ease_front(); } } }
|
关于block_ease_front()
,参考石の状态更新
云の状态更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| void cloud_update() { uint8_t i, j; for (i = 0; i < cloud_amo; ++i) { clouds[i].x--; } for (i = 0; i < cloud_amo; ++i) { if (clouds[i].x + clouds[i].w < 0) { for (j = i + 1; j < cloud_amo; ++j) { clouds[j - 1] = clouds[j]; } cloud_amo--; } } if (rand() % 16 == 2 && cloud_amo < cloud_max_amount) { clouds[cloud_amo].x = 127; clouds[cloud_amo].y = rand() % 6; clouds[cloud_amo].w = 16 + (rand() % 16); clouds[cloud_amo].h = 4 + (rand() % 12); clouds[cloud_amo].r = 2 + (rand() % 4); cloud_amo++; } }
|
石の状态更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| void block_update() { uint8_t i, j; for (i = 0; i < block_amo; ++i) { blocks[i].x--; } for (i = 0; i < block_amo; ++i) { if (blocks[i].x + block_w < 0) { block_ease_front(); } } if (rand() % 128 <= 2 && block_amo < block_max_amount) { if (block_amo > 0) { if (blocks[0].x >= 128 - block_w) { return; } } blocks[block_amo].x = 127; block_amo++; } }
|
关于清除最前面的石头的代码……
1 2 3 4 5 6 7 8 9
| void block_ease_front() { uint8_t j; for (j = 1; j < block_amo; ++j) { blocks[j - 1] = blocks[j]; } block_amo--; }
|
状态机更新时钟
代码都在user/arduiduidui.c
里。
初始化代码
时钟每隔10ms更新一次状态机
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| void timer_init() { RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE); TIM_InternalClockConfig(TIM3); TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure; TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1; TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up; TIM_TimeBaseInitStructure.TIM_Period = 100 - 1; TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1; TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM3, TIM_FLAG_Update);
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1; NVIC_Init(&NVIC_InitStructure); TIM_Cmd(TIM3, ENABLE); }
|
至于为什么使用TIM3:因为TIM2被串口通信拿去计算fps了。
时钟中断
1 2 3 4 5 6 7 8
| void TIM3_IRQHandler(void) { if (TIM_GetITStatus(TIM3, TIM_IT_Update) == SET) { game_update(); TIM_ClearITPendingBit(TIM3, TIM_IT_Update); } }
|
外部中断
初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| void Key_Init(void) { RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure);
GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource0); GPIO_EXTILineConfig(GPIO_PortSourceGPIOA, GPIO_PinSource1);
EXTI_InitTypeDef EXTI_InitStructure; EXTI_InitStructure.EXTI_Line = EXTI_Line0; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Falling; EXTI_Init(&EXTI_InitStructure); EXTI_InitStructure.EXTI_Line = EXTI_Line1; EXTI_InitStructure.EXTI_LineCmd = ENABLE; EXTI_InitStructure.EXTI_Mode = EXTI_Mode_Interrupt; EXTI_InitStructure.EXTI_Trigger = EXTI_Trigger_Rising_Falling; EXTI_Init(&EXTI_InitStructure);
NVIC_InitTypeDef NVIC_InitStructure; NVIC_InitStructure.NVIC_IRQChannel = EXTI0_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 2; NVIC_Init(&NVIC_InitStructure); NVIC_InitStructure.NVIC_IRQChannel = EXTI1_IRQn; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 3; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3; NVIC_Init(&NVIC_InitStructure); }
|
外部中断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| void EXTI0_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line0) == SET) { key1_onPress(); EXTI_ClearITPendingBit(EXTI_Line0); } }
void EXTI1_IRQHandler(void) { if (EXTI_GetITStatus(EXTI_Line1) == SET) { key2_onPress(); EXTI_ClearITPendingBit(EXTI_Line1); } }
|
状态机更新
处理按键
两个按键key1
和key2
有不同的任务。key1负责跳跃,而key2负责加速下落和低头。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| void key1_onPress() { if (!dino_jump && dino_alive) { dino_jump = 1; dino_lie = 0; ctweeny_foreward(&dino_jump_ctw); ctweeny_from(&dino_jump_ctw, dino_y); ctweeny_to(&dino_jump_ctw, dino_jump_max_height); ctweeny_during(&dino_jump_ctw, dino_jump_during); } if (!dino_alive) { if (game_pause_counter >= game_pause_counter_continue) { game_restart(); } } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void key2_onPress() { if (dino_jump) { ctweeny_during(&dino_jump_ctw, dino_jump_during / 2); } else { dino_lie = !dino_lie; } if (!dino_alive) { if (game_pause_counter >= game_pause_counter_continue) { game_restart(); } } }
|
其它
game_start() & game_restart()
1 2 3 4 5 6 7
| void game_start() { dino_y = 0; dino_alive = dino_life; ctweeny_init(&dino_jump_ctw, 0, dino_jump_max_height, dino_jump_during); ctweeny_via(&dino_jump_ctw, ctweeny_easeOutCubic); }
|
1 2 3 4 5 6 7 8 9 10 11
| void game_restart() { cloud_amo = 0; block_amo = 0; dino_y = 0; score = 0; game_pause_counter = 0; dino_alive = dino_life; ctweeny_init(&dino_jump_ctw, 0, dino_jump_max_height, dino_jump_during); ctweeny_via(&dino_jump_ctw, ctweeny_easeOutCubic); }
|
还是有一点小区别的。
关于调试
对外部中断不太熟悉,调试的时候用了一些时间。比如不熟悉 GPIO 的输出方式……
其它还算顺利,花了一个半小时随便敲的东西,有很多细节没有打磨,比如dino可能会落到第二个石块上(但没有碰到第一个石头),但我代码里写的是block_ease_front(),这样会导致碰一个头扣两滴血。
关于OLED屏幕刷新率……
它达到了惊人的116fps!它竟然是块高刷屏!!!(doge)
关于如此抽象的画风
因为我懒得提取图片然后转换……
Arduiduidui
下一步是想很方便地移植到Arduino上去