用Godot 4尝试编一个电子书软件,初步效果已经出来,并且通过管道通信接口可以获取、设置属性、调用函数,貌似能处理各种事宜了。
其实不然,外因通过内因起作用,如果没把里面搞明白,功能没有开放出来,则有些需求就不能实现。
比如,现在想动态加载新的图书,这就是一个实际需求。如果每一本电子书,都需要导出一次,那这个软件就太不通用了。
之前加载图片时,GDScript代码:
# 目标对象上加载图片
func loadPng(obj, pngFileName) -> bool:
var texture = load(pngFileName) as Texture2D;
if(texture != null):
obj.set_texture(texture);
obj.scale.y = get_viewport_rect().size.y / texture.get_height();
obj.scale.x = obj.scale.y;
obj.position.y = get_viewport_rect().size.y / 2; # 垂直居中
adjustPos();
return true;
return false;
直接加载另一图片,结果不成功
这就得研究一下了。
把这个图片拷贝到Godot开发环境中,发现切换回Godot时,会快速闪过一个import对话框,那想必Godot干了什么事
到资源管理器,看到多了一个对应的.import文件,打开看了一下内容
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://rv5gm15xbcaf"
path="res://.godot/imported/DrGraph_Page24.png-1dd935fbb11807a645ea1ea79ec38662.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://Pages/DrGraph_Page24.png"
dest_files=["res://.godot/imported/DrGraph_Page24.png-1dd935fbb11807a645ea1ea79ec38662.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
既然是动态生成的,那肯定是代码里写的。到源码中找,从哪里下手呢?查import、.import都是一大堆结果。再试试内容,查找[remap],结果很少
定位一看,是EditorFileSystem类的两个函数_reimport_group、_reimport_file,那肯定是用_reimport_file。但这个函数是不可访问的。好在源码在手,直接public出来,可以访问了。
但这个要给GDScript调用,还得做一些工作,需要加入到ClassDB中去。
为了简单一些,直接加到DllStream类中【见Godot 4 源码分析 - 增加管道通信_DrGraph的博客-CSDN博客】,增加一个import函数
ClassDB::bind_method(D_METHOD("import", "fileName"), &DllStream::import);
String DllStream::import(String fileName) {
if (FileAccess::exists(fileName + ".import") == false)
EditorFileSystem::get_singleton()->_reimport_file(fileName);
return fileName;
}
运行后,还是不成功。调试发现,_find_file(p_file, &fs, cpos)返回false。而_find_file居然要求文件位于"res://"目录以下,也就是说,得在工程目录下。
bool EditorFileSystem::_find_file(const String &p_file, EditorFileSystemDirectory **r_d, int &r_file_pos) const {
//todo make faster
if (!filesystem || scanning) {
return false;
}
String f = ProjectSettings::get_singleton()->localize_path(p_file);
if (!f.begins_with("res://")) {
return false;
}
f = f.substr(6, f.length());
f = f.replace("\\", "/");
Vector path = f.split("/");
if (path.size() == 0) {
return false;
}
String file = path[path.size() - 1];
path.resize(path.size() - 1);
EditorFileSystemDirectory *fs = filesystem;
for (int i = 0; i < path.size(); i++) {
if (path[i].begins_with(".")) {
return false;
}
int idx = -1;
for (int j = 0; j < fs->get_subdir_count(); j++) {
if (fs->get_subdir(j)->get_name() == path[i]) {
idx = j;
break;
}
}
if (idx == -1) {
//does not exist, create i guess?
EditorFileSystemDirectory *efsd = memnew(EditorFileSystemDirectory);
efsd->name = path[i];
efsd->parent = fs;
int idx2 = 0;
for (int j = 0; j < fs->get_subdir_count(); j++) {
if (efsd->name.naturalnocasecmp_to(fs->get_subdir(j)->get_name()) < 0) {
break;
}
idx2++;
}
if (idx2 == fs->get_subdir_count()) {
fs->subdirs.push_back(efsd);
} else {
fs->subdirs.insert(idx2, efsd);
}
fs = efsd;
} else {
fs = fs->get_subdir(idx);
}
}
int cpos = -1;
for (int i = 0; i < fs->files.size(); i++) {
if (fs->files[i]->file == file) {
cpos = i;
break;
}
}
r_file_pos = cpos;
*r_d = fs;
return cpos != -1;
}
这个要求就有些过分了。不过程序处理就两种方法:一是将图片自动拷贝到工程目录下,二是绕过这个。
随便试一种吧,抛个硬币,背面选二。
既然选绕过,那就直接实现EditorFileSystem::_reimport_file函数,这样也省得再把这个私有函数public出来。所谓实现,就是把EditorFileSystem::_reimport_file的代码全部拷贝过来,然后改呗改呗:
Error DllStream::_import(String destFileName) {
HashMap params = HashMap();
String importer_name; //empty by default though
ResourceUID::ID uid = ResourceUID::INVALID_ID;
Variant generator_parameters;
Ref importer = ResourceFormatImporter::get_singleton()->get_importer_by_extension(destFileName.get_extension());
if (importer.is_null())
ERR_FAIL_V_MSG(ERR_FILE_CANT_OPEN, "BUG: File queued for import, but can't be imported, importer for type '" + importer_name + "' not found.");
//mix with default params, in case a parameter is missing
List opts;
importer->get_import_options(destFileName, &opts);
for (const ResourceImporter::ImportOption &E : opts) {
if (!params.has(E.option.name)) { //this one is not present
params[E.option.name] = E.default_value;
}
}
if (ProjectSettings::get_singleton()->has_setting("importer_defaults/" + importer->get_importer_name())) {
//use defaults if exist
Dictionary d = GLOBAL_GET("importer_defaults/" + importer->get_importer_name());
List v;
d.get_key_list(&v);
for (const Variant &E : v)
params[E] = d[E];
}
//finally, perform import!!
String base_path = ResourceFormatImporter::get_singleton()->get_import_base_path(destFileName);
List import_variants;
List gen_files;
Variant meta;
Error err = importer->import(destFileName, base_path, params, &import_variants, &gen_files, &meta);
ERR_FAIL_COND_V_MSG(err != OK, ERR_FILE_UNRECOGNIZED, "Error importing '" + destFileName + "'.");
//as import is complete, save the .import file
Vector dest_paths;
{
Ref f = FileAccess::open(destFileName + ".import", FileAccess::WRITE);
ERR_FAIL_COND_V_MSG(f.is_null(), ERR_FILE_CANT_OPEN, "Cannot open file from path '" + destFileName + ".import'.");
//write manually, as order matters ([remap] has to go first for performance).
f->store_line("[remap]");
f->store_line("");
f->store_line("importer=\"" + importer->get_importer_name() + "\"");
int version = importer->get_format_version();
if (version > 0) {
f->store_line("importer_version=" + itos(version));
}
if (!importer->get_resource_type().is_empty()) {
f->store_line("type=\"" + importer->get_resource_type() + "\"");
}
if (uid == ResourceUID::INVALID_ID) {
uid = ResourceUID::get_singleton()->create_id();
}
f->store_line("uid=\"" + ResourceUID::get_singleton()->id_to_text(uid) + "\""); //store in readable format
if (err == OK) {
if (importer->get_save_extension().is_empty()) {
//no path
} else if (import_variants.size()) {
//import with variants
for (const String &E : import_variants) {
String path = base_path.c_escape() + "." + E + "." + importer->get_save_extension();
f->store_line("path." + E + "=\"" + path + "\"");
dest_paths.push_back(path);
}
} else {
String path = base_path + "." + importer->get_save_extension();
f->store_line("path=\"" + path + "\"");
dest_paths.push_back(path);
}
} else {
f->store_line("valid=false");
}
if (meta != Variant()) {
f->store_line("metadata=" + meta.get_construct_string());
}
if (generator_parameters != Variant()) {
f->store_line("generator_parameters=" + generator_parameters.get_construct_string());
}
f->store_line("");
f->store_line("[deps]\n");
if (gen_files.size()) {
Array genf;
for (const String &E : gen_files) {
genf.push_back(E);
dest_paths.push_back(E);
}
String value;
VariantWriter::write_to_string(genf, value);
f->store_line("files=" + value);
f->store_line("");
}
f->store_line("source_file=" + Variant(destFileName).get_construct_string());
if (dest_paths.size()) {
Array dp;
for (int i = 0; i < dest_paths.size(); i++) {
dp.push_back(dest_paths[i]);
}
f->store_line("dest_files=" + Variant(dp).get_construct_string() + "\n");
}
f->store_line("[params]");
f->store_line("");
//store options in provided order, to avoid file changing. Order is also important because first match is accepted first.
for (const ResourceImporter::ImportOption &E : opts) {
String base = E.option.name;
String value;
VariantWriter::write_to_string(params[base], value);
f->store_line(base + "=" + value);
}
}
// Store the md5's of the various files. These are stored separately so that the .import files can be version controlled.
{
Ref md5s = FileAccess::open(base_path + ".md5", FileAccess::WRITE);
ERR_FAIL_COND_V_MSG(md5s.is_null(), ERR_FILE_CANT_OPEN, "Cannot open MD5 file '" + base_path + ".md5'.");
md5s->store_line("source_md5=\"" + FileAccess::get_md5(destFileName) + "\"");
if (dest_paths.size()) {
md5s->store_line("dest_md5=\"" + FileAccess::get_multiple_md5(dest_paths) + "\"\n");
}
}
if (ResourceUID::get_singleton()->has_id(uid)) {
ResourceUID::get_singleton()->set_id(uid, destFileName);
} else {
ResourceUID::get_singleton()->add_id(uid, destFileName);
}
}
String DllStream::import(String fileName) {
if (FileAccess::exists(fileName + ".import") == false)
_import(fileName);
return fileName;
}
运行后,还是失败,再跟进,发现是
importer->import(destFileName, base_path, params, &import_variants, &gen_files, &meta)
失败。单步调试发现,ResourceFormatImporter的importers为空。从源码中找到其add_importer函数会添加ResourceImporter,在EditorNode::EditorNode()构造函数中,添加了多个ResourceImporter。
{
// Register importers at the beginning, so dialogs are created with the right extensions.
Ref import_texture;
import_texture.instantiate();
ResourceFormatImporter::get_singleton()->add_importer(import_texture);
Ref import_cubemap;
import_cubemap.instantiate();
import_cubemap->set_mode(ResourceImporterLayeredTexture::MODE_CUBEMAP);
ResourceFormatImporter::get_singleton()->add_importer(import_cubemap);
Ref import_array;
import_array.instantiate();
import_array->set_mode(ResourceImporterLayeredTexture::MODE_2D_ARRAY);
ResourceFormatImporter::get_singleton()->add_importer(import_array);
Ref import_cubemap_array;
import_cubemap_array.instantiate();
import_cubemap_array->set_mode(ResourceImporterLayeredTexture::MODE_CUBEMAP_ARRAY);
ResourceFormatImporter::get_singleton()->add_importer(import_cubemap_array);
Ref import_3d;
import_3d.instantiate();
import_3d->set_mode(ResourceImporterLayeredTexture::MODE_3D);
ResourceFormatImporter::get_singleton()->add_importer(import_3d);
Ref import_image;
import_image.instantiate();
ResourceFormatImporter::get_singleton()->add_importer(import_image);
Ref import_texture_atlas;
import_texture_atlas.instantiate();
ResourceFormatImporter::get_singleton()->add_importer(import_texture_atlas);
Ref import_font_data_dynamic;
import_font_data_dynamic.instantiate();
ResourceFormatImporter::get_singleton()->add_importer(import_font_data_dynamic);
Ref import_font_data_bmfont;
import_font_data_bmfont.instantiate();
ResourceFormatImporter::get_singleton()->add_importer(import_font_data_bmfont);
Ref import_font_data_image;
import_font_data_image.instantiate();
ResourceFormatImporter::get_singleton()->add_importer(import_font_data_image);
Ref import_csv_translation;
import_csv_translation.instantiate();
ResourceFormatImporter::get_singleton()->add_importer(import_csv_translation);
Ref import_wav;
import_wav.instantiate();
ResourceFormatImporter::get_singleton()->add_importer(import_wav);
Ref import_obj;
import_obj.instantiate();
ResourceFormatImporter::get_singleton()->add_importer(import_obj);
Ref import_shader_file;
import_shader_file.instantiate();
ResourceFormatImporter::get_singleton()->add_importer(import_shader_file);
Ref import_scene;
import_scene.instantiate();
ResourceFormatImporter::get_singleton()->add_importer(import_scene);
Ref import_animation;
import_animation = Ref(memnew(ResourceImporterScene(true)));
ResourceFormatImporter::get_singleton()->add_importer(import_animation);
{
Ref import_collada;
import_collada.instantiate();
ResourceImporterScene::add_importer(import_collada);
Ref import_obj2;
import_obj2.instantiate();
ResourceImporterScene::add_importer(import_obj2);
Ref import_escn;
import_escn.instantiate();
ResourceImporterScene::add_importer(import_escn);
}
Ref import_bitmap;
import_bitmap.instantiate();
ResourceFormatImporter::get_singleton()->add_importer(import_bitmap);
}
EditorNode会在编辑器模式下自动创建,但对于最终的运行程序而言,没有ProjectManager,也没有EditorNode,所以,想导入的话,需要自己创建。
从图片导入过程可知,电子书只需要导入png图片,是Texture格式,所以只导入一种即可
Ref import_texture;
import_texture.instantiate();
ResourceFormatImporter::get_singleton()->add_importer(import_texture);
运行,图片成功显示。
至此,核心工作完成,下一步的工作主要有两个
一是目录导入,即可以将目标电子书的图片所在目录中的所有文件一次性导入
二是强制动态导入,非强制导入是指只要存在相应的.import文件,就不用再导入;强制导入是无论该.import文件是否存在,均导入。这个用于动态处理,比如文件中查找关键词后,关键词需要高亮显示,从而导致图片不一样。这些图片可置于动态目录下,强制导入即可。