跳到主要内容

基础语法介绍

内置类型

内置类型是分配在栈上的,按值传递。这意味着在每次赋值或将它们作为参数传递给函数时都会创建一个副本。唯一的例外是数组Array和字典 Dictionary,它们是共享的,按引用传递。

基本内置类型简介

IVRScript 中的变量可以赋值为不同的内置类型。

null

null 是一个空数据类型,不包含任何信息,不能赋值为任何其他值。

bool

“boolean”(布尔)的缩写,只能包含 true 或 false 。

int

“integer”(整数)的缩写,存储整数(正数和负数)。存储的是 64 位值,等效于 C++ 中的 int64_t 。

float

使用浮点值存储实数,包括小数。存储的是 64 位值,等效于 C++ 中的 double 。注意:目前类似 Vector2、Vector3、PoolRealArray 的数据结构存储的是 32 位单精度“float”值。

String Unicode格式 的字符序列。字符串可以包含以下转义序列:

转义序列转义为
\n换行
\t水平制表符
\r回车
\a警报
\b退格键
\f换页符
\v垂直制表符
\"双引号
\'单引号
\\反斜杠
\uXXXXUnicode 代码点 XXXX (十六进制, 不区分大小写)

内置向量类型简介

Vector2

2D 向量类型,包含 x 和 y 字段,也可以像数组一样访问。

Rect2

2D 矩形类型,包含两个向量字段:positionsize。还包含一个end字段,即position + size

Vector3

3D 向量类型,包含xyz字段,也可以像数组一样访问。

Transform2D

用于2D变换的2x3矩阵。

Plane

3D平面类型的标准形式包含一个normal向量字段以及一个d标量距离。

Quat

四元数是一种用于表示3D旋转的数据类型。它对于内插旋转很有用。

AABB

轴对齐边界框(或3D框)包含两个向量字段:positionsize。还包含一个end字段,即position + size

Basis

3×3矩阵被用于3D旋转与缩放。其包含3个向量字段(xyz) 并且可以像3D向量数组那样访问。

Transform

3D变换包含一个 Basis类型字段 basis 和一个 Vector3 类型字段 origin

引擎内置类型

Color

颜色数据类型包含rgb,和a 字段。它也可以作为hs,和v 来访问色相/饱和度/值。

NodePath

编译路径,到一个主要用在场景系统中的节点。它可以很容易地从一个字符串获得,或获得一个字符串。

RID

资源ID(RID)。服务使用通用的RID来引用不透明数据。

Object

任何非内置类型的基类。

Array

任意对象类型的序列,数组可以动态调整大小。索引是从0开始。负索引表示从尾部开始计数。

var arr = [1, 2, 3] #定义一个数组
var b = arr[1] # b=2
var c = arr[arr.size() - 1] # c=3
var d = arr[-1] # d=3
arr[0] = "Hello" # 将数组第一个元素换为Hello
arr.append(4) # 数组内容现在为["Hello", 2, 3, 4]

Dictionary

关联容器,其中包含唯一键引用的值。

var d = {
key1 : "value",
key2 : 100,
key3 : [2, 3],
key4 : "Hello"
}

# 向现有字典添加或者修改键
d.age = 50 # 如果字典中没有名为age的键,则将字符串age作为键,并添加整数50作为值
d[5] = "test" # 如果字典中没有名为5的键,则将整数5作为键,并添加字符串test作为值

d.key2 = 50 # 由于字典中已经有名为key2的键,所以该语句意思为将key2键对应的值修改为50

数据

变量

变量可以作为类的成员存在,也可以作为函数的局部变量存在。它们使用var关键字创建的,并且可以在初始化时指定一个值。

var a # 数据类型默认为空(null)
var b = 5
var c = 3.8
var d = b + c # 变量会按顺序创建

变量可以选择具有类型的声明,指定类型时,变量将强制始终具有相同的类型,试图分配不兼容的值将引发错误。

类型在变量声明中使用:(冒号)符号在变量名后面指定,后面是类型。

