【Godot】使用正则表达式进行解析自定义文件内容

Godot 3.4.2

要解析的文件内容例如:

======================== <Resource>
[Player_CircleShape] CircleShape2D { radius : 16 }
[Enemy_CircleShape] CircleShape2D { radius : 16 }
[DetectObjectShape] RectangleShape2D { extents : (32, 32) }
=================================================================



======================== RoleBase.gd
class_name RoleBase
extends KinematicBody2D


export var property : Dictionary = {
  "health": 1,
  "move_speed": 60,
}


=================================================================


======================== Player.gd
class_name Player
extends RoleBase


func _ready():
    pass


=================================================================


======================== Enemy.gd
class_name Enemy
extends RoleBase


func _ready():
    pass


=================================================================


======================== PlayerBlackboard.gd
class_name PlayerBlackboard
extends BaseStateBlackboard

=================================================================

======================== PlayerState.gd
class_name PlayerBaseState
extends BaseState


func get_host() -> Player:
  return .get_host() as Player

func get_blackboard() -> PlayerBlackboard:
  return .get_blackboard() as PlayerBlackboard


=================================================================


======================== EnemyBlackboard.gd
class_name EnemyBlackboard
extends BaseStateBlackboard



=================================================================

======================== EnemyState.gd
class_name EnemyBaseState
extends BaseState


func get_host() -> Enemy:
  return .get_host() as Enemy

func get_blackboard() -> EnemyBlackboard:
  return .get_blackboard() as EnemyBlackboard


=================================================================




======================== Player.tscn 
Player<KinematicBody2D>{script: @Player.gd}
  CollisionShape2D<CollisionShape2D>{shape:Player_CircleShape}
  TypeLayer<Node2D>
    Canvas<Node2D>
      Sprite<Sprite>
        AnimationPlayer<AnimationPlayer>
    Collision<Node2D>
      DetectEnemy<Area2D>
        CollisionShape2D<CollisionShape2D>{shape:DetectObjectShape}
    Data<Node>
    StateMachine<Node:PlayerStateMachine>
      Blackboard<Node>{script:@PlayerBlackboard.gd}
      Base<Node:PlayerState>{script:@PlayerState.gd}
      Idle<Node:PlayerState>
      Run<Node:PlayerState>
=================================================================


======================== Enemy.tscn 
Enemy<KinematicBody2D>{script: @Enemy.gd}
  CollisionShape2D<CollisionShape2D>{shape:Enemy_CircleShape}
  TypeLayer<Node2D>
    Canvas<Node2D>
      Sprite<Sprite>
        AnimationPlayer<AnimationPlayer>
    Collision<Node2D>
      DetectPlayer<Area2D>
        CollisionShape2D<CollisionShape2D>{shape:DetectObjectShape}
    Data<Node>
    StateMachine<Node:EnemyStateMachine>
      Blackboard<Node>{script:@EnemyBlackboard.gd}
      Base<Node:EnemyState>{script:@EnemyState.gd}
      Idle<Node:EnemyState>
      Run<Node:EnemyState>
=================================================================


ParseGodotStructFile.gd

解析代码的脚本

#============================================================
#  Parse Godot Struct File
#============================================================
#  解析文件,创建 Godot 场景
#============================================================
#  调用下面方法创建生成场景和文件
# * 调用 parse_file 解析对应的文件
# * 调用 parse_code 解析代码
#============================================================
# @datetime: 2022-4-19 04:19:28
#============================================================

tool
extends EditorScript


enum DataType {
    None,
    Resources,
    Scene,
    Code,
    Else,
}

var directory := Directory.new()
var create_to_path : String = "res://" setget set_create_to_path

func set_create_to_path(value: String):
    if value.replace(" ", "") == "":
        value = "res://"
    elif not directory.dir_exists(value):
        printerr("不存在 ", value, " 这个目录!")
        value = "res://"
    create_to_path = value
    return self


func _run():
    pass

    # 设置文件保存位置
    self.set_create_to_path("res://test/test_create/")
    # 测试解析文件并创建场景及文件
    self.parse_file("C:\\Users\\z\\Desktop\\GameStruct\\Role.gdstruct")

#  var data = parse_resource("""
#[ID_CircleShape] CircleShape2D { radius : 10 }
#[DetectObjectShape] RectangleShape2D { extents : (20, 20) }
#  """)
#  print(resource_data)

#  Logger.print_data(parse_property("name:hello, age:20"))

#  print(JSON.print(list, "\t"))

#  parse_and_save_scene(create_to_path.plus_file("Player.tscn"), code)



#============================================================
#   解析执行
#============================================================
##  解析文件内容
## @path  文件路径
func parse_file(path: String):
    var file := File.new()
    file.open(path, File.READ)
    var text = file.get_as_text()
    file.close()
    # 解析代码
    parse_code(text)


