STM32DinoRun

浅写了一个,项目地址在这里

基本框架

采用了类似于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);
}
}

绘制鸟

1
2
3
4
void bird_draw()
{

}

皇帝的新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);
// 实际上,把 Game Over 的绘制代码写在这里不太合理,不过它能用。
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_onstep()实现更好……但是能用就行。
{
ctweeny_backward(&dino_jump_ctw);
}
if (dino_y == 0 && ctweeny_direction(&dino_jump_ctw) == backward) // 这一步用ctweeny_onstep()实现更好……但是能用就行。
{
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(); // 与dino相碰撞的一定是blocks[]里最前面的那一个,如果不是,那就是我懒得优化的bug
}
}
}

关于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--;
}
}
// 生成云,保证数量不超过 cloud_max_amount
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)
{
// 移动到屏幕外的一定是blocks[]里最前面的石头
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; // 自动重装器的值(“周期”),-1由公式得来
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1; // 预分频器,-1由公式得来(在 10KHz下记1w个数)
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0; // 重复计数器
TIM_TimeBaseInit(TIM3, &TIM_TimeBaseInitStructure);

TIM_ClearFlag(TIM3, TIM_FLAG_Update); // 手动清除更新中断标志位

// 使能中断
TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE); // 开启更新中断到NVIC的通路
// NVIC
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);
}
}

状态机更新

处理按键

两个按键key1key2有不同的任务。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上去

作者

勇敢梧桐树

发布于

2023-01-02

更新于

2023-01-04

许可协议

评论

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×