GodotGodot 插件制作流程

Posted 张学徒

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了GodotGodot 插件制作流程相关的知识,希望对你有一定的参考价值。

Godot 3.4 beta5

以下为创建一个插件基本工作流程,也许与一些人的不同,有更好的可以在评论区交流分享。

如果你只想了解一下插件制作流程的话,下面的代码不需自己手动写,直接复制粘贴就好。

创建插件

在 Godot 中创建添加插件很简单,点击编辑器左上角的 Project 菜单

在弹出的 Project Settings 窗口中,点击 Plugins 选项卡,点击 Create 按钮

弹出 Create a Plugin 窗口,在里面输入 PluginName(插件名)、Subfolder(插件的文件夹名)、Description(对插件的功能及其他的相关描述)、Author(作者)、Version(版本)、Language(使用哪种脚本语言来写插件)、Script Name(脚本的名字,必须以 .gd 结尾才能创建),Activate now(是否现在激活使用,先不勾选,因为在插件脚本里面写代码添加功能的时候,有时需要重新激活功能才能生效)。

创建一个如下的插件

点击 Create 按钮后,会自动切换到脚本视图,并有如下代码

tool
extends EditorPlugin


func _enter_tree() -> void:
	pass


func _exit_tree() -> void:
	pass

_enter_tree 方法会在激活插件的时候执行这个方法,_exit_tree() 方法会在取消激活的时候执行这个方法。

EditorPlugin 这个类继承的是 Node,是一个节点,和我们做游戏时使用的节点是一样的,激活插件的过程就是把这个插件节点添加到编辑器中,编辑器内部会对这些插件做一些其他初始化的操作,这个插件节点就会被添加到编辑中,就可以使用其中的功能了。

创建出来的文件如下:

  1. addons :全部插件文件夹。每次创建的新的插件,或者安装的插件,都会添加到这个文件夹里。
  2. test_plugin:刚刚创建的插件的文件夹
  3. plugin.gd:刚刚创建的插件的脚本

添加功能

我个人习惯是:如果是功能比较简答的脚本,那么就直接在这个插件文件夹下方创建文件;如果功能较多,那么我会添加一个 src 文件夹存放功能的源代码。

每个脚本的代码功能都是一步步按需求增加的,向这个脚本里添加一点,,而不是一下子就写好的,如果是预先设计了的除外。

比如说我们想做个行为树插件,那么我们先将行为树给设计出来。为了方便,我在这里只创建几个没有功能的脚本,官方的资源商店也有行为树的插件,如果想自己做行为树可以自己看看 行为树(一)了解与设计行为树代码 了解一下。

src 文件夹里创建了三个脚本,都是继承自 Node 节点 ,没有修改脚本。

如果想熟练使用插件,必须要了解 EditorPlugin (编辑器插件)和 EditorInterface (编辑器接口)类,在 EditorPlugin 中可通过 get_editor_interface() 方法获取 编辑器接口,来操作更多功能。

事先也要知道,刚上来做这些都会感到陌生、麻烦,但是这种东西必须多做用起来才能随心所欲,需要你自己多写写小玩意,然后经常 F1 查看帮助文档内容。比如这个插件脚本 EditorPlugin 类,看看里面有什么方法,不会了就看看,但是也要注意,不要一下子看太多,贪多嚼不烂,反而会消耗你的耐心,容易产生厌倦的负面情绪,这是学习方法之一,一定要注意。

那么我么现在给他添加到自定义节点里,双击 plugin.gd 脚本,在 _enter() 方法里添加一行代码:

add_custom_type("Root", "Node", preload("res://addons/test_plugin/src/Root.gd"), get_editor_interface().get_base_control().get_icon("Node", "EditorIcons"))

上行代码有两个方法:

  • add_custom_type
    用于添加自定义节点类型,在添加子节点的时候,会出现这个类型的节点。
    add_custom_type(添加时的名称, 继承的节点的类型,脚本类,显示的图标)

  • get_editor_interface().get_base_control().get_icon()
    这么一大行代码是用来获取节点的图标的。

    get_editor_interface().get_base_control() 用来获取 Godot 编辑器窗口的主容器。他是一个 Control 类型的节点(因为 Godot 编辑器就是用 Godot 做出来的,自然都是通过节点组成的)。可以通过它获取节点的图标,使用 get_icon() 进行获取。
    get_icon(图标类型的名称,主题类型) 上面我们获取 EditorIcons 编辑器图标,类型为 Node 类型的图标。

然后我们在 _exit_tree 方法下添加一行代码:

remove_custom_type("Root")

如果关闭这个插件,则选择添加的节点的列表中就不会出现这个节点了。

然后我们激活这个插件

要注意我为什么要先写代码再激活。是因为如果先激活再写,里面的代码有时不会生效,或者生效后,再修改后上次的脚本添加的节点就会失效,且有时里面的功能不能使用且报错。它并不会自动更新。注意是“有些情况下”,而非全部情况,但如果是拥有复杂功能的插件,还是建议取消激活,然后打开。

可以看我这个脚本没有在 _exit_tree 中添加 remove_custom_type("Root") 时激活,然后写完重新激活后出现的情况。

第一次添加的没有被移除,因为第二次激活时会重新添加,而第一次的有没有被移除,所以出现了两个,所以在添加功能的时候最好先不要激活插件。如果出现了这个情况,可以关闭这个项目重新打开。

然后我们添加时搜索 Root,就会出现我们添加的自定义节点


最后代码为

tool
extends EditorPlugin


func _enter_tree() -> void:
	# 添加自定义类型节点
	add_custom_type("Root", "Node", preload("res://addons/test_plugin/src/Root.gd"), get_editor_interface().get_base_control().get_icon("Node", "EditorIcons"))


func _exit_tree() -> void:
	# 移除自定义类型接节点
	remove_custom_type("Root")

我想在选中 Root 节点的时候在底部出现一个功能栏,我可以自己从中点击节点进行添加节点,而不必每次都要点击上方的添加节点按钮。

现在我们在 test_plugin 文件夹下创建一个 util 文件夹,用于存放我们各种其他辅助功能的文件夹。

创建一个新的场景,添加 PanelContainer 类型的节点为根节点,再添加 ScrollContainer 节点,ScrollContainer 下添加 HBoxContainer 节点。如下:

设置 PanelContainerrect_min_size 属性的 y 值为 100,让这个节点显示出来最小高度为 100

util 文件夹下创建一个 add_node_panel 文件夹,保存这个场景,命名为 AddNodePanel 。给这个场景创建一个脚本,脚本名称按默认的名称 AddNodePanel.gd 即可。

下面的脚本代码可以自己复制粘贴,不需自己手动抄写(要注意,插件里的场景的脚本都要加 tool,否则不生效 )

tool
extends PanelContainer


## 行为树脚本列表
var bt_script_list = {
	"Composite": load("res://addons/test_plugin/src/Composite.gd"),
	"Leaf": load("res://addons/test_plugin/src/Leaf.gd"),
}
var _plugin : EditorPlugin


onready var hbox = $ScrollContainer/HBoxContainer


## 这个场景在哪个插件中
func set_plugin(value: EditorPlugin) -> void:
	_plugin = value
	
	# 如果当前这个节点还没有添加到节点上
	# hbox 就会为 null,等到节点发出 ready 之前
	# hbox 才会准备好获取到了节点
	if not self.is_inside_tree():
		yield(self, "ready")
	
	# 添加 BT 脚本对应的按钮
	for key in bt_script_list:
		# 创建一个工具按钮
		var b = ToolButton.new()
		# 设置它的一个自定义数据
		b.set_meta("bt_data", {
			script = bt_script_list[key],
			name = key,
		})
		b.text = key
		b.connect("pressed", self, "_on_Button_pressed", [b])
		b.icon = _plugin.get_editor_interface().get_base_control().get_icon("Node", "EditorIcons")
		b.rect_min_size = Vector2(100, 40)
		hbox.add_child(b)


func _on_Button_pressed(button: Button) -> void:
	# 添加到场景中
	var data = button.get_meta("bt_data")
	var bt_node = data.script.new() as Node
	bt_node.name = data.name
	# 选中的节点
	var nodes = _plugin.get_editor_interface().get_selection().get_selected_nodes()
	if nodes.size() > 0:
		nodes[0].add_child(bt_node, true)
		bt_node.owner = nodes[0].owner if nodes[0].owner != null else nodes[0]
	else:
		print("没有选中节点")

plugin.gd 脚本中添加变量 add_node_panel

var add_node_panel = preload("res://addons/test_plugin/util/add_node_panel/AddNodePanel.tscn").instance()

_enter_tree() 方法下添加代码:

add_control_to_bottom_panel(add_node_panel, "Behavitor Tree")
add_node_panel.set_plugin(self)

_exit_tree() 下添加如下一行代码:

remove_control_from_bottom_panel(add_node_panel)

最终 plugin.gd 里的全部代码

tool
extends EditorPlugin


var add_node_panel = preload("util/add_node_panel/AddNodePanel.tscn").instance()


func _enter_tree() -> void:
	# 添加自定义类型节点
	add_custom_type("Root", "Node", preload("src/Root.gd"), get_editor_interface().get_base_control().get_icon("Node", "EditorIcons"))
	
	# 添加底部功能栏
	add_control_to_bottom_panel(add_node_panel, "Behavitor Tree")
	add_node_panel.set_plugin(self)	# 设置 add_node_panel 所在的插件
	add_node_panel.show()	# 让节点显示出来,而不是默认的折叠


func _exit_tree() -> void:
	# 移除自定义类型接节点
	remove_custom_type("Root")
	# 移除底部功能栏
	remove_control_from_bottom_panel(add_node_panel)

我们新创建一个场景,添加根节点为 Node2D 类型的节点,保存为 res://Test.tscn 场景。

再添加 Root 节点,下面就出现了 Behavitor Tree 功能面板了,点击上面的按钮,节点就会添加到选中的节点上。

功能的增改

但是上面的功能逻辑不是很好,选中行为树类型的节点后才显示下面的功能栏,就像选中 AnimationPlayer 类型的节点后,才在下面显示对应的内容一样。

所以我们将 plugin.gd 代码修改一下

tool
extends EditorPlugin


var add_node_panel = preload("util/add_node_panel/AddNodePanel.tscn").instance()



func _enter_tree() -> void:
	# 添加自定义类型节点
	add_custom_type("Root", "Node", preload("src/Root.gd"), get_editor_interface().get_base_control().get_icon("Node", "EditorIcons"))
	
	# 设置 add_node_panel 所在的插件
	add_node_panel.set_plugin(self)


func _exit_tree() -> void:
	# 移除自定义类型接节点
	remove_custom_type("Root")


# 进行操作时,比如选中节点,或者编辑脚本等情况时会调用这个方法
func handles(object: Object) -> bool:
	# 如果操作的对象是 Root
	if object is preload("src/Root.gd"):
		return true
	return false

# visible 值为 handles() 方法返回值
func make_visible(visible: bool) -> void:
	if visible:
		# 添加底部功能栏
		add_control_to_bottom_panel(add_node_panel, "Behavitor Tree")
		# 让节点显示出来,而不是默认的折叠
		add_node_panel.show()
	else:
		# 移除底部功能栏
		remove_control_from_bottom_panel(add_node_panel)

但是还有一点问题就是在 handles() 方法部分,这种只能在选中了 Root 类型的节点才能返回 true,显示出面板,其他的就不行了,所以我们还添加个判断方法。在插件里添加如下代码:

const BTNode = [
	preload("res://addons/test_plugin/src/Root.gd"),
	preload("res://addons/test_plugin/src/Composite.gd"),
	preload("res://addons/test_plugin/src/Leaf.gd"),
]

## 是否为 行为树节点
func is_bt_node(node: Node) -> bool:
	for n in btnode:
		if node is n:
			return true
	return false

handle() 方法内容稍改一下:

func handles(object: Object) -> bool:
	# 如果操作的对象是 Root
	if is_bt_node(object):
		return true
	return false

完整代码:

tool
extends EditorPlugin


const BTNode = [
	preload("res://addons/test_plugin/src/Root.gd"),
	preload("res://addons/test_plugin/src/Composite.gd"),
	preload("res://addons/test_plugin/src/Leaf.gd"),
]
var add_node_panel = preload("util/add_node_panel/AddNodePanel.tscn").instance()



func _enter_tree() -> void:
	# 添加自定义类型节点
	add_custom_type("Root", "Node", preload("src/Root.gd"), get_editor_interface().get_base_control().get_icon("Node", "EditorIcons"))
	
	# 设置 add_node_panel 所在的插件
	add_node_panel.set_plugin(self)


func _exit_tree() -> void:
	# 移除自定义类型接节点
	remove_custom_type("Root")


# 进行操作时,比如选中节点,或者编辑脚本等情况时会调用这个方法
func handles(object: Object) -> bool:
	# 如果操作的对象是 Root
	if is_bt_node(object):
		return true
	return false


# visible 值为 handles() 方法返回值
func make_visible(visible: bool) -> void:
	if visible:
		# 添加底部功能栏
		add_control_to_bottom_panel(add_node_panel, "Behavitor Tree")
		add_node_panel.show()
	else:
		# 移除底部功能栏
		remove_control_from_bottom_panel(add_node_panel)


## 是否为 行为树节点
func is_bt_node(node: Node) -> bool:
	for n in BTNode:
		if node is n:
			return true
	return false

然后就是不断地对内容进行增加删除,增加删除,使脚本越来越强,功能越来越丰富。随之而来的问题就是内容太多了,代码太乱了,如果把所有内容都写到 plugin.gd 这一个脚本里,那未免太多、太复杂、太乱,所以我们可以对脚本中的代码中有共同特点的方法都放在一个脚本里,这样我们就符合了面向对象的六大基本原则之一的“单一职责原则”,让一个类(也就是一个脚本)只拥有一个功能。

我们这个脚本太简单,但是其实可以不用这么做,但是如果你的脚本代码太多的时候建议一定要这样做。

我们就按现在的代码来讲,我们上方的 is_bt_node() 方法和 BTNode 属性可以将同特征的属性和方法都封装到一个 BTClassDB (行为树类数据库)脚本里

我们在 util 文件夹下创建一个 Reference 类型的脚本,名称为 BTClassDB

BTClassDB.gd 脚本代码如下:

## 行为树 Class 数据库
extends Reference


## 行为树节点
const BTNode = [
	preload("res://addons/test_plugin/src/Root.gd"),
	preload("res://addons/test_plugin/src/Composite.gd"),
	preload("res://addons/test_plugin/src/Leaf.gd"),
]


## 是否为 行为树节点
static func is_bt_node(node: Node) -> bool:
	for n in BTNode:
		if node is n:
			return true
	return false

plugin.gd 脚本则修改为如下代码

tool
extends EditorPlugin


const BTClassDB = preload("util/BTClassDB.gd")	#(增加的部分)行为树的 Class 数据库 

var add_node_panel = preload("util/add_node_panel/AddNodePanel.tscn").instance()


func _enter_tree() -> void:
	# 添加自定义类型节点
	add_custom_type("Root", "Node", preload("src/Root.gd"), get_editor_interface().get_base_control().get_icon("Node", "EditorIcons"))
	
	# 设置 add_node_panel 所在的插件
	add_node_panel.set_plugin(self)


func _exit_tree() -> void:
	# 移除自定义类型接节点
	remove_custom_type("Root")


# 进行操作时,比如选中节点,或者编辑脚本等情况时会调用这个方法
func handles(object: Object) -> bool:
	# 如果操作的对象是 Root
	if BTClassDB.is_bt_node(object):	#(修改的部分)判断操作对象是否是行为树
		return true
	return false


# visible 值为 handles() 方法返回值
func make_visible(visible: bool) -> void:
	if visible:
		# 添加底部功能栏
		add_control_to_bottom_panel(add_node_panel, "Behavitor Tree")
		add_node_panel.show()
	else:
		# 移除底部功能栏
		remove_control_from_bottom_panel(add_node_panel)

之后就是这样,不断地增加功能,然后功能太多了就创建出来脚本,将代码都剪进去,然后整理一下,不断重复 不断重复 ……

结语

孰能生巧,做多了思考多了,有些编程思想你不去学你都能自己领悟出来,当然如果是有人引领,那确实会进步飞快,如果没有 还是得多做…

以上是关于GodotGodot 插件制作流程的主要内容,如果未能解决你的问题,请参考以下文章

Cadence Allegro贴片和插件元器件封装制作流程总结

Blender制作多个动画片段时踩的坑

VSCode自定义代码片段15——git命令操作一个完整流程

VSCode自定义代码片段15——git命令操作一个完整流程

VIM 代码片段插件 ultisnips 使用教程

WordPress - 代码片段插件