##  解析代码内容
## @code  代码
func parse_code(code: String):
    # 解析块内容
    var block_list = parse_block(code)
    var list = parse_block_code(block_list)
#  GodotUtil.Logger.print_data(list)
#  return

    # 先保存脚本代码文件
    for data in list:
        var filename : String = data['filename']
        if data['type'] == DataType.Code:
            var script = save_code(create_to_path.plus_file(filename), data['data'])
            resource_data["@" + data['filename']] = script

    # 解析创建资源
    for data in list:
        if data['type'] == DataType.Resources:
            parse_resource(data['data'])

    # 解析并保存场景
    for data in list:
        var filename : String = data['filename']
        if data['type'] == DataType.Scene:
            parse_and_save_scene(data['data'], create_to_path.plus_file(filename))


##  解析代码块
## @code  
## @return  
func parse_block(code: String) -> Array:
    var block_regex = RegEx.new()
    block_regex.compile("(={3,})\\s*(?<BlockType>\\S+)\\s*")

    var lines = code.split('\n')

    var count : int = 0
    var blocks : Array = []
    var block_type: String = ""
    var text : String = ""

    for line in lines:
        line = str(line)
        if line.begins_with("==="):
            count += 1
            # 新行
            if count % 2 == 1:
                var result = block_regex.search(line)
                if result:
                    block_type = result.get_string("BlockType")
                text = ""
                continue
            else:
                if text != "":
                    blocks.append({
                        "blocktype": block_type,
                        "context": text,
                    })
        text += line + "\n"
    return blocks


##  解析代码块中的代码 
## @block_code_list  
## @return  
var block_type_regex := RegEx.new()
func parse_block_code(block_code_list: Array) -> Array:
    var pattern = "\\<\\s*(?<Type>\\S+)\\s*\\>"
    block_type_regex.compile(pattern)

    # 解析代码
    var list := []
    for code in block_code_list:
        var blocktype : String = code['blocktype']
        var context : String = code['context']

        # 文件名
        var filename : String = blocktype
        var extension : String = filename.get_extension()
        var data = {}
        data["filename"] = filename
        if extension in ["tscn", "scn"]:
            # 场景类型
            data['type'] = DataType.Scene
            data['data'] = context
        elif extension == "gd":
            # 代码类型
            data['type'] = DataType.Code
            data['data'] = context
        else:
            # 资源类型
            var result = block_type_regex.search(blocktype)
            if result:
                var type : String = result.get_string("Type")
                if type.replace(" ", "") == "Resource":
                    data['type'] = DataType.Resources
                    data['data'] = context

        # 没有设置,则默认为 none
        if not data.has('type'):
            data["type"] = DataType.None
            data['data'] = context
        list.append(data)
    return list



#============================================================
#   场景
#============================================================
##  解析场景 
## @context  
## @return  
var scene_regex := RegEx.new()
func parse_scene(context: String) -> Array:
    var pattern = ""
    # 匹配注释
    pattern += "\\s*-{2,}\\s*(?<Description>[^\\n]+)"
    pattern += "\\n|"

    # 匹配缩进
    pattern += "\\n?(?<Indent>\\s*)"    # 贪婪匹配空白字符,直到没有匹配到空白字符为止
    # 匹配节点
    pattern += "(?<NodeName>[^\\<]+?)\\s*"  # 匹配到非 < 之前为止
    # 匹配节点类型和继承类
    pattern += "\\<\\s*(((?<NodeType>\\S+?)(\\s*:\\s*(?<Extends>\\S+))?))\\s*\\>"
    # 匹配 {} 内容
    pattern += "(\\{(?<Content>.*)\\})?"    # 匹配 {} 内容
    pattern += "[^\\n]*"    # 匹配到非 \n 符号为止
    pattern += "\\n?"
    scene_regex.compile(pattern)

    # 开始匹配
    var results : Array = scene_regex.search_all(context)
    var list : Array = []
    for result in results:
        result = result as RegExMatch
        list.append({
            "node_name": result.get_string("NodeName"),
            "node_type": result.get_string("NodeType"),
            "extends": result.get_string("Extends"),    # 继承的脚本或类型,没有则自动创建
            "description": result.get_string("Description"),
            "indent": result.get_string("Indent").length(),
            "property_context": result.get_string("Content")
        })
    return list


