跳到主要内容

基础使用

这一节中会用一些实例代码来实现一些常用操作,有关代码中使用到的方法以及单例的详细描述,可以点击IdeaXR的脚本编辑界面上的搜索帮助按钮来查看详细信息。

search_help

以Input单例为例,点击搜索帮助后在搜索框输入Input,双击或点击打开按钮即可查看该类中定义的方法:

search_help

节点操作

该部分将介绍涉及到节点操作的常用方法,包括对场景中节点状态的改变以及在脚本中创建新的节点。

获取场景中已存在的节点

对于场景中以及存在的节点,我们可以通过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添加编组标签:

create_group

第二种方法时从代码中添加。下面的脚本会在当前节点出现在场景树中后立即将其添加到 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")

跨脚本调用

想要调用或者修改其他脚本中的方法,只需要获取到那个脚本所挂载的节点即可。

get_other_script_function

对于如何封装以及调用通用的方法和变量,请看声明全局变量

动态加载资源

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并把脚本附加上去。加载其他任何场景前,这个节点就会被附加到根节点下。

要自动加载场景或者脚本,请从菜单中选择编辑->项目设置,然后切换到自动加载选项卡。

autoload

你可以在这里添加任意数量的场景或脚本。列表中的每个条目都需要一个名称,会被用来给该节点的name属性赋值。使用上下箭头键可以操纵将条目添加到全局场景树时的顺序。与普通场景一样,引擎读取这些节点的顺序是从上到下的。

set_autoload

启用后,就可以直接在任意脚本中获取这个节点,并调用其中的变量或者方法。

var global = Global.some_function

如果你查看正在运行的场景树,就会看到自动加载的节点出现:

autoload_remote

获取输入

IdeaXR中的输入事件主要通过Input单例以及_input()_unhandled_input()方法获取。目前在IdeaXR中可支持鼠标,键盘和触摸事件。在相关方法中可以直接对输入的事件进行判断,也可以使用InputMap对相关事件进行映射。

详细介绍请看使用输入事件