📒
[Godot] Resource を JSON として保存するやり方メモ。
きっかけ
Resource をセーブデータにしようかと思いましたが、汎用性・互換性を考えて JSON に変換して保存しようと考えました。ただ、そのままリソースを JSON.stringify
しても復元できる状態で保存できなかったので自作しました。(※運用実績は無いです)
作る際につまずいた点
-
JSON.stringify
に Resource を渡しても展開されず"<Resource#-9223371945368615100>"
のような文字列になる。- =>
get_property_list()
関数でプロパティ一覧を取得して自前で Dictionary 型にいったん変換しました。
- =>
-
get_property_list()
関数だと自分が定義したプロパティ以外も取得されてしまう。- => script に関する
get_property_list()
を間引きました。
- => script に関する
- ネストしたリソースが存在する場合に再帰的にパースする必要があった。
- => 各リソースに
parse_mapping()
関数を定義し、特定のプロパティが子リソースである場合の判断を追加。
- => 各リソースに
できたもの
src/helpers/resource_helper.gd
extends Node
class_name ResourceHelper
static func parse(klass, json_or_props) -> Resource:
var resource = klass.new()
var props = JSON.parse_string(json_or_props) \
if typeof(json_or_props) == TYPE_STRING \
else json_or_props
var mapper = klass.parse_mapping() \
if klass.has_method('parse_mapping') else {}
for key in props:
if mapper.has(key):
for hash in props[key]:
var child = parse(mapper[key], hash)
resource.get(key).push_back(child)
else: resource.set(key, props[key])
return resource
static func json(resource: Resource) -> String:
return JSON.stringify(dict(resource))
static func dict(value: Variant) -> Variant:
match typeof(value):
TYPE_OBJECT:
return properties_of(value).reduce(func(h, prop):
h[prop] = dict(value.get(prop))
return h, {})
TYPE_ARRAY:
return value.map(func(v): return dict(v))
_: return value
static func properties_of(resource: Resource) -> Array:
var script = resource.get_script()
var s_arr = script.get_property_list().map(func(p): return p.name)
var p_arr = resource.get_property_list().map(func(p): return p.name)
return p_arr.filter(func(x): return not s_arr.has(x)).filter(func(x):
return x != 'script' and x != 'Built-in script' \
and (not x.ends_with('.gd')))
使い方
extends Resource
class_name FooResource
@export var name : String = 'test'
@export var bars : Array[BarResource]
static func parse_mapping() -> Dictionary:
return { 'bars': BarResource }
var foo_resource = FooResource.new()
# Resource => JSON
var json = ResourceHelper.json(foo_resource)
# JSON => Resource
var parsed = ResourceHelper.parse(FooResource, json)
テストコード
test/helpers/test_resource_helper.gd
extends GutTest
class TestResource:
extends Resource
var flag : bool = true
var name : String = 'test'
var items : Array = [1.0, '2', null]
var children : Array[ChildResource] = [
ChildResource.new()
]
static func parse_mapping() -> Dictionary:
return { 'children': ChildResource }
class ChildResource:
extends Resource
var foo : float = 1.234
var bar = null
var children : Array[GrandChildResource] = [
GrandChildResource.new()
]
static func parse_mapping() -> Dictionary:
return { 'children': GrandChildResource }
class GrandChildResource:
extends Resource
var piyo : String = 'bar'
func test_init():
var res = TestResource.new()
assert_eq(res.flag, true)
assert_eq(res.children[0].foo, 1.234)
func test_properties_of():
var res = TestResource.new()
assert_eq(ResourceHelper.properties_of(res),
['flag', 'name', 'items', 'children'])
assert_eq(ResourceHelper.properties_of(res.children[0]),
['foo', 'bar', 'children'])
assert_eq(ResourceHelper.properties_of(res.children[0].children[0]),
['piyo'])
func test_json():
var res = TestResource.new()
var dict = ResourceHelper.dict(res)
var json = ResourceHelper.json(res)
var parsed = JSON.parse_string(json)
assert_eq(res.name, dict.name)
assert_eq(res.name, parsed.name)
assert_eq(res.items[0], dict.items[0])
assert_eq(res.items[0], parsed.items[0])
assert_eq(res.children[0].foo, dict.children[0].foo)
assert_eq(res.children[0].foo, parsed.children[0].foo)
assert_eq(
res.children[0].children[0].piyo,
dict.children[0].children[0].piyo)
assert_eq(
res.children[0].children[0].piyo,
parsed.children[0].children[0].piyo)
assert_eq(dict.items, parsed.items)
assert_eq(dict.children, parsed.children)
assert_eq(dict.children[0].children, parsed.children[0].children)
assert_eq(dict, parsed)
func test_parse():
var res = TestResource.new()
var json = ResourceHelper.json(res)
var parsed = ResourceHelper.parse(TestResource, json)
for key in ResourceHelper.properties_of(res):
if key == 'children':
var child = res.children[0]
assert_eq(child.foo, parsed.children[0].foo)
var grand_child = child.children[0]
assert_eq(grand_child.piyo, parsed.children[0].children[0].piyo)
else:
assert_eq(res.get(key), parsed.get(key))
余談
今回初めて知りましたが、JSON を JSON.parse_string
で復元すると数値は必ずfloat型になる仕様らしいです。
なので、リソースでint型の変数 1
を作成してもJSONから戻す際に 1.0
になりますが、リソースの変数に代入したらキャストしてくれるので問題はなかったです。
Discussion