##  解析并保存场景
## @context  上下文代码
## @filename  保存文件名
func parse_and_save_scene(context: String, filename: String):
    # 解析
    var list : Array = parse_scene(context)
    # 记录缩进数据
    var indent_data : Dictionary = {}
    # 场景中的节点
    var root : Node = null
    for data in list:
        var node_type : String = data["node_type"]
        var node : Node = InstanceObject.instance(node_type)
        # 没有这个类,则创建一个
        if node == null:
            node = Node.new()
            var script = GDScript.new()
            var extend : String = data["extends"]
            if extend:
                node_type = extend
            script.source_code = "class_name %s\nextends Node\n " % node_type
            node.set_script(script)

        # 创建对应场景结构
        var node_name : String = data["node_name"]
        var indent : int = data["indent"]
        node.name = node_name
        if root == null:
            root = node
            indent_data[-1] = node
        else:
            var parent_node : Node = parent_node(indent_data, indent)
            parent_node.add_child(node, true)
            node.owner = root
            # 清除上一个相同缩进的节点,记录这个缩进
            # 这样这个数据就在最后一个位置了
            indent_data.erase(indent)
            indent_data[indent] = node

        # 设置属性
        var property_context : String = data['property_context']
        set_object_property(node, parse_property(property_context))

    # 保存场景包
    var pack = PackedScene.new()
    pack.pack(root)
    ResourceSaver.save(filename, pack)


##  上级缩进的节点
func parent_node(data: Dictionary, curr_indent: int) -> Node:
    var list = data.keys()
    list.invert()
    for key in list:
        if key < curr_indent:
            return data[key] as Node
    return null



#============================================================
#   代码
#============================================================
##  保存代码
func save_code(path: String, code: String) -> Script:
    assert(path.get_extension() == "gd", "文件名必须以 .gd 结尾!")
    var script = GDScript.new()
    script.source_code = code
    ResourceSaver.save(path, script)
    return script



#============================================================
#   资源
#============================================================
##  解析资源 
## @context  
## @return  
var resource_regex : RegEx
var resource_data : Dictionary = {}
func parse_resource(context: String):
    if resource_regex == null:
        resource_regex = RegEx.new()
        var pattern = ""
        # 匹配资源ID名称
        pattern += "\\[\\s*(?<IdName>\\S+)\\s*\\]\\s*"
        # 匹配资源类型
        pattern += "(?<ResType>\\S+)\\s*"
        # 匹配资源属性
        pattern += "\\{\\s*(?<PropertyContext>[^\\}]*)\\s*\\}"
        resource_regex.compile(pattern)

    # 开始匹配解析字符串
    var results = resource_regex.search_all(context)
    for result in results:
        result = result as RegExMatch
        var id : String = result.get_string("IdName")
        var res_type : String = result.get_string("ResType")
        var property_context : String = result.get_string("PropertyContext")

        # 创建资源
        var res : Resource = InstanceObject.custom_class(res_type)
        if res is Resource:
            set_object_property(res, parse_property(property_context))
            resource_data[id] = res



#============================================================
#   属性
#============================================================
##  解析属性
## @code  
var property_regex : RegEx
func parse_property(code: String) -> Array:
    if property_regex == null:
        property_regex = RegEx.new()
        # 匹配属性和值
        property_regex.compile(
            "(\\s*(?<Property>\\S+)\\s*:\\s*(?<Value>("
            + "\\([^\\)]+\\)\\s*|\\S+"+
            ")[^,]?)\\s*)"
        )

    var list := []
    var results = property_regex.search_all(code)
    for result in results:
        list.append({
            "property": result.get_string("Property"),
            "value": result.get_string("Value")
        })

    return list


##  设置对象属性
## @object  对象
## @property_data_list  属性数据列表
func set_object_property(object: Object, property_data_list: Array):
    if property_data_list.size() == 0:
        return

    for data in property_data_list:
        var property : String = data['property']
        var value = conver_property_value(object, property, data['value'])
        if value:
            object.set(property, value)


##  转换属性值 
## @object  
## @property  
## @value  
func conver_property_value(object: Object, property: String, value: String):
    value = value.strip_edges()
    if resource_data.has(value):
        # 引用
        if value.begins_with("@"):
            value = value.trim_prefix("@")
            if not directory.file_exists(value):
                value = create_to_path.plus_file(value)
            return load(value)
        return resource_data[value] as Resource

    else:
        # 有后缀名
        if value.get_extension() != "":
            if not value.begins_with("res://"):
                value = create_to_path.plus_file(value)
            if directory.file_exists(value):
                return load(value)
            return null

        # 绝对路径资源
        elif (value.begins_with("res://") 
            and directory.file_exists(value)
        ):
            return load(value)
        else:
            # 其他
            if validate_json(value) == "":
                return parse_json(value)
    return value



#============================================================
#   实例化对象
#============================================================
class InstanceObject:

    static func instance(_name: String):
        if ClassDB.can_instance(_name):
            return ClassDB.instance(_name)
        return custom_class(_name)

    static func custom_class(_name: String):
        var script = GDScript.new()
        script.source_code = """
static func create():
    return %s.new()
        """ % _name
        # 脚本重新加载
        if script.reload() == OK:
            return script.new().create()
        else:
            print("对象创建错误,没有 ", _name, " 这个类")
        return null

执行脚本,将会创建场景及脚本到指定目录下,默认创建到 res:// 文件下

发表评论