这次需要用 C 语言库 Allegro 写爆破彗星游戏。项目有一些描述如需要绘制飞船、彗星、子弹,需要响应按键实现飞船加速、减速、转向、开火,需要绘制弹道,需要实现彗星旋转、缩放,需要碰撞检测,需要显示计分。
这些用 wxPython 不难实现,因为有面向对象能帮我们很好地组织代码,甚至能在写代码前先画好类图。但在 C 语言里,没有类实现,难道靠注释约定函数们谁跟谁负责什么功能。
模糊的印象,类是用来描述具有相同属性和方法的一组事物的数据结构。在 Python 中,没有属性,可以只是函数;没有方法,可以是命名元组或字典;有存储变量和一个函数,可以是闭包。
没有属性可以是类吗?好像也行,比如写个通用的工具类,里面放一些常用的日志、计算之类的只和类绑定的静态方法。
没有方法可以是类吗?也可以,避免使用全局变量,写一个类专门存放只与类绑定的全局变量,然后通过类来调用。
闭包是类吗?它不能继承,也不能重写闭包里的函数。
最后似乎可以通过这些简单逻辑粗略得出结论,类是封装、继承、多态这三个面向对象概念的具体实现。
C 语言虽然没有直接的类实现,但可以通过结构体封装属性变量和方法,结构体也能通过嵌套实现继承效果,至于多态,指针是不是更自由。
这里 C 语言实现封装,我见过的一种是结构体中的方法用函数指针表示,还有一种是结构体模拟类只负责保存属性,方法函数单独写,但函数必须有一个参数是指向模拟类结构体变量的指针。
再看《嗨翻C语言》第 535 页的void draw_ship(Spaceship* s)
,应该是第二种方法,这样的话先按这种方式写出飞船类。
对引用和指针也很困惑,直接查看《引用与指针的区别》https://blog.csdn.net/HUAERBUSHI521/article/details/118368696
在其他模块中调用全局变量,需要使用 extern 声明,表明是调用外部变量。
多个模块怎么避免重复包含头文件?在头文件中使用预处理#ifndef #define #end
,通过判断是否定义指定变量来跳过其他代码,有点像 Python 中判断if __name__ == "__main__"
。
进展缓慢,只写了飞船的显示部分,写的时候查漏补缺挺好。
main.c
#include <stdio.h>
#include <allegro5/allegro.h>
#include <allegro5/allegro_primitives.h>
#include <allegro5/allegro_font.h>
#include <allegro5/allegro_ttf.h>
#include "./utility.h"
#include "./spaceship.h"
// 全局变量
const int WIDTH = 800; // 屏幕尺寸
const int HEIGHT = 600;
const int FPS = 60; // 帧率
ALLEGRO_FONT *font_40; // 字体
int main() {
// 初始化 Allegro
if (!al_init()) {
fprintf(stderr, "Failed to initialize Allegro!\n");
return -1;
}
// 安装键盘驱动,安装成功或已经安装过则返回 true
if (!al_install_keyboard()) {
fprintf(stderr, "Failed to initialize keyboard!\n");
return -1;
}
// 初始化图形绘制插件
if (!al_init_primitives_addon()) {
fprintf(stderr, "Failed to initialize primitives addon!\n");
return -1;
}
// 初始化字体插件
if (!al_init_font_addon()) {
fprintf(stderr, "Failed to initialize font addon!\n");
return -1;
}
// 初始化 TTF 字体插件
if (!al_init_ttf_addon()) {
fprintf(stderr, "Failed to initialize ttf font addon!\n");
return -1;
}
// 加载 TTF 字体,字号 40
font_40 = al_load_ttf_font("arial.ttf", 40, 0);
// 启用多重采样
al_set_new_display_option(ALLEGRO_SAMPLE_BUFFERS, 1, ALLEGRO_SUGGEST);
al_set_new_display_option(ALLEGRO_SAMPLES, 4, ALLEGRO_SUGGEST);
// 创建指定宽高的窗口
ALLEGRO_DISPLAY *display = al_create_display(WIDTH, HEIGHT);
if (!display) {
fprintf(stderr, "Failed to create display!\n");
return -1;
}
// 创建事件队列
ALLEGRO_EVENT_QUEUE *event_queue = al_create_event_queue();
if (!event_queue) {
fprintf(stderr, "Failed to create event queue!\n");
al_destroy_display(display);
return -1;
}
// 创建定时器,每 1.0 / FPS 秒触发一次
ALLEGRO_TIMER *timer = al_create_timer(1.0 / FPS);
// 注册事件源到事件队列
al_register_event_source(event_queue, al_get_display_event_source(display));
al_register_event_source(event_queue, al_get_keyboard_event_source());
al_register_event_source(event_queue, al_get_timer_event_source(timer));
// 清除屏幕并填充黑色
al_clear_to_color(al_map_rgb(0, 0, 0));
// 绘制背景 logo
draw_logo();
// 初始化并绘制飞船
Spaceship s = {WIDTH / 2.0, HEIGHT / 2.0, 0.0, 3.0, 0, al_map_rgb(0, 255, 0)};
draw_ship(&s);
// 交换缓冲区
al_flip_display();
// 启动定时器
al_start_timer(timer);
// 轮询事件
bool done = false;
while (!done) {
ALLEGRO_EVENT event;
// 等待从事件队列取出事件
al_wait_for_event(event_queue, &event);
// 处理按键事件,这里响应 ESC 按键
if (event.type == ALLEGRO_EVENT_KEY_DOWN && event.keyboard.keycode == ALLEGRO_KEY_ESCAPE) {
done = true;
}
// 处理窗口事件,这里响应点击窗口右上角关闭
if (event.type == ALLEGRO_EVENT_DISPLAY_CLOSE) {
done = true;
}
// 处理定时器事件
if (event.type == ALLEGRO_EVENT_TIMER) {
// 清屏并更新绘制
al_clear_to_color(al_map_rgb(0, 0, 0));
draw_logo();
// 每次更新转动 5 度
s.heading += 5.0; // s.heading 相当于 (&s)->heading
draw_ship(&s);
al_flip_display();
}
}
// 销毁资源,释放内存
al_destroy_timer(timer);
al_destroy_font(font_40);
al_destroy_display(display);
al_destroy_event_queue(event_queue);
return 0;
}
spaceship.h
#ifndef _SPACESHIP_H
#define _SPACESHIP_H
#include <allegro5/allegro.h>
typedef struct {
float sx; // 飞船中屏幕中的坐标
float sy;
float heading; //飞船朝向角度,如 30 度为 30.0
float speed;
int gone; // 是否阵亡
ALLEGRO_COLOR color;
} Spaceship;
void draw_ship(Spaceship*);
#endif
spaceship.c
#include <allegro5/allegro_primitives.h>
#include "./spaceship.h"
#define DEGREES(x) ((x) * ALLEGRO_PI / 180.0)
// 绘制飞船
void draw_ship(Spaceship* s) {
ALLEGRO_TRANSFORM transform;
al_identity_transform(&transform);
al_rotate_transform(&transform, DEGREES(s->heading));
al_translate_transform(&transform, s->sx, s->sy);
al_use_transform(&transform);
// 画线需要在调用 al_create_display 前设置多重采样以抗锯齿
al_draw_line(-8, 9, 0, -11, s->color, 3.0f);
al_draw_line(0, -11, 8, 9, s->color, 3.0f);
al_draw_line(-6, 4, -1, 4, s->color, 3.0f);
al_draw_line(6, 4, 1, 4, s->color, 3.0f);
// 重置变换矩阵,不然会影响其他绘制内容
al_identity_transform(&transform);
al_use_transform(&transform);
}
utility.h
#ifndef _UTILITY_H
#define _UTILITY_H
extern const int WIDTH;
extern const int HEIGHT;
extern ALLEGRO_FONT *font_40;
void draw_logo();
#endif
utility.c
#include <allegro5/allegro.h>
#include <allegro5/allegro_primitives.h>
#include <allegro5/allegro_font.h>
#include "./utility.h"
void draw_logo() {
const char *text = "SmileBasic";
// 获取指定字体的字符串外边框尺寸
int bbx, bby, bbw, bbh;
al_get_text_dimensions(font_40, text, &bbx, &bby, &bbw, &bbh);
// 居中绘制字符串,纵轴偏上显示
al_draw_text(font_40, al_map_rgb(255, 255, 255), WIDTH / 2.0, HEIGHT / 2.0 - bbh * 1.5, ALLEGRO_ALIGN_CENTRE, text);
// 绘制红色矩形边框,四周设置边距 10
float padding = 10.0;
float rect_x1 = (WIDTH - bbw) / 2.0 - padding;
float rect_y1 = HEIGHT / 2.0 - bbh * 1.5 - padding;
float rect_x2 = rect_x1 + bbw + padding * 2;
float rect_y2 = rect_y1 + bbh * 1.5 + padding * 2;
al_draw_rectangle(rect_x1, rect_y1, rect_x2, rect_y2, al_map_rgb(255, 0, 0), 2.0);
}