eduwin 游戏开发指南
eduwin 的画布组件(Canvas)不仅仅是绘图工具——它是一套完整的 2D 游戏引擎,内置了游戏开发所需的一切核心机制:
| 能力 | 说明 |
|---|---|
| 帧循环 | 定时器驱动,帧率可设(30/60fps 等) |
| 键盘输入 | 实时按键查询 + 按下/释放事件回调 |
| 鼠标输入 | 实时坐标查询 + 移动/按下/释放回调 |
| 精灵系统 | 图像/纯色精灵,支持平移、缩放、旋转、克隆、碰撞检测 |
| 精灵事件 | 一键注册精灵的 click、down、up、hover 回调,无需手动处理坐标 |
| 音效 | WAV 播放、蜂鸣器 |
| 双缓冲 | GDI+ 离屏位图,所有绘制先在位图上完成再一次性显示到屏幕,天然无闪烁 |
其中 双缓冲 是一个很大的优势:eduwin 的 Canvas 内部使用 GpBitmap 作为离屏画布,所有 canvas_draw_* 操作都在位图上进行,仅在 InvalidateRect 时才触发 WM_PAINT 刷新屏幕——不会有传统的闪烁问题,也无需手动管理缓冲切换。
- 打靶等低速游戏:30fps 即可
- 动作游戏、像素滚动:60fps
- 不建议超过 60fps,Windows 定时器精度有限
samples\shooting_demo\shooting_demo.c 是一个完整的打靶游戏示例,编译运行就能玩。
游戏开发核心 API
帧循环(游戏主循环)
void canvas_start_timer(HWND canvas, int fps, void (*callback)(void *), void *data);
void canvas_stop_timer(HWND canvas);
canvas_start_timer启动一个定时器,每秒调用fps次callback。通常设为 60(60fps)- 回调中执行游戏逻辑:更新状态 → 清理画布 → 重新绘制
canvas_stop_timer停止帧循环
void game_loop(void *data)
{
// 1. 处理输入
// 2. 更新游戏状态
// 3. 绘制画面
}
canvas_start_timer(canvas, 60, game_loop, NULL);
键盘输入
int canvas_is_key_down(HWND canvas, int vkey);
void canvas_on_key(HWND canvas, void (*down)(int), void (*up)(int));
两种方式获取键盘状态:
| 方式 | 适用场景 | 用法 |
|---|---|---|
| 实时查询 | WASD 移动、持续按住 | 每帧调用 canvas_is_key_down(canvas, 'W') |
| 事件回调 | 跳跃(按下一次)、菜单 | canvas_on_key(canvas, on_down, NULL) |
常用虚拟键码:
| 键 | 常量 | 数值 |
|---|---|---|
| 方向键左 | VK_LEFT | 37 |
| 方向键上 | VK_UP | 38 |
| 方向键右 | VK_RIGHT | 39 |
| 方向键下 | VK_DOWN | 40 |
| 空格 | VK_SPACE | 32 |
| ESC | VK_ESCAPE | 27 |
| A-Z | 'A' — 'Z' | 65—90 |
鼠标输入
void canvas_on_mouse(HWND canvas, void (*move)(int, int), void (*down)(int, int, int), void (*up)(int, int, int));
int canvas_get_mouse_x(HWND canvas);
int canvas_get_mouse_y(HWND canvas);
int canvas_is_mouse_down(HWND canvas, int btn);
鼠标回调参数约定:
void on_down(int button, int x, int y);
button:0=左键, 1=右键x,y:鼠标在画布客户区的坐标
精灵系统(Sprite)
eduwin 提供了完整的精灵系统,支持图像精灵和形状精灵,所有精灵内部统一为位图处理。内置平移、缩放、旋转、克隆、碰撞检测:
typedef struct Sprite Sprite;
/* 形状类型 */
#define SHAPE_RECT 0
#define SHAPE_ELLIPSE 1
#define SHAPE_TRIANGLE 2
#define SHAPE_DIAMOND 3
#define SHAPE_PENTAGON 4
#define SHAPE_HEXAGON 5
#define SHAPE_STAR 6
#define SHAPE_ARROW 7
#define SHAPE_PIE 8
Sprite *sprite_create_from_shape(int type, int w, int h);
Sprite *sprite_create_from_file(const WCHAR *filepath);
Sprite *sprite_create_from_image(void *image);
void sprite_destroy(Sprite *s);
void sprite_set_pos(Sprite *s, float x, float y);
void sprite_move(Sprite *s, float dx, float dy);
void sprite_set_scale(Sprite *s, float sx, float sy);
void sprite_set_rotation(Sprite *s, float degrees);
void sprite_set_color(Sprite *s, int r, int g, int b, int a);
void sprite_set_image(Sprite *s, void *image);
float sprite_get_x(Sprite *s);
float sprite_get_y(Sprite *s);
float sprite_get_w(Sprite *s);
float sprite_get_h(Sprite *s);
float sprite_get_sx(Sprite *s);
float sprite_get_sy(Sprite *s);
Sprite *sprite_clone(Sprite *s);
void sprite_draw(HWND canvas, Sprite *s);
int sprite_collide(Sprite *a, Sprite *b);
int sprite_hit_test(Sprite *s, float px, float py);
| 函数 | 说明 |
|---|---|
sprite_create_from_shape(type, w, h) | 创建形状精灵(矩形/椭圆/三角等),自动渲染为位图 |
sprite_create_from_file(filepath) | 从图片文件创建精灵,尺寸自动匹配 |
sprite_create_from_image(image) | 从 load_image_from_file 返回的句柄创建精灵(内部克隆副本) |
sprite_destroy(s) | 销毁精灵,引用计数归零时释放资源 |
sprite_set_pos(s, x, y) | 设置位置(左上角) |
sprite_move(s, dx, dy) | 平移 |
sprite_set_scale(s, sx, sy) | 缩放因子(1.0=原大,2.0=放大一倍) |
sprite_set_rotation(s, deg) | 设置旋转角度(度),绕原点(默认中心)旋转 |
sprite_set_color(s, r, g, b, a) | 正片叠底混色,透明像素不受影响,多次调用不累积 |
sprite_set_image(s, image) | 替换精灵图像,内部克隆为新副本 |
sprite_get_x/w/h/... | 读取精灵坐标、尺寸、缩放值 |
sprite_clone(s) | 克隆精灵(从原始图克隆独立工作副本) |
sprite_draw(canvas, s) | 在画布上绘制精灵,自动处理旋转/缩放/位置 |
sprite_collide(a, b) | AABB 碰撞检测,返回 1 表示碰撞 |
sprite_hit_test(s, px, py) | 检测点是否在精灵范围内(含缩放) |
on_sprite_click(canvas, s, cb, d) | 注册精灵点击事件回调 |
on_sprite_down(canvas, s, cb, d) | 注册精灵按下事件回调 |
on_sprite_up(canvas, s, cb, d) | 注册精灵释放事件回调 |
on_sprite_hover(canvas, s, cb, d) | 注册精灵悬停事件回调 |
Sprite 结构体字段可以直接读写:
sprite->x = 100; // 直接设坐标
sprite->visible = 0; // 隐藏
sprite->ox = 0; // 原点移到左上角
sprite->oy = 0;
Sprite 内部引用计数机制:克隆时共享同一份 GpImage*,最后一个精灵销毁时才释放图像内存。
load_image_from_file 配合 sprite_create_from_image 可以实现图像复用:
void *img = load_image_from_file(L"player.png", NULL, NULL); // 加载一次
Sprite *player1 = sprite_create_from_image(img); // 创建两个精灵共享同一张图
Sprite *player2 = sprite_create_from_image(img);
sprite_set_pos(player1, 50, 50);
sprite_set_pos(player2, 200, 50);
// 每个精灵独立销毁,但图像在最后一个销毁时才释放
碰撞检测
int sprite_collide(Sprite *a, Sprite *b);
AABB 轴对齐矩形碰撞检测,自动考虑缩放:
if (sprite_collide(player, enemy))
{
player->visible = 0; // 玩家消失
sprite_set_pos(player, 0, 0); // 重置位置
}
在游戏中,精灵本身就充当按钮——用 on_sprite_click 处理点击,用 on_sprite_hover 做悬停反馈,完全不需要额外的按钮控件。详见下文「精灵事件(推荐)」。
精灵事件
更简洁的方式是直接注册精灵事件回调,无需手动处理鼠标坐标:
void on_sprite_click(HWND canvas, Sprite *s, void (*cb)(Sprite *, void *), void *data);
void on_sprite_down(HWND canvas, Sprite *s, void (*cb)(Sprite *, void *), void *data);
void on_sprite_up(HWND canvas, Sprite *s, void (*cb)(Sprite *, void *), void *data);
void on_sprite_hover(HWND canvas, Sprite *s, void (*cb)(Sprite *, void *), void *data);
注册后,框架自动检测精灵上的鼠标事件并调用回调:
Sprite *btn = sprite_create_from_shape(SHAPE_RECT, 120, 40);
sprite_set_pos(btn, 100, 100);
sprite_set_color(btn, 80, 160, 240, 255);
void on_btn_click(Sprite *s, void *data)
{
sprite_set_color(s, 0, 200, 80, 255); // 点击变绿
play_beep(1000, 100);
}
void on_btn_hover(Sprite *s, void *data)
{
sprite_set_color(s, 120, 200, 255, 255); // 悬停高亮
}
on_sprite_click(canvas, btn, on_btn_click, NULL);
on_sprite_hover(canvas, btn, on_btn_hover, NULL);
事件生命周期自动管理:销毁精灵时会自动清理注册的事件。同一个精灵可以同时注册 click、down、up、hover 中的任意组合。
辅助工具
int canvas_get_width(HWND canvas);
int canvas_get_height(HWND canvas);
void canvas_clear_rect(HWND canvas, int x, int y, int w, int h, int r, int g, int b, int a);
int rand_int(int min, int max);
canvas_clear_rect局部清除并填充颜色,适合需要保留背景的游戏场景rand_int生成[min, max]闭区间随机整数
音效
int play_sound(const WCHAR *filepath);
int play_sound_loop(const WCHAR *filepath);
int stop_sound(void);
int play_beep(int freq, int duration);
play_sound 播放音频文件(异步,不阻塞游戏循环),成功返回 1,失败返回 0。
play_sound_loop 循环播放音频文件。
stop_sound 停止当前正在播放的音频。
play_beep 通过 PC 喇叭发出指定频率和时长的蜂鸣声。freq 为频率(Hz),duration 为时长(毫秒)。成功返回 1。
适合游戏的用法:
play_sound(L"hit.wav"); // 击中音效(异步,不卡帧)
play_beep(1200, 80); // 高音短促蜂鸣代替
游戏模板
创建一个新游戏的最简模板:
#include "eduwin.h"
static HWND canvas;
static void game_tick(void *data)
{
int cw = canvas_get_width(canvas);
int ch = canvas_get_height(canvas);
canvas_clear(canvas, 0, 0, 0); // 黑色背景
/* 游戏逻辑写在这里 */
canvas_draw_text(canvas, L"Hello Game!", 10, 10, 255, 255, 255, 16);
}
int main(void)
{
init_app();
HWND win = create_window(L"我的游戏", 800, 600);
canvas = create_canvas(win, 0, 0, 800, 600);
canvas_start_timer(canvas, 60, game_tick, NULL);
canvas_on_key(canvas, NULL, NULL); // 让画布获得键盘焦点
show_window(win);
run_app();
return 0;
}
编译命令(参考 samples\shooting_demo\build.bat):
cd samples\shooting_demo
build.bat