var my_vector2: Vector2
var my_node: Node = Spatial.new()

如果在声明中初始化变量,则可以推断类型,因此可以省略类型名称:

var my_vector2 := Vector2() #  my_vector2是vector2类型的变量
var my_node := Sprite.new() # “my_node”是“Sprite”类型

当然,类型推断必须是要在指定的值具有定义定义的类型才有可能,否则将引发错误。 有效的类型有:

  • 内置类型(Array, Vector2, int, String, 等)。
  • 引擎类(Node, Resource, Reference, 等)。
  • 常量名, 如果它们包含脚本资源。
  • 在同一个脚本中的其他类, 遵循作用域。
  • 脚本类使用 class_name 关键字声明。

常量

常量代表项目运行时不可更改的值。其值在编译时必须已知。使用const关键字即可为常量值赋予名称。尝试为常量重写赋值会引发错误。常量一般用来存储。不应当修改的值。

const A = 5
const B = Vector3(1,1,1)
Const C = A+50

常量与变量一样可以添加显示说明。

Const A:int = 5
Const B:Vector2 = Vector2(1,1)

枚举

枚举可以理解为常量的简写,如果你想为某些常量分配连续的整数,使用枚举就会比较方便。

enum {A,B,C,D}

#等同于
const A = 0
const B = 1
const C = 2
const D = 3

enum {STATE_A,STATE_B = 4,STATE_C}

#等同于
const State = {STATE_A = 0,STATE_B = 4,STATE_C = 5}

函数

函数属于一个类。变量查找的作用域优先级是:本地→类成员→全局。 self 变量始终可用,并作为访问类成员的选项提供,但并不一定是必需的(与Python不同,不应该将self作为函数的第一个参数传递)。

func my_function(a,b):
Print(a)
Print(b)
Return a+b #return可选,没有则会返回null

函数可以在任何时候return,默认返回null。 函数也可以具有参数和返回值的类型声明. 可以使用与变量类似的方式添加参数的类型:

func my_function(a: int, b: String):
pass

如果函数参数具有默认值, 则可以推断类型:

func my_function(int_arg := 42, String_arg := "string"):
pass

可以在参数列表之后使用箭头标记(->)指定函数的返回类型:

func my_int_function() -> int: # 指定返回值类型为int return 0

有返回类型的函数必须返回正确的值。将类型设置为void意味着函数不返回任何内容。Void函数可以使用 return 关键字提前返回,但不能返回任何值。

func void_function() -> void:
return # 不会返回值

引用函数 与python不同,IVRScript中的函数不能储存在变量中,不能作为参数传递给另一个函数,也不能从其他函数返回,这是出于性能原因。 若要在运行时按名称引用一个函数(例如,将其存储在一个变量中,或将其作为参数传递给另一个函数),必须使用callfuncref帮助函数。

# 按名称调用函数
my_node.call("my_function", args)

# 存储函数的引用
var my_func = funcref(my_node, "my_function")
# 调用存储的函数引用
my_func.call_func(args)

语句和控制流程

if/elif/else

使用if/elif/else可以实现条件控制。条件的括号是可选的,不是必须添加的。

if expression:
statement(s)
elif (expression):
statement(s)
else:
statement(s)

while

使用while可以创建简单的循环,可以使用break来中断循环,或者使用continue继续。

while expression:
statement(s)

for

for循环可以用来遍历数组或者字典。在数组上迭代时,当前数组元素存储在循环变量中。在遍历字典时,键(key) 存储在循环变量中。

for i in [2, 4, 6]:
print(i) # 循环执行三次,分别打印2,4,6

var dict = {"a": 0, "b": 1, "c": 2}
for i in dict:
print(dict[i]) # 循环执行三次,分别打印1,2,3

for a in "Hello":
print(a) # 循环执行五次,分别打印H,e,l,l,o

for a in 3:
print(a) # 循环执行三次,分别打印0,1,2

match

match语句用于分支程序的执行。它相当于在许多其他语言中出现的switch语句,每一个分支中会默认执行break。如果不想使用默认的break,可以使用continue作向下穿透匹配。


