Add a run-time saving/loading demo project

This is useful to load user-generated content without requiring users
to create a PCK file for it.
This commit is contained in:
Hugo Locurcio
2023-10-30 08:28:07 +01:00
parent 40ce32c5da
commit a2557fce51
28 changed files with 902 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
# Run-time File Saving and Loading
This project showcases how to load and save various file types without going
through Godot's resource importing system.
This is useful to load/save images, sounds, 3D scenes and ZIP archives at
run-time such as user-generated content, without requiring users to generate a
PCK file through Godot.
Can be loaded and saved at run-time:
- Images (JPEG, PNG, WebP)
- 3D scenes (glTF 2.0)
- ZIP archives
- Plain text files[^1]
Can be loaded at run-time:
- Images (TGA, BMP, SVG[^2])
- Audio (Ogg Vorbis)
- Fonts (TTF, OTF, WOFF, WOFF2, PFB, PFM, BMFont)
[^1]: Manipulating custom binary formats is possible using the FileAccess and
PackedByteArray classes, but this is not shown in this demo.
[^2]: It is possible to procedurally generate SVG as text and save it to a file
with `.svg` extension using the FileAccess class, but this is not shown in
this demo.
See the [Saving and Loading (Serialization)](/loading/serialization/) demo for
an example of saving/loading game progress.
Language: GDScript
Renderer: Compatibility
## Screenshots
![Screenshot](screenshots/runtime_save_load.webp)
## Licenses
- Files in `examples/3d_scenes/plastic_monobloc_chair_01_1k/` are copyright
[Poly Haven](https://polyhaven.com/a/plastic_monobloc_chair_01)
and are licensed under [CC0 1.0 Universal](https://creativecommons.org/publicdomain/zero/1.0/).
- Files in `examples/audio/` are copyright [Red Eclipse](https://redeclipse.net)
and are licensed under [CC BY-SA 4.0 International](https://www.creativecommons.org/licenses/by-sa/4.0/).

View File

@@ -0,0 +1,159 @@
{
"asset": {
"generator": "Khronos glTF Blender I/O v3.3.32",
"version": "2.0"
},
"scene": 0,
"scenes": [
{
"name": "Scene",
"nodes": [
0
]
}
],
"nodes": [
{
"mesh": 0,
"name": "plastic_monobloc_chair_01"
}
],
"materials": [
{
"doubleSided": true,
"name": "plastic_monobloc_chair_01",
"normalTexture": {
"index": 0
},
"pbrMetallicRoughness": {
"baseColorTexture": {
"index": 1
},
"metallicRoughnessTexture": {
"index": 2
}
}
}
],
"meshes": [
{
"name": "Plane.002",
"primitives": [
{
"attributes": {
"POSITION": 0,
"NORMAL": 1,
"TEXCOORD_0": 2
},
"indices": 3,
"material": 0
}
]
}
],
"textures": [
{
"sampler": 0,
"source": 0
},
{
"sampler": 0,
"source": 1
},
{
"sampler": 0,
"source": 2
}
],
"images": [
{
"mimeType": "image/jpeg",
"name": "plastic_monobloc_chair_01_nor_gl",
"uri": "textures/plastic_monobloc_chair_01_nor_gl_1k.jpg"
},
{
"mimeType": "image/jpeg",
"name": "plastic_monobloc_chair_01_diff",
"uri": "textures/plastic_monobloc_chair_01_diff_1k.jpg"
},
{
"mimeType": "image/jpeg",
"name": "plastic_monobloc_chair_01_metal-plastic_monobloc_chair_01_rough",
"uri": "textures/plastic_monobloc_chair_01_arm_1k.jpg"
}
],
"accessors": [
{
"bufferView": 0,
"componentType": 5126,
"count": 3271,
"max": [
0.3209305703639984,
0.8798216581344604,
0.2916412651538849
],
"min": [
-0.3209305703639984,
-0.00001953914761543274,
-0.335950642824173
],
"type": "VEC3"
},
{
"bufferView": 1,
"componentType": 5126,
"count": 3271,
"type": "VEC3"
},
{
"bufferView": 2,
"componentType": 5126,
"count": 3271,
"type": "VEC2"
},
{
"bufferView": 3,
"componentType": 5123,
"count": 10068,
"type": "SCALAR"
}
],
"bufferViews": [
{
"buffer": 0,
"byteLength": 39252,
"byteOffset": 0,
"target": 34962
},
{
"buffer": 0,
"byteLength": 39252,
"byteOffset": 39252,
"target": 34962
},
{
"buffer": 0,
"byteLength": 26168,
"byteOffset": 78504,
"target": 34962
},
{
"buffer": 0,
"byteLength": 20136,
"byteOffset": 104672,
"target": 34963
}
],
"samplers": [
{
"magFilter": 9729,
"minFilter": 9987
}
],
"buffers": [
{
"byteLength": 124808,
"uri": "plastic_monobloc_chair_01.bin"
}
]
}

View File

@@ -0,0 +1,94 @@
Copyright (c) 2016-2020 The Inter Project Authors.
"Inter" is trademark of Rasmus Andersson.
https://github.com/rsms/inter
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="256"
height="384"
viewBox="0 0 256 384"
version="1.1"
id="svg6"
sodipodi:docname="godot_icon.svg"
inkscape:version="1.3 (0e150ed6c4, 2023-07-21)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs6" />
<sodipodi:namedview
id="namedview6"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.8828125"
inkscape:cx="161.72614"
inkscape:cy="126.40664"
inkscape:window-width="3840"
inkscape:window-height="2132"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg6" />
<g
fill="#ffffff"
id="g4"
transform="matrix(0.25,0,0,0.25,0,-0.330465)">
<path
d="m 105,673 v 33 q 407,354 814,0 v -33 z"
id="path1" />
<path
fill="#478cbf"
d="m 105,673 152,14 q 12,1 15,14 l 4,67 132,10 8,-61 q 2,-11 15,-15 h 162 q 13,4 15,15 l 8,61 132,-10 4,-67 q 3,-13 15,-14 L 919,673 V 427 q 30,-39 56,-81 -35,-59 -83,-108 -43,20 -82,47 -40,-37 -88,-64 7,-51 8,-102 -59,-28 -123,-42 -26,43 -46,89 -49,-7 -98,0 -20,-46 -46,-89 -64,14 -123,42 1,51 8,102 -48,27 -88,64 -39,-27 -82,-47 -48,49 -83,108 26,42 56,81 z m 0,33 v 39 c 0,276 813,276 813,0 v -39 l -134,12 -5,69 q -2,10 -14,13 l -162,11 q -12,0 -16,-11 L 577,735 H 447 l -10,65 q -4,11 -16,11 L 259,800 q -12,-3 -14,-13 l -5,-69 z"
id="path2" />
<path
d="m 483,600 c 3,34 55,34 58,0 v -86 c -3,-34 -55,-34 -58,0 z"
id="path3" />
<circle
cx="725"
cy="526"
r="90"
id="circle3" />
<circle
cx="299"
cy="526"
r="90"
id="circle4" />
</g>
<g
fill="#414042"
id="g6"
transform="matrix(0.25,0,0,0.25,0,-0.330465)">
<circle
cx="307"
cy="532"
r="60"
id="circle5" />
<circle
cx="717"
cy="532"
r="60"
id="circle6" />
</g>
<path
style="font-size:85.3333px;font-family:Montserrat;-inkscape-font-specification:Montserrat;fill:#ffffff;stroke-width:5.96887;stroke-opacity:0.3;paint-order:stroke fill markers"
d="m 83.213934,327.31768 q -4.966091,0 -9.550176,-1.5917 -4.520416,-1.65536 -7.003461,-4.20207 l 1.846367,-3.62907 q 2.355709,2.29205 6.303115,3.88375 4.011074,1.52802 8.404155,1.52802 4.202076,0 6.812459,-1.01869 2.674047,-1.08235 3.883737,-2.86505 1.273357,-1.78271 1.273357,-3.9474 0,-2.61038 -1.528028,-4.20208 -1.46436,-1.59171 -3.883738,-2.48305 -2.419378,-0.95501 -5.348098,-1.65536 -2.928721,-0.70036 -5.857441,-1.46436 -2.928722,-0.82768 -5.411767,-2.16471 -2.419378,-1.33703 -3.947406,-3.50172 -1.46436,-2.22839 -1.46436,-5.79378 0,-3.31073 1.719032,-6.04844 1.7827,-2.80139 5.411767,-4.45676 3.629065,-1.71903 9.295503,-1.71903 3.756402,0 7.449137,1.08236 3.692735,1.01869 6.366783,2.86505 l -1.591697,3.7564 q -2.865052,-1.91004 -6.112111,-2.80138 -3.183392,-0.89136 -6.175781,-0.89136 -4.011072,0 -6.621454,1.08236 -2.61038,1.08236 -3.883738,2.92872 -1.209688,1.7827 -1.209688,4.07474 0,2.61037 1.46436,4.20209 1.528027,1.59169 3.947406,2.48303 2.483045,0.89136 5.411766,1.59169 2.92872,0.70036 5.793773,1.52804 2.92872,0.82768 5.348098,2.1647 2.483044,1.27337 3.947405,3.43806 1.528024,2.16471 1.528024,5.66644 0,3.24706 -1.782694,6.04845 -1.782701,2.73771 -5.475435,4.45676 -3.629066,1.65535 -9.359171,1.65535 z m 39.537726,-0.382 -19.73704,-44.5675 h 5.09342 l 18.46366,41.95712 h -2.80137 l 18.591,-41.95712 h 4.7751 l -19.73703,44.5675 z m 49.97074,0.382 q -5.09342,0 -9.42284,-1.65535 -4.26574,-1.71905 -7.44915,-4.77511 -3.11972,-3.05605 -4.90241,-7.19446 -1.71903,-4.13842 -1.71903,-9.04083 0,-4.90241 1.71903,-9.04083 1.78269,-4.13841 4.96609,-7.19446 3.1834,-3.05606 7.44914,-4.71143 4.32942,-1.71903 9.42283,-1.71903 5.09343,0 9.29552,1.5917 4.26574,1.59168 7.25813,4.83876 l -2.92872,2.99239 q -2.80139,-2.80139 -6.1758,-4.01108 -3.37438,-1.20969 -7.25812,-1.20969 -4.13843,0 -7.64015,1.40069 -3.43805,1.33703 -6.04843,3.88374 -2.54672,2.48305 -4.01108,5.85744 -1.40069,3.31072 -1.40069,7.3218 0,3.94742 1.40069,7.3218 1.46436,3.37439 4.01108,5.92111 2.61038,2.48305 6.04843,3.88375 3.50172,1.33701 7.57648,1.33701 3.82007,0 7.19446,-1.14602 3.43808,-1.14602 6.30313,-3.88374 l 2.67404,3.56541 q -3.1834,2.80138 -7.44914,4.26574 -4.26574,1.40069 -8.91349,1.40069 z m 11.84221,-6.23945 v -16.4263 h 4.52042 v 16.99932 z"
id="text6"
aria-label="SVG" />
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 384 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

View File

@@ -0,0 +1 @@
Plain text file.

View File

@@ -0,0 +1 @@
<svg height="128" viewBox="0 0 128 128" width="128" xmlns="http://www.w3.org/2000/svg"><path d="m27.999994 10.00007a7.9999944 7.9999944 0 0 0 -7.999994 7.999994v91.999936a7.9999944 7.9999944 0 0 0 7.999994 7.99999h71.99995a7.9999944 7.9999944 0 0 0 7.999996-7.99999v-65.999954a1.9999986 1.9999986 0 0 0 -.57-1.413999l-31.999979-31.999977a1.9999986 1.9999986 0 0 0 -1.429999-.586zm0 3.999997h43.99997v23.999984a7.9999944 7.9999944 0 0 0 7.999994 7.999994h23.999982v63.999955a3.9999972 3.9999972 0 0 1 -3.999996 4h-71.99995a3.9999972 3.9999972 0 0 1 -3.999997-4v-91.999936a3.9999972 3.9999972 0 0 1 3.999997-3.999997z" fill="#8a0" stroke-width="2"/></svg>

After

Width:  |  Height:  |  Size: 654 B

View File

@@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bpf0p4mn3trr3"
path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://icon.svg"
dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@@ -0,0 +1,36 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="Run-time File Saving and Loading"
config/description="This project showcases how to load and save various file types without going
through Godot's resource importing system.
This is useful to load/save images, sounds, 3D scenes and ZIP archives at
run-time such as user-generated content, without requiring users to generate a
PCK file through Godot."
config/tags=PackedStringArray("demo", "filesystem", "official")
run/main_scene="res://runtime_save_load.tscn"
config/features=PackedStringArray("4.2")
run/low_processor_mode=true
config/icon="res://icon.svg"
[display]
window/stretch/mode="canvas_items"
window/stretch/aspect="expand"
window/vsync/vsync_mode=0
[rendering]
renderer/rendering_method="gl_compatibility"
lights_and_shadows/directional_shadow/size=8192
lights_and_shadows/directional_shadow/soft_shadow_filter_quality=5

View File

@@ -0,0 +1,249 @@
extends Control
@onready var file_path_edit := $MarginContainer/VBoxContainer/HBoxContainer/FilePath as LineEdit
@onready var file_dialog := $MarginContainer/VBoxContainer/HBoxContainer/FileDialog as FileDialog
@onready var plain_text_viewer := $MarginContainer/VBoxContainer/Result/PlainTextViewer as ScrollContainer
@onready var plain_text_viewer_label := $MarginContainer/VBoxContainer/Result/PlainTextViewer/Label as Label
@onready var texture_viewer := $MarginContainer/VBoxContainer/Result/TextureViewer as TextureRect
@onready var audio_player := $MarginContainer/VBoxContainer/Result/AudioPlayer as Button
@onready var audio_stream_player := $MarginContainer/VBoxContainer/Result/AudioPlayer/AudioStreamPlayer as AudioStreamPlayer
@onready var scene_viewer := $MarginContainer/VBoxContainer/Result/SceneViewer as SubViewportContainer
@onready var scene_viewer_camera := $MarginContainer/VBoxContainer/Result/SceneViewer/SubViewport/Camera3D as Camera3D
@onready var font_viewer := $MarginContainer/VBoxContainer/Result/FontViewer as Label
@onready var zip_viewer := $MarginContainer/VBoxContainer/Result/ZIPViewer as HSplitContainer
@onready var zip_viewer_file_list := $MarginContainer/VBoxContainer/Result/ZIPViewer/FileList as ItemList
@onready var zip_viewer_file_preview := $MarginContainer/VBoxContainer/Result/ZIPViewer/FilePreview as Label
@onready var error_label := $MarginContainer/VBoxContainer/Result/ErrorLabel as Label
@onready var export_button := $MarginContainer/VBoxContainer/Export as Button
@onready var export_file_dialog := $MarginContainer/VBoxContainer/Export/FileDialog as FileDialog
var zip_reader := ZIPReader.new()
# Keeps reference to the root node imported in the 3D scene viewer,
# so that it can be exported later.
var scene_viewer_root_node: Node
func _on_browse_pressed() -> void:
file_dialog.popup_centered_ratio()
func _on_file_path_text_submitted(new_text: String) -> void:
open_file(new_text)
# Put the caret at the end of the submitted text.
file_path_edit.caret_column = file_path_edit.text.length()
func _on_file_dialog_file_selected(path: String) -> void:
open_file(path)
func reset_visibility() -> void:
plain_text_viewer.visible = false
texture_viewer.visible = false
audio_player.visible = false
scene_viewer.visible = false
var last_child := scene_viewer.get_child(-1)
if last_child is Node3D:
scene_viewer.remove_child(last_child)
last_child.queue_free()
font_viewer.visible = false
zip_viewer.visible = false
zip_viewer_file_list.clear()
error_label.visible = false
export_button.disabled = false
func _on_audio_player_pressed() -> void:
audio_stream_player.play()
func _on_scene_viewer_zoom_value_changed(value: float) -> void:
# Slider uses negative value so that it can be inverted easily
# (lower Camera3D orthogonal size is more zoomed *in*).
scene_viewer_camera.size = abs(value)
func _on_zip_viewer_item_selected(index: int) -> void:
zip_viewer_file_preview.text = zip_reader.read_file(
zip_viewer_file_list.get_item_text(index)
).get_string_from_utf8()
#region File exporting
func _on_export_pressed() -> void:
export_file_dialog.popup_centered_ratio()
func _on_export_file_dialog_file_selected(path: String) -> void:
if plain_text_viewer.visible:
var file_access := FileAccess.open(path, FileAccess.WRITE)
file_access.store_string(plain_text_viewer_label.text)
file_access.close()
elif texture_viewer.visible:
var image := texture_viewer.texture.get_image()
if path.ends_with(".png"):
image.save_png(path)
if path.ends_with(".jpg") or path.ends_with(".jpeg"):
const JPG_QUALITY = 0.9
image.save_jpg(path, JPG_QUALITY)
if path.ends_with(".webp"):
# Saving WebP is lossless by default, but can be made lossy using
# optional parameters in `Image.save_webp()`.
image.save_webp(path)
elif audio_player.visible:
# Ogg Vorbis audio can't be exported at runtime to a standard format
# (only WAV files can be using `AudioStreamWAV.save_to_wav()`).
pass
elif scene_viewer.visible:
var gltf_document := GLTFDocument.new()
var gltf_state := GLTFState.new()
gltf_document.append_from_scene(scene_viewer_root_node, gltf_state)
# The file extension in the output `path` (`.gltf` or `.glb`) determines
# whether the output uses text or binary format. Binary format is faster
# to write and smaller, but harder to debug. The binary format is also
# more suited to embedding textures.
gltf_document.write_to_filesystem(gltf_state, path)
elif font_viewer.visible:
# Fonts can't be exported at runtime to a standard format
# (only to a Godot-specific `.res` format using the ResourceSaver class).
pass
elif zip_viewer.visible:
var zip_packer := ZIPPacker.new()
var error := zip_packer.open(path)
if error != OK:
push_error("An error occurred while trying to save a ZIP archive to: %s" % path)
return
for file in zip_reader.get_files():
zip_packer.start_file(file)
zip_packer.write_file(zip_reader.read_file(file))
zip_packer.close_file()
zip_packer.close()
#endregion
func show_error(message: String) -> void:
reset_visibility()
error_label.text = "ERROR: %s" % message
error_label.visible = true
func open_file(path: String) -> void:
print_rich("Opening: [u]%s[/u]" % path)
file_path_edit.text = path
var path_lower := path.to_lower()
# Images.
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")
):
# This method handles everything, from format detection based on
# file extension to reading the file from disk. If you need error handling
# or more control (such as changing the scale SVG is loaded at),
# use the `load_*_from_buffer()` (where `*` is a file extension)
# and `load_svg_from_string()` methods from the Image class.
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)
# Audio.
# Run-time MP3 and WAV loading aren't supported by the engine yet.
elif path_lower.ends_with(".ogg"):
# `AudioStreamOggVorbis.load_from_buffer()` can alternatively be used
# if you have Ogg Vorbis data in a PackedByteArray instead of a file.
audio_stream_player.stream = AudioStreamOggVorbis.load_from_file(path)
reset_visibility()
export_button.disabled = true
audio_player.visible = true
# 3D scenes.
elif path_lower.ends_with(".gltf") or path_lower.ends_with(".glb"):
# GLTFState is used by GLTFDocument to store the loaded scene's state.
# GLTFDocument is the class that handles actually loading glTF data into a Godot node tree,
# which means it supports glTF features such as lights and cameras.
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)])
# Fonts.
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")
):
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
else:
show_error('Couldn\'t load "%s" as a font.' % path.get_file())
# ZIP archives.
elif path_lower.ends_with(".zip"):
# This supports any ZIP file, including files generated by Godot's "Export PCK/ZIP" functionality
# (although these will contain imported Godot resources rather than the original project files).
#
# Use `ProjectSettings.load_resource_pack()` to load PCK or ZIP files exported by Godot as
# additional data packs. That approach is preferred for DLCs, as it makes interacting with
# additional data packs seamless (virtual filesystem).
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)
# Make folders disabled in the list.
zip_viewer_file_list.set_item_disabled(-1, file.ends_with("/"))
zip_viewer.visible = true
# Fallback.
else:
# Open as plain text and display contents if possible.
var file_contents := FileAccess.get_file_as_string(path)
if file_contents.is_empty():
show_error("File is empty or is a binary file.")
else:
plain_text_viewer_label.text = file_contents
reset_visibility()
plain_text_viewer.visible = true

