[{"data":1,"prerenderedAt":1489},["ShallowReactive",2],{"doc-\u002Fdocs\u002Ftutorial\u002Feduwin-game":3},{"id":4,"title":5,"body":6,"description":1482,"extension":1483,"meta":1484,"navigation":266,"path":1485,"seo":1486,"stem":1487,"__hash__":1488},"content\u002Fdocs\u002Ftutorial\u002Feduwin-game.md","eduwin 游戏开发指南",{"type":7,"value":8,"toc":1469},"minimark",[9,13,22,112,135,155,161,164,169,174,197,222,274,276,291,294,342,345,457,459,484,487,495,513,517,528,721,928,931,956,963,972,1015,1018,1026,1029,1057,1068,1070,1073,1098,1101,1184,1187,1190,1215,1233,1235,1260,1266,1272,1278,1292,1295,1310,1312,1315,1318,1452,1459,1465],[10,11,5],"h1",{"id":12},"eduwin-游戏开发指南",[14,15,16,17,21],"p",{},"eduwin 的画布组件（Canvas）不仅仅是绘图工具——它是一套完整的 ",[18,19,20],"strong",{},"2D 游戏引擎","，内置了游戏开发所需的一切核心机制：",[23,24,25,38],"table",{},[26,27,28],"thead",{},[29,30,31,35],"tr",{},[32,33,34],"th",{},"能力",[32,36,37],{},"说明",[39,40,41,52,62,72,82,92,102],"tbody",{},[29,42,43,49],{},[44,45,46],"td",{},[18,47,48],{},"帧循环",[44,50,51],{},"定时器驱动，帧率可设（30\u002F60fps 等）",[29,53,54,59],{},[44,55,56],{},[18,57,58],{},"键盘输入",[44,60,61],{},"实时按键查询 + 按下\u002F释放事件回调",[29,63,64,69],{},[44,65,66],{},[18,67,68],{},"鼠标输入",[44,70,71],{},"实时坐标查询 + 移动\u002F按下\u002F释放回调",[29,73,74,79],{},[44,75,76],{},[18,77,78],{},"精灵系统",[44,80,81],{},"图像\u002F纯色精灵，支持平移、缩放、旋转、克隆、碰撞检测",[29,83,84,89],{},[44,85,86],{},[18,87,88],{},"精灵事件",[44,90,91],{},"一键注册精灵的 click、down、up、hover 回调，无需手动处理坐标",[29,93,94,99],{},[44,95,96],{},[18,97,98],{},"音效",[44,100,101],{},"WAV 播放、蜂鸣器",[29,103,104,109],{},[44,105,106],{},[18,107,108],{},"双缓冲",[44,110,111],{},"GDI+ 离屏位图，所有绘制先在位图上完成再一次性显示到屏幕，天然无闪烁",[14,113,114,115,117,118,122,123,126,127,130,131,134],{},"其中 ",[18,116,108],{}," 是一个很大的优势：eduwin 的 Canvas 内部使用 ",[119,120,121],"code",{},"GpBitmap"," 作为离屏画布，所有 ",[119,124,125],{},"canvas_draw_*"," 操作都在位图上进行，仅在 ",[119,128,129],{},"InvalidateRect"," 时才触发 ",[119,132,133],{},"WM_PAINT"," 刷新屏幕——不会有传统的闪烁问题，也无需手动管理缓冲切换。",[136,137,138,146,152],"ul",{},[139,140,141,142,145],"li",{},"打靶等低速游戏：",[18,143,144],{},"30fps"," 即可",[139,147,148,149],{},"动作游戏、像素滚动：",[18,150,151],{},"60fps",[139,153,154],{},"不建议超过 60fps，Windows 定时器精度有限",[14,156,157,160],{},[119,158,159],{},"samples\\shooting_demo\\shooting_demo.c"," 是一个完整的打靶游戏示例，编译运行就能玩。",[162,163],"hr",{},[165,166,168],"h2",{"id":167},"游戏开发核心-api","游戏开发核心 API",[170,171,173],"h3",{"id":172},"帧循环游戏主循环","帧循环（游戏主循环）",[175,176,181],"pre",{"className":177,"code":178,"language":179,"meta":180,"style":180},"language-c shiki shiki-themes github-light github-dark","void canvas_start_timer(HWND canvas, int fps, void (*callback)(void *), void *data);\nvoid canvas_stop_timer(HWND canvas);\n","c","",[119,182,183,191],{"__ignoreMap":180},[184,185,188],"span",{"class":186,"line":187},"line",1,[184,189,190],{},"void canvas_start_timer(HWND canvas, int fps, void (*callback)(void *), void *data);\n",[184,192,194],{"class":186,"line":193},2,[184,195,196],{},"void canvas_stop_timer(HWND canvas);\n",[136,198,199,213,216],{},[139,200,201,204,205,208,209,212],{},[119,202,203],{},"canvas_start_timer"," 启动一个定时器，每秒调用 ",[119,206,207],{},"fps"," 次 ",[119,210,211],{},"callback","。通常设为 60（60fps）",[139,214,215],{},"回调中执行游戏逻辑：更新状态 → 清理画布 → 重新绘制",[139,217,218,221],{},[119,219,220],{},"canvas_stop_timer"," 停止帧循环",[175,223,225],{"className":177,"code":224,"language":179,"meta":180,"style":180},"void game_loop(void *data)\n{\n    \u002F\u002F 1. 处理输入\n    \u002F\u002F 2. 更新游戏状态\n    \u002F\u002F 3. 绘制画面\n}\n\ncanvas_start_timer(canvas, 60, game_loop, NULL);\n",[119,226,227,232,237,243,249,255,261,268],{"__ignoreMap":180},[184,228,229],{"class":186,"line":187},[184,230,231],{},"void game_loop(void *data)\n",[184,233,234],{"class":186,"line":193},[184,235,236],{},"{\n",[184,238,240],{"class":186,"line":239},3,[184,241,242],{},"    \u002F\u002F 1. 处理输入\n",[184,244,246],{"class":186,"line":245},4,[184,247,248],{},"    \u002F\u002F 2. 更新游戏状态\n",[184,250,252],{"class":186,"line":251},5,[184,253,254],{},"    \u002F\u002F 3. 绘制画面\n",[184,256,258],{"class":186,"line":257},6,[184,259,260],{},"}\n",[184,262,264],{"class":186,"line":263},7,[184,265,267],{"emptyLinePlaceholder":266},true,"\n",[184,269,271],{"class":186,"line":270},8,[184,272,273],{},"canvas_start_timer(canvas, 60, game_loop, NULL);\n",[170,275,58],{"id":58},[175,277,279],{"className":177,"code":278,"language":179,"meta":180,"style":180},"int canvas_is_key_down(HWND canvas, int vkey);\nvoid canvas_on_key(HWND canvas, void (*down)(int), void (*up)(int));\n",[119,280,281,286],{"__ignoreMap":180},[184,282,283],{"class":186,"line":187},[184,284,285],{},"int canvas_is_key_down(HWND canvas, int vkey);\n",[184,287,288],{"class":186,"line":193},[184,289,290],{},"void canvas_on_key(HWND canvas, void (*down)(int), void (*up)(int));\n",[14,292,293],{},"两种方式获取键盘状态：",[23,295,296,309],{},[26,297,298],{},[29,299,300,303,306],{},[32,301,302],{},"方式",[32,304,305],{},"适用场景",[32,307,308],{},"用法",[39,310,311,327],{},[29,312,313,318,321],{},[44,314,315],{},[18,316,317],{},"实时查询",[44,319,320],{},"WASD 移动、持续按住",[44,322,323,324],{},"每帧调用 ",[119,325,326],{},"canvas_is_key_down(canvas, 'W')",[29,328,329,334,337],{},[44,330,331],{},[18,332,333],{},"事件回调",[44,335,336],{},"跳跃（按下一次）、菜单",[44,338,339],{},[119,340,341],{},"canvas_on_key(canvas, on_down, NULL)",[14,343,344],{},"常用虚拟键码：",[23,346,347,360],{},[26,348,349],{},[29,350,351,354,357],{},[32,352,353],{},"键",[32,355,356],{},"常量",[32,358,359],{},"数值",[39,361,362,375,388,401,414,427,440],{},[29,363,364,367,372],{},[44,365,366],{},"方向键左",[44,368,369],{},[119,370,371],{},"VK_LEFT",[44,373,374],{},"37",[29,376,377,380,385],{},[44,378,379],{},"方向键上",[44,381,382],{},[119,383,384],{},"VK_UP",[44,386,387],{},"38",[29,389,390,393,398],{},[44,391,392],{},"方向键右",[44,394,395],{},[119,396,397],{},"VK_RIGHT",[44,399,400],{},"39",[29,402,403,406,411],{},[44,404,405],{},"方向键下",[44,407,408],{},[119,409,410],{},"VK_DOWN",[44,412,413],{},"40",[29,415,416,419,424],{},[44,417,418],{},"空格",[44,420,421],{},[119,422,423],{},"VK_SPACE",[44,425,426],{},"32",[29,428,429,432,437],{},[44,430,431],{},"ESC",[44,433,434],{},[119,435,436],{},"VK_ESCAPE",[44,438,439],{},"27",[29,441,442,445,454],{},[44,443,444],{},"A-Z",[44,446,447,450,451],{},[119,448,449],{},"'A'"," — ",[119,452,453],{},"'Z'",[44,455,456],{},"65—90",[170,458,68],{"id":68},[175,460,462],{"className":177,"code":461,"language":179,"meta":180,"style":180},"void canvas_on_mouse(HWND canvas, void (*move)(int, int), void (*down)(int, int, int), void (*up)(int, int, int));\nint canvas_get_mouse_x(HWND canvas);\nint canvas_get_mouse_y(HWND canvas);\nint canvas_is_mouse_down(HWND canvas, int btn);\n",[119,463,464,469,474,479],{"__ignoreMap":180},[184,465,466],{"class":186,"line":187},[184,467,468],{},"void canvas_on_mouse(HWND canvas, void (*move)(int, int), void (*down)(int, int, int), void (*up)(int, int, int));\n",[184,470,471],{"class":186,"line":193},[184,472,473],{},"int canvas_get_mouse_x(HWND canvas);\n",[184,475,476],{"class":186,"line":239},[184,477,478],{},"int canvas_get_mouse_y(HWND canvas);\n",[184,480,481],{"class":186,"line":245},[184,482,483],{},"int canvas_is_mouse_down(HWND canvas, int btn);\n",[14,485,486],{},"鼠标回调参数约定：",[175,488,493],{"className":489,"code":491,"language":492},[490],"language-text","void on_down(int button, int x, int y);\n","text",[119,494,491],{"__ignoreMap":180},[136,496,497,503],{},[139,498,499,502],{},[119,500,501],{},"button","：0=左键, 1=右键",[139,504,505,508,509,512],{},[119,506,507],{},"x",", ",[119,510,511],{},"y","：鼠标在画布客户区的坐标",[170,514,516],{"id":515},"精灵系统sprite","精灵系统（Sprite）",[14,518,519,520,523,524,527],{},"eduwin 提供了完整的精灵系统，支持",[18,521,522],{},"图像精灵","和",[18,525,526],{},"形状精灵","，所有精灵内部统一为位图处理。内置平移、缩放、旋转、克隆、碰撞检测：",[175,529,531],{"className":177,"code":530,"language":179,"meta":180,"style":180},"typedef struct Sprite Sprite;\n\n\u002F* 形状类型 *\u002F\n#define SHAPE_RECT      0\n#define SHAPE_ELLIPSE   1\n#define SHAPE_TRIANGLE  2\n#define SHAPE_DIAMOND   3\n#define SHAPE_PENTAGON  4\n#define SHAPE_HEXAGON   5\n#define SHAPE_STAR      6\n#define SHAPE_ARROW     7\n#define SHAPE_PIE       8\n\nSprite *sprite_create_from_shape(int type, int w, int h);\nSprite *sprite_create_from_file(const WCHAR *filepath);\nSprite *sprite_create_from_image(void *image);\nvoid sprite_destroy(Sprite *s);\nvoid sprite_set_pos(Sprite *s, float x, float y);\nvoid sprite_move(Sprite *s, float dx, float dy);\nvoid sprite_set_scale(Sprite *s, float sx, float sy);\nvoid sprite_set_rotation(Sprite *s, float degrees);\nvoid sprite_set_color(Sprite *s, int r, int g, int b, int a);\nvoid sprite_set_image(Sprite *s, void *image);\nfloat sprite_get_x(Sprite *s);\nfloat sprite_get_y(Sprite *s);\nfloat sprite_get_w(Sprite *s);\nfloat sprite_get_h(Sprite *s);\nfloat sprite_get_sx(Sprite *s);\nfloat sprite_get_sy(Sprite *s);\nSprite *sprite_clone(Sprite *s);\nvoid sprite_draw(HWND canvas, Sprite *s);\nint sprite_collide(Sprite *a, Sprite *b);\nint sprite_hit_test(Sprite *s, float px, float py);\n",[119,532,533,538,542,547,552,557,562,567,572,578,584,590,596,601,607,613,619,625,631,637,643,649,655,661,667,673,679,685,691,697,703,709,715],{"__ignoreMap":180},[184,534,535],{"class":186,"line":187},[184,536,537],{},"typedef struct Sprite Sprite;\n",[184,539,540],{"class":186,"line":193},[184,541,267],{"emptyLinePlaceholder":266},[184,543,544],{"class":186,"line":239},[184,545,546],{},"\u002F* 形状类型 *\u002F\n",[184,548,549],{"class":186,"line":245},[184,550,551],{},"#define SHAPE_RECT      0\n",[184,553,554],{"class":186,"line":251},[184,555,556],{},"#define SHAPE_ELLIPSE   1\n",[184,558,559],{"class":186,"line":257},[184,560,561],{},"#define SHAPE_TRIANGLE  2\n",[184,563,564],{"class":186,"line":263},[184,565,566],{},"#define SHAPE_DIAMOND   3\n",[184,568,569],{"class":186,"line":270},[184,570,571],{},"#define SHAPE_PENTAGON  4\n",[184,573,575],{"class":186,"line":574},9,[184,576,577],{},"#define SHAPE_HEXAGON   5\n",[184,579,581],{"class":186,"line":580},10,[184,582,583],{},"#define SHAPE_STAR      6\n",[184,585,587],{"class":186,"line":586},11,[184,588,589],{},"#define SHAPE_ARROW     7\n",[184,591,593],{"class":186,"line":592},12,[184,594,595],{},"#define SHAPE_PIE       8\n",[184,597,599],{"class":186,"line":598},13,[184,600,267],{"emptyLinePlaceholder":266},[184,602,604],{"class":186,"line":603},14,[184,605,606],{},"Sprite *sprite_create_from_shape(int type, int w, int h);\n",[184,608,610],{"class":186,"line":609},15,[184,611,612],{},"Sprite *sprite_create_from_file(const WCHAR *filepath);\n",[184,614,616],{"class":186,"line":615},16,[184,617,618],{},"Sprite *sprite_create_from_image(void *image);\n",[184,620,622],{"class":186,"line":621},17,[184,623,624],{},"void sprite_destroy(Sprite *s);\n",[184,626,628],{"class":186,"line":627},18,[184,629,630],{},"void sprite_set_pos(Sprite *s, float x, float y);\n",[184,632,634],{"class":186,"line":633},19,[184,635,636],{},"void sprite_move(Sprite *s, float dx, float dy);\n",[184,638,640],{"class":186,"line":639},20,[184,641,642],{},"void sprite_set_scale(Sprite *s, float sx, float sy);\n",[184,644,646],{"class":186,"line":645},21,[184,647,648],{},"void sprite_set_rotation(Sprite *s, float degrees);\n",[184,650,652],{"class":186,"line":651},22,[184,653,654],{},"void sprite_set_color(Sprite *s, int r, int g, int b, int a);\n",[184,656,658],{"class":186,"line":657},23,[184,659,660],{},"void sprite_set_image(Sprite *s, void *image);\n",[184,662,664],{"class":186,"line":663},24,[184,665,666],{},"float sprite_get_x(Sprite *s);\n",[184,668,670],{"class":186,"line":669},25,[184,671,672],{},"float sprite_get_y(Sprite *s);\n",[184,674,676],{"class":186,"line":675},26,[184,677,678],{},"float sprite_get_w(Sprite *s);\n",[184,680,682],{"class":186,"line":681},27,[184,683,684],{},"float sprite_get_h(Sprite *s);\n",[184,686,688],{"class":186,"line":687},28,[184,689,690],{},"float sprite_get_sx(Sprite *s);\n",[184,692,694],{"class":186,"line":693},29,[184,695,696],{},"float sprite_get_sy(Sprite *s);\n",[184,698,700],{"class":186,"line":699},30,[184,701,702],{},"Sprite *sprite_clone(Sprite *s);\n",[184,704,706],{"class":186,"line":705},31,[184,707,708],{},"void sprite_draw(HWND canvas, Sprite *s);\n",[184,710,712],{"class":186,"line":711},32,[184,713,714],{},"int sprite_collide(Sprite *a, Sprite *b);\n",[184,716,718],{"class":186,"line":717},33,[184,719,720],{},"int sprite_hit_test(Sprite *s, float px, float py);\n",[23,722,723,732],{},[26,724,725],{},[29,726,727,730],{},[32,728,729],{},"函数",[32,731,37],{},[39,733,734,744,754,768,778,788,798,808,818,828,838,848,858,868,878,888,898,908,918],{},[29,735,736,741],{},[44,737,738],{},[119,739,740],{},"sprite_create_from_shape(type, w, h)",[44,742,743],{},"创建形状精灵（矩形\u002F椭圆\u002F三角等），自动渲染为位图",[29,745,746,751],{},[44,747,748],{},[119,749,750],{},"sprite_create_from_file(filepath)",[44,752,753],{},"从图片文件创建精灵，尺寸自动匹配",[29,755,756,761],{},[44,757,758],{},[119,759,760],{},"sprite_create_from_image(image)",[44,762,763,764,767],{},"从 ",[119,765,766],{},"load_image_from_file"," 返回的句柄创建精灵（内部克隆副本）",[29,769,770,775],{},[44,771,772],{},[119,773,774],{},"sprite_destroy(s)",[44,776,777],{},"销毁精灵，引用计数归零时释放资源",[29,779,780,785],{},[44,781,782],{},[119,783,784],{},"sprite_set_pos(s, x, y)",[44,786,787],{},"设置位置（左上角）",[29,789,790,795],{},[44,791,792],{},[119,793,794],{},"sprite_move(s, dx, dy)",[44,796,797],{},"平移",[29,799,800,805],{},[44,801,802],{},[119,803,804],{},"sprite_set_scale(s, sx, sy)",[44,806,807],{},"缩放因子（1.0=原大，2.0=放大一倍）",[29,809,810,815],{},[44,811,812],{},[119,813,814],{},"sprite_set_rotation(s, deg)",[44,816,817],{},"设置旋转角度（度），绕原点（默认中心）旋转",[29,819,820,825],{},[44,821,822],{},[119,823,824],{},"sprite_set_color(s, r, g, b, a)",[44,826,827],{},"正片叠底混色，透明像素不受影响，多次调用不累积",[29,829,830,835],{},[44,831,832],{},[119,833,834],{},"sprite_set_image(s, image)",[44,836,837],{},"替换精灵图像，内部克隆为新副本",[29,839,840,845],{},[44,841,842],{},[119,843,844],{},"sprite_get_x\u002Fw\u002Fh\u002F...",[44,846,847],{},"读取精灵坐标、尺寸、缩放值",[29,849,850,855],{},[44,851,852],{},[119,853,854],{},"sprite_clone(s)",[44,856,857],{},"克隆精灵（从原始图克隆独立工作副本）",[29,859,860,865],{},[44,861,862],{},[119,863,864],{},"sprite_draw(canvas, s)",[44,866,867],{},"在画布上绘制精灵，自动处理旋转\u002F缩放\u002F位置",[29,869,870,875],{},[44,871,872],{},[119,873,874],{},"sprite_collide(a, b)",[44,876,877],{},"AABB 碰撞检测，返回 1 表示碰撞",[29,879,880,885],{},[44,881,882],{},[119,883,884],{},"sprite_hit_test(s, px, py)",[44,886,887],{},"检测点是否在精灵范围内（含缩放）",[29,889,890,895],{},[44,891,892],{},[119,893,894],{},"on_sprite_click(canvas, s, cb, d)",[44,896,897],{},"注册精灵点击事件回调",[29,899,900,905],{},[44,901,902],{},[119,903,904],{},"on_sprite_down(canvas, s, cb, d)",[44,906,907],{},"注册精灵按下事件回调",[29,909,910,915],{},[44,911,912],{},[119,913,914],{},"on_sprite_up(canvas, s, cb, d)",[44,916,917],{},"注册精灵释放事件回调",[29,919,920,925],{},[44,921,922],{},[119,923,924],{},"on_sprite_hover(canvas, s, cb, d)",[44,926,927],{},"注册精灵悬停事件回调",[14,929,930],{},"Sprite 结构体字段可以直接读写：",[175,932,934],{"className":177,"code":933,"language":179,"meta":180,"style":180},"sprite->x = 100;       \u002F\u002F 直接设坐标\nsprite->visible = 0;   \u002F\u002F 隐藏\nsprite->ox = 0;        \u002F\u002F 原点移到左上角\nsprite->oy = 0;\n",[119,935,936,941,946,951],{"__ignoreMap":180},[184,937,938],{"class":186,"line":187},[184,939,940],{},"sprite->x = 100;       \u002F\u002F 直接设坐标\n",[184,942,943],{"class":186,"line":193},[184,944,945],{},"sprite->visible = 0;   \u002F\u002F 隐藏\n",[184,947,948],{"class":186,"line":239},[184,949,950],{},"sprite->ox = 0;        \u002F\u002F 原点移到左上角\n",[184,952,953],{"class":186,"line":245},[184,954,955],{},"sprite->oy = 0;\n",[14,957,958,959,962],{},"Sprite 内部引用计数机制：克隆时共享同一份 ",[119,960,961],{},"GpImage*","，最后一个精灵销毁时才释放图像内存。",[14,964,965,967,968,971],{},[119,966,766],{}," 配合 ",[119,969,970],{},"sprite_create_from_image"," 可以实现图像复用：",[175,973,975],{"className":177,"code":974,"language":179,"meta":180,"style":180},"void *img = load_image_from_file(L\"player.png\", NULL, NULL);  \u002F\u002F 加载一次\n\nSprite *player1 = sprite_create_from_image(img);  \u002F\u002F 创建两个精灵共享同一张图\nSprite *player2 = sprite_create_from_image(img);\nsprite_set_pos(player1, 50, 50);\nsprite_set_pos(player2, 200, 50);\n\n\u002F\u002F 每个精灵独立销毁，但图像在最后一个销毁时才释放\n",[119,976,977,982,986,991,996,1001,1006,1010],{"__ignoreMap":180},[184,978,979],{"class":186,"line":187},[184,980,981],{},"void *img = load_image_from_file(L\"player.png\", NULL, NULL);  \u002F\u002F 加载一次\n",[184,983,984],{"class":186,"line":193},[184,985,267],{"emptyLinePlaceholder":266},[184,987,988],{"class":186,"line":239},[184,989,990],{},"Sprite *player1 = sprite_create_from_image(img);  \u002F\u002F 创建两个精灵共享同一张图\n",[184,992,993],{"class":186,"line":245},[184,994,995],{},"Sprite *player2 = sprite_create_from_image(img);\n",[184,997,998],{"class":186,"line":251},[184,999,1000],{},"sprite_set_pos(player1, 50, 50);\n",[184,1002,1003],{"class":186,"line":257},[184,1004,1005],{},"sprite_set_pos(player2, 200, 50);\n",[184,1007,1008],{"class":186,"line":263},[184,1009,267],{"emptyLinePlaceholder":266},[184,1011,1012],{"class":186,"line":270},[184,1013,1014],{},"\u002F\u002F 每个精灵独立销毁，但图像在最后一个销毁时才释放\n",[170,1016,1017],{"id":1017},"碰撞检测",[175,1019,1020],{"className":177,"code":714,"language":179,"meta":180,"style":180},[119,1021,1022],{"__ignoreMap":180},[184,1023,1024],{"class":186,"line":187},[184,1025,714],{},[14,1027,1028],{},"AABB 轴对齐矩形碰撞检测，自动考虑缩放：",[175,1030,1032],{"className":177,"code":1031,"language":179,"meta":180,"style":180},"if (sprite_collide(player, enemy))\n{\n    player->visible = 0;          \u002F\u002F 玩家消失\n    sprite_set_pos(player, 0, 0); \u002F\u002F 重置位置\n}\n",[119,1033,1034,1039,1043,1048,1053],{"__ignoreMap":180},[184,1035,1036],{"class":186,"line":187},[184,1037,1038],{},"if (sprite_collide(player, enemy))\n",[184,1040,1041],{"class":186,"line":193},[184,1042,236],{},[184,1044,1045],{"class":186,"line":239},[184,1046,1047],{},"    player->visible = 0;          \u002F\u002F 玩家消失\n",[184,1049,1050],{"class":186,"line":245},[184,1051,1052],{},"    sprite_set_pos(player, 0, 0); \u002F\u002F 重置位置\n",[184,1054,1055],{"class":186,"line":251},[184,1056,260],{},[14,1058,1059,1060,1063,1064,1067],{},"在游戏中，精灵本身就充当按钮——用 ",[119,1061,1062],{},"on_sprite_click"," 处理点击，用 ",[119,1065,1066],{},"on_sprite_hover"," 做悬停反馈，完全不需要额外的按钮控件。详见下文「精灵事件（推荐）」。",[170,1069,88],{"id":88},[14,1071,1072],{},"更简洁的方式是直接注册精灵事件回调，无需手动处理鼠标坐标：",[175,1074,1076],{"className":177,"code":1075,"language":179,"meta":180,"style":180},"void on_sprite_click(HWND canvas, Sprite *s, void (*cb)(Sprite *, void *), void *data);\nvoid on_sprite_down(HWND canvas, Sprite *s, void (*cb)(Sprite *, void *), void *data);\nvoid on_sprite_up(HWND canvas, Sprite *s, void (*cb)(Sprite *, void *), void *data);\nvoid on_sprite_hover(HWND canvas, Sprite *s, void (*cb)(Sprite *, void *), void *data);\n",[119,1077,1078,1083,1088,1093],{"__ignoreMap":180},[184,1079,1080],{"class":186,"line":187},[184,1081,1082],{},"void on_sprite_click(HWND canvas, Sprite *s, void (*cb)(Sprite *, void *), void *data);\n",[184,1084,1085],{"class":186,"line":193},[184,1086,1087],{},"void on_sprite_down(HWND canvas, Sprite *s, void (*cb)(Sprite *, void *), void *data);\n",[184,1089,1090],{"class":186,"line":239},[184,1091,1092],{},"void on_sprite_up(HWND canvas, Sprite *s, void (*cb)(Sprite *, void *), void *data);\n",[184,1094,1095],{"class":186,"line":245},[184,1096,1097],{},"void on_sprite_hover(HWND canvas, Sprite *s, void (*cb)(Sprite *, void *), void *data);\n",[14,1099,1100],{},"注册后，框架自动检测精灵上的鼠标事件并调用回调：",[175,1102,1104],{"className":177,"code":1103,"language":179,"meta":180,"style":180},"Sprite *btn = sprite_create_from_shape(SHAPE_RECT, 120, 40);\nsprite_set_pos(btn, 100, 100);\nsprite_set_color(btn, 80, 160, 240, 255);\n\nvoid on_btn_click(Sprite *s, void *data)\n{\n    sprite_set_color(s, 0, 200, 80, 255);    \u002F\u002F 点击变绿\n    play_beep(1000, 100);\n}\n\nvoid on_btn_hover(Sprite *s, void *data)\n{\n    sprite_set_color(s, 120, 200, 255, 255);  \u002F\u002F 悬停高亮\n}\n\non_sprite_click(canvas, btn, on_btn_click, NULL);\non_sprite_hover(canvas, btn, on_btn_hover, NULL);\n",[119,1105,1106,1111,1116,1121,1125,1130,1134,1139,1144,1148,1152,1157,1161,1166,1170,1174,1179],{"__ignoreMap":180},[184,1107,1108],{"class":186,"line":187},[184,1109,1110],{},"Sprite *btn = sprite_create_from_shape(SHAPE_RECT, 120, 40);\n",[184,1112,1113],{"class":186,"line":193},[184,1114,1115],{},"sprite_set_pos(btn, 100, 100);\n",[184,1117,1118],{"class":186,"line":239},[184,1119,1120],{},"sprite_set_color(btn, 80, 160, 240, 255);\n",[184,1122,1123],{"class":186,"line":245},[184,1124,267],{"emptyLinePlaceholder":266},[184,1126,1127],{"class":186,"line":251},[184,1128,1129],{},"void on_btn_click(Sprite *s, void *data)\n",[184,1131,1132],{"class":186,"line":257},[184,1133,236],{},[184,1135,1136],{"class":186,"line":263},[184,1137,1138],{},"    sprite_set_color(s, 0, 200, 80, 255);    \u002F\u002F 点击变绿\n",[184,1140,1141],{"class":186,"line":270},[184,1142,1143],{},"    play_beep(1000, 100);\n",[184,1145,1146],{"class":186,"line":574},[184,1147,260],{},[184,1149,1150],{"class":186,"line":580},[184,1151,267],{"emptyLinePlaceholder":266},[184,1153,1154],{"class":186,"line":586},[184,1155,1156],{},"void on_btn_hover(Sprite *s, void *data)\n",[184,1158,1159],{"class":186,"line":592},[184,1160,236],{},[184,1162,1163],{"class":186,"line":598},[184,1164,1165],{},"    sprite_set_color(s, 120, 200, 255, 255);  \u002F\u002F 悬停高亮\n",[184,1167,1168],{"class":186,"line":603},[184,1169,260],{},[184,1171,1172],{"class":186,"line":609},[184,1173,267],{"emptyLinePlaceholder":266},[184,1175,1176],{"class":186,"line":615},[184,1177,1178],{},"on_sprite_click(canvas, btn, on_btn_click, NULL);\n",[184,1180,1181],{"class":186,"line":621},[184,1182,1183],{},"on_sprite_hover(canvas, btn, on_btn_hover, NULL);\n",[14,1185,1186],{},"事件生命周期自动管理：销毁精灵时会自动清理注册的事件。同一个精灵可以同时注册 click、down、up、hover 中的任意组合。",[170,1188,1189],{"id":1189},"辅助工具",[175,1191,1193],{"className":177,"code":1192,"language":179,"meta":180,"style":180},"int canvas_get_width(HWND canvas);\nint canvas_get_height(HWND canvas);\nvoid canvas_clear_rect(HWND canvas, int x, int y, int w, int h, int r, int g, int b, int a);\nint rand_int(int min, int max);\n",[119,1194,1195,1200,1205,1210],{"__ignoreMap":180},[184,1196,1197],{"class":186,"line":187},[184,1198,1199],{},"int canvas_get_width(HWND canvas);\n",[184,1201,1202],{"class":186,"line":193},[184,1203,1204],{},"int canvas_get_height(HWND canvas);\n",[184,1206,1207],{"class":186,"line":239},[184,1208,1209],{},"void canvas_clear_rect(HWND canvas, int x, int y, int w, int h, int r, int g, int b, int a);\n",[184,1211,1212],{"class":186,"line":245},[184,1213,1214],{},"int rand_int(int min, int max);\n",[136,1216,1217,1223],{},[139,1218,1219,1222],{},[119,1220,1221],{},"canvas_clear_rect"," 局部清除并填充颜色，适合需要保留背景的游戏场景",[139,1224,1225,1228,1229,1232],{},[119,1226,1227],{},"rand_int"," 生成 ",[119,1230,1231],{},"[min, max]"," 闭区间随机整数",[170,1234,98],{"id":98},[175,1236,1238],{"className":177,"code":1237,"language":179,"meta":180,"style":180},"int play_sound(const WCHAR *filepath);\nint play_sound_loop(const WCHAR *filepath);\nint stop_sound(void);\nint play_beep(int freq, int duration);\n",[119,1239,1240,1245,1250,1255],{"__ignoreMap":180},[184,1241,1242],{"class":186,"line":187},[184,1243,1244],{},"int play_sound(const WCHAR *filepath);\n",[184,1246,1247],{"class":186,"line":193},[184,1248,1249],{},"int play_sound_loop(const WCHAR *filepath);\n",[184,1251,1252],{"class":186,"line":239},[184,1253,1254],{},"int stop_sound(void);\n",[184,1256,1257],{"class":186,"line":245},[184,1258,1259],{},"int play_beep(int freq, int duration);\n",[14,1261,1262,1265],{},[119,1263,1264],{},"play_sound"," 播放音频文件（异步，不阻塞游戏循环），成功返回 1，失败返回 0。",[14,1267,1268,1271],{},[119,1269,1270],{},"play_sound_loop"," 循环播放音频文件。",[14,1273,1274,1277],{},[119,1275,1276],{},"stop_sound"," 停止当前正在播放的音频。",[14,1279,1280,1283,1284,1287,1288,1291],{},[119,1281,1282],{},"play_beep"," 通过 PC 喇叭发出指定频率和时长的蜂鸣声。",[119,1285,1286],{},"freq"," 为频率（Hz），",[119,1289,1290],{},"duration"," 为时长（毫秒）。成功返回 1。",[14,1293,1294],{},"适合游戏的用法：",[175,1296,1298],{"className":177,"code":1297,"language":179,"meta":180,"style":180},"play_sound(L\"hit.wav\");           \u002F\u002F 击中音效（异步，不卡帧）\nplay_beep(1200, 80);               \u002F\u002F 高音短促蜂鸣代替\n",[119,1299,1300,1305],{"__ignoreMap":180},[184,1301,1302],{"class":186,"line":187},[184,1303,1304],{},"play_sound(L\"hit.wav\");           \u002F\u002F 击中音效（异步，不卡帧）\n",[184,1306,1307],{"class":186,"line":193},[184,1308,1309],{},"play_beep(1200, 80);               \u002F\u002F 高音短促蜂鸣代替\n",[162,1311],{},[165,1313,1314],{"id":1314},"游戏模板",[14,1316,1317],{},"创建一个新游戏的最简模板：",[175,1319,1321],{"className":177,"code":1320,"language":179,"meta":180,"style":180},"#include \"eduwin.h\"\n\nstatic HWND canvas;\n\nstatic void game_tick(void *data)\n{\n    int cw = canvas_get_width(canvas);\n    int ch = canvas_get_height(canvas);\n\n    canvas_clear(canvas, 0, 0, 0);          \u002F\u002F 黑色背景\n\n    \u002F* 游戏逻辑写在这里 *\u002F\n    canvas_draw_text(canvas, L\"Hello Game!\", 10, 10, 255, 255, 255, 16);\n}\n\nint main(void)\n{\n    init_app();\n    HWND win = create_window(L\"我的游戏\", 800, 600);\n    canvas = create_canvas(win, 0, 0, 800, 600);\n\n    canvas_start_timer(canvas, 60, game_tick, NULL);\n    canvas_on_key(canvas, NULL, NULL);       \u002F\u002F 让画布获得键盘焦点\n\n    show_window(win);\n    run_app();\n    return 0;\n}\n",[119,1322,1323,1328,1332,1337,1341,1346,1350,1355,1360,1364,1369,1373,1378,1383,1387,1391,1396,1400,1405,1410,1415,1419,1424,1429,1433,1438,1443,1448],{"__ignoreMap":180},[184,1324,1325],{"class":186,"line":187},[184,1326,1327],{},"#include \"eduwin.h\"\n",[184,1329,1330],{"class":186,"line":193},[184,1331,267],{"emptyLinePlaceholder":266},[184,1333,1334],{"class":186,"line":239},[184,1335,1336],{},"static HWND canvas;\n",[184,1338,1339],{"class":186,"line":245},[184,1340,267],{"emptyLinePlaceholder":266},[184,1342,1343],{"class":186,"line":251},[184,1344,1345],{},"static void game_tick(void *data)\n",[184,1347,1348],{"class":186,"line":257},[184,1349,236],{},[184,1351,1352],{"class":186,"line":263},[184,1353,1354],{},"    int cw = canvas_get_width(canvas);\n",[184,1356,1357],{"class":186,"line":270},[184,1358,1359],{},"    int ch = canvas_get_height(canvas);\n",[184,1361,1362],{"class":186,"line":574},[184,1363,267],{"emptyLinePlaceholder":266},[184,1365,1366],{"class":186,"line":580},[184,1367,1368],{},"    canvas_clear(canvas, 0, 0, 0);          \u002F\u002F 黑色背景\n",[184,1370,1371],{"class":186,"line":586},[184,1372,267],{"emptyLinePlaceholder":266},[184,1374,1375],{"class":186,"line":592},[184,1376,1377],{},"    \u002F* 游戏逻辑写在这里 *\u002F\n",[184,1379,1380],{"class":186,"line":598},[184,1381,1382],{},"    canvas_draw_text(canvas, L\"Hello Game!\", 10, 10, 255, 255, 255, 16);\n",[184,1384,1385],{"class":186,"line":603},[184,1386,260],{},[184,1388,1389],{"class":186,"line":609},[184,1390,267],{"emptyLinePlaceholder":266},[184,1392,1393],{"class":186,"line":615},[184,1394,1395],{},"int main(void)\n",[184,1397,1398],{"class":186,"line":621},[184,1399,236],{},[184,1401,1402],{"class":186,"line":627},[184,1403,1404],{},"    init_app();\n",[184,1406,1407],{"class":186,"line":633},[184,1408,1409],{},"    HWND win = create_window(L\"我的游戏\", 800, 600);\n",[184,1411,1412],{"class":186,"line":639},[184,1413,1414],{},"    canvas = create_canvas(win, 0, 0, 800, 600);\n",[184,1416,1417],{"class":186,"line":645},[184,1418,267],{"emptyLinePlaceholder":266},[184,1420,1421],{"class":186,"line":651},[184,1422,1423],{},"    canvas_start_timer(canvas, 60, game_tick, NULL);\n",[184,1425,1426],{"class":186,"line":657},[184,1427,1428],{},"    canvas_on_key(canvas, NULL, NULL);       \u002F\u002F 让画布获得键盘焦点\n",[184,1430,1431],{"class":186,"line":663},[184,1432,267],{"emptyLinePlaceholder":266},[184,1434,1435],{"class":186,"line":669},[184,1436,1437],{},"    show_window(win);\n",[184,1439,1440],{"class":186,"line":675},[184,1441,1442],{},"    run_app();\n",[184,1444,1445],{"class":186,"line":681},[184,1446,1447],{},"    return 0;\n",[184,1449,1450],{"class":186,"line":687},[184,1451,260],{},[14,1453,1454,1455,1458],{},"编译命令（参考 ",[119,1456,1457],{},"samples\\shooting_demo\\build.bat","）：",[175,1460,1463],{"className":1461,"code":1462,"language":492},[490],"cd samples\\shooting_demo\nbuild.bat\n",[119,1464,1462],{"__ignoreMap":180},[1466,1467,1468],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":180,"searchDepth":193,"depth":193,"links":1470},[1471,1481],{"id":167,"depth":193,"text":168,"children":1472},[1473,1474,1475,1476,1477,1478,1479,1480],{"id":172,"depth":239,"text":173},{"id":58,"depth":239,"text":58},{"id":68,"depth":239,"text":68},{"id":515,"depth":239,"text":516},{"id":1017,"depth":239,"text":1017},{"id":88,"depth":239,"text":88},{"id":1189,"depth":239,"text":1189},{"id":98,"depth":239,"text":98},{"id":1314,"depth":193,"text":1314},"eduwin 的画布组件（Canvas）不仅仅是绘图工具——它是一套完整的 2D 游戏引擎，内置了游戏开发所需的一切核心机制：","md",{},"\u002Fdocs\u002Ftutorial\u002Feduwin-game",{"title":5,"description":1482},"docs\u002Ftutorial\u002Feduwin-game","NconF5ePeV29pmS1GqvB3ss7fbPItP7hXINsEVTp3dM",1782488679845]