这次需要用 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() {// 初始化 Allegroif (!al_init()) {fprintf(stderr, "Failed to initialize Allegro!\n");return -1;}// 安装键盘驱动,安装成功或已经安装过则返回 trueif (!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)->headingdraw_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_Hextern 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);
}