match x:
1: # x = 1时执行
print("Number one!")
2: # x = 2时执行
print("Two are better than one!")
"hello": # x = "hello"时执行
print("Why is a string?")
_: # x不等于以上任何值的时候执行,与switch语句中的default等效
print("invalid value")

默认情况下,所有脚本文件都是未命名的类。在这种情况下,只能使用文件的路径引用它们,使用相对路径或绝对路径。

类(以文件形式保存)可以继承自:

  • 全局类。
  • 另一个类文件。
  • 另一个类文件中的内部类。

不允许多重继承。 继承使用 extends 关键字:

# 继承/扩展一个全局可用的类
extends SomeClass

# 继承/扩展一个命名的类文件
extends "somefile.is"

# 继承/扩展另一个文件中的内部类
extends "somefile.is".SomeInnerClass

要检查给定的实例是否从给定的类继承,可以使用 is 关键字:

const someclass = preload("somefile.is")

#判断somevar是否继承自someclass
if somevar is someclass:
somevar.func_in_someclass()

要调用基类(即当前类extends的类)中的函数,请在函数名前面加上 .

.base_func(args)

这特别有用,因为扩展类中的函数会替换基类中同名的函数。所以如果您仍然想调用它们,您可以使用 .(这就像其他语言中的super关键字一样):

func some_func(x):
.some_func(x) # 调用父类中的相同方法。
note

默认函数像_init和大多数通知像_enter_tree_exit_tree _process_physics_process 等,将自动调用在所有父类中的函数。重载它们时无需显式调用它们。

类的构造函数

在类实例化时调用的类构造函数名为 _init。如前所述,父类的构造函数在继承类时被自动调用。所以通常不需要显式调用 ._init()

setter/getter

知道类的成员变量何时出于任何原因更改通常是很有用的。也可能需要以某种方式封装其访问。

为此,IVRScript使用 setget 关键字提供了一个 setter/getter 语法。在变量定义后可直接使用:

var my_var setget my_var_set, my_var_get


func my_var_set(new_value):
my_var = new_value


func my_var_get():
return my_var # Getter 必须返回一个值。

每当 my_var 的值被外部代码(即不是来自该类中的本地使用)修改时,setter 函数(上面的 setterfunc)就会被调用。这发生在值改变之前。setter 必须决定如何处理新值。反之亦然,当 my_var 被访问时,getter 函数(上面的 getterfunc)必须 return 所需的值。

常用关键词

onready

使用节点时,通常希望将对场景部分的引用保留在变量中。由于仅在进入活动场景树时才保证要配置场景,因此只有在调用 Node._ready() 时才能获得子节点。

var my_node

func _ready():
my_node = get_node("MyNode")

这可能会有些麻烦,尤其是当节点和外部引用堆积时。为此,IVRScript 具有关键字onready,将成员变量的初始化推迟到调用_ready()。它可以用一行替换上面的代码:

onready var my_node = get_node("MyNode")

export

使用export关键字定义的变量可以将变量开放到编辑器中修改。

export var my_name = "Aaron"
export var age = 50

export关键字后,导出的变量必须初始化为常量表达式,或者具有使用的参数形式的导出提示。(参见下方代码示例)

export var number = 5 
export(int) var num # 可以将基本数据类型作为参数
export(int, "A", "B", "C") var character # 枚举为0,1,2
export(String, "Rebecca", "Mary", "Leah") var character_name # 枚举字符串名称
export(int,-1020) var a # 允许调整为-10 - 20的整数值
export(Color, RGBA) var col # 颜色以红-绿-蓝-alpha值给出

tool

默认情况下,脚本不在编辑器内运行,并且只能更改导出的属性。在某些情况下,确实希望它们在编辑器中运行。为此,可以用tool关键字并将它放在文件的顶部。

详细介绍请看在编辑器中运行代码

extends

关于extends关键字的作用请看class(类)

preload

