- 示例工程
【免费下载链接】godot-demo-projects
Demonstration and Template Projects
本指南围绕 godot-demo-projects 仓库中的 runtime_save_load 演示项目,系统讲解如何在不经过 Godot 资源导入系统(Import System)的前提下,于运行时直接加载与保存图片、音频、3D 场景(glTF)、ZIP 压缩包、字体与纯文本等各类文件。读完本文,你将掌握Image、AudioStreamOggVorbis/MP3/WAV、GLTFDocument/FBXDocument、FontFile、ZIPReader/ZIPPacker与FileAccess等核心类的运行时读写用法,并理解其与 PCK 打包、资源导入、游戏存档序列化之间的边界,可直接应用到用户生成内容(UGC)、DLC 扩展包等真实场景。
项目概览:为什么需要绕过资源导入系统
在 Godot 中,常规的资源使用方式是把图片、音频、场景等资源导入(Import)进项目,最终随导出打包进 PCK 文件。这种方式适合「开发者预先准备、内容固定」的资源。但当内容来自用户运行时产生(如玩家上传的头像、自制的模型、自定义关卡包)时,不可能在每次导出前重新导入,因此需要一套「运行时直接从磁盘读写文件」的方案。
runtime_save_load 演示项目 正是展示这一能力的官方示例。它的核心主张体现在项目自述中:
本项目展示如何在不经过 Godot 资源导入系统的情况下,加载和保存各种文件类型。这对于在运行时加载/保存图片、声音、3D 场景和 ZIP 压缩包等用户生成内容非常有用,且无需用户通过 Godot 生成 PCK 文件。
项目基于 GDScript 编写,渲染后端为 Compatibility(OpenGL),对应配置可在 project.godot 中看到:renderer/rendering_method="gl_compatibility",主场景为res://runtime_save_load.tscn,并开启了run/low_processor_mode=true以降低闲置时的 CPU 占用。
支持的文件类型清单
| 能力 | 文件类型 |
|---|---|
| 可运行时加载并保存 | 图片(JPEG、PNG、WebP)、3D 场景(glTF 2.0)、ZIP 压缩包、纯文本文件 |
| 可运行时加载(仅加载) | 图片(TGA、BMP、SVG)、3D 场景(FBX)、音频(Ogg Vorbis、MP3、WAV)、字体(TTF、OTF、WOFF、WOFF2、PFB、PFM、BMFont) |
演示目录下 examples 为每种类型都准备了可直接试用的样例:images/下同源的godot_icon共 6 种格式(png、jpg、webp、tga、bmp、svg),audio/下的item_spawn有 ogg、mp3、wav 三种编码,fonts/提供 TTF/OTF/WOFF2 三种字体,3d_scenes/gltf/提供带纹理贴图的塑料椅 glTF 场景,misc/下则是example.zip与file.txt。
快速上手:运行演示项目
- 用 Godot 4.x 打开 loading/runtime_save_load/project.godot(项目配置
config/features=PackedStringArray("4.7"),需 Godot 4.7 或兼容版本)。 - 点击运行,主场景 runtime_save_load.tscn 会加载一个文件浏览器 UI。
- 在顶部的
FilePath输入框中直接输入文件路径,或点击Browse按钮通过文件对话框选择examples/目录中的任一文件。 - 程序会根据文件扩展名自动识别类型,并在下方的
Result区域显示对应预览(图片纹理、音频播放按钮、3D 场景视图、字体字形预览、ZIP 文件列表或纯文本内容)。 - 预览成功后,点击Export按钮可将当前内容保存为指定格式的新文件。
界面结构:一个场景承载六种查看器
runtime_save_load.tscn 将全部 UI 组织在一个Control根节点下,并通过visible切换不同查看器,其结构对理解脚本逻辑很有帮助:
- 输入区:
FilePath(LineEdit)、Browse按钮与FileDialog(file_mode=0即打开文件模式)。 - 结果区(
Result/CenterContainer):PlainTextViewer:滚动容器 + Label,用于显示纯文本。TextureViewer:TextureRect,显示图片。AudioPlayer:按钮内含AudioStreamPlayer(volume_db=-10.0)与时长信息 Label。SceneViewer:SubViewportContainer + SubViewport(开启 MSAA、scaling_3d_scale=2.0)+ Camera3D + 三盏方向光(KeyLight/FillLight/BackLight)+ Zoom 滑块(HSlider,min_value=-100.0、max_value=-0.1)。FontViewer:Label,用预览字体渲染字母、数字与符号表。ZIPViewer:HSplitContainer,左侧FileList(ItemList)列出压缩包内文件,右侧FilePreview(Label)显示选中文件的 UTF-8 文本内容。ErrorLabel:加载失败时的错误提示。
- 导出区:
Export按钮与独立FileDialog(access=2即文件系统访问)。
场景中所有信号均在 tscn 末尾通过[connection]声明绑定,例如text_submitted、file_selected、item_selected、value_changed分别对应脚本中的_on_file_path_text_submitted、_on_file_dialog_file_selected、_on_zip_viewer_item_selected、_on_scene_viewer_zoom_value_changed等回调。
核心实现:open_file 的分发逻辑
整个加载流程集中在 runtime_save_load.gd 的open_file(path)方法中。它将路径转为小写后,按扩展名依次匹配图片、音频、3D 场景、字体、ZIP,最后以纯文本作为兜底:
func open_file(path: String) -> void: var path_lower := path.to_lower() # 图片分支 if path_lower.ends_with(".jpg") or path_lower.ends_with(".jpeg") \ or path_lower.ends_with(".png") or path_lower.ends_with(".webp") \ or path_lower.ends_with(".svg") or path_lower.ends_with(".tga") \ or path_lower.ends_with(".bmp"): # ... # 音频分支 elif path_lower.ends_with(".ogg") or path_lower.ends_with(".mp3") \ or path_lower.ends_with(".wav"): # ... # 3D 场景分支 elif path_lower.ends_with(".gltf") or path_lower.ends_with(".glb") \ or path_lower.ends_with(".fbx"): # ... # 字体分支 elif path_lower.ends_with(".ttf") or path_lower.ends_with(".otf") \ or path_lower.ends_with(".woff") or path_lower.ends_with(".woff2") \ or path_lower.ends_with(".pfb") or path_lower.ends_with(".pfm") \ or path_lower.ends_with(".fnt") or path_lower.ends_with(".font"): # ... # ZIP 分支 elif path_lower.ends_with(".zip"): # ... # 兜底:按纯文本打开 else: var file_contents := FileAccess.get_file_as_string(path) # ...图片:一行代码完成格式探测与加载
图片分支是整个演示中代码最简洁的,因为它直接调用Image.load_from_file(path)—— 该方法会根据文件扩展名自动探测格式并完成读取,同时覆盖 JPEG/PNG/WebP/SVG/TGA/BMP 七种格式:
var image := Image.load_from_file(path) reset_visibility() export_file_dialog.filters = ["*.png ; PNG Image", "*.jpg, *.jpeg ; JPEG Image", "*.webp ; WebP Image"] texture_viewer.visible = true texture_viewer.texture = ImageTexture.create_from_image(image)脚本注释明确指出,如果需要错误处理或更细粒度的控制(例如控制 SVG 的加载缩放比例),应改用Image类的load_*_from_buffer()系列方法与load_svg_from_string()。
音频:按格式选择对应的 AudioStream 类
音频分支依据扩展名分别使用三个静态工厂方法,将文件直接构造成可播放的AudioStream资源:
if path_lower.ends_with(".ogg"): audio_stream_player.stream = AudioStreamOggVorbis.load_from_file(path) elif path_lower.ends_with(".mp3"): audio_stream_player.stream = AudioStreamMP3.load_from_file(path) elif path_lower.ends_with(".wav"): audio_stream_player.stream = AudioStreamWAV.load_from_file(path)加载成功后脚本会显示播放按钮并计算音频时长(roundi(stream.get_length())后格式化为MM:SS)。需要注意:如果数据已在内存中(PackedByteArray),可改用AudioStreamOggVorbis.load_from_buffer()这类缓冲区方法。
3D 场景:GLTFDocument 与 FBXDocument 的运行时装配
glTF 分支使用GLTFDocument+GLTFState完成「文件 → 节点树」的转换,前者负责加载数据,后者保存加载状态。成功加载后调用generate_scene(gltf_state)生成根节点并挂到 SubViewport 下的场景查看器中:
var gltf_document := GLTFDocument.new() var gltf_state := GLTFState.new() var error := gltf_document.append_from_file(path, gltf_state) if error == OK: scene_viewer_root_node = gltf_document.generate_scene(gltf_state) reset_visibility() scene_viewer.add_child(scene_viewer_root_node) export_file_dialog.filters = ["*.gltf ; glTF Text Scene", "*.glb ; glTF Binary Scene"] scene_viewer.visible = true else: show_error('Couldn\'t load "%s" as a glTF scene (error code: %s).' % [path.get_file(), error_string(error)])FBX 分支的结构与之完全对称,只是换用FBXDocument与FBXState。场景查看器中的Zoom滑块通过_on_scene_viewer_zoom_value_changed控制正交相机(projection=1)的size:滑块取值范围为负值(-100.0 ~ -0.1),scene_viewer_camera.size = abs(value),数值越小(绝对值越大)表示放大倍率越高。
字体:动态字体与位图字体的区分加载
字体分支创建FontFile,根据扩展名区分两类加载方式:.fnt/.font(BMFont 位图字体)走load_bitmap_font(),其余 TTF/OTF/WOFF/WOFF2/PFB/PFM 走load_dynamic_font():
var font_file := FontFile.new() if path_lower.ends_with(".fnt") or path_lower.ends_with(".font"): font_file.load_bitmap_font(path) else: font_file.load_dynamic_font(path) if not font_file.data.is_empty(): font_viewer.add_theme_font_override(&"font", font_file) reset_visibility() font_viewer.visible = true export_button.disabled = true成功加载后通过add_theme_font_override把字体应用到一个 48 号字号的 Label 上,直接预览字形。脚本用font_file.data.is_empty()判断加载是否成功,失败则进入show_error流程。
ZIP 压缩包:ZIPReader 的只读浏览
ZIP 分支创建ZIPReader,打开压缩包后列出内部全部文件,并禁用列表中以/结尾的目录项:
zip_reader.open(path) var files := zip_reader.get_files() files.sort() export_file_dialog.filters = ["*.zip ; ZIP Archive"] reset_visibility() for file in files: zip_viewer_file_list.add_item(file, null) zip_viewer_file_list.set_item_disabled(-1, file.ends_with("/")) zip_viewer.visible = true当用户选中列表项时,_on_zip_viewer_item_selected用zip_reader.read_file(文件名)取出原始字节,再get_string_from_utf8()转为文本预览。值得一提的扩展场景:Godot 的「Export PCK/ZIP」功能生成的 ZIP 文件同样可以被读取(其中包含的是导入后的 Godot 资源而非原始工程文件);而若要作为 DLC 追加数据包无缝集成到虚拟文件系统,演示注释推荐改用ProjectSettings.load_resource_pack()。
运行时保存:Export 按钮背后的写入逻辑
_on_export_file_dialog_file_selected(path)根据当前可见的查看器类型决定写入方式:
- 纯文本:用
FileAccess.open(path, FileAccess.WRITE)+store_string()写回,最后close()。 - 图片:从
texture_viewer.texture.get_image()取回Image,再按目标扩展名调用save_png()、save_jpg()(质量常量为JPG_QUALITY = 0.9)或save_webp()。注释特别说明 WebP 默认无损保存,需要有损时可通过Image.save_webp()的可选参数调整。 - 3D 场景:用
GLTFDocument.append_from_scene(scene_viewer_root_node, gltf_state)把预览中的节点树转回 glTF 状态,再write_to_filesystem(gltf_state, path)落盘。输出是文本(.gltf)还是二进制(.glb)由目标路径扩展名决定:二进制写盘更快、体积更小、更适合内嵌纹理,但文本格式更易于调试。 - ZIP:创建
ZIPPacker,open(path)后遍历zip_reader.get_files(),对每个文件依次start_file()→write_file()→close_file(),最后close()。若打开失败会push_error并中止。
两处「无法导出」的限制也在代码中明确标注:音频中的 Ogg Vorbis 与 MP3 无法在运行时导出为标准格式(只有 WAV 源可通过AudioStreamWAV.save_to_wav()保存为 WAV);字体同样只能通过ResourceSaver保存为 Godot 专属的.res格式。因此这两个分支下export_button.disabled = true。
边界与注意事项
原文档的脚注部分给出了三条关键边界,项目中均有对应实现佐证:
- 自定义二进制格式:操作自定义二进制格式可以通过
FileAccess与PackedByteArray类实现,但演示未展示。这意味着文本/压缩包之外的结构化数据读写需要自行封装字节流逻辑。 - SVG 程序化生成:可以先用文本方式程序化生成 SVG 内容,再用
FileAccess写入带.svg扩展名的文件。仓库中 examples/images/godot_icon.svg 正是可被Image.load_from_file()直接加载的 SVG 样例。 - FBX 运行时加载的已知问题:文档明确提到 Godot 的 issue #96043,即运行时 FBX 加载存在已知缺陷。FBX 分支在
append_from_file返回非OK时同样会通过show_error展示error_string(error)错误码,实际项目中建议优先使用 glTF 作为跨引擎交换格式。
与 Serialization 演示的互补关系
本演示关注「文件级 I/O」,而仓库中的 loading/serialization 演示(Saving and Loading / Serialization)关注「游戏进度级 I/O」,展示如何使用ConfigFile与JSON保存游戏状态。原文档明确给出分工建议:前者解决「导出的游戏如何在运行时读取未导入的文件」,后者解决「游戏进度如何持久化」,两者配合即可覆盖从存档数据到 UGC 资源的完整读写需求。
示例资产与许可
examples/3d_scenes/gltf/ 下的plastic_monobloc_chair_01_1k.gltf及配套纹理(diff/nor/arm 三张 JPG 与一个.bin缓冲文件)来自 Poly Haven,采用 CC0 1.0 通用许可;examples/audio/ 下的音频文件来自 Red Eclipse 项目,采用 CC BY-SA 4.0 国际许可;examples/fonts/ 内的 Inter 字体遵循 SIL Open Font License 1.1。在自有项目中使用或二次分发这些示例资产时,需保留对应的许可声明。
- 示例工程
【免费下载链接】godot-demo-projects
Demonstration and Template Projects
相关推荐
Godot 音频输出设备运行时切换实战:Audio Device Changer 演示项目源码解析(godot-demo-projects)
Godot 音频输出设备运行时切换实战:Audio Device Changer 演示项目源码解析(godot demo projects) 本文基于 godo
示例工程komorebi 快速保存与加载窗口 Resize 布局尺寸:quick-save-resize 与 quick-load-resize 实战指南
komorebi 快速保存与加载窗口 Resize 布局尺寸:quick save resize 与 quick load resize 实战指南 导读 qui
桌面应用CANN Runtime 运行时配置接口实战:系统参数与 Device/Stream 资源限制管理
CANN Runtime 运行时配置接口实战:系统参数与 Device/Stream 资源限制管理 本篇技术指南聚焦 CANN Runtime 的运行时配置接口
CANNAscend人工智能性能剖析系统编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考