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 启动一个定时器,每秒调用 fpscallback。通常设为 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_LEFT37
方向键上VK_UP38
方向键右VK_RIGHT39
方向键下VK_DOWN40
空格VK_SPACE32
ESCVK_ESCAPE27
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