View File

@@ -0,0 +1,195 @@
[gd_scene load_steps=2 format=3 uid="uid://ca0d8q5aicxfr"]
[ext_resource type="Script" path="res://runtime_save_load.gd" id="1_2gu2h"]
[node name="RuntimeLoadSave" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("1_2gu2h")
[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_left = 20
theme_override_constants/margin_top = 20
theme_override_constants/margin_right = 20
theme_override_constants/margin_bottom = 20
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
layout_mode = 2
theme_override_constants/separation = 10
[node name="Help" type="Label" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
theme_override_colors/font_color = Color(1, 1, 1, 0.752941)
text = "This project showcases how to load and save various file types without going through Godot's resource importing system.
This is useful to load/save images, sounds, 3D scenes and ZIP archives at run-time such as user-generated content,
without requiring users to generate a PCK file through Godot."
autowrap_mode = 2
[node name="Instructions" type="Label" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
text = "Select a file to load (look in the \"examples\" folder):"
autowrap_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="FilePath" type="LineEdit" parent="MarginContainer/VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Click \"Browse\" on the right or enter path to file"
[node name="Browse" type="Button" parent="MarginContainer/VBoxContainer/HBoxContainer"]
custom_minimum_size = Vector2(100, 0)
layout_mode = 2
text = "Browse"
[node name="FileDialog" type="FileDialog" parent="MarginContainer/VBoxContainer/HBoxContainer"]
title = "Open a File"
size = Vector2i(392, 159)
ok_button_text = "Open"
file_mode = 0
access = 2
[node name="Result" type="CenterContainer" parent="MarginContainer/VBoxContainer"]
custom_minimum_size = Vector2(0, 400)
layout_mode = 2
[node name="PlainTextViewer" type="ScrollContainer" parent="MarginContainer/VBoxContainer/Result"]
visible = false
custom_minimum_size = Vector2(1050, 400)
layout_mode = 2
horizontal_scroll_mode = 0
[node name="Label" type="Label" parent="MarginContainer/VBoxContainer/Result/PlainTextViewer"]
custom_minimum_size = Vector2(1050, 400)
layout_mode = 2
theme_override_colors/font_color = Color(1, 0.941176, 0.627451, 1)
autowrap_mode = 2
[node name="TextureViewer" type="TextureRect" parent="MarginContainer/VBoxContainer/Result"]
visible = false
custom_minimum_size = Vector2(0, 400)
layout_mode = 2
expand_mode = 3
[node name="AudioPlayer" type="Button" parent="MarginContainer/VBoxContainer/Result"]
visible = false
layout_mode = 2
theme_override_font_sizes/font_size = 24
text = "Play Audio"
[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="MarginContainer/VBoxContainer/Result/AudioPlayer"]
volume_db = -10.0
[node name="SceneViewer" type="SubViewportContainer" parent="MarginContainer/VBoxContainer/Result"]
visible = false
custom_minimum_size = Vector2(1050, 400)
layout_mode = 2
stretch = true
[node name="SubViewport" type="SubViewport" parent="MarginContainer/VBoxContainer/Result/SceneViewer"]
handle_input_locally = false
msaa_3d = 2
size = Vector2i(1050, 400)
render_target_update_mode = 0
[node name="Camera3D" type="Camera3D" parent="MarginContainer/VBoxContainer/Result/SceneViewer/SubViewport"]
transform = Transform3D(0.877582, -0.229849, 0.420736, 0, 0.877582, 0.479426, -0.479426, -0.420736, 0.770151, 26.1772, 30.2846, 47.917)
projection = 1
size = 1.2
near = 0.001
far = 100.0
[node name="KeyLight" type="DirectionalLight3D" parent="MarginContainer/VBoxContainer/Result/SceneViewer/SubViewport"]
transform = Transform3D(0.775472, 0.453626, -0.439166, 0, 0.695563, 0.718465, 0.631382, -0.557149, 0.53939, -2.78761, 4.56046, 3.42378)
shadow_enabled = true
directional_shadow_mode = 0
directional_shadow_fade_start = 1.0
directional_shadow_max_distance = 20.0
[node name="FillLight" type="DirectionalLight3D" parent="MarginContainer/VBoxContainer/Result/SceneViewer/SubViewport"]
transform = Transform3D(-0.775472, -0.453626, 0.439166, 4.2942e-09, 0.695563, 0.718465, -0.631382, 0.557149, -0.53939, -2.78761, 2.56046, 3.42378)
light_energy = 0.3
shadow_bias = 0.04
directional_shadow_mode = 0
directional_shadow_max_distance = 30.0
[node name="BackLight" type="DirectionalLight3D" parent="MarginContainer/VBoxContainer/Result/SceneViewer/SubViewport"]
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, -1, 0, 1, -4.37114e-08, -2.78761, 0.56046, 3.42378)
light_energy = 0.1
shadow_bias = 0.04
directional_shadow_mode = 0
directional_shadow_max_distance = 30.0
[node name="Zoom" type="HSlider" parent="MarginContainer/VBoxContainer/Result/SceneViewer"]
custom_minimum_size = Vector2(1050, 0)
layout_mode = 2
min_value = -100.0
max_value = -0.1
step = 0.0
value = -1.2
[node name="FontViewer" type="Label" parent="MarginContainer/VBoxContainer/Result"]
visible = false
custom_minimum_size = Vector2(1050, 400)
layout_mode = 2
theme_override_font_sizes/font_size = 48
text = "abcdefghijklmnopqrstuvwxyz
ABCDEFGHIJKLM
NOPQRSTUVWXYZ
1234567890
()[]{}<> -+:!?$&#@ éàç ×÷±≠ø ↔"
horizontal_alignment = 1
vertical_alignment = 1
[node name="ZIPViewer" type="HSplitContainer" parent="MarginContainer/VBoxContainer/Result"]
visible = false
custom_minimum_size = Vector2(1050, 400)
layout_mode = 2
split_offset = 525
[node name="FileList" type="ItemList" parent="MarginContainer/VBoxContainer/Result/ZIPViewer"]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
[node name="FilePreview" type="Label" parent="MarginContainer/VBoxContainer/Result/ZIPViewer"]
custom_minimum_size = Vector2(150, 0)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 1
theme_override_colors/font_color = Color(0.631373, 0.862745, 1, 1)
autowrap_mode = 2
text_overrun_behavior = 3
[node name="ErrorLabel" type="Label" parent="MarginContainer/VBoxContainer/Result"]
visible = false
layout_mode = 2
theme_override_colors/font_color = Color(1, 0.501961, 0.501961, 1)
theme_override_font_sizes/font_size = 24
[node name="Export" type="Button" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
text = "Export"
[node name="FileDialog" type="FileDialog" parent="MarginContainer/VBoxContainer/Export"]
title = "Export File"
access = 2
[connection signal="text_submitted" from="MarginContainer/VBoxContainer/HBoxContainer/FilePath" to="." method="_on_file_path_text_submitted"]
[connection signal="pressed" from="MarginContainer/VBoxContainer/HBoxContainer/Browse" to="." method="_on_browse_pressed"]
[connection signal="file_selected" from="MarginContainer/VBoxContainer/HBoxContainer/FileDialog" to="." method="_on_file_dialog_file_selected"]
[connection signal="pressed" from="MarginContainer/VBoxContainer/Result/AudioPlayer" to="." method="_on_audio_player_pressed"]
[connection signal="value_changed" from="MarginContainer/VBoxContainer/Result/SceneViewer/Zoom" to="." method="_on_scene_viewer_zoom_value_changed"]
[connection signal="item_selected" from="MarginContainer/VBoxContainer/Result/ZIPViewer/FileList" to="." method="_on_zip_viewer_item_selected"]
[connection signal="pressed" from="MarginContainer/VBoxContainer/Export" to="." method="_on_export_pressed"]
[connection signal="file_selected" from="MarginContainer/VBoxContainer/Export/FileDialog" to="." method="_on_export_file_dialog_file_selected"]

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB