基础使用
这一节中会用一些实例代码来实现一些常用操作,有关代码中使用到的方法以及单例的详细描述,可以点击IdeaXR的脚本编辑界面上的搜索帮助按钮来查看详细信息。
以Input单例为例,点击搜索帮助后在搜索框输入Input,双击或点击打开按钮即可查看该类中定义的方法:
节点操作
该部分将介绍涉及到节点操作的常用方法,包括对场景中节点状态的改变以及在脚本中创建新的节点。
获取场景中已存在的节点
对于场景中以及存在的节点,我们可以通过get_node(path:NodePath)
方法获取节点。NodePath
可以是一个相对路径(从脚本所在节点)或绝对路径(在场景树)到一个节点。如果路径不存在,则返回null instance,并记录错误,导致运行中断。如果不想让运行中断,可以使用get_node_or_null(path:NodePath)
,该方法在路径不存在时仅会返回null
,不会导致报错。
以get_node()
方法为例:假设当前的节点是Character,整体树的构造如下:
/root
/root/Character
/root/Character/Camera
/root/Character/Backpack/Book
/root/MyScene
/root/Swamp/Alligator
/root/Swamp/Mosquito
在character上获取不同节点的方法有:
get_node("Camera") # 获取Camera节点
get_node("Backpack/Book") # 获取Book节点
get_node("../Swamp/Alligator") # 获取Alligator节点
get_node("/root/MyScene") # 获取MyGame节点
节点的创建和删除
要通过代码创建节点,请像其他任何基于类的数据类型一样,调用其 new()
方法。
你可以将新创建的节点的引用保存在一个变量中,然后调用 add_child() 将其添加为脚本所在节点的子节点。
var spatial
func _ready():
spatial = Spatial.new() # 创建一个新的Spatial(空间)节点
add_child(spatial) # 将其添加为此节点的子节点
要删除节点、将其从内容中释放,你可以调用其 queue_free() 方法。这样该节点的删除任务就会被添加到队列中,在当前帧完成处理后就会执行。删除时,引擎会把该节点从场景中删除,然后释放对象内存中的对象。
spatial.queue_free()
你也可以调用 free()
来立即删除该节点。调用时需要小心,因为所有对它的引用都会立即变成null
。除非你知道自己在干什么,否则建议你使用queue_free()
。
释放节点时也会释放它的所有子项。所以,你只需删除最顶端的父节点,就可以删除整个场景树分支了。
备注
你可能会注意到,add_child()
有一个对应的方法remove_child()
。顾名思义,该方法就是将已经放进场景树的节点移除,这只会将节点移出当前的场景树,并没有从内存中释放节点。
实例化场景
场景就是模板,你可以用来创建任意数量的复制品。这样的操作叫作实例化(instancing),在代码中进行实例化总共分两步:
1.从硬盘加载场景 2.创建加载到的场景资源实例
var scene = load("res://MyScene.scene")
使用预加载场景可以提升用户体验,因为加载操作发生在编辑器读取脚本时,而非运行时。
var scene = preload("res://MyScene.scene")
此时的scene还只是个打包场景资源,还不是节点。要创建时机的系欸但,还需要调用instance()
方法。该方法会返回一颗节点树,然后就可以调用add_child()
方法添加为当前场景的子节点。
var instance = scene.instance()
add_child(instance)
此两步过程的优点在于,打包的场景可以保持加载状态并可以随时使用。这样你就可以快速添加多个实例化的场景。
节点属性的修改以及方法调用
节点属性的修改以及方法调用其实很简单,只要节点中存在相关属性和方法就可以直接调用。
# 假设meshinstance是一个以及获取到的网格实例节点
meshinstance.translation = vec3(1, 1, 1) # 将meshinstance的平移属性进行修改
# 调用节点中的方法
meshinstance.hide() # 将该节点隐藏。也就是将可见性变为false。
信号的创建、发送和连接
该部分将以一个简单的案例介绍如何在代码中创建,发送和连接信号:
extends Spatial
# 定义信号
signal num_changed
var num = 2
func _ready():
# 连接信号,信号接收方为自己(self),接收信号时会调用_on_number_changed方法
connect("num_changed",self,"_on_number_changed")
func _input(event):
# 获取输入,Input为处理输入的单例,调用其中的is_key_pressed()方法获取按键输入
if Input.is_key_pressed(KEY_E):
num += 1
# 发出信号
emit_signal("num_changed")
func _on_number_changed():
print(num)
以该案例为例,我们在开头使用signal
关键字定义一个num_changed
信号,再定义一个num
变量,并在_ready
函数中使用connect()
方法进行信号的连接,当键盘按下e键时num
加1,并发出信号,接收到信号后会调用_on_num_changed
函数并在输出端口打印num
。
Groups的应用
IdeaXR 中的编组的工作方式类似于你可能在其他软件中遇到的标记。一个节点可以根据需要添加到任意多个编组,这是组织大型场景的一个有用特性。有两种方法可以向编组中添加节点,第一个是从编辑器界面,选中节点后切到“节点面板”,点击Groups
添加编组标签:
第二种方法时从代码中添加。下面的脚本会在当前节点出现在场景树中后立即将其添加到 target 编组中。
func _ready():
add_to_group(“targets”)
这样,可以对同一个组中的节点进行统一管理。下面的代码可以统一隐藏所有 targets 组中的节点。
func targets_hide():
get_tree().call_group(“targets”,”hide”)
也可以通过调用 SceneTree.get_nodes_in_group() 获得 targets 节点的完整列表:
var targets = get_tree().get_nodes_in_group("targets")
跨脚本调用
想要调用或者修改其他脚本中的方法,只需要获取到那个脚本所挂载的节点即可。
对于如何封装以及调用通用的方法和变量,请看声明全局变量。
动态加载资源
IdeaXR从磁盘保存或加载的任何内容都是一种资源。在IdeaXR中,所有的资源都继承自Resource
类。它可以是场景,图像,脚本等。这是一些Resource
示例:Texture,Script,Mesh,Animation,AudioStream,Font。
当引擎从磁盘加载资源时,它只加载一次。如果该资源的副本已在内存中,则每次尝试再次加载该资源将返回相同的副本。由于资源只包含数据,因此无需复制它们。
从代码中加载资源
有两种方法可以从代码加载资源。首先,你可以随时使用load()
函数:
func _ready():
var res = load("res://rock.png") # 当代码执行到这一行时加载指定路径的资源
get_node("纹理图").texture = res
你也可以 预加载(preload)
资源。与load
不同,preload
会从硬盘中读取文件,并在编译时加载它。因此,不能使用一个变量化的路径调用预加载:需要使用常量字符串。
func _ready():
var res = load("res://rock.png") # 在编译时加载资源
get_node("纹理图").texture = res
释放资源
当资源(Resource)
不再使用时,它将自动释放自己。由于在大多数情况下,资源包含在节点中,因此当您释放节点时,如果没有其他节点使用该节点拥有的所有资源,则引擎也会释放它们。
文件系统
文件系统管理资源的存储方式和访问方式。
文件系统将资源存储在磁盘上,从脚本到场景或者PNG,JPG图像等内容都是以前宁的资源。如果一个资源包含引用磁盘上其他资源的下属性,则它还将包括这些资源的路径。如果一个资源具有内置的子资源,则该资源与所有捆绑的子资源一起保存在单个文件中。例如,字体资源通常与字体纹理捆绑在一起。
路径分隔符
IdeaXR 只支持用/
作为路径分隔符。因此诸如E:\AVA\avatarsamples\project.IdeaXR
之类的路径需要输入为E:/AVA/avatarsamples/project.ideavr
资源路径
使用特殊路径res://
,该路径之中指向项目的根目录(即project.ideavr
所在的位置)。
仅当从编辑器本地运行项目时,此文件系统才是读写的。导出时或在其他设备上运行时,文件系统将变为只读状态,并且将不再允许写入。
用户路径
保存项目状态和下载内容包之类的任务仍然需要对磁盘进行写入。为此,引擎保证特殊路径 user:// 始终可写。根据运行项目的操作系统的不同,该路径会被解析为不同的路径。
不同系统的位置路径如下:
windows: %APPDATA%\Ideavr\app_userdata\[project_name]
macOS: ~/Library/Application Support/Ideavr/app_userdata/[project_name]
可能导致的问题
这种简单的文件系统可能会导致问题,当资源四处移动时(重新命名资源或将其从项目中的一条路径移动到另一条路径)将破坏现有对这些资源的引用。这些引用将必须重新定义以指向新资源的位置。
为避免这种情况,请在IdeaXR的文件面板中进行所有的移动和重命名。切勿从IdeaXR外部移动资源,否则必须手动修复依赖关系。
文件的读取和保存
对于文件的读写处理使用File
类。
以下是有关如何写入和读取文件的示例:
func save(content):
var file = File.new()
file.open("user://save.json", File.WRITE)
var json = to_json(data) # 使用to_json()将数据转换成一个容易存储的字符串
file.store_line(json)
file.close()
func load():
var file = File.new()
file.open("user://save.json", File.READ)
var content = parse_json(file.get_as_text()) # 使用parse_json()将数据解析成保存时的样子
file.close()
return content
在上面的示例中,文件将保存在数据路径文档中指定的用户数据文件夹中。
声明全局变量
IdeaXR 中的场景系统存在一个缺点:无法保存多个场景都需要的信息,可以通过一些变通的方法来解决这个问题,但都有其局限性:
- 你可以使用“主”场景来把其它场景当作自己的子节点来加载和卸载。然而,这就意味着这些场景无法再独立正常运行。
- 使用文件将信息存储在磁盘的 user:// 下,然后由需要它的场景加载,但是经常保存和加载数据很麻烦并且可能很慢。
由此,我们引入单例模式,单例模式是一种软件设计模式,它将类的实例化限制为一个“单一”实例。当需要一个对象来协调整个系统的动作时进行使用。 利用这个概念,你可以创建这样的对象:
- 无论当前运行哪个场景,始终加载。
- 可以存储全局变量,如玩家信息。
- 可以处理切换场景和场景间的过渡。
- 行为类似单例,因为 IVRScript 在设计上就不支持全局变量。
自动加载的节点和脚本可以为我们提供这些特征。
使用自动加载实现全局变量
你可以创建自动加载(AutoLoad)来加载场景或者继承自Node
的脚本,将一些需要全局调用的变量和方法定义在里面。
备注
自动加载脚本时,会创建一个Node
并把脚本附加上去。加载其他任何场景前,这个节点就会被附加到根节点下。
要自动加载场景或者脚本,请从菜单中选择编辑->项目设置,然后切换到自动加载选项卡。
你可以在这里添加任意数量的场景或脚本。列表中的每个条目都需要一个名称,会被用来给该节点的name
属性赋值。使用上下箭头键可以操纵将条目添加到全局场景树时的顺序。与普通场景一样,引擎读取这些节点的顺序是从上到下的。
启用后,就可以直接在任意脚本中获取这个节点,并调用其中的变量或者方法。
var global = Global.some_function
如果你查看正在运行的场景树,就会看到自动加载的节点出现:
获取输入
IdeaXR中的输入事件主要通过Input
单例以及_input()
和_unhandled_input()
方法获取。目前在IdeaXR中可支持鼠标,键盘和触摸事件。在相关方法中可以直接对输入的事件进行判断,也可以使用InputMap
对相关事件进行映射。
详细介绍请看使用输入事件。