本文是本人编写的 Galgame Maker 的引擎框架的README文件,您可以点击此处查看该框架,或是点击此处查看母项目。
Galgame Maker 运行时引擎 (framework)
基于 Python 3.10 + pygame 的视觉小说引擎, 用于运行 .gal 脚本,兼容 Galgame-Maker 编辑器的语法风格, 并预留完整插件 API。
1. 项目概览
framework/
├── api/ 插件 API
│ ├── __init__.py 导出 GameEngine / Plugin / command / event_listener...
│ ├── events.py 事件总线 (on/emit/off)
│ ├── commands.py 指令注册表 (按命名空间分组: main/builtin/<插件名>)
│ └── plugin.py 插件管理器 (发现/装载/实例化/卸载/模块级注册追踪)
├── engine/ 引擎核心
│ ├── core.py GameEngine 主循环 / 输入处理 / 动作分发 / 音频 API
│ ├── parser.py .gal DSL 解析器
│ ├── loader.py import 递归展开合并 (循环导入检测)
│ ├── runtime.py 脚本执行器 (变量/分支/跳转/调用栈/存档/角色/场景/
│ │ 菜单/声音/命名空间/using/plugin/python:: 指令)
│ ├── display.py 渲染层 (精灵/背景/文本框/确认框/选择列表/槽位界面/
│ │ 过渡/立绘效果/文字模式/截图/缩略图钩子/
│ │ 背景与立绘动态渲染钩子 (视频/Live2D))
│ ├── audio.py BGM 淡入淡出状态机 / 音效 / 语音独立通道 / 全局静音
│ ├── save.py 存档/读档 (JSON 槽位 + 元数据 API + 编解码钩子)
│ ├── rich.py 富文本解析渲染 + LaTeX 公式 (MathRenderer)
│ ├── ui.py UI 绘制原语 (面板/文字/换行/九宫格 nine_slice)
│ ├── error.py 错误处理 (日志 + 弹窗 + 剪贴板复制)
│ ├── styles.py 内置 5 套 UI 主题样式
│ └── transitions.py 背景过渡效果 (7 种 + 插件可注册)
├── plugins/ 插件目录 (自动装载)
│ ├── fx.py 屏幕特效 (shake/flash/blackflash/tint/strobe/pulse)
│ ├── notice.py 通知: BGM + 场景切换 (合并 bgm/scene 通知)
│ ├── transitions_plus.py 扩展背景过渡 (wipe/iris/curtain/sweep/fade_white/
│ │ checker/stripes)
│ ├── custom_actions.py 动作 (explode/quake/freeze/blackout) + do_action
│ │ 指令 + 立绘效果 (wobble/sway/zoom_bounce/
│ │ fade_rotate/float/squash) + 文字模式
│ │ (wave/bounce/speedup/rainbow/shiver)
│ ├── debug_mode.py 调试模式 (快捷键切换, 开启显示 FPS 等)
│ ├── slot_thumbnails.py 存档画面快照 (槽位缩略图)
│ ├── gallery.py 鉴赏: 标题菜单按钮 + CG/BGM/角色/场景鉴赏
│ │ (结局解锁, 配置在 gallery.gal)
│ └── auto_skip.py 自动模式 (自动推进, 非正式界面自动暂停)
│ + 跳过剧情 (直达下一个选择支/结局)
└── tests/
└── smoke.py 冒烟测试 (dummy 驱动, 无窗口可跑, 779 项断言)
项目根: gamelauncher.py 独立启动器 (命令行传参 / 拖拽 .gal 文件)。
2. 快速开始
# 运行内置演示 (test/engine_demo/)
py -3.10 gamelauncher.py
# 运行自己的脚本 (也支持把 .gal 文件拖到 gamelauncher.py 上)
py -3.10 gamelauncher.py path/to/your.gal
# 命令行参数 (启动器支持)
# --width/--height/--fullscreen 等窗口参数
# --plugin-dir <目录> 指定插件目录
# 运行冒烟测试
py -3.10 framework/tests/smoke.py
引擎最小用法:
from framework.api import GameEngine
engine = GameEngine(1280, 720, "My Game")
engine.run("script.gal")
操作: 左键/空格 推进, 方向键/WASD 在菜单中移动活动选项, Enter/空格 确认, ESC 打开系统菜单。
3. 脚本拆分 (import)
.gal 支持按功能拆分多个文件, 顶层用 import 合并:
import "ui.gal" # 界面样式定义
import "cast.gal" # 角色与场景定义
import "audio.gal" # 声音注册
import "story.gal" # 剧情流程 (game_start 开场 + 分支标签)
合并规则:
- 被导入文件的标签全部并入 (重复标签报错), 子文件的
start标签忽略 - 顶层声明 (window/style/char/scene/plugins/selection_style/sound/using)按 import 位置顺序并入, 运行时静态注册
- 相对路径 (相对 import 语句所在文件), 支持链式 import, 循环导入报错
demo (test/engine_demo/) 拆分: demo.gal (主流程) + ui.gal (样式/菜单)+ cast.gal (角色/场景) + audio.gal (声音) + story.gal (剧情)。窗口配置 / 插件配置的预解析 (启动器) 同样递归展开 import。
4. DSL 语法全参考
脚本为 UTF-8 文本, # 开头为注释, 缩进 (空格) 表示块结构。
4.1 表达式
支持 + - * /、比较 == != < > <= >=、逻辑 and or not。
变量引用可写 $var 或裸 var (命名空间: $main::x / $plugin::cnt),
字符串用引号。求值环境无内置函数, 表达式内不允许函数调用 (安全限制)。
set love = 0
set love = love + 1
if love > 0 and name == "xx":
...
4.2 标签与流程控制
start: # 脚本入口 (必为 start)
label_a: # 普通标签
jump label_a # 跳转
call sub_routine # 调用 (可 return)
return
if cond: ... elif ...: ... else: ... endif
choice: # 选择支 (也支持行内参数, 见 4.9)
"选项一" -> label_a
"选项二" -> label_b
4.3 背景与场景
scene school # 场景定义 (可放顶层, 静态注册)
name: "学校"
type: normal # normal (默认) / cg
default: "materials/image/bg.png"
morning: "materials/image/bg.png" # 背景名: 路径
bg school # 切到场景默认背景 (触发 scene_change)
bg school morning # 场景内切换背景
bg "materials/bg.png" # 直接指定路径
bg school with fade # 过渡: fade/dissolve/blinds/slide/
# circle/pixelate/zoom + 插件自定义
场景分类: type: cg 的 CG 场景显示逻辑与 normal 完全一致,但每次用 bg 展示 (含背景名) 都会记入全局 CG 收集(save/global.json, 跨存档), 供鉴赏插件分门别类展示(见 4.15 鉴赏系统)。
4.4 立绘与角色
char producer
name: "制作人" # 显示名 (台词名字框用)
default: "materials/char/producer1.png"
normal: "materials/char/producer1.png" # 立绘名: 路径
happy: "materials/char/producer2.png"
voice_volume: 0.6 # 该角色语音音量 0-1 (可选, 默认 1.0)
desc: "引擎的制作者,温和而执着。" # 描述性信息 (角色鉴赏用, 可选)
cv: "演示配音" # 声优 / 生日 / 身高 / 年龄 等均可
show producer normal # 显示角色立绘 (默认居中)
show producer happy # 切换立绘 (保持中心点原位替换)
show producer normal with slide_right # 登场效果
hide producer # 隐藏 (withdraw 同义)
hide producer with slide_left # 退场效果 (动画播完自动隐藏)
clear # 清除全部立绘
move producer to left 1 # 位移: 瞬间或缓动动画 (时长+缓动)
move producer to 400,300 2 ease in_out
rotate producer 90 1 # 旋转 (逆时针为正, 带时长=动画)
flip producer # 水平翻转 (再次调用恢复)
flip producer vertical # 垂直翻转
描述性信息: desc/description/bio/intro/cv/birthday/height/age等键不进入立绘表, 存于 characters[<id>]["meta"], 供角色鉴赏等使用。
4.5 对话与语音
text "无角色旁白"
nar "旁白别名" # text 的别名
say 主角 "角色对话, 支持 $变量 插值"
say producer "角色 id -> 显示名"
say producer "台词" voice voice_demo # 语音: say/nar 结束立即停止
say 旁白 "也按旁白处理" # 兼容写法
4.6 富文本与 LaTeX
行内标记 (对话/旁白/标题文字/按钮文字通用):
say a "普通 {c=#ff6600}彩色文字{/c}"
say a "加粗 {b}粗体{/b} 与 {u}下划线{/u} 和 {i}斜体{/i}"
say a "大字号 {s=34}放大{/s}"
say a "公式: {m}\frac{1}{2}{/m} 行内显示"
- 富文本标记:
{c=颜色}{b}{u}{i}{s=字号}{m}公式{/m}(颜色支持#RRGGBB/R,G,B/ 命名色) - 公式语法:
{m}LaTeX 源码{/m}或{math}...{/math}(成对闭合)。公式源码允许嵌套大括号 (\frac{1}{2}的参数), 解析器按大括号配对扫描, 不做标记解析, 原样交给 MathRenderer 渲染。 - 与逐字模式兼容: 公式在打字机效果中整体一次性出现 (逻辑字符计 1), 不会被逐字符截断成残缺源码; reveal 推进/完成判定基于”逻辑长度” (普通文字逐字 + 公式整体计 1), 见
rich.logic_len()。
4.7 文字显示模式 (typing)
typing typewriter # 默认: 打字机逐字符
typing instant # 整段直接出现
typing terminal # 终端: 逐字输入 + 行尾闪烁光标 (光标按富文本布局
# 精确定位在已输入文本末尾)
typing lines # 逐行显示 + 节奏停顿
typing wave # 插件自定义模式 (custom_actions 插件示例)
插件 API: display.register_text_mode(name, {"reset": fn(display), "update": fn(display, dt)})。
4.8 声音系统
# 声音注册 (建议独立 audio.gal)
sound sfx_click
type: sfx_ui # music / sfx_ui / sfx_story / voice
file: "materials/audio/sfx_click.wav"
volume: 0.6
# 音乐: 播放 / 切换 / 循环 / 暂停 / 恢复 / 音量 / 停止
music bgm_piano41 # 播放 (注册名或直接路径, 默认循环)
music bgm_piano39 fade 1.0 # 切换/淡入 (fade 秒; 切换自动旧曲淡出新曲淡入)
music bgm_piano39 loop 0 # loop 1=循环(播完自动重播) / 0=单次
pause music fade 0.8 # 暂停 (淡出后暂停)
resume music fade 0.8 # 恢复 (淡入)
volume music 0.3 # 临时音量 (music/sfx)
stop music # 停止 (淡出)
# 全局静音
stop all # BGM 淡出停止 + 音效/语音全停
pause all # BGM 淡出暂停 + 音效/语音全停
# 音效与语音
sfx sfx_boom # 剧情音效
say producer "..." voice voice_demo # 台词语音 (可省略)
# 语音音量分层 (四层相乘, 每层默认 1.0 不衰减):
# 全局 sfx 音量 × 全局 voice 音量 × 声音块 volume × 角色 voice_volume
volume voice 0.5 # 全局语音音量
volume voice producer 0.3 # 指定角色的语音音量 (改 char voice_volume)
# 角色语音音量也可在 char 块里预设: voice_volume: 0.6
# 自动行为
# ending 指令 / 标题"开始游戏" 动作: 自动 stop music (淡出, 非全局静音)
# 存档保存 BGM 注册名 (非路径), 读档按名称恢复
fade 淡入淡出: 所有 fade/stop/pause 的时长值支持变量/表达式 (如 stop music fade $f)。默认时长由 window 配置music_fade: 1.0 控制; 指令显式 fade N 覆盖 (0=无淡变)。淡入淡出为每帧音量线性渐变 (引擎 update 驱动), 暂停菜单时渐变不中断。
UI 交互音效 (三层配置):
# window 全局默认 (按钮确认时播放)
window
ui_click_sound: "sfx_click"
music_fade: 1.0
# menu 块级 (该菜单活动项变化/确认时)
menu system
...按键...
ui_hover_sound: "sfx_hover"
ui_click_sound: "sfx_click"
# choice 行内参数
choice ui_click sfx_a ui_hover sfx_b
"A" -> a
点击音只在明确确认操作时播放 (菜单/选择支/确认框确认/槽位选中);
文本推进与取消返回不响。语音在台词推进时先停语音再播 UI 音效(避免抢语音通道)。
4.9 菜单与键盘导航
# 命名菜单 (标题画面 / ESC 系统菜单统一架构)
menu title # title 块用 menu: title 引用
start_button
text: "开始游戏"
image: "默认.png, 焦点.png"
width: 262
height: 98
stretch: false # 不拉伸 (原尺寸居中)
text_visible: false # 图自带文字时不渲染文案
action: start game_start # 动作: 类型 [参数]
ui_hover_sound: "sfx_hover" # 菜单级 UI 音效 (见 4.8)
ui_click_sound: "sfx_click"
menu system # ESC 菜单: 定义即覆盖内置五项
continue_button ... # 按键属性同 title (action: continue/save/...)
# 按键属性: text / image(默认,焦点) / image_disabled(禁用图) /
# image_active(激活图, 自动模式等切换用) / width / height /
# stretch / text_visible / action (无名参数按类型映射:
# start->label, slot_menu->mode, save/load->slot;
# 自定义动作默认 label)
# 选择支行内参数 (见 4.2) 也支持 ui_click/ui_hover 配置
键盘导航 (开始/ESC/选择支菜单通用):
window
key_up: "up, w" # 上移 (可配多键, 逗号分隔)
key_down: "down, s" # 下移
key_confirm: "return, space" # 确认活动选项
- 键名: 方向键/功能键 (up/down/left/right/return/space/esc/tab/…) +单个字母/数字 (“a”/“1”)
- 活动选项 (active_index) 由键盘移动或鼠标悬停激活; 初始无活动项 (-1),不高亮, 无活动项时 Enter/空格不触发确认
- 键盘移动循环切换; 鼠标点击同步活动索引 (键盘/鼠标状态一致)
- ESC 菜单 (paused) 下鼠标悬停同步依然工作 (见
sync_mouse_active)
4.9.1 系统菜单两种模式: popup 弹窗 / bar 常驻菜单栏
开发者可在 window 块选择系统菜单形态 (也可用 window config 运行时切换):
window
menu_mode: "popup" # popup=ESC 弹窗 (默认) / bar=常驻按钮条
menu_bar_pos: "bottom" # bar 模式位置: bottom=对话框下方 / top=窗口上方
- popup (默认): 游戏时按 ESC 打开系统菜单 (选择列表覆盖层, 暂停游戏)
- bar: 对话框下方或窗口上方常驻一排按钮 (存档/读档/返回标题/退出),游戏过程中随时点击; ESC 不再弹窗 (仅用于关闭槽位界面等覆盖层)。bar 按钮复用
menu system块的按钮定义 (自动过滤无意义的continue), 未定义时用默认四项。标题画面始终使用自己的按钮, bar 隐藏。
bar 样式 (独立 menu_bar 块, menu_bar default 重置):
menu_bar
bg: "#1a1a2e" # 条背景 (含 alpha: "#1a1a2ee0" 或 R,G,B,A)
border: "#5a5a7a"
align: center # 按钮水平对齐: left / center / right
gap: 12 # 按钮间距
padding: 18 # 按钮左右内边距 (决定按钮宽度)
height: 56 # 条高度
btn_h: 38 # 按钮高度
y_offset: 0 # 位置微调 (bottom 向上 / top 向下)
button_bg: "#2a2a44" / button_bg_hover: "#e94560"
button_border: "#44446a" / button_border_hover: "#ffd282"
button_radius: 8
text_color: "#eaeaea" / text_color_hover: "#ffffff"
text_size: 22
# --- UI 图片键 (可配; 有图优先于纯色, 支持 {lang} 变体) ---
bg_image: "bar.png" # 条背景图 (九宫格铺满整条)
button_image: "btn.png" # 按钮默认图
button_image_hover: "btn_focus.png" # 按钮悬停图
button_image_active: "btn_active.png" # 按钮激活图 (cfg.active 时)
button_image_disabled: "btn_off.png" # 按钮禁用图 (cfg.enabled=False 时)
bar 模式下对话框自动上移让位 (bottom); 打开槽位界面/确认框等覆盖层时菜单栏被 dim 盖住, 关闭覆盖层后恢复。
4.10 界面样式
use style modern # 内置: modern / classic / dark / light / cyber
use style default # 恢复默认
style my_theme # 自定义样式块 (同名可重载内置)
textbox_bg: "#1a1a2e"
textbox_alpha: 210
textbox_border: "#e94560"
textbox_border_width: 3
textbox_radius: 12
text_color: "#eaeaea"
text_size: 28
speaker_color: "#ffd282"
speaker_bg: "#1e3a5f"
arrow_color: "#e94560"
font: "ui" # 字体族 (register_font/font 指令注册; 缺省默认字体)
textbox_image: "..." # 文本框背景图 (9-slice)
speaker_image: "..."
choice_image: "..." / choice_image_hover: "..."
choice_text_size: 26 / choice_text_color / choice_text_color_hover
choice_bg / choice_bg_hover / choice_border / choice_border_hover
selection_style # 选择列表全局样式 (标题/ESC 菜单按钮)
width_ratio: 0.32 / width: 400 / height: 56 / gap: 14
anchor_x: center / anchor_y: center
button_bg / button_bg_hover / button_border / button_border_hover
button_radius / text_size / dim_alpha
button_image / button_image_hover / button_stretch / button_text
text_color / text_color_hover / dialog_image
selection_style default # 重置
ui # UI 主题素材 (九宫格切片, 相对路径)
textbox: "materials/.../对话_adv对话框_llf.png"
title_buttons: "默认.png, 焦点.png; 默认2.png, 焦点2.png"
# 逗号 = 默认图,焦点图; 分号 = 多组按按钮索引取图
# 单个路径 = 无状态图
# 支持: textbox/choice_button/title_buttons/menu_button/
# confirm_panel/confirm_button/slot_frame/slot_panel/
# error_panel/error_button/notice_panel
# style 图片键优先级更高, 值为 none 禁用主题图
4.11 标题画面
title
image: "materials/title.png" # 可选: 标题图片
caption: "Galgame Maker" # 标题文字 (支持富文本)
title_x: center / title_y: 210
start: game_start # "开始游戏" -> 跳转标签 (必填)
start_text: "开始游戏"
load: 0 / load_text: "读取存档" # 可选: 读档槽位按钮
quit: true / quit_text: "退出游戏"
button_x: center / button_y: 420
menu: title # 引用命名菜单 (menu title) 替代内置按钮
4.12 窗口配置 (window 块)
window
title: "我的游戏"
width: 1280
height: 720
icon: "materials/image/icon.png"
fps: 60
fullscreen: false # 全屏启动
resizable: true # 允许拖拽窗口边缘 (内容等比缩放)
confirm_quit: true # 对话框统一配置 (退出/读档/返回标题):
confirm_quit_text: "确定要退出游戏吗?"
confirm_quit_yes: "退出" / confirm_quit_no: "继续游戏"
confirm_load: true / confirm_load_text / confirm_load_yes / confirm_load_no
confirm_title: true / confirm_title_text / confirm_title_yes / confirm_title_no
key_up: "up, w" # 键盘导航 (见 4.9)
key_down: "down, s"
key_confirm: "return, space"
key_left: "left, a" # 确认框: 左移活动项
key_right: "right, d" # 确认框: 右移活动项
ui_click_sound: "sfx_click" # 全局 UI 点击音
music_fade: 1.0 # BGM 淡入淡出默认时长 (秒)
save_slots: 6 # 存档槽位数量 (默认 6, 可配置)
menu_continue: "继续游戏" # ESC 菜单文案 (可自定义)
menu_save: "存档" / menu_load: "读取存档" / menu_title: "返回标题"
menu_quit: "退出游戏"
4.12.1 运行时窗口配置 (window config) 与等比缩放
window config 命令可在程序运行过程中即时修改窗口配置(标题/大小/图标/全屏/可缩放/帧率), 无需重启:
window config
title: "新的标题" # 窗口名
width: 1600 # 窗口大小 (像素)
height: 900
icon: "materials/image/x.png" # 图标 (相对脚本目录)
fullscreen: true # 全屏开关
resizable: false # 是否允许拖拽缩放
fps: 60
fullscreen true # 独立全屏指令 (true/false)
等比缩放 (letterbox): 引擎以固定逻辑分辨率 (初始 width/height) 绘制,窗口可自由调整大小 (拖拽边缘 / window config / 全屏), 画面整体等比拉伸,保持比例不变; 宽高比不一致时上下/左右留黑边。鼠标坐标自动映射回逻辑坐标,命中检测/悬停在任何窗口尺寸下都准确。
相关 API (插件/游戏代码):
engine.set_window_title(title) # 改窗口名
engine.set_window_size(w, h) # 改窗口大小 (内容等比缩放)
engine.set_fullscreen(True/False) # 全屏切换
engine.apply_window_config(cfg) # 批量应用 window 配置 dict
engine.to_logical(pos) # 窗口坐标 -> 逻辑坐标
4.13 插件装载配置
plugins
only: "fx, notice" # 只装载列出的
# 或
except: "debug_mode" # 排除列出的
4.14 存档/结束/转场
save # 存档到槽位 0 (save/slot0.json)
load # 读档
fade / fadeout # 黑幕淡入/淡出
ending # 结束画面 (同时淡出停止 BGM) 后回标题
ending 真结局 # 带结局名: 显示 "— 结局:真结局 —",
# 并记入全局进度 (save/global.json, 跨存档)
全局进度 (save/global.json, 跨存档):
endings— 已达成结局名列表 (ending <名>触发, 事件ending_recorded)cgs— 已解锁 CG:{场景id: [背景名列表]}(CG 场景bg展示触发,事件cg_unlocked)
engine.get_endings() # -> [结局名]
engine.record_ending(name) # 记录结局 (ending 指令内部调用)
engine.get_unlocked_cgs() # -> {场景id: [背景名]}
engine.record_cg(scene_id, pose) # 记录 CG (bg 指令内部调用)
engine.cg_unlocked(scene_id, pose) # -> bool
4.15 鉴赏系统 (gallery 插件)
标题画面的「鉴赏」按钮可解锁 CG / BGM / 角色 / 场景 四类鉴赏。配置在独立 .gal 文件 (如 gallery.gal, 被主脚本 import);gallery 块由 gallery 插件解析 (引擎将未处理的属性块通过script_block 事件广播给插件, 未装载插件时安全忽略):
gallery
unlock_ending: "真结局" # 达成此结局解锁鉴赏按钮 (空=不锁)
button_text: "鉴赏" # 标题菜单按钮文本
title: "鉴赏" # 鉴赏界面标题
categories: "cg, bgm, character, scene" # 可用分类
locked_hint: "达成「真结局」后解锁鉴赏"
# --- 界面样式 (可选) ---
bg: "materials/.../bg.png" # 界面背景图 (cover 铺满)
cat_image: "默认.png, 焦点.png" # 分类按钮图 (默认, 焦点)
back_image: "默认.png, 焦点.png" # 返回按钮图
cat_text: false # 图自带文字时关闭分类文案
cg_frame: "默认.png, 焦点.png" # CG 插画框 (九宫格)
cg_placeholder: "占位.png" # 未解锁 CG 占位图 (默认灰色框+问号)
scene cg_school # CG 场景定义 (type: cg)
type: cg
name: "学园CG"
default: "materials/cg/a.png"
morning: "materials/cg/b.png" # 每张背景 = 一张可收集 CG
- 解锁: 达成
unlock_ending指定的结局 (用ending <名>指令) 后,标题菜单的「鉴赏」按钮从禁用态变为可用; 未解锁时按钮呈禁用态 - 按钮样式: 在
ui.gal的menu title中定义(子块名任意,action: gallery_open, 样式照常配置);未在脚本中定义时插件自动追加默认样式按钮 - 禁用图: 按钮支持
image_disabled键 (禁用态专用图片, 系统菜单通用;未配置时禁用态为纯色暗化) - 数据来源: CG 来自
type: cg场景的已解锁背景; BGM 来自sound注册表type: music; 角色来自char(含 desc 等描述);场景来自scene定义 (CG 场景只出现在 CG 鉴赏, 场景鉴赏仅展示normal 场景) - CG 鉴赏: 同一 CG 场景的多个形态合并为一个条目 (缩略图 +形态进度 x/y); 点击放大后点击图片轮播切换形态, 播完最后一个形态后再点击退出 (ESC 随时退出); 未解锁的 CG 显示灰色占位框(问号 + “未解锁”), 点击无效
- 界面: 分类按钮行 + 内容网格 (BGM 点击后先在页面显示”正在切换”提示再实际切换试听), ESC/返回 关闭回标题
- 标题 BGM:
start:块可用music配置标题背景音乐, 回标题/退出鉴赏时自动恢复播放 (engine 记录runtime.title_bgm)
4.16 设置系统
设置界面可在标题菜单 / ESC 菜单 / bar 常驻栏中打开(action: settings_open), 配置在独立 setting.gal (被主脚本 import):
settings
title: "设置" # 界面标题
columns: 2 # 条目列数
bg: "panel.png" # 面板背景图 (九宫格, 可选)
# --- UI 图片键 (可配; 有图优先于纯色, 支持 {lang} 变体) ---
item_image: "item.png" # 条目背景图
item_image_hover: "item_focus.png" # 条目悬停图
tab_image: "tab.png" # 分栏 (tab) 图
tab_image_hover: "tab_focus.png" # 分栏激活/悬停图
back_image: "back.png" # 返回按钮图
slider_track_image: "slider.png" # 滑条轨道图
setting bgm_volume # setting <key> 子块 (引用/覆盖内置项)
label: "音乐音量"
section: "音量" # 分栏 (tab); 可自定义归并
setting voice:producer # 角色语音音量 (动态项: voice:<角色id>,
label: "制作人语音" # 默认自动归并到"语音"栏)
section: "语音"
setting player_name
label: "主角名字"
type: cycle # 类型覆盖
options: "阿明, 小明"
setting key_up
label: "上移键" # keybind 类型: 点击后按任意键绑定
section: "按键"
分栏: 设置界面顶部为分栏 (tab) 行, 点击切换显示对应条目;section 属性自定义归类 (未指定时按内置默认: 音量/语音/显示/游戏/按键,voice:<角色id> 自动归入”语音”栏); 插件注册项可指定任意分栏。
内置设置项 (key / 类型):
| key | 类型 | 说明 |
|---|---|---|
bgm_volume / sfx_volume / voice_volume |
slider | 音乐/音效/全局语音音量 |
voice:<角色id> |
slider | 指定角色的语音音量 (动态) |
text_speed |
slider | 文字速度 (字符/秒) |
resolution |
cycle | 分辨率 (全屏时按当前分辨率全屏, 关全屏后窗口按此尺寸) |
fullscreen / resizable |
checkbox | 全屏 / 窗口可缩放 |
player_name |
cycle | 主角名字 ($player_name 变量) |
key_up / key_down / key_confirm |
keybind | 键盘导航键位 |
自定义设置项 (setting <key> 子块, var 绑定引擎变量自动生成存取):
setting my_slider # 开发者自定义项: 名称/标签/绑定变量/类型
label: "自定滑条"
type: slider # slider / checkbox / cycle / input / keybind / button
var: my_val # 绑定引擎变量 ($my_val), 自动读写
min: 0 / max: 100 / step: 5 # slider 范围与步长 (可配)
setting my_input
label: "自定输入"
type: input # 文本输入 (pygame 文本输入, Enter 确认)
var: my_name
default: "未设置"
setting my_choice
label: "自定选择"
type: cycle
var: my_color
options: "红, 绿, 蓝" # cycle 选项
主角名字提供两种方式 (开发者二选一): type: cycle (给定名字循环选择)或 type: input (文本框输入, 两者都可绑定 var: player_name)。
插件 API (自定义设置项):
engine.settings.register(
"my_setting", # 唯一 key
label="插件开关",
kind="checkbox", # slider / checkbox / cycle / input / keybind / button
getter=lambda: 取值, # 读取 (与 setter 二选一; 或直接用 var)
setter=lambda v: 应用, # 写入 (自动保存到 save/settings.json)
var="my_var", # 绑定引擎变量, 自动生成存取 (忽略 getter/setter)
default="默认值", # var 绑定时未设置过的默认值
min=0, max=1, step=0.05, # slider 用 (可配范围与步长)
options=["a", "b"], # cycle 用
section="分栏", # 自定义分栏
on_click=lambda engine: ..., # button 用
)
engine.settings.set("my_setting", True) # 程序化读写 (持久化)
engine.settings.get("my_setting", False)
- 设置值保存在
save/settings.json(跨存档, 重启恢复, 可手动编辑) - read_settings: 脚本开头 (start 标签首行) 调用, 从
save/settings.json读取设置并赋值到对应引擎变量 (setting.gal 中default为缺省值,文件里读不到的项用默认值补齐); 随后按变量重新应用 window 配置(window 块声明支持$变量, 如width: "$res_w"—— 由设置项resolution写入res_w/res_h变量, 全屏/窗口尺寸随之联动) - 内置音量项 (
bgm_volume/sfx_volume/voice_volume等) 值存引擎变量,语音等运行时行为全局读这些变量; 插件可用engine.get_var("bgm_volume")/engine.settings.get("bgm_volume")读取 - 插件注册的项自动出现在界面末尾;
setting.gal可引用调整 label/顺序,或直接在此定义自定义项 (var 绑定) - 点击滑条轨道/用左右方向键调节; checkbox 点击切换; cycle 左右点击切换;input 点击后直接键入文本 (Enter 确认, ESC 取消); keybind 点击后按任意键绑定; 界面内 ESC 关闭并保存
4.17 询问对话框 (confirm)
退出/读档/回标题等确认框统一由同一组件管理 (display.show_confirm +engine.ask_confirm)。开发者可在游戏中任意位置弹类似对话框,阻塞等待玩家选择并把结果存入变量:
confirm "继续吗?" -> choice # 结果: "yes" / "no"
if choice == "yes":
...继续剧情...
else:
...取消...
endif
confirm "再来一次?" yes "好的" no "算了" -> again # 自定义按钮文本
键盘控制 (确认框通用): 初始无活动项; 左右键在确认/取消间循环移动活动项 (可配 key_left/key_right, 默认方向键);鼠标悬停优先激活; Enter/空格/点击确认活动项 (无活动项时忽略);ESC 不干预确认框。
叠加退出确认: 若当前已有其他确认框 (如设置/鉴赏中), 点关闭窗口会再叠加一层退出确认 —— 确认才退出, 取消则恢复原来的确认框。
相关 API:
engine.ask_confirm(text, yes_text, no_text, on_yes, on_no=None)
# on_no: 点"否"时回调 (confirm DSL 用; 退出/读档确认不传则"否"无动作)
4.18 快捷键系统 (keybinds)
所有键盘事件统一由 engine.keybinds (KeyBindManager) 管理。每个命令含 主键 (primary) + 副键 (alt) 两个槽位 (各绑一个键,可留空); 设置界面中同一命令的主/副键显示在同一行, 不同命令不同行。
- 核心键位 (上下左右共 8 个移动槽位 + 确认 + ESC):
key_up(上移, 主 up 副 w) /key_down(下移, down/s) /key_left(左移, left/a) /key_right(右移, right/d) /key_confirm(确认, return/space) /key_escape(菜单键, esc) - 自动生成设置项 (“按键”分栏),
setting.gal可直接引用调整(如setting key_up调 label/section; 主副在同一项内, 一行显示)
插件注册 (自动生成设置项, 如 debug_mode 插件):
engine.keybinds.register(
"debug_toggle", # 唯一名 (同时是设置项 key)
"调试模式", # 设置界面显示名
callback=lambda key: ..., # 触发回调 (返回 False = 不消费该按键)
primary="f3", # 主/副键 (键名串或 pygame 键常量, 可留空)
)
- 录入: 设置界面点击条目左半=主键槽, 右半=副键槽; 按任意键直接绑定, Backspace 清空, ESC 取消; 空槽显示槽位名 (“主”/“副”)
- 冲突: 绑定被其他命令占用时自动让位 (移除冲突键) 并弹出提示
- 值持久化在
save/settings.json; 查询/设置:keybinds.get_key(name, "primary")/set_key(name, "primary", K)
调试模式插件 (debug_mode): 注册 debug_toggle 快捷键(默认 F3, 设置界面可调整/留空), 切换调试模式; 开启时才显示右上角 FPS / 窗口分辨率 / 当前标签等调试信息。
4.19 多语言系统 (i18n)
三层语言, 各层在其目录下配置 JSON 语言文件 (文件名 = 语言码):
| 层 | 位置 | 内容 |
|---|---|---|
| 框架 | framework/lang/<code>.json |
日志/提示/内置 UI 文案 (确认框默认、菜单文案、设置项 label 等), 引擎构造时自动加载 |
| 插件 | plugins/lang/<code>.json |
插件文案 (key 建议带插件前缀, 如 gallery.button), 插件装载时自动加载 |
| 游戏 | 项目目录 lang/<code>.json |
游戏文本 (检测到目录即启用), 可选 |
游戏文本 (对开发者友好: 文本存额外文件, DSL 不臃肿): 台词/选项/标题中用 {@key} 占位符引用, 如:
say producer "{@welcome}" # 文本取自 lang/<语言>.json 的 welcome
nar "{@love_high}" # key 文本内的 $var 支持再插值
choice:
"{@choice_like}" -> like_it
主文件 language 块 (声明项目支持的语言, 主文件如 demo.gal 顶部):
language
default: en # 默认语言: 当前语言缺翻译时回退到它
en: "English" # 语言码 -> 设置中显示的名字
zh-CN: "简体中文"
游戏语言文件只从列出的语言中加载 (
lang/<语言码>.json), 脚本中任何语言文本资源均在其中查找设置页”语言”项显示的是语言名字 (如 English / 简体中文), 切换后立即全界面生效
未翻译的 key 回退默认语言, 再回退原文 (便于开发调试);
{@key}占位符的查找顺序: 游戏表 → 核心表 → 原文 —— 脚本可直接引用框架文案, 如confirm_quit_text: "{@dialog.quit.text}"复用确认框默认文案切换语言后所有显示中的界面即时生效: 新文本、标题/系统菜单按钮(脚本文本可用 {@key})、bar 常驻栏、对话框/确认框 (含按钮)、设置界面 (title/label/section/选项值)、角色与场景显示名、{lang} UI 图片
语言写入
$lang/$language变量 (脚本可引用)可在设置页面的”语言”项切换 (也可脚本/插件调用
engine.i18n.set_lang)
对话框 / 菜单文案 (window 块): 确认框/ESC 菜单文案支持 {@key},显示时按当前语言解析 —— 语言切换即时刷新, 脚本可覆写为任意语言(见 demo.gal window 块):
window
confirm_quit: true
confirm_quit_text: "{@dialog.quit.text}" # 或直接写任意语言的文本
menu_continue: "{@menu.continue}" # ESC 菜单文案同理
设置界面多语言 (setting.gal): title / label / section / options均支持 {@key} 占位符 (切换语言即时生效); 内置设置项自带 label_key(核心语言表 settings.*), setting.gal 的 label 仅为覆盖/兜底。分栏 (tab) 显示名 按 settings.section.<值> 翻译, 未配置的自定义分栏原样显示:
settings
title: "{@settings.title}"
columns: 2
setting my_slider
label: "{@settings.my_slider}"
type: slider
var: my_val
section: "游戏" # 内置分栏值: 音量/语音/显示/游戏/按键/通用
角色与场景显示名: char/scene 块的 name (及 desc/cv 等描述) 支持 {@key} —— 注册时存占位符原文, 显示时解析 (名字框/鉴赏/场景通知均按当前语言):
char producer
name: "{@char.producer.name}"
desc: "{@char.producer.desc}"
结局名约定: 结局名按原文记录 (跨存档/解锁比较用), 显示时解析 —— 脚本与 gallery 配置写同一个占位符即可, 任何语言下解锁判断一致:
ending {@ending.true_end} # story.gal
gallery
unlock_ending: "{@ending.true_end}" # gallery.gal
内置默认文案 (未覆写时): 确认框默认按钮 confirm.yes/no、”开始游戏” menu.start、结束画面 ending.name/ending.thanks 等均走核心语言表, 无 language 块的项目也会按当前语言显示。
UI 图片语言变体: 图片路径含 {lang} 占位符时按当前语言替换(如 materials/title_{lang}.png → materials/title_en.png), 切换语言自动重载。
渲染字体: window 块 font: 配置 — 相对脚本目录的字体文件(font: "fonts/Ubuntu-R.ttf") 或系统字体 (font: "sys:MicrosoftYaHei"); 运行时 engine.apply_font(font) 立即生效。
多字体: font <名> <spec> 指令或 engine.register_font(name, spec)注册命名字体 (spec = 文件路径 或 sys:系统字体名); style 块font: <名> 让该样式的文本/名字框/选择支/标题/富文本使用该字体(切换样式即换字体); 引擎 API engine.get_font(size, family=<名>)直接取用。
日志多语言: log.i / log.w / log.e(key, **fmt) 按语言表翻译日志文案 (如 log.i("log.script_loaded", path=...)); 框架全部日志(引擎/解析/插件/启动器) 均已键化, 核心语言表含 log.* key,开发者可在语言文件中自行扩展。
API:
engine.i18n.set_lang("en") # 切换语言
engine.i18n.t("menu.quit") # 取文本 (回退默认/原文)
engine.i18n.t("gallery.button", ns="plugin") # 插件/游戏层: ns="plugin"/"game"
engine.i18n.resolve("hi {@name}") # 替换 {@key} 占位符 (游戏→核心→原文)
engine.i18n.langs() # 可用语言列表
# 快捷键显示名走 i18n (插件 API, 自动生成设置项时生效)
engine.keybinds.register("my_toggle", "调试模式", callback,
primary="f3", label_key="debug_mode.toggle")
语言文件格式 (JSON): {"key": "文本"} (单语言) 或{"en": {...}, "zh-CN": {...}} (多语言合并)。核心文案 key 见framework/lang/zh-CN.json。
4.20 嵌入 Python 代码 (python::)
python:: 双冒号块把块内行原样捕获 (含空行、# 注释与缩进),不按 DSL 解析, 交给引擎在受限命名空间中执行:
start:
python::
import random
engine.set_var("luck", random.randint(1, 100)) # 写引擎变量
runtime.vars["note"] = "来自 python 块"
text "你的幸运值: $luck ($note)"
- 命名空间提供
engine/runtime/display/audio/save/i18n/ui/pygame/os/math等常用对象; 代码拥有完整解释器权限 (如同插件, 仅在可信脚本中使用) - 异常记录日志 (
log.runtime.python_exec_failed) 不中断游戏 - 语法约定:
python::用双冒号以免与普通标签 (python:) 冲突;块内缩进需大于块自身缩进, 缩进回退即块结束 - 插件也可注册自己的指令处理 raw 块 (parser 生成
Statement(op="python", kwargs={"code": 原文}))
5. 命名空间系统
三个域:
| 域 | 内容 |
|---|---|
builtin:: |
引擎内置指令/变量 (builtin::set、builtin::text) |
main:: |
项目文件定义的变量 (set love = 1 归此域) |
<插件名>:: |
插件注册的指令/变量 (shake::shake、custom_actions::do_action) |
无命名空间解析顺序:
- 变量:
main::→builtin::(引擎预置builtin_vars) → 找不到返回默认 (插值空串) / 表达式求值报错 - 指令:
builtin::→main::→ 已 using 的插件命名空间 →找不到时报错并提示所在命名空间 (“指令 shake 位于 shake::shake,需 using 或用完整命名空间调用”)
using 导入 (类似 C++):
using shake # 之后 shake 可省略前缀
using shake custom_actions # 一次导入多个
没有 using 时, 插件指令裸名调用被拒绝并提示 —— 杜绝名称冲突。顶层 using 在 load_script 时静态生效 (不依赖执行流程)。
变量细节:
set love = 1与set main::love = 1等价 (main 域键规范化)set plugin::cnt = 3存为带前缀键plugin::cnt$main::love/$love/$plugin::cnt均可用于插值/表达式/条件
指令注册表 (engine.commands):
register(name, fn, ns="main")/register_builtin(name, fn)has(name, ns=None)/get(name, ns=None)/find(name)(定位提示)call(name, stmt, ns=None)/unregister(name, ns)names(ns=None)
6. 声音系统详解
6.1 三类声音
| 类型 | 管理 | 播放 |
|---|---|---|
| 音乐 music | mixer.music 流式 + fade 状态机 | music <名/路径> |
| 音效 sfx_ui / sfx_story | Sound 实例 | UI 自动 / sfx <名> |
| 语音 voice | 独立通道 (Channel 0) | say/nar ... voice <名> |
6.2 BGM 淡入淡出状态机 (audio.update)
play_music(path, loop, fade, name)— 切换时旧曲淡出 (音量渐降) →完成后新曲淡入 (0 音量渐升); loop=True 循环 / False 单次stop_music(fade)/pause_music(fade)/resume_music(fade)set_bgm_volume(v)/set_sfx_volume(v)stop_all(fade)/pause_all(fade)— 全局 (BGM 淡出 + 音效/语音停)play_voice(path)/stop_voice()/voice_playing()current_bgm(路径) /current_bgm_name(注册名, 存档/显示用)- 事件:
music_play(name/loop/fade/path) /music_pause/music_resume/music_stop/voice_play/voice_stop/sound_all_pause/sound_all_stop/sound_register
6.3 语音生命周期
- 台词带
voice时显示即播; 该句推进时立即停止 (先停语音再播UI 音效, 避免通道竞争); 回标题/读档也停语音 - 语音独立通道, 不干扰音效
6.4 存档与 BGM
- 存档保存
current_bgm_name(注册名, 非绝对路径); 读档用engine.play_music(名称/路径都兼容) 恢复 ending指令与标题”开始游戏”动作自动stop_music(淡出)
7. 存档系统
- 槽位: 默认 6 个 (save/slot0-5.json), 数量可由 window 块
save_slots配置 (如save_slots: 12); 由 ESC 菜单或标题”读取存档”打开 - 自动分页: 槽位数超过 6 时槽位界面自动分页 (底部 ◀ 页码 ▶ 翻页,命中与存档/读档按全局槽位索引)
- 存档内容: 变量 / 剧情位置 (标签+语句索引) / 调用栈 / 阻塞状态 /背景 (场景 id + 背景名) / 立绘 (id/立绘名/透明度/旋转/翻转/中心点) /BGM 注册名 / 当前样式名 / 文本与选择支状态
- 对象以脚本 id 存储 (不存图片路径) —— 重命名图片不破坏旧存档
- 读档精确恢复;
sleep阻塞中读档不恢复剩余等待时间 - 元数据 API (快照插件用):
save.set_meta(slot, key, value)/get_meta(slot, key, default)/meta_path(slot, rel)(相对路径 → 存档目录绝对路径) /list_slots(count)(含 time/label/preview/screenshot/empty)
存档画面快照 (插件 slot_thumbnails.py):
- 引擎每帧维护
_last_game_frame(无覆盖层时的纯游戏画面,槽位面板/菜单/确认框/选择支/错误弹窗都算覆盖层) - 存档事件 →
engine.get_last_game_frame()截图 → 缩小 150×84 →存存档目录 (相对路径thumb_slotN.png) → 元数据写screenshot相对名 (不存绝对路径) - 槽位界面通过
display.register_slot_thumbnail_provider(fn)绘制,缩略图高度自动适配槽位 (保持比例), 文字区自适应字号+单行截断,宽度不足时省略时间
8. 渲染系统
| 组件 | 说明 |
|---|---|
| 背景 | scene + bg <场景> <背景名> [with 过渡]; cover/fit/center/stretch |
| 立绘 | char + show/hide/move/rotate/flip; 登场/退场 9 种效果 |
| 文本框 | style 配置 (背景图/颜色/字号); 富文本 + LaTeX |
| 选择支 | choice: 分支; 按钮样式/图片/字号/宽度可配 |
| 文字显示 | typewriter / instant / terminal (光标) / lines / 插件 |
| 过渡 | fade/dissolve/blinds/slide/circle/pixelate/zoom + 插件 |
| 错误弹窗 | 温和提示 + 剪贴板复制 + 日志追加 |
| UI 主题 | ui 块九宫格切片 (默认/焦点双态) |
8.1 截图与槽位界面 API
engine.display.capture() # 当前画面 Surface 副本
engine.get_last_game_frame() # 最近纯游戏画面帧
engine.display.register_slot_thumbnail_provider(fn) # 槽位缩略图绘制钩子
# fn(slot_index, slot_info) -> Surface | None
9. 插件体系
插件是 framework/plugins/ 下的 .py 文件 (下划线开头忽略),引擎启动自动发现。命名空间 = 插件文件名。
9.1 插件清单
| 插件 | 提供 |
|---|---|
| fx | 屏幕特效: shake/flash/blackflash/tint/strobe/pulse |
| notice | 通知: BGM 播放/暂停/恢复/停止 (右上) + 场景切换 (左上), 合并自 bgm/scene_notice |
| transitions_plus | 扩展背景过渡: wipe/iris/curtain/sweep/fade_white/checker/stripes |
| custom_actions | 动作 (explode/quake/freeze/blackout) + do_action 指令 + 立绘效果 (wobble/sway/zoom_bounce/fade_rotate/float/squash) + 文字模式 (wave/bounce/speedup/rainbow/shiver) |
| debug_mode | 调试模式 (快捷键切换, 开启显示 FPS) |
| slot_thumbnails | 存档画面快照 + 槽位缩略图 |
| auto_skip | 系统菜单 (ESC/bar) 加「自动模式」「跳过剧情」按钮 |
auto_skip 用法: 插件自动向系统菜单追加两个按钮 (popup 弹窗与bar 常驻栏共用, 未定义 menu system 时先补内置五项); 按钮样式在ui.gal 的 menu system 中配置 (action: auto_toggle / skip_once,支持 image/image_focus/image_active/image_disabled 等):
- 自动模式: 文本显示完毕后自动推进下一句 (间隔
AUTO_DELAY,默认 1.2s); 选择支/标题自动暂停; 在标题/鉴赏/菜单等非正式界面自动关闭, 回到正式游戏界面自动恢复 (按用户意图); 开启时按钮切换image_active激活图; 点击后退出 ESC 菜单; 右上角 [自动] - 跳过剧情: 直达下一个选择支/标题/结局之前 (跳过文本/等待/移动动画阻塞; 背景/立绘等场景指令正常执行到位); 点击后退出 ESC菜单; 再次点击可取消; 右上角 [跳过]
- 跳过由引擎
runtime.skip_mode支持 (advance()快进, 语音静音);插件不装载时系统菜单不含这两个按钮, 不影响核心
9.2 两种写法
# 1. 装饰器写法 (推荐)
from framework.api import command, event_listener
@command("mycmd") # 自定义 DSL 指令 (注册到 <插件名>::)
def mycmd(engine, stmt, **kw):
engine.show_notice("指令被调用!")
return None # 返回 "block" 可阻塞等外部事件
@event_listener("bg_change") # 订阅事件 (engine 参数自动注入)
def on_bg(path, engine, **kw):
print("背景切换到", path)
# 2. 类写法 (生命周期管理)
from framework.api import Plugin
class MyPlugin(Plugin):
name = "my_plugin"
version = "1.0"
def on_load(self): # 装载时 (可用 self.engine)
@self.listen("draw_overlay") # 每帧渲染钩子
def overlay(surface, **kw):
pass
@self.add_command("greet") # 指令 (命名空间 = 插件名)
def greet(engine, stmt, **kw):
engine.say("插件", "你好")
def on_unload(self): # 卸载时清理 (如注销 provider)
pass
9.3 事件一览
| 事件 | 载荷 |
|---|---|
| engine_start / engine_quit | engine |
| script_load / script_start / script_end | path / name |
| script_block (静态扫描属性块) | op, stmt (插件自定义块, 如 gallery) |
| label_enter | label |
| statement | stmt, label |
| text_show / text_advance / text_complete | text, speaker |
| choice_show / choice_made | choices / index, label, text |
| choice_prepare (显示前) | options (可变列表, 插件可原地改写/注入选项) |
| bg_change | path, effect |
| scene_change | id, name, background, pose |
| sprite_show / sprite_hide | id, path |
| sprite_effect_complete | id, direction |
| var_set | name, value |
| using | namespaces |
| sound_register | name, type |
| music_play / music_pause / music_resume / music_stop | name, path, loop, fade |
| voice_play / voice_stop | path |
| save / load | slot, path |
| confirm_show / confirm_choice | text / index |
| action | type, params, source |
| draw_overlay (每帧) | surface |
| error / error_dismiss | exc / level |
9.4 引擎公共 API
engine.display # 渲染: set_bg/show_sprite/hide_sprite/show_text/show_choices...
engine.audio # BGM 状态机 / play_sound / play_voice / stop_voice
engine.save # save(slot,data)/load(slot)/set_meta/get_meta/meta_path
engine.runtime # vars / evaluate / jump / _interp / sounds / using_ns...
engine.events # 事件总线 (on/emit/off)
engine.commands # 指令注册表 (register/has/get/call/find/unregister)
engine.plugins # 插件管理器 (discover/load_module_from_path/unload_module)
engine.ui # UI 绘制原语
engine.say(speaker, text)
engine.set_var(name, value) / engine.get_var(name)
engine.show_notice(text)
engine.save_game(slot) / engine.load_game(slot)
engine.resolve_path(rel) # 相对脚本目录解析资源路径
# 音频 API (名称可为注册名或路径)
engine.play_music(name_or_path, loop=True, fade=None)
engine.stop_music(fade) / engine.pause_music(fade) / engine.resume_music(fade)
engine.play_sfx(name) / engine.play_voice(name) / engine.stop_voice()
engine.set_music_volume(v) / engine.set_sfx_volume(v)
engine.stop_all_sounds(fade) / engine.pause_all_sounds(fade)
engine.get_last_game_frame()
# 错误处理
engine.handle_error(exc, level="error")
engine.copy_to_clipboard(text)
9.5 注册接口汇总
| 接口 | 用途 |
|---|---|
engine.events.on/emit/off |
事件总线 |
engine.commands.register / @command |
自定义 DSL 指令 |
engine.register_action(name, fn) |
选择列表按钮动作 |
display.register_transition(name, cls) |
背景过渡效果 |
display.register_sprite_effect(name, fn, dur) |
立绘登场/退场动画 |
display.register_bg_renderer(fn) |
动态背景渲染器 (视频帧/程序背景; fn(display)->Surface|None) |
display.register_sprite_renderer(sid, fn) |
立绘动态渲染器 (Live2D; fn(display, sprite)->Surface|None, sid=None 全局兜底) |
display.register_text_mode(name, spec) |
文字显示模式 |
display.register_slot_thumbnail_provider(fn) |
槽位缩略图 |
engine.register_file_codec(scope, decode, encode) |
文件编解码钩子 (加密存档/资源/语言/脚本/插件文件; scope=save/resource/lang/script/plugin, fn(bytes)->bytes, None=原样) |
engine.register_frame_hook(fn) |
每帧钩子 (主循环无条件调用 fn(dt), 暂停时也执行; Steam 回调/网络轮询/心跳用, 异常隔离) |
engine.register_text_char_hook(fn) |
文本输出钩子 (打字音效/字幕高亮; fn(display, start_idx, count) 按逻辑字符增量触发) |
engine.snapshot_state() / engine.restore_state(data) |
状态快照/恢复 (内存, 与存档同构; 撤销/回滚/分支探索用, 静默不落盘) |
engine.register_action + do_action 指令 |
脚本触发动作 |
engine.register_menu_button(mid, text, action, cfg) |
插件向命名菜单添加按钮 (cfg 支持 enabled/image_disabled/image_active 等) |
engine.set_menu_button_state(mid, key, enabled) |
按钮启用/禁用 (禁用态暗色/禁用图) |
engine.set_menu_button_cfg(mid, key, cfg) |
更新按钮 cfg (动态切换图/文本, 显示中同步刷新) |
文件编解码钩子 (插件加密存档/资源/语言/脚本/插件文件):
engine.register_file_codec(
"save", # scope: save / resource / lang / script / plugin
decode=my_decrypt, # fn(bytes)->bytes 读取时解码 (None=原样)
encode=my_encrypt, # fn(bytes)->bytes 写入时编码 (None=原样)
)
- scope 覆盖:
save(存档/settings/global JSON) ·resource(图片/音频/字体, pygame 以 file-like 加载解密结果) ·lang(语言文件) ·script(.gal 脚本, 引擎构造时绑定到解析器) ·plugin(插件源码,解密后经 importlib 加载, 临时文件自动清理) - 未注册的 scope 读写完全原样; 解码/编码异常记录日志并回退原数据
- 加密强度说明: 客户端密钥必然随包分发, 用于防明文/轻度防篡改;防逆向建议把核心算法/密钥逻辑编译为
pyd再配合本钩子 - 密钥可运行时从服务器获取: 插件在
on_load(早于一切资源加载)拉取并缓存, 注册的闭包直接使用 —— 无需再动内核
9.6 动作系统
| 动作 | 参数 | 说明 |
|---|---|---|
start |
label | 启动游戏 (跳转标签) |
quit |
关闭游戏 (走退出确认) | |
title |
回到标题画面 | |
continue |
关闭菜单继续游戏 | |
slot_menu |
mode=save/load | 打开存档/读档页面 |
save / load |
slot | 直接存档/读档 |
close |
关闭当前选择列表 |
自定义动作: engine.register_action("explode", fn) 其中fn(engine, params, source), 返回 True 执行后关闭选择列表。
9.7 运行时插件管理 (DSL 语句)
plugin load custom_actions # 装载 (自动加入 using)
plugin unload shake # 卸载 (清类实例 on_unload + 指令/事件/订阅)
plugin list # 列出已加载插件
PluginManager 记录 directory (discover 时), 运行时装载按插件名找<目录>/<名>.py; unload_module 先卸载该模块的 Plugin 类实例再清模块级注册。
10. UI 绘制原语 (engine.ui)
from pygame import Rect
engine.ui.panel(surface, Rect(10, 10, 200, 60),
bg_color=(0, 0, 0, 185),
border_color=(255, 255, 255, 80), border_width=2, radius=8)
engine.ui.text(surface, font, "文字", center=(100, 40))
engine.ui.wrap_text(font, "长文本", max_width=180) # -> [行]
engine.ui.multiline_text(surface, font, "多行", 20, 20,
max_width=180, max_lines=3)
engine.ui.dim_overlay(surface, alpha=150)
插件在 draw_overlay 事件里可直接叠加 HUD / 调试信息。
11. 错误处理
- 主循环隔离: 每帧 update/draw/事件处理包在 try/except 中
- 全局兜底:
sys.excepthook捕获主线程未捕获异常 - 温和弹窗: 错误面板 (摘要 + 日志路径), 三按钮: 继续/复制/退出
- 日志文件: 所有日志 (INFO/WARN/ERROR) 同时输出 console 与
<项目目录>/logs/engine.log; 错误详情另写logs/errors.log
错误分级: warn 仅记录 + 游戏界面顶部小提示 (提醒检查日志,节流 2 秒, 不阻塞); error 记录 + 弹窗 (跳转到不存在标签/表达式求值失败/脚本插件异常)。
12. 测试 (779 项断言)
framework/tests/smoke.py, dummy 视频/音频驱动, 无窗口可跑:
py -3.10 framework/tests/smoke.py
覆盖: 解析器 / 运行时逻辑 (变量/分支/跳转/调用栈/表达式) / 交互推进 / 样式表 / selection / 存档 (含 BGM 名称/快照元数据) / 过渡 / 角色 / 场景 / 对话框 / 菜单 / 动作 / 立绘效果 / 文字模式 / 插件装载配置 / import 拆分 / 键盘导航 / 命名空间 / 音频 (fade 状态机/全局静音/UI 音效) / 存档快照 / 运行时插件管理 / LaTeX 逐字兼容 / 语音生命周期 / 分角色语音音量 / 窗口配置与等比缩放 (to_logical/present letterbox/window config 指令) / 常驻菜单栏 (bar 模式: 构建/命中/点击/ESC 语义/样式/自定义项) / 错误弹窗。
13. DSL 指令速查表
| 指令 | 语法 | 说明 |
|---|---|---|
| 对话 | say <角色> "文本" [voice 名] / nar "文本" / text "文本" |
台词/旁白, 支持富文本与 $变量 |
| 背景 | bg <场景> [背景名] [with 效果] |
场景背景或直接路径 |
| 场景 | scene <id> 块 |
场景定义 (name/default/背景名) |
| 角色 | char <id> 块 |
角色定义 (name/立绘名:路径) |
| 立绘 | show <角色> [立绘名] [with 效果] / hide <角色> [with 效果] |
显示/隐藏 |
| 变换 | move/rotate/flip <角色> ... |
位移/旋转/翻转 |
| 清除 | clear |
清除全部立绘 |
| 样式 | use style <名> / style <名> 块 / selection_style 块 |
主题切换/定义 |
| UI 素材 | ui 块 |
九宫格主题切片 |
| 菜单 | menu <id> 块 |
命名菜单定义 (按钮支持 image_disabled 禁用图) |
| 菜单栏 | menu_bar 块 |
常驻菜单栏样式 (bar 模式) |
| 标题 | title 块 |
标题画面 (menu: 引用; button_columns 多列) |
| 选择支 | choice [ui_click X] [ui_hover Y] 块 |
分支选择 |
| 询问 | confirm <文本> [yes X] [no Y] [-> 变量] |
确认框, 结果 yes/no 存变量 |
| 变量 | set <名> = <表达式> |
赋值 (main::/插件:: 前缀) |
| 条件 | if/elif/else/endif |
分支 |
| 跳转 | jump <标签> / call <标签> / return |
流程控制 |
| 等待 | sleep <秒> |
阻塞等待 |
| 文字模式 | typing <模式> |
typewriter/instant/terminal/lines/插件 |
| 声音 | sound <名> 块 |
声音注册 |
| 音乐 | music <名/路径> [loop 0/1] [fade 秒] |
播放/切换 |
| 暂停 | pause music [fade 秒] / pause all |
暂停音乐/全局 |
| 恢复 | resume music [fade 秒] |
恢复 |
| 音量 | volume music <0-1> / volume sfx <0-1> / volume voice [角色] <0-1> |
临时音量 (voice 可全局或按角色) |
| 停止 | stop music [fade 秒] / stop all |
停止音乐/全局 |
| 音效 | sfx <声音名> |
剧情音效 |
| 窗口 | window config 块 |
运行时改 标题/尺寸/图标/全屏/可缩放/fps (即时生效) |
| 全屏 | fullscreen true/false |
切换全屏 (内容等比缩放) |
| 命名空间 | using <命名空间...> |
导入插件命名空间 |
| 插件 | plugin load/unload/list <名> |
运行时插件管理 |
| 存档 | save / load |
槽位 0 存档/读档 |
| 转场 | fade / fadeout |
黑幕淡入/淡出 |
| 结束 | ending [结局名] |
结束画面 + 结局记录 (全局进度) |
| 鉴赏 | gallery 块 / scene type: cg |
鉴赏配置 / CG 场景 (展示即收集) |
| 设置 | settings 块 + setting <key> 子块 |
设置界面配置 (布局 + 条目) |
| 结束 | ending [结局名] |
结束画面 + 结局记录 (全局进度) 回标题 |
14. 打包发行 (PyInstaller)
# 1. 安装 PyInstaller (Python 3.10)
py -3.10 -m pip install pyinstaller
# 2. 打包 (onedir 模式, 全部产物输出到 test/release, 不污染项目根)
py -3.10 -m PyInstaller --noconfirm --onedir --name GalgameMaker ^
--distpath test/release --workpath test/release/build ^
--specpath test/release ^
--add-data "C:\<项目绝对路径>\framework\plugins;framework\plugins" ^
gamelauncher.py
# 3. 复制 demo 数据与字体到发行目录 (exe 同目录):
# test/engine_demo -> test/release/GalgameMaker/test/engine_demo
# (排除 save/__pycache__; 素材/脚本随发行版分发)
# fonts -> test/release/GalgameMaker/test/engine_demo/fonts
# 4. 双击 test/release/GalgameMaker/GalgameMaker.exe 运行
说明:
gamelauncher.py已支持 frozen 环境: 打包后项目根基于 exe 所在目录(sys.executable), demo/字体等外部数据与 exe 同放即可- 插件目录打包进 exe (
--add-data), 运行时自动装载; demo 数据与字体为外部文件, 便于直接替换素材/脚本/存档 - 也可拖拽任意
.gal文件到 exe 上运行 (启动器传参)