使用preload可以预加载一个类或变量

var ball = preload(ball.is)

func _ready():
var a = ball.new()
a.some_function()

yield

IVRScript通过内置的 yield 提供对协程的支持。调用yield()将立即从当前函数返回,并且使用该函数的当前冻结状态作为返回值。在此结果对象上调用resume()将继续执行并返回函数返回的任何内容。恢复后,该状态对象将失效。这是一个例子:

func my_func():
print("Hello")
yield()
print("World")


func _ready():
var y = my_func()
print(my_dear)
y.resume

这将会打印:

Hello
my dear
world

使用 yield 的真正优势在于与信号结合使用。yield 可以接受两个参数,一个对象和一个信号。收到信号后,将重新开始执行。


# 当动画播放完成时恢复执行
yield(get_node("AnimationPlayer"), "animation_finished")

# 等待五秒钟,然后恢复执行
yield(get_tree().create_timer(5.0), "timeout")

内置函数方法

一下介绍的方法都是相关操作触发后的回调

_init(),_enter_tree(),_exit_tree()和_ready():

func _init():初始化对象时被调用。

func _enter_tree():在节点进入SceneTree时被调用(例如,在实例化,场景更改时或在脚本中调用add_child()之后)。

func _exit_tree():在节点即将离开SceneTree时被调用(例如,释放,更改场景或在脚本中调用remove_child()之后)。

关于add_child()和remove_child()方法,可以暂时理解为为某节点添加子节点以及删除子节点的方法。

func _ready():在节点“就绪”时调用,即在节点及其子节点都进入场景树时调用。也就是说当节点及其子节点都进入场景树时。如果节点有子节点,它们的_ready()回调会首先被触发,然后父节点会收到_ready()通知。每个节点只能对_ready()调用一次。 从场景树中删除节点并再次添加后,将不会第二次调用_ready()。一般在其中定义初始化内容。

当实例化连接到第一个执行场景的场景时,IdeaXR 将实例化树下的节点(进行_init调用),并构建从根向下的树。这导致_enter_tree()调用,向下级联树。当树构建完成,叶子节点调用_ready()。一旦所有子节点都完成了对它们的子节点的调用,,一个节点就会调用_ready()方法。然后,这将导致反向级联回到树的根部。

实例化脚本或独立场景时,节点不会在创建时添加到场景树,因此不会触发 _enter_tree 回调。相反,只发生 _init()调用。当场景添加到场景树时,会发生_enter_tree()_ready()调用。

一般来说来说,_ready()函数的使用会比较频繁,因为会在其中设置初始的内容。其他两个函数相对使用不多。

_input(event)和_unhandle_input(event)

func _input(event):有输入事件时调用。 输入事件在节点树中向上传播,直到一个节点将其消耗为止。仅当节点在场景树中存在时(即,如果它不是孤立的),才调用此方法。

func _unhandled_input(event):当输入事件未被_input或任何 GUI使用时调用。输入事件通过节点树向上传播,直到节点使用它。对于项目输入,该函数通常比_input更适合,因为它们允许 GUI 首先拦截事件。

_process(delta)和_physic_process(delta)

func _process(delta):在主循环的处理步骤中调用。_process()在每个帧上都以尽可能快的速度发生,因此自上一帧以来的增量(delta)时间不是恒定的。仅当节点在场景树中存在时(即,如果它不是孤立的),才调用此方法。

func _physics_process(delta):在主循环的物理处理步骤中调用。物理处理意味着将帧速率同步到物理,即增量变量应为常数。(delta≈0.016s)

当需要帧之间依赖于帧速率的 deltatime 时,请使用 _process()。如果更新对象数据的代码,需要尽可能频繁地更新,那么_process()中是正确放置这些代码的地方。

当一个帧之间需要独立于帧速率的 deltatime 时,请使用_physics_process()。如果不管时间是快还是慢,代码需要随着时间的推移进行一致的更新,那么_physics_process()中是正确的放置这些代码的地方。重复的运动学和对象变换操作,应在此处执行。