i3D Act使用手册
i3D Act 使用手册
一、引言
1. i3D Act 引擎概述
i3D Act 实时三维引擎是由湖南三岳数维科技有限公司自主研发的全信创元宇宙三维开发引擎。该引擎不仅具备高性能渲染能力,还特别针对国产 CPU 架构进行了优化,展现出卓越的兼容性和性能。
2. 技术特性
i3D Act 实时三维引擎集成了多项先进技术,包括:
自主研发的渲染技术:确保了引擎的独立性和创新性。
国产CPU兼容性:支持龙芯、申威、鲲鹏、飞腾等非X86架构,强化了国产软硬件的协同效应。
云渲染支持:利用云计算资源,实现高效能的远程渲染处理。
多平台导出:确保了引擎在不同操作系统和硬件平台上的应用灵活性。
3. 功能亮点
i3D Act 实时三维引擎提供了一系列强大的功能,包括:
3D 模型渲染:创建高质量的视觉动画效果。
增强现实体验:通过 AR 技术提升 3D 场景的沉浸感。
自定义行为和编辑器:允许用户根据特定需求定制功能。
4. 应用领域
i3D Act 实时三维引擎在多个行业领域有着广泛的应用,如影视制作、国防军事模拟、油气行业可视化、水利数字孪生以及民用建筑的数字孪生系统等。
5. 结语{6}
i3D Act 实时三维引擎作为三岳数维科技有限公司的核心产品,不仅体现了公司在三维技术领域的专业实力,也标志着国产三维引擎技术的重要进步。本教材将进一步深入探讨 i3D Act 实时三维引擎的技术细节和应用实践,为读者提供全面的学习资源。
二、 基础知识
1. i3D Act 编辑器
(1) 启动器
启动i3D Act后,你会首先看到“项目启动器”窗口。你可以在这里新建、移除、导入、运行工程项目。
创建新项目:
单击窗口左上角的 新建 按钮。
为项目命名,在计算机上选择一个空文件夹来保存文件,然后选择Forward+。
单击创建并编辑按钮,这样就会创建项目文件夹并将其在编辑器中打开。
(2) 文件系统
i3D Act实时三维引擎的文件系统是管理项目资源的核心机制,它支持各种常见的资源类型,如图像、声音、字体、材质、场景等,每种资源类型都有对应的Resource类进行管理和操作。
(3) 场景
在i3D Act实时三维引擎中,你把你的工程分解成可重复使用的场景。场景可以是一个角色、一辆车、用户界面中的一个菜单、一座房子,或者任何你能想到的东西。i3D Act实时三维引擎的场景很灵活,既能够充当预制件(Prefab),又能够用作其他实时渲染引擎中的场景。
场景面板会列出活动场景中的节点。
(4) 属性
在i3D Act实时三维引擎中,每个场景对象都被表示为一个节点(Node)。节点属性就是用来定义和控制这些节点的各种特性和行为。
你可以在检查器中编辑所选节点的属性。
(5) 状态栏
i3D Act实时三维引擎的状态栏是编辑器界面的底部区域,它提供了许多有用的实时信息和交互功能,帮助开发者更高效地进行功能开发。
(6) 3D 视图
•3D视图允许游戏对象以3维空间的形式呈现,具有x、y、z三个坐标轴。
•在3D视图中,你可以操作网格、灯光、设计各种3D场景。
•3D视图需要更强大的硬件支持,对性能要求较高。但可以创造出更加沉浸式的视觉体验。
•i3D Act实时三维引擎的3D引擎支持物理模拟、照明、材质、动画等3D项目开发所需的各种功能。
(7) 节点信号
节点信号是i3D Act实时三维引擎中一种非常强大的事件驱动机制,它允许节点之间通过信号连接的方式进行通信和交互,节点在发生某些事件时发出信号,无需在代码中硬连接它们就能让节点相互通信,为构建场景提供了灵活性。
2. i3D Act 的组成
图形模块、物理模块、音频模块、动画模块、导航模块、脚本模块(S3:与 Python 类似的语言;C#:一种安全、稳定的,由 C 和 C++衍生出来的面向对象的编程语言)
三、 入门体验
1. 节点与场景
在 i3D Act 的关键概述中,我们看到 i3D Act 就是由场景构成的树状结构,而每一个场景又是一个由节点构成的树状结构。在这一节中,我们将更详细地解释这些概念,你还将创建你的第一个场景。
(1) 节点
节点是你的项目的基本构件。它们就像食谱里的食材。i3D Act包含很多种节点,可以用来显示图像、播放声音、表示摄像机等等。
所有节点都具备以下特性:
·名称
·可编辑的属性
·每帧都可以接收回调以进行更新
·可以使用新的属性和函数来进行扩展
·可以将它们添加为其他节点的子节点
最后一个特征很重要。节点会组成一棵树,这个功能组织起项目来非常 强大。 因为不同的节点有不同的功能,将它们组合起来可以产生更复杂的 行为。
(2) 场景
当你在场景树中组织节点时,就像我们的角色一样,我们称之为场景构造。保存后,场景的工作方式类似于编辑器中的新节点类型,你可以在其中将它们添加为现有节点的子节点。在这种情况下,场景实例显示为隐藏其内部结构的单个节点。
场景允许你以你想要的方式来构造你的场景。你可以组合节点来创建自定义和复杂的节点类型,比如能跑能跳的场景角色、汽车、可以互动的箱子等等。
本质上,i3D Act编辑器就是一个场景编辑器。它有很多用于编辑3D场景以及用户界面的工具。i3D Act项目中可以包含任意数量你所需要的场景。引擎只要求将其中之一设为程序的主场景。这是你或者用户运行场景时,i3D Act最初加载的场景。
除了像节点一样工作之外,场景还具有以下特点:
1\.它们始终有一个根节点,就像我们示例中的“Player”一样。
2\.你可以把它们保存到你的硬盘上,以后再加载。
3\.你可以根据需要创建任意数量的场景实例。你的场景中可以有五个或十个角色,这些角色是从角色场景中创建的。
(3) 创建第一个场景
让我们只用一个节点来创建我们的第一个场景吧。首先你需要创建一个新项目 。在打开项目后,你看到的应该是一个空的编辑器。
在空场景中,左侧的“场景”停靠面板提供了几个快速添加根节点的选项。“3D场景”会添加Node3D 节点,“用户界面”会添加Control节点。这些预设是为了提供方便;不是强制选择的。“其他节点”可以选择任何节点作为根节点。在空场景中,“其他节点”等价于点击“场景”停靠面板左上角的“添加子节点”按钮,这个按钮的作用通常是为当前选中的节点添加一个新的子节点。
我们要往场景中添加一个Label 节点。它的功能是在屏幕上显示文字。点击“添加子节点”按钮或者“其他节点”,创建根节点。
“新建 Node”对话框打开,展示一大串可用节点。
选择Label节点。你可以输入这个名字来对列表进行过滤。
点击Label节点将其选中,然后点击窗口底部的“创建”按钮。
添加场景中的第一个节点时会发生很多事。场景会切换到2D工作区,因为Label是2D节点类型。该Label会以选中的状态出现在视图的左上角。这个节点也会出现在左侧的“场景”面板中,它的属性会出现在右侧的“检查器”面板里。
(4) 修改节点属性
下一步是修改Label的“Text”属性。我们把它改成“Hello World“。前往视图右侧的“检查器”面板。点击Text属性右方的字段,然后填入“Hello World”。
在你打字的同时,你会发现视图中也绘制出了这段文字。
你可以修改检查器列出的任何属性,正如我们对文本所作的那样。如果想要查看有关检查器面板的完整参考信息,详见检查器。
选择工具栏上的移动工具,就可以在视图中移动你的Label节点。
选中Label,点击并拖拽视图中的任何位置,将它移动到矩形框所表示的视图中心。
(5) 运行场景
运行场景一切就绪!请按下屏幕右上角的“运行场景”按钮或 F6按钮。
会有一个弹出框请你保存场景,这是运行这个场景前所必须做的。在文件浏览器中点击保存按钮将它另存为Label.iscn 。
备注:“场景另存为”对话框,和编辑器中的其他文件对话框一样,只允许你将文件保存在项目之中。窗口顶部的 res:// 路径表示项目的根目录,表示“ResourcePath”(资源路径)。
程序会打开一个新窗口,显示“Hello World”字样。
关闭窗口或按 F8 就可以退出正在运行的场景。
(6) 设置主场景
我们运行测试场景用的是“运行场景”按钮。它旁边的另一个按钮可以用来设置并运行项目的主场景。你也可以按 F5达到同样的效果。
出现弹出窗口让你选择主场景。
点击“选择”按钮,出现文件对话框,双击 label.iscn 。
演示程序又会开始运行。此后,每次你运行项目,i3D Act都会使用该场景作为起点。
辑器会将主场景的路径保存到项目目录的project.i3d文件中。你能够通过编辑这个文本文件来修改项目设置,但你也可以使用“项目->项目设置”窗口来达到同样的目的。详细请参阅项目设置。
2. 创建实例
上一部分中,我们了解到场景是一系列组织成树状结构的节点,其中只有一个节点是根节点。你可以将项目拆分成任意数量的场景。这一特性可以帮你将场景拆解成不同的组件,并进行组织。
你可以创建任意数量的场景并将他们保存成扩展名为 .iscn (“text scene”文本场景)的文件。上节课的 label.iscn 文件就是一个例子。我们把这些文件叫作“打包的场景”(Packed Scene),因为它们将场景的内容信息进行了打包。
这有一个小球的例子。它由以下内容组成:一个叫“Ball”的 RigBody2D 节点是根节点,可以让小球下落、在撞墙后反弹;一个 Sprite2D 节点以及一个 CollisionShape2D。
保存场景过后,这个场景就可以作为蓝图使用。你可以在其他场景中进行任意次数的重用。将对象根据模板进行重用的这一过程就叫作实例化。
(1) 实践
让我们来实践一下实例化,看看到底在i3D Act里是如何使用的。我们为你准备了小球的示例项目。
将存档解压到你的计算机里。你需要项目管理器来导入它。通过打开i3D Act来访问项目管理器,或者如果你已经打开了i3D Act,请单击 项目->退出到项目列表 。
在项目管理器中点击 导入 按钮来导入这个项目。
在弹出的窗口中,导航至你提取的文件夹。双击 project.i3d 文件打开它。
这个项目里包含两个打包场景:包含了小球会碰撞的墙体的 main.iscn ,以及 ball.iscn 。而Main场景应该会被自动打开。如果你看到的是空的3D场景而不是Main场景,请单击选中一个2D节点。
让我们为Main节点添加一个小球作为子节点。在“场景”面板中,选择Main节点。然后点击场景面板顶部的链接图标。这个按钮的作用是为当前选中节点添加另一个场景的实例作为子节点。
双击小球场景来实例化。
小球会出现在视口的左上角。
点击它,然后拖拽到视图的中心。
点击“运行场景”按钮或按 F5 (在macOS上是 Cmd + B )运行项目。你应该会看到它往下掉。
现在我们希望创建更多的Ball节点实例。保持小球仍处于选中的状态,按下 Ctrl + D(macOS则是 Cmd + D)调用制作副本命令。点击并将新的小球拖到别的位置。
你可以重复这个过程在场景中多建几个。
再次运行项目。现在你应该看到每个小球都各自下落。这就是实例的作用。每一个都是模板场景的独立副本。
(2) 编辑场景和实例
实例还有很多用法。使用独立副本这个特性,你可以:
使用“检查器”修改一个小球的属性,不影响其他实例。
打开 ball.iscn 场景修改Ball节点,从而修改所有Ball的默认属性。在保存时,项目中所有Ball的实例都会更新其属性值。
修改实例上的属性总是会覆盖对应打包场景中的值。
让我们来试一试。打开 ball.iscn 然后选中Ball节点。在右侧的“检查器”中,点击展开PhysicsMaterial属性。
将其Bounce(弹力)属性设为 0.5 ,只要点击对应的数字字段、输入 0.5 、然后按 Enter 就可以了。
按 F5 (在macOS中使用 Cmd+B)运行项目,请注意所有的小球都更有弹性了。因为Ball场景是所有实例的模板,对它进行修改并保存,就会导致所有实例同时进行更新。
进入main场景,选择一个Ball实例节点,然后“检查器”中将Gravity Scale(重力缩放)设为 8。
在被调整过的属性旁边就会多一个“复原”按钮。
这个图标表示你覆盖了源打包场景中的值。即使你修改了原始场景中的这个属性,这个覆盖后的值也还是会保留在这个实例中。点击复原图标会将属性恢复成保存场景中的值。
重新运行项目,请注意这个小球会比其他小球落得快得多。
你可能注意到了你无法改变小球 PhysicsMaterial 的值。这是因为 PhysicsMaterial 是一个资源,在你能在一个场景中编辑被原始场景所引用的资源之前,需要先把这个资源唯一化。要让某个实例的资源唯一,请在“检查器”中对其右键,然后在弹出的菜单中选择“唯一化”。
(3) 作为设计语言的场景实例
i3D Act中的实例和场景提供了一种优秀的设计语言,使该引擎与其他引擎不同。我们从一开始就围绕这个概念设计i3D Act。
我们建议在使用i3D Act制作项目时忽略架构代码模式,例如模型-视图-控制器(MVC)或实体关系图。相反,你可以从想象玩家将在项目中看到的元素开始,并围绕它们构建代码。
例如,你可以这样拆解一个射击项目:
对于几乎任何类型的项目,都可以想出这样的图表。矩形表示的是从玩家角度可以在项目中看到的实体,箭头表示的是场景之间的从属关系。
在得到这样的图之后,建议你为其中的每一个元素都创建一个场景。你可以通过代码或者直接在编辑器里将其实例化来构建你的场景树。
程序员们乐于花费大量时间来设计抽象的架构,尽力使得组件能够适用于这个架构。基于场景的设计取代了这种方法,使得开发更快、更直接,能够让你去专注于项目逻辑本身。因为大多数项目的组件都是直接映射成一个场景,所以使用基于场景实例化的设计意味着需要很少的其他架构代码。
这里是另一个更复杂的开放世界类项目的示例,这个示例包括有很多资产和嵌套元素:
想象一下,我们从创建房间开始。我们可以制作几个不同的房间场景,在其中有独特的家具安排。后来,我们可以制作一个房屋场景,在内部使用多个房间实例。我们将用许多实例化的房子和一个大的地形来创建一个城堡,我们将把城堡放在这个地形上。每一个场景都将是一个或多个子场景的实例。
之后,我们可以创建代表守卫的场景,将它们加到城堡之中。也就会间接地加到了项目世界里。
使用i3D Act,就可以很容易地像这样迭代你的项目,因为你需要做的就是创建并实例化更多的场景。我们将编辑器设计成了易于程序员、设计师、艺术家使用的形式。一个典型的团队开发过程会涉及2D或3D美术、关卡设计师、项目设计师、动画师等,他们都可以用i3D Act编辑器工作。
3. 创建第一个脚本
在本课中,你将用 S3Script 编写第一个脚本,使 i3D Act 图标转圈。正如我们本介绍中提到的,我们假设你有编程基础。方便起见,我们在单独的选项卡中包含了等价的 C#代码。
(1) 项目设置
请从头开始创建一个新项目。你的项目应该包含一张图片:i3D Act图标,我们经常在场景中使用它来制作原型。
我们需要创建一个Sprite2D节点来在场景中显示它。在“场景”面板中,点击“其他节点”按钮。
在搜索栏中输入“Sprite2D”来过滤节点,双击Sprite2D来创建节点。
你的“场景”选项卡现在应该只有一个Sprite2D节点。
Sprite2D节点需要用于显示的纹理。在右边的“检查器”中,你可以看到Texture(纹理)属性写着“[空]”。要显示i3D Act图标,请点击并拖拽“文件系统”面板中的 icon.svg 文件到Texture插槽上。
你可以通过将图像拖放到视图上来自动创建Sprite2D节点。
然后,点击并拖动视图中的图标,使其在场景视图中居中。
(2) 新建脚本
在场景面板的Sprite2D上点击右键并选择“添加脚本”,来创建或附加一个新的脚本到我们的节点上。
弹出“添加节点脚本”窗口。你可以选择脚本的语言和文件路径,以及其他选项。
把模板字段从“Node:Default”改为“Object:Empty”从而得到一个干净的脚本文件。其他选项保持默认,然后点击“创建”按钮来创建脚本。
C#脚本名需要与它们的类名匹配。在本例中,您应该将文件命名为MySprite2D.cs。
此时Script工作区将自动打开并显示你新建的 sprite\_2d.s3 文件,显示以下代码行:
extends Sprite2D
在S3Script中,如果你没有写带有 extends 关键字的一行,你的类将隐式地扩展自RefCounted,i3D Act使用这个类来管理你的应用程序的内存。
继承的属性包括你可以在“检查器”面板中看到的属性,例如节点的 texture。
“检查器”默认使用“Title Case”形式展示节点的属性,将单词的首字母大写、用空格分隔。在S3Script代码中,这些属性使用的是“snake\_case”,全小写、单词之间使用下划线分隔。
你可以在检查器中悬停任何属性的名称来查看它的描述和在代码中的标识符。
(3) 你好,世界!
我们的脚本目前没有做任何事情。让我们开始打印文本“Hello, world!”到底部输出面板。
往脚本中添加以下代码:
S3Script
func _init():
print("Hello, world!")
C#
public MySprite2D(){
S3.Print("Hello, world!");
}
让我们把它分解一下。 func 关键字定义了一个名为 _init 的新函数。这是类构造函数的一个特殊名称。如果你定义了这个函数,引擎会在内存中创建每个对象或节点时调用 _init() 。
S3Script是基于缩进的语言。行首的制表符是 print() 代码正常工作的必要条件。如果你省略了它或者没有正确缩进一行,编辑器将以红色高亮显示,并显示以下错误信息:“Indented block expected”(应有缩进块)。
如果你还没有保存场景为 sprite_2d.iscn,请保存,然后按 “运行场景”来运行它。看一下底部展开的输出面板。它应该显示“Hello, world!”。
然后将 _init() 函数删除,接着开始下一步。
(4) 旋转移动
是时候让我们的节点移动和旋转了。为此,我们将向脚本中添加两个成员变量:以像素每秒为单位的移动速度,和以弧度每秒为单位的角速度。将下述内容添加到 extends Sprite2D 行的后面。
S3Script
var speed =400
var angular_speed=PI
C#
private int _speed = 400;
private float _angularSpeed = Mathf.Pi;
成员变量位于脚本的顶部,在“extends”之后、函数之前。附加了此脚本的每个节点实例都将具有自己的 speed 和 angular\_speed 属性副本。
与其他一些引擎一样,i3D Act中的角度默认使用弧度为单位,但如果你更喜欢以度为单位计算角度,则可以使用内置函数和属性。
为了移动我们的图标,我们需要在场景循环中每一帧更新其位置和旋转。我们可以使用 Node 类中的虚函数 _process() 。如果你在任何扩展自Node类的类中定义它,如Sprite2D,i3D Act将在每一帧调用该函数,并传递给它一个名为 delta 的参数,即从上一帧开始经过的时间。
i3D Act引擎开发者尽最大努力以恒定的时间间隔更新场景世界和渲染图像,但在帧的渲染时间上总是存在着微小的变化。这就是为什么引擎为我们提供了这个delta时间值,使我们的运动与我们的帧速率无关。
在脚本的底部,定义该函数:
S3Script
func _process(delta):
rotation += angular_speed * delta
C#
public override void Process(double delta){
Rotation +=angularSpeed * (float)delta;
}
func 关键字定义了一个新函数。在它之后,我们必须写上函数的名称和括号里它所接受的参数。冒号结束定义,后面的缩进块是函数的内容或指令。
请注意 _process() 和 _init() 一样都是以下划线开头的。按照约定,这是i3D Act的虚函数,也就是你可以覆盖的与引擎通信的内置函数。
函数内部的那一行 rotation += angular_speed * delta 每一帧都会增加我们的精灵的旋转量。这里 rotation 是从 Sprite2D 所扩展的 Node2D 类继承的属性。它可以控制我们节点的旋转,以弧度为单位。
在C#中,请注意 \_Process() 所采用的 delta 参数类型是 double 。故当我们将其应用于旋转时,需要将其转换为 float 。
现在我们来让节点移动。在 _process() 函数中添加下面两行代码,确保每一行都和之前的 rotation += angular_speed * delta 行的缩进保持一致。
S3Script
var velocity
Velocity = Vector2.UP.rotated(rotation) * speed
position += velocity * delta
C#
var velocity=Vector2.Up.Rotated(Rotation) * _speed;
Position += velocity * (float)delta;
正如我们所看到的,var 关键字可以定义新变量。如果你把它放在脚本顶部,定义的就是类的属性。在函数内部,定义的则是局部变量:只在函数的作用域中存在。
我们定义一个名为 velocity 的局部变量,该变量是用于表示方向和速度的2D向量。要让节点向前移动,我们可以从Vector2类的常量 Vector2.UP 入手,这个向量指向上方,调用 Vector2 的 rotated() 方法可以将其进行旋转。表达式 Vector2.UP.rotated(rotation) 表示的是指向图标前方的向量。用这个方向与我们的 speed 属性相乘后,得到的就是用来移动节点的速度。
我们在节点的 position 里加上 velocity * delta 来实现移动。位置本身是Vector2 类型的,是i3D Act用于表示2D向量的内置类型。
运行场景就可以看到i3D Act图标在绕圈圈。
使用这样的方法不会考虑与墙壁和地面的碰撞。在你的第一个2D场景中,你会学到另一种移动对象的方法,可以检测碰撞。
我们的节点目前是自行移动的。在下一部分监听玩家的输入 中,我们会让玩家的输入来控制它。
(5) 完整脚本
这是完整的 sprite_2d.s3 文件,仅供参考。
extends Sprite2D
var speed = 400
var angular_speed = PI
func _process(delta):
rotation += angular_speed * delta
var velocity = Vector2.UP.rotated(rotation) * speed
position += velocity * delta
4. 按键监听
在上一课 创建第一个脚本 的基础上,让我们看看任何场景的另一个重要特征:将控制权交给玩家。为了增加这一点,我们需要修改 sprite_2d.s3 的代码。
在 i3D Act 中,你有两个主要工具来处理玩家的输入:
1.内置的输入回调,主要是 _unhandled_input()。像 _process(),它是一个内置的虚函数,i3D Act 每次在玩家按下一个键时都会调用。它是你想用来对那些不是每一帧都发生的事件做出反应的工具,比如按 Space 来跳跃。要了解更多关于输入回调的信息,请参阅使用 InputEvent 。
2.Input 单例。单例是一个全局可访问的对象。i3D Act 在脚本中提供对几个对象的访问。它是每一帧检查输入的有效工具。
我们这里将使用 Input 单例,因为我们需要知道在每一帧中玩家是否想转身或者移动。
对于转弯,我们应该使用一个新的变量:direction。在我们的 _process() 函数中,将 rotation += angular_speed * delta 替换成以下代码。
var direction = 0
if Input.is_action_pressed("ui\_left"):
` `direction = -1
if Input.is_action_pressed("ui_right"):
` `direction = 1
rotation += angular_speed * direction * delta
我们的 direction 局部变量是一个乘数,代表玩家想要转向的方向。0 的值表示玩家没有按左或右方向键。1 表示玩家想向右转,而 -1 表示他们想向左转。
为了产生这些值,我们引入了 if 判断和 Input 的使用。if 判断以 S3Script 中的 if 关键字开始,以冒号结束。判断条件是关键字和行末之间的表达式。
为了检查当前帧玩家是否按下了某个键,我们需要调用 Input.is_action_pressed()。这个方法使用一个字符串来表示一个输入动作。当该按键被按下时,函数返回 true,否则这个函数将返回 false。
上面我们使用的两个动作,“ui_left”和“ui_right”,是每个 i3D Act 项目中预定义的。它们分别在玩家按键盘上的左右箭头或场景手柄上的左右键时触发。
打开“项目->项目设置”并点击“输入映射”选项卡,就可以查看并编辑项目中的输入动作。
最后,当我们更新节点的 rotation 时,我们使用 direction 作为乘数:rotation += angular_speed direction delta。
如果你用这段代码运行场景,当你按下 Left(左方向键)和 Right(右方向键)时,图标应该会旋转。
(1) 向上移动
为了在只有按下一个键时移动,我们需要修改计算速度的代码。用下面的代码替换从 var velocity 那一行开始的代码。
var velocity = Vector2.ZERO
if Input.is_action_pressed("ui_up"):
velocity = Vector2.UP.rotated(rotation) * speed
我们将 velocity 的值初始化为 Vector2.ZERO,这是内置 Vector 类型的一个常量,代表长度为0的二维向量。
如果玩家按下“ui_up”动作,我们就会更新速度的值,使精灵向前移动。
(2) 完整脚本
这是完整的 sprite_2d.s3 文件,仅供参考。
func _process(delta):
var direction = 0
if Input.is\_action\_pressed("ui\_left"):
direction = -1
if Input.is\_action\_pressed("ui\_right"):
direction = 1
rotation += angular\_speed \* direction \* delta
var velocity = Vector2.ZERO
if Input.is\_action\_pressed("ui\_up"):
velocity = Vector2.UP.rotated(rotation) \* speed
position += velocity * delta
如果你运行这个场景,你现在应该能够用左右方向键进行旋转,并通过按 Up 向前移动。
总之,i3D Act中的每个脚本都代表一个类,并扩展了引擎的一个内置类。在我们sprite的例子中,你的类所继承的节点类型可以让你访问一些属性,例如在“精灵”例子中的 rotation 和 position 。你还继承了许多函数,但我们在这个例子中没有使用这些函数。
在S3Script中,放在文件顶部的变量是类的属性,也称为成员变量。除了变量之外,你还可以定义函数,在大多数情况下,这些函数将是类的方法。
i3D Act提供了几个虚函数,你可以定义这些函数来将类与引擎连接起来。其中包括 _process() ,用于每帧将更改应用于节点,以及 _unhandled_input() ,用于接收用户的输入事件,如按键和按钮。还有很多。
Input 单例允许你在代码中的任何位置对玩家的输入做出反应。尤其是,你在 _process() 循环中使用它。
5. 使用信号
在本课中,我们将介绍信号。它们是节点在发生特定事件时发出的消息,例如按下按钮。其他节点可以连接到该信号,并在事件发生时调用函数。
信号是 i3D Act 内置的委派机制,允许一个场景对象对另一个场景对象的变化做出反应,而无需相互引用。使用信号可以限制耦合,并保持代码的灵活性。
例如,你可能在屏幕上有一个代表玩家生命值的生命条。当玩家受到伤害或使用治疗药水时,你希望生命条反映变化。要做到这一点,在 i3D Act 中,你会使用到信号。
现在,我们将使用信号来让上一节课(按键监听)中的 i3D Act 图标移动,并通过按下按钮来停止。
(1) 场景设置
要为我们的场景添加按钮,我们需要新建一个“主”场景,包含一个按钮以及之前课程创建第一个脚本 编写的 sprite_2d.iscn 场景。
通过转到菜单“场景->新建场景”来创建新场景。
在场景面板中,单击“2D 场景”按钮。这样就会添加一个Node2D 作为我们的根节点。
在文件系统面板中,单击之前保存的 sprite\_2d.iscn 文件并将其拖动到Node2D上,对其进行实例化。
我们想要添加另一个节点作为Sprite2D的同级节点。为此,请右键单击Node2D,然后选择“添加子节点”。
查找并添加Button 节点。
该节点默认比较小。在视图中,点击并拖拽该按钮右下角的手柄来调整大小。
如果看不到手柄,请确保工具栏中的选择工具处于活动状态。
点击并拖拽按钮使其更接近精灵。
你可以通过修改检查器中的Text属性来给Button上写一个标签。请输入Toggle motion。
你的场景树和视图应该是类似这样的。
如果你还没保存场景的话,保存新建的场景为 node_2d.iscn。然后你就可以使用 F6(macOS 则为 :kbd:Cmd + R)来运行。此时,你可以看到按钮,但是按下之后不会有任何反应。
(2) 快捷连接信号
然后,我们希望将按钮的“pressed”信号连接到我们的Sprite2D,并且我们想要调用一个新函数来打开和关闭其运动。我们需要像我们在上一课中所做的操作一样,将一个脚本附加到Sprite2D节点。
你可以在“节点”面板中连接信号。选择Button节点,然后在编辑器的右侧,单击检查器旁边名为“节点”的选项卡。
停靠栏显示所选节点上可用的信号列表。
双击“pressed”信号,打开节点连接窗口。
然后,你可以将信号连接到Sprite2D节点。该节点需要一个用于接收按钮信号的函数,当按钮发出信号时,i3D Act将调用该函数。编辑器会为你生成一个。按照规范,我们将这些回调方法命名为"_on_node_name_signal_name"。在这里,它被命名为"_on_button_pressed"。
通过编辑器的节点面板连接信号时,可以使用两种模式。简单的一个只允许你连接到附加了脚本的节点,并在它们上面创建一个新的回调函数。
你可以在高级视图中连接到任何节点和任何内置函数、向回调添加参数、设置选项。你可以单击窗口右下角的“高级”按钮来切换模式。
单击“连接”按钮以完成信号连接并跳转到脚本工作区。你应该会看到新方法,并在左边距中带有连接图标。
如果单击该图标,将弹出一个窗口并显示有关连接的信息。此功能仅在编辑器中连接节点时可用。
让我们用代码替换带有 pass 关键字的一行,以切换节点的运动。
我们的Sprite2D由于 _process() 函数中的代码而移动。i3D Act提供了一种打开和关闭处理的方法:Node.set_process()。Node的另一个方法 is_processing() ,如果空闲处理处于活动状态,则返回 true。我们可以使用 not 关键字来反转该值。
extends Sprite2D
var speed = 400
var angular_speed = PI
func _on_button_pressed():
set_process(not is_processing())
此函数将切换处理,进而切换按下按钮时图标的移动。
func _process(delta):
rotation += angular_speed * delta
var velocity = Vector2.UP.rotated(rotation) * speed
position += velocity * delta
你的完整的 Sprite_2d.s3 代码应该是类似下面这样的。
extends Sprite2D
var speed = 400
var angular_speed = PI
func _process(delta):
rotation += angular_speed * delta
var velocity = Vector2.UP.rotated(rotation) * speed
position += velocity * delta
func _on_button_pressed():
set_process(not is_processing())
运行该场景,然后点击按钮,就可以看到精灵开始或停止运行。
(3) 脚本连接信号
你可以通过代码连接信号,而不是使用编辑器。这在脚本中创建节点或实例化场景时是必需的。
让我们在这里使用一个不同的节点。i3D Act有一个 Timer 节点,可用于实现技能冷却时间、武器重装等。
回到2D工作区。你可以按 Ctrl + F1(macOS上则是 Ctrl + Cmd + 1)。
在“场景”面板中,右键点击Sprite2D节点并添加新的子节点。搜索Timer并添加对应节点。你的场景现在应该类似这样。
选中Timer节点,在“检查器”中勾选 Autostart 属性。
点击Sprite2D旁的脚本图标,返回脚本工作区。
我们需要执行两个操作来通过代码连接节点:
从Sprite2D获取Timer的引用。
通过Timer的“timeout”信号调用 connect() 方法。
要使用代码来连接信号,你需要调用所需监听节点信号的 connect() 方法。这里我们要监听的是Timer的“timeout”信号。
我们想要在场景实例化时连接信号,我们可以使用Node.ready()内置函数来实现这一点,当节点完全实例化时,引擎会自动调用该函数。
为了获取相对于当前节点的引用,我们使用方法Node.get_node()。我们可以将引用存储在变量中。
func _ready():
var timer = get_node("Timer")
get_node() 函数会查看Sprite2D的子节点,并按节点的名称获取节点。例如,如果在编辑器中将Timer节点重命名为“BlinkingTimer”,则必须将调用更改为 get_node("BlinkingTimer")。
func _ready():
var timer = get_node("Timer")
timer.timeout.connect(_on_timer_timeout)
现在,我们可以在 _ready() 函数中将Timer连接到Sprite2D。
该行读起来是这样的:我们将计时器的“timeout”信号连接到脚本附加到的节点上。当计时器发出“timeout”时,去调用我们需要定义的函数_on_timer_timeout()。让我们将其定义添加到脚本的底部,并使用它来切换sprite的可见性。
按照惯例,我们将这些回调方法在S3Script中命名为“_on_node_name_signal_name”,在C#中命名为“OnNodeNameSignalName”。故此处的S3Script为“_on_timer_timeout”,C#为“OnTimerTimeout()”。
func _on_timer_timeout():
visible = not visible
visible 属性是一个布尔值,用于控制节点的可见性。visible = not visible 行切换该值。如果 visible是true,它就会变成 false,反之亦然。
如果你现在运行场景,就会看到精灵在闪啊闪的,间隔为一秒。
(4) 完整脚本
这就是我们小小的i3D Act图标移动闪烁演示了!这是完整的 sprite_2d.s3 文件,仅供参考。
extends Sprite2D
var speed = 400
var angular_speed = PI
func _ready():
var timer = get_node("Timer")
timer.timeout.connect(_on_timer_timeout)
func _process(delta):
rotation += angular_speed * delta
var velocity = Vector2.UP.rotated(rotation) * speed
position += velocity * delta
func _on_button_pressed():
set_process(not is_processing())
func _on_timer_timeout():
visible = not visible
(5) 自定义信号
本节介绍的是如何定义并使用你自己的信号,不依赖之前课程所创建的项目。
你可以在脚本中定义自定义信号。例如,假设你希望在玩家的生命值为零时通过屏幕显示场景结束。为此,当他们的生命值达到0时,你可以定义一个名为 “died”或“health_depleted”的信号。
extends Node2D
signal health_depleted
var health = 10
自定义信号的工作方式与内置信号相同:它们显示在“节点”选项卡中,你可以像连接其他信号一样连接到它们。
要通过代码发出信号,请调用信号的 emit() 方法。
func take_damage(amount):
health -= amount
if health <= 0:
health_depleted.emit()
信号还可以选择声明一个或多个参数。在括号之间指定参数的名称:
extends Node2D
signal health_changed(old_value, new_value)
var health = 10
这些信号参数显示在编辑器的节点停靠面板中,i3D Act可以使用它们为你生成回调函数。但是,发出信号时仍然可以发出任意数量的参数;所以由你来决定是否发出正确的值。
要在发出信号的同时传值,请将它们添加为 emit() 函数的额外参数。
func take_damage(amount):
var old_health = health
health -= amount
health_changed.emit(old_health, health)
四、 脚本语言
本课将向你介绍 i3D Act 中可用的脚本语言。你将了解每个选项的优点和缺点。在下一部分,你将使用 S3Script 编写你的第一个脚本。
脚本附加到节点并扩展其行为。这意味着脚本继承所附加节点的全部函数和属性。
1. 四种编程语言
i3D Act 提供了四种编程语言:S3Script、C# 以及通过 S3Extension 技术提供的 C 和 C++。还有更多社区支持的语言,但这四个是官方所支持的语言。
你可以在一个项目中使用多种语言。例如,在团队中,你可以在 S3Script 中编写逻辑,编写起来很快,然后使用 C# 或 C++ 来实现复杂的算法,最大限度地提高其性能。你也可以使用 S3Script 或 C# 来编写所有内容。这些都由你自己决定。
我们提供这种灵活性以满足不同项目和开发者的需求。
(1) 我应该使用哪种语言?
如果你是初学者,我们推荐从S3Script入手。这门语言是我们针对i3D Act和场景开发者的需求制作的。语法简单直白,与i3D Act结合得最为紧密。 使用C#时,你需要使用 VSCode 或Visual Studio等外部编辑器。虽然对C#支持目前已经成熟,但相对S3Script而言,能找到的学习资源会比较少。因此,我们主要推荐已经熟悉C#语言的用户去使用C#。
让我们来看看各个语言的特性,以及优缺点。
(2) S3Script
S3Script是一门面向对象的指令式编程语言,专为i3D Act构建。是场景开发者为场景开发者制作的,目的是节省编写场景代码的时间。它的特性包括:
·简单的语法,让文件更短。
·极快的编译和加载速度。
·紧密的编辑器集成,包括节点、信号、所附加场景更多信息的代码补全。
·内置向量和变换类型,让海量线性代数计算更高效,场景必备。
·支持多线程,与静态类型的语言一样高效。
·没有垃圾回收,因为最终会影响场景的开发。引擎会默认进行引用计数,在大多数情况下为你管理内存,但你也可以在需要时自行控制内存。
·渐进类型,变量默认是动态类型,但你也可以使用类型提示来做强类型检查。
(3) .NET/C#
·C#是一门成熟灵活的语言,拥有大量的库。
·C#在性能和易用性之间提供了不错的权衡,不过你应该了解它的垃圾回收器。
(4) 通过 S3Extension 使用 C++
C++(c plus plus)是一种计算机高级程序设计语言,由[C语言]扩展升级而产生 ,最早于1979年由[本贾尼·斯特劳斯特卢普]在AT&T贝尔工作室研发。
C++既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。
C++擅长面向对象程序设计的同时,还可以进行基于过程的程序设计。C++几乎可以创建任何类型的程序:游戏、备驱动程序、HPC、桌面、嵌入式和移动应用等。甚至用于其他编程语言的库和编译器也使用C++编写。
C++拥有计算机运行的实用性特征,同时还致力于提高大规模程序的编程质量与程序设计语言的问题描述能力。
在i3D Act中不能直接使用C++语言,但是可以通过S3Extension简介使用。
2. S3Script
S3Script 是一种面向对象的高级指令式渐进类型编程语言,专为 i3D Act 构建。S3Script 的语法基于缩进,与 Python 等语言类似。设计 S3Script 这门语言旨在对 i3D Act 引擎进行优化,与 i3D Act 引擎紧密集成,从而为内容的创建与继承提供灵活的手段。
(1) S3Script 参考
考虑到部分开发者了解过编程语法,学起S3Script来可能会快些,因此这里给出一个S3Script的简单示例供参考学习。
\# 在 "#" 后面的都是注释。
\# 一个文件就是一个类!
\# (可选) 在编辑器中展示的图标:
@icon("res://path/to/optional/icon.svg")
#(可选) 类定义:
class\_name MyClass
\# 继承:
extends BaseClass
\# 变量:
var a = 5
var s = "Hello"
var arr = [1, 2, 3]
var dict = {"key": "value", 2: 3}
var other\_dict = {key = "value", other\_key = 2}
var typed\_var: int
var inferred\_type := "String"
\# 常数:
const ANSWER = 42
const THE\_NAME = "Charly"
\# 枚举:
enum {UNIT\_NEUTRAL, UNIT\_ENEMY, UNIT\_ALLY}
enum Named {THING\_1, THING\_2, ANOTHER\_THING = -1}
\# 内置向量类:
var v2 = Vector2(1, 2)
var v3 = Vector3(1, 2, 3)
\# 函数。
func some\_function(param1, param2, param3):
` `const local\_const = 5
` `if param1 < local\_const:
` `print(param1)
` `elif param2 > 5:
` `print(param2)
\# 构造函数
func \_init():
` `print("Constructed!")
` `var lv = Something.new()
` `print(lv.a)
(2) S3Script 动态语言入门
本教程旨在快速介绍如何更有效地使用S3Script,且只关注特定于该语言的常见情况,同时也会涉及许多关于动态类型语言的信息。
对于完全没有或几乎没有动态类型语言编程经验的程序员而言,本教程将会十分有用。
1. **动态类型的优缺点**
S3Script是一种动态类型语言,其主要优点为:
·语言简单易学。
·大部分代码均可进行快速编写与更改,无任何麻烦。
·更少的代码编写量,需要修复的错误也更少。
·代码简单易读(但可能会有些许杂乱)。
·无需编译,即用即测。
·运行时(Runtime)小。
·从骨子上就注定了会有鸭子类型和多态这两个特性。
主要缺点有:
·性能要低于静态类型语言。
·重构会更加困难(无法追踪符号)。
·由于表达式解析更为严格,使得一些通常会在静态类型语言编译时可以检测到的错误只会在运行代码时出现。
·由于某些变量的类型只能在运行时确定,导致代码补全的灵活性较低。
2. **变量与赋值**
动态类型语言中的所有变量都可以是“var”类型的变量,即这些变量的类型不是固定的,只能通过赋值修改。例如:
静态类型编写示例:
int a; //数值未定
a=5; //有效赋值
a=“Hi!“; //无效赋值
动态类型编写示例:
var a #默认为”null”
a=5 #有效a成为整数,变量a=“Hi!”#有效,a 成为字符串
3. **函数参数的动态类型化**
函数也是动态类型的,即这些函数可以用不同类型的参数调用,例如:
静态类型编写示例:
void print_value(int value) {
` `printf("value is %i\n", value);
}
[..]
print_value(55); //有效
print_value("Hello"); //无效.
动态类型编写示例:
func print_value(value):
` `print(value)
[..]
print_value(55) # 有效
print_value("Hello") # 有效
在S3Script中,只有基础类型(int、float、string和向量类型)会按值传递给函数(通过复制值来传递),而其他所有类型(对象实例、数组、字典等)都会按引用进行传递。继承自RefCounted 的类(未指定父类时会默认继承该类)的实例在不被使用时释放,而对于继承自Object 的类则需要手动管理内存。
4. **数组**
动态类型语言中的数组,其内部可包含许多混合在一起的不同类型的数据,且始终动态(可以随时调整大小)。拿静态类型语言中的数组示例作个比较:
int *array = new int[4]; // 创建数组
array[0]= 10;// 手动初始化
array[0]= 20;/ 不能混入其他类型
array[0]= 40;
array[0]= 60;// 无法重新设置数组大小
use\_array(array); //用作指针
deletel array; //必须被释放
//or
std: vector<int> array;
array.resize(4);
array[0]= 10;// 手动初始化
array[0]= 20; // 不能混入其他类型
array[0]= 40;
array[0]= 60;
array.resize(3); //可以重新设置数组大小
use\_array(array); // 用作参考或值
//在stack结束时释放
而在S3Script中:
var array=[10,“hello”,40,60]#可以混合类型
array.resize(3)# 可以重新设置数组大小
use\_array(array)#用作参考
//不再需要释放
在动态类型语言中,数组还可兼作其他数据类型使用,比如列表:
var array = []
array.append(4)
array.append(5)
array.pop \_front()
或无序集合:
var a=20
if a in [10, 20,30]:
` `print("We have a winner!")
5. **字典**
字典是动态类型语言中一个十分强大的工具。大多数用静态类型语言(例如C++或C#)编写代码的程序员都忽略了字典的存在,而不必要地增加了他们的工作难度。字典这种数据类型通常不存在于此类语言中(或仅以受限的形式出现)。
字典可以完全忽略键或值的数据类型,从而将任意一个值映射到其他值上。由于这些映射可以通过哈希表实现,因此字典十分高效,这一点与目前流行的观点相反。事实上,由于字典的高效性,在一些编程语言里甚至可以用数组的方式来实现字典。
字典示例:
var d ={"name": "John","age": 22}
print("Name: ", d["name"]," Age: ",d["age"])
字典也是动态的,可随时添加或删除一个键,但性能开销很低:
d["mother"] = "Rebecca" #补充说明
d["age"] = 11 # 修改
d.erase("name")# 移除
大多数情况下,使用字典可以更容易地实现二维数组。
6. **For 循环**
S3Script在可迭代项上使用for循环,配合in关键字来指定迭代范围:
for s in strings:
print(s)
容器数据类型(数组和字典)是可迭代的,其中,字典允许通过键来进行迭代:
for key in dict:
` `print(key,"->",dict[key])
迭代索引也是可以的:
for i in range(strings.size()):
` `print(strings[i])
C语言风格的一些for循环示例:
for(inti=0;i< 10;i++){}
for(inti=5;i< 10;i++){}
for(inti=5;i< 10;i += 2){}
用动态类型语言翻译一下:
for i in range(10):
` `pass
for i in range(5, 10):
` `pass
for i in range(5,10,2):
` `Pass
7. **While循环**
while()循环的用法在任何地方都是相同的:
var i = 0
while i< strings.size():
` `print(strings[i])
` `i += 1
(3) S3Script 导出属性
在 i3D Act 中,你可以导出类成员,其值会与其所附加的资源(例如场景)一起保存,也可以在属性编辑器中进行编辑。导出使用 @export 注解来实现。
@export var number: int = 5
在上面这个例子中,数值 5 会保存起来,并在属性编辑器中显示。
导出变量必须使用常量表达式来进行初始化,部分导出注解具有特殊类型,对变量类型不作要求(请参见下面的示例部分)。
导出成员变量的基本好处之一,便是让这些变量在编辑器中可见可改,这样一来,美术师和场景设计师就可以修改这些会影响程序运行方式的值。为此,i3D Act 提供了一种特殊的导出语法。
- 基本用法
如果为导出变量赋予了常量值或常量表达式值,则可以对该变量的值进行类型推断,并使该变量在编辑器中可用。
@export var number = 5
@export var number: int
如果导出变量没有默认值,那么你可以为该变量添加类型限定。
可以导出资源和节点。
@export var resource: Resource
@export var node: Node
2.导出项分组
可以使用@export\_group 注解来在检查器中对导出属性进行分组管理,在该注解后的每个导出变量均会被添加到该分组中。用一个新分组开头,或使用 @export\_group("") 开头会结束之前的分组的作用范围。
@export\_group("My Properties")
@export var number = 3
该注解的第二个参数仅囊括名称以该参数开头的导出变量。
导出项分组无法嵌套定义,需要用@export\_subgroup 来在一个大的导出项分组里定义一个小分组。
@export\_subgroup("Extra Properties")
@export var string = ""
@export var flag = false
你还可以使用@export\_category 注解来更改主类别的名称,亦或在属性列表中创建其他类别。
@export\_category("Main Category")
@export var number = 3
@export var string = ""
@export\_category("Extra Category")
@export var flag = false
(4) S3Script 文档注释
S3Script 文档注释也可以用来为你的代码中的成员提供文档说明。普通注释和文档注释有两点区别,其一:文档注释均以 ## 开头;其二:文档注释必须写在成员声明之前,亦或写于脚本顶部来为脚本提供基本说明。若使用文档注释注释了导出变量,则可在编辑器中通过悬停窗来查看该导出变量的文档注释。文档注释也可以由编辑器生成为 XML 文档。
为脚本编写文档
使用文档注释编写脚本文档时,必须写于成员声明之前。这里推荐大家将脚本文档分为以下三块编写。
·脚本类概述。
·脚本类详细描述。
·教程与特殊标记——废弃标记/实验性标记。
为了将这些部分区分开来,文档注释采用了一些特殊标记。特殊标记须写于文本行开头(忽略其之前的空格),且需要以 @ 符号开头,后接标记名。
例如:
extends Node2D
## 该类的作用和功能的简要描述。
##
## 脚本的描述,它的功能,
## 以及任何进一步的详细信息。
##
## @tutorial:https://the/tutorial1/url.com
## @tutorial(Tutorial2): https://the/tutorial2/url.com
## @experimenta
(5) S3Script 编写风格指南
本编写风格指南列出了几条规定,能够让用户将 S3Script 代码编写得更加优雅。本指南旨在让用户编写更为整洁、可读的代码,促进项目、讨论和教程之间的一致性。本教程也希望能够因此鼓励开发者们能够开发出来代码自动格式化工具。
由于 S3Script 与 Python 非常接近,因此本指南的灵感来自 Python 的 PEP8 编程风格指南。
风格指南并不是硬性的规则教条,有些情况下,你可能无法施行下面的一些规范。如果这种情况发生在你身上,最好自行进行选择,并询问其他开发人员的见解。
一般来说,在项目和团队中保持代码风格的一致性,比一板一眼地遵循本指南更为重要。
下面是基于这些规范的完整的类的示例:
class_name StateMachine
extends Node
## 玩家使用的分层状态机
##
## 初始化状态并将引擎回调([method Node._physics_process]、
## [method Node._unhandled_input])委托给状态处理。
signal state_changed(previous, new)
@export var initial_state: Node
var is_active = true:
set = set_is_active
@onready var _state = initial_state:
set = set_state
@onready var _state_name = _state.name
func _init():
add_to_group("state_machine")
func _enter_tree():
print("this happens before the ready method!")
func _ready():
state_changed.connect(_on_state_changed)
_state.enter()
func _unhandled_input(event):
_state.unhandled_input(event)
func _physics_process(delta):
_state.physics_process(delta)
func transition_to(target_state_path, msg={}):
if not has_node(target_state_path):
return
var target_state = get_node(target_state_path)
assert(target_state.is_composite == false)
_state.exit()
self._state = target_state
_state.enter(msg)
Events.player_state_changed.emit(_state.name)
func set_is_active(value):
is_active = value
set_physics_process(value)
set_process_unhandled_input(value)
set_block_signals(not value)
func set_state(value):
_state = value
_state_name = _state.name
func _on_state_changed(previous, new):
print("state changed")
state_changed.emit()
class State:
var foo = 0
func _init():
print("Hello!")
格式
编码与特殊字符
·使用换行符(LF)换行,而非 CRLF 或 CR。(编辑器默认)
·在每个文件的末尾使用一个换行符。(编辑器默认)
·使用不带字节顺序标记的 UTF-8 编码。(编辑器默认)
·使用制表符代替空格进行缩进。(编辑器默认)
缩进
每个缩进的缩进级别必须大于包含该缩进的代码块的缩进级别。
规范示例 :
for i in range(10):
print("hello")
不规范示例 :
for i in range(10):
print("hello")
for i in range(10):
print("hello")
使用 2 个缩进级别来区分续行代码块与常规代码块。
规范示例 :
effect.interpolate_property(sprite, "transform/scale",
sprite.get_scale(), Vector2(2.0, 2.0), 0.3,
Tween.TRANS_QUAD, Tween.EASE_OUT)
不规范示例 :
effect.interpolate_property(sprite, "transform/scale",
sprite.get_scale(), Vector2(2.0, 2.0), 0.3,
Tween.TRANS_QUAD, Tween.EASE_OUT)
此规则的例外:数组、字典和枚举。使用单个缩进级别来区分续行代码块:
var party = [
"I3D Act",
"Steve",
]
var character_dict = {
"Name": "Bob",
"Age": 27,
"Job": "Mechanic",
}
enum Tiles {
TILE_BRICK,
TILE_FLOOR,
TILE_SPIKE,
TILE_TELEPORT,
}
var party = [
"I3D Act",
"I3D Act",
"Steve",
]
var character_dict = {
"Name": "Bob",
"Age": 27,
"Job": "Mechanic",
}
enum Tiles {
TILE_BRICK,
TILE_FLOOR,
TILE_SPIKE,
TILE_TELEPORT,
}
规范示例 :
var party = [
"I3D Act",
"I3D Act",
"Steve",
]
var character_dict = {
"Name": "Bob",
"Age": 27,
"Job": "Mechanic",
}
enum Tiles {
TILE_BRICK,
TILE_FLOOR,
TILE_SPIKE,
TILE_TELEPORT,
}
不规范示例 :
行尾逗号
请在数组、字典和枚举的最后一行使用逗号,这样,在添加新元素时就不需要修改最后一行了,既能让版本控制中的重构更容易,也会让 Diff 也更美观。
规范示例 :
enum Tiles {
TILE_BRICK,
TILE_FLOOR,
TILE_SPIKE,
TILE_TELEPORT,
}
不规范示例 :
enum Tiles {
TILE_BRICK,
TILE_FLOOR,
TILE_SPIKE,
TILE_TELEPORT
}
单行列表中不需要行尾逗号,故在此情况下不要添加逗号。
规范示例 :
enum Tiles {TILE_BRICK, TILE_FLOOR, TILE_SPIKE, TILE_TELEPORT}
不规范示例 :
enum Tiles {TILE_BRICK, TILE_FLOOR, TILE_SPIKE, TILE_TELEPORT,}
空白行
用两个空行来包围函数和类定义:
func heal(amount):
health += amount
health = min(health, max_health)
health_changed.emit(health)
func take_damage(amount, effect=null):
health -= amount
health = max(0, health)
health_changed.emit(health)
函数内部使用一个空行来分隔每个逻辑片段。
布尔运算
推荐使用英文布尔运算符,简单易懂:
·用 and 代替 &&。
·用 or 代替 ||。
·用 not 代替 !。
也可以在布尔运算符周围使用括号来消除歧义,这样还可以使长表达式更有可读性。
规范示例 :
if (foo and bar) or not baz:
print("condition is true")
不规范示例 :
if foo && bar || !baz:
print("condition is true")
注释间距
普通注释( # )与文档注释( ## )均应与注释正文保持一个空格的距离,而被注释掉的代码开头则不需要空格间隙。
命名规定
这些命名规定遵循 i3D Act 引擎风格,不遵循这些规定都会使你的代码与内置的命名规定相冲突,导致风格不一致的代码。
命名用 snake_case(蛇形)命名法,将其名字从帕斯卡命名(大驼峰命名,PascalCase)转化为 snake_case 命名。
(6) S3Script 警告系统
S3Script 警告系统可以在开发过程中帮助避免因难以发现的错误而导致运行时错误。
可以在项目设置的 S3Script 部分配置警告:
要在侧边栏中看到 S3Script 部分的设置,必须启用 高级设置 选项;如果该选项未开启,也可以搜索"S3Script"来进行查找。
可以在脚本编辑器的状态栏中找到当前 S3Script 文件的警告列表。下例中有 2 个警告:
要忽略一个文件中的特定警告,请插入 @warning_ignore("warning-id") 注解,或点击警告说明右侧的忽略链接,i3D Act 将在相应的行上方添加该注解,该代码将不再触发相应的警告:
警告不会阻止项目的运行,但是你可以根据需要将其转换为错误,除非你修复所有警告,否则项目无法编译。前往项目设置的 S3Script 部分打开此选项。这是与前一个示例相同的文件,在启用了警告转成错误这个设置之后:
3. C#/.NET
(1) C#基础(#58)
C#是由微软开发的高级编程语言。i3D Act支持将C#作为一种脚本语言的选择,与i3D Act自有的 S3Script 并列。
标准的i3D Act可执行文件并不自带C#支持。要为你的项目启用C#支持,你需要从i3D Act网站 下载.NET版本 的编辑器。
(2) C#语言特性
本页概述了C#和i3D Act的常用特征以及它们如何一起使用。
**类型转换和强制转换**
C#是一种静态类型语言。因此,你无法执行以下操作:
var mySprite = GetNode("MySprite");
mySprite.SetFrame(0);
GetNode()方法返回一个 Node 实例。需要显式将其转换为所需的派生类型 Sprite2D 。为此,在C#中有多种选择。
**强制转换和类型检查**
如果返回的节点无法转换为Sprite2D,则会抛出 InvalidCastException 异常。如果你确定不会发生异常,则可以直接使用而不必用 as 运算符。
Sprite2D mySprite =(Sprite2D)GetNode("MySprite");
mySprite.SetFrame(0);
**使用AS运算符**
如果节点无法转换为Sprite2D, as 运算符将返回 null ,因此不能作为具体的值类型。
Sprite2D mySprite = GetNode("MySprite") as Sprite2D;
mySprite?.SetFrame(0);
**使用泛型方法**
还提供了泛型方法以使该类型转换透明。
GetNode <T>() 在返回之前强制转换节点。如果节点无法强制转换为所需类型,它将抛出一个 InvalidCastException。
Sprite2D mySprite = GetNode
mySprite.SetFrame(0);
GetNodeOrNull <T>() 使用 as 运算符,如果节点无法强制转换为所需类型,则返回 null。
Sprite2D mySprite = GetNodeOrNull
// Only call SetFrame() if mySprite is not null
mySprite?.SetFrame(0);
**使用IS运算符进行类型检查**
可以使用 is 运算符,检查节点是否可以转换为Sprite2D。如果节点无法转换为Sprite2D, is 运算符将返回 false ,否则返回 true 。请注意,当 is 运算符针对 null 使用时,结果始终为 false 。
if (GetNode("MySprite") is Sprite2D)
{
// Yup, it's a Sprite2D!
}
if (null is Sprite2D)
{
// This block can never happen.
}
如果 is 运算符返回 true ,你可以声明一个新变量来按条件存储转换的结果。
if (GetNode("MySprite") is Sprite2D)
{
// Yup, it's a Sprite2D!
}
if (null is Sprite2D)
{
// This block can never happen.
}
(3) C#风格指南
对于每个项目而言,拥有定义良好且一致的编码约定非常重要,i3D Act也不例外。
本页面包含一份编码风格指南,i3D Act本身的开发人员和贡献者都遵循该指南。因此,它的目标读者是希望为该项目做出贡献的人员,但是由于本文中提到的约定和规范被该语言用户最广泛采用,所以我们建议你也这样做,尤其是如果你还没有这样的指南。
**格式**
**总体规范**
·使用换行符( LF )来换行,而不是 CRLF 或 CR。
·在每个文件末尾使用一个换行符,但 csproj 文件除外。
·使用不带 字节顺序标记(BOM) 的 UTF-8 编码。
·使用 4 空格 代替制表符进行缩进(称为"软制表符")。
·如果长度超过 100 个字符,请考虑将其分成几行。
换行符和空白行
对于一般缩进规则,请遵循 Allman 风格,它建议将与控制语句关联的大括号放在下一行,缩进到同一级别:
// Use this style:
if (x > 0)
{
DoSomething();
}
// NOT this:
if (x > 0) {
DoSomething();
}
但是,你可以选择省略括号内的换行符:
·对于简单的属性访问者。
·对于简单对象,数组,或集合初始化。
·对于抽象的自动属性,索引器,或事件声明。
// You may put the brackets in a single line in following cases:
public interface MyInterface
{
int MyProperty { get; set; }
}
public class MyClass : ParentClass
{
public int Value
{
get { return 0; }
set
{
ArrayValue = new [] {value};
}
}
}
插入一个空行:
·在一列 using 语句之后。
·在方法,属性,和内部类型声明之间。
·在每个文件的末尾。
字段声明和常量声明可以根据相关性编组在一起。在这种情况下,请考虑在编组之间插入空白行以便于阅读。
避免插入空白行:
·在开括号 { 之后。
·在闭合括号 } 之前。
·在注释块或单行注释之后。
·与另一个空白行相邻。
using System;
using I3D;
// Blank line after
using
list.
public class MyClass
{
public enum MyEnum
{
Value,
AnotherValue
}
public const int SomeConstant = 1;
public const int AnotherConstant = 2;
private Vector3 _x;
private Vector3 _y;
private float _width;
private float _height;
public int MyProperty { get; set; }
public void MyMethod()
{
AnotherMethod();
}
public void AnotherMethod()
{
}
}
命名规定
对所有命名空间、类型名称、成员级别标识符(即方法、属性、常量、事件)使用 PascalCase,私有字段除外:
namespace ExampleProject
{
public class PlayerCharacter
{
public const float DefaultSpeed = 10f;
public float CurrentSpeed { get; set; }
protected int HitPoints;
private void CalculateWeaponDamage()
{
}
}
}
将 camelCase 用于所有其他标识符(即局部变量、方法参数),并使用下划线(\_)作为私有字段的前缀(但不能用于方法或属性,如上所述):
private Vector3 _aimingAt; // 私有字段使用\_
前缀。
private void Attack(float attackStrength)
{
Enemy targetFound = FindTarget(_aimingAt);
targetFound?.Hit(attackStrength);
}
类似 UI 这种只有两个字母的首字母缩写应特殊处理,使用 PascalCase 时都应写作大写字母,否则都应写作小写字母。
请注意,id 不是首字母缩写,因此应将其视为普通标识符:
public string Id { get; }
public UIManager UI
{
get { return uiManager; }
}
通常不建议将类型名称用作标识符的前缀,例如 string strText 或 float fPower。但是,对于接口来说是个例外,实际上,接口应该在其名称前加上大写字母 I,例如 IInventoryHolder 或 IDamageable。
最后,请考虑有意义的名称,请勿对名称进行过度缩写,以免影响可读性。
例如,如果你想编写代码来查找附近的敌人并用武器击中它,请选择:
FindNearbyEnemy()?.Damage(weaponDamage);
而不是:
FindNode()?.Change(wpnDmg);
成员变量
如果变量只在方法中使用,请勿将该变量声明为成员变量,因为难以定位在何处使用了该变量。相反,你应该将这些变量在方法内部定义为局部变量。
局部变量
局部变量的声明位置离首次使用该局部变量的位置越近越好,让人更容易跟上代码的思路,而不需要上翻下找该变量的声明位置。
(4) C#诊断
i3D Act 中包含了分析器,会检查 C#源代码是否有非法或不支持的代码,并向你告知在构建时出现了错误。
S30001:从 I3DObject 派生的类型声明中缺少部分修饰符
S30002:在包含从 I3DObject 派生的嵌套类的类型声明上缺少部分修饰符
S30003:在同一脚本文件中找到多个具有相同名称的类
S30101:导出的成员是静态的
S30102:导出的成员类型不受支持
S30103:导出的成员是只读的
S30104:导出属性为只写属性
S30105:导出属性是索引器
S30106:导出属性是显式接口实现 S30107:未继承自 Node 的类型不应导出 Node 成员
S30201:委托名称必须以"EventHandler"结尾
S30202:不支持信号委托签名的参数
S30203:信号的委托签名必须返回 void
S30301:泛型参数必须是与变量兼容的类型
S30302:通用类型参数必须使用"[MustBeVariant]"属性注释
S30303:未处理必须与变量兼容的类型参数的父符号
S30401:类必须派生自 I3D.I3DObject 或派生类
S30402:类必须不是泛型
(5) C#平台支持
从 I3D 5.2.6 开始,用 C#编写的项目支持所有桌面平台(Windows、Linux 和 macOS)。
4. S3Extension
(1) S3Extension 是什么?
S3Extension是I3D专有的技术,可以让引擎在运行时与原生共享库交互。你可以通过它来执行原生代码,无需和引擎一同编译。S3Extension不是脚本语言,只是I3D特有的一项拓展机制,使用这种机制,能够在引擎中将C/C++实现的动态库作为插件接入I3D中。
S3Extension的优点:当需要进行一些高性能的任务或者需要使用系统的原生API时,使用S3Extension是理想的选择。
(2) S3Extension C++示例
S3Extension的C++绑定是在C S3Extension API之上构建的,并提供了一种更好的方法来使用C++”扩展”I3D中的节点和其他内置类。这个新系统允许将I3D扩展到与静态链接的C++模块几乎相同的级别。
为了使用S3Extension,你需要以下先决条件
a.i3d引擎
b.C++编译器
c.SCons作为构建工具
d.i3d-cpp库
有了这些后就可以开始制作S3Extension插件库了。
首先需要编译i3d-cpp库。通过i3d-cpp编译的库可以使用i3d提供的接口。
进入i3d-cpp目录,可以看到如下:
要编译i3d-cpp,需要在命令行中输入:
scons platform=windows -j12 target=editor
Scons是一个开放源码、以Python语言编码的自动化构建工具。所以要使用上面的命令需要下载Python。platform指定编译平台,这里平台为windows。构建过程会自动检测用于并行构建的CPU线程数,如果要手动指定使用的CPU线程数,通过-jN参数实现,其中N是要使用的CPU线程数。target指定编译目标,这里固定为editor。
这一步将需要一段时间。完成后,我们拥有了一个静态库,可以编译到我们的项目中,存储在i3d-cpp/bin/中。
下一步,就可以开始插件的编写了。
首先,需要创建一个C/C++工程,在这里,我们使用Visual Studio创建一个空项目。
由于我们的目标是生成一个动态库插件,所以需要在工程配置中将配置类型修改为”.dll”
为了使用i3d提供的接口,需要将i3d头文件引入包含目录,并将i3d-cpp生成的静态库引入依赖。
在下图中我们可以看到编译后的i3d-cpp目录结构:
为了引入i3d头文件目录,需要在工程属性的“C/C++”/常规/附加包含目录中加入包含目录。
其中i3d-cpp\include、i3d-cpp\gen\include、i3d-cpp\i3D Act-headers、i3d-cpp\s3extension就是i3d头文件目录。
引入头文件后需要引入静态库依赖,首先需要引入依赖的静态库所在的目录,在工程属性 链接器/常规/附加库目录 中选择库目录,如下图所示:
其中i3d-cpp/bin就是库目录,这是i3d-cpp编译后生成的输出文件所在的目录。然后在 链接器/输入/附加依赖项 中写入库名字,如下图所示:
libi3d-cpp.windows.editor.x86\_64.lib就是i3d-cpp编译后生成的静态库文件。
配置完成后就可以开始编写C++代码了。
首先,我们需要实现一个C++文件:register\_types.cpp。我们在插件中会继承i3d中的类并实现自己的类,这些类都需要在register\_types.cpp中进行注册,并且在这个文件中还要实现插件的入口函数。
我们等一下再来看register\_types.cpp的实现,现在,先来实现一个自定义类。
如图所示,我们定义一个类Demo1,继承于i3d中的类型Node。定义了一个成员变量id和可以读写id的函数。还有一个函数print\_class用于打印类名。\_bind\_methods这个函数是只要继承i3d中的类型的都要实现,在这个函数中,实现对属性和函数的”绑定”,这样就可以在i3d编辑器和S3中使用相应的属性和函数了。\_bind\_methods的具体实现如下图所示:
void Demo1::\_bind\_methods() {
` `ClassDB::bind\_method(D\_METHOD("set\_id", "num"), &Demo1::set\_id);
` `ClassDB::bind\_method(D\_METHOD("get\_id"), &Demo1::get\_id);
` `ClassDB::bind\_method(D\_METHOD("print\_class"), &Demo1::print\_class);
}
通过ClassDB::bind\_method方法对函数实现绑定。这样,我们就有了一个类:Demo1,但是想要在i3d中使用这个类,还需要一个步骤:对类进行注册。我们需要在register\_types.cpp中注册类,首先需要实现插件的”入口函数”:
class Demo1 : public Node {
S3CLASS(Demo1, Node);
public:
Demo1() {}
void print_class();
void set_id(int num);
int get_id();
private:
int id;
protected:
static void _bind_methods();
};
plugin\_init是插件的入口函数,当i3d启动时,会根据配置文件去加载插件文件,加载后会调用入口函数,入口函数就相当于一个回调函数。实现入口函数时除了名字外其它部分都有严格的定义,所以最好不要修改。
extern "C" {
// Initialization.
S3ExtensionBool S3E_EXPORT plugin_init(
S3ExtensionInterfaceGetProcAddress p_get_proc_address,
S3ExtensionClassLibraryPtr p_library,
S3ExtensionInitialization *r_initialization) {
S3ExtensionBinding::InitObject init_obj(p_get_proc_address, p_library, r_initialization);
init_obj.register_initializer(initialize_plugin);
init_obj.register_terminator(uninitialize_plugin);
init_obj.set_minimum_library_initialization_level(MODULE_ INITI ALIZATION_LEVEL_SERVERS);
return init_obj.init();}}
在入口函数中需要定义初始化函数和清理函数,就是initialize\_plugin和uninitialize\_plugin这两个函数。initialize\_plugin会在模块初始化时被调用,我们需要在这个函数中进行一些插件的初始化操作。比如注册类。
void initialize\_plugin(ModuleInitializationLevel p\_level) {
` `if (p\_level != MODULE\_INITIALIZATION\_LEVEL\_SCENE) {
` `return;
` `}
` `ClassDB::register\_class<Demo1>();
}
可以看到,在初始化函数中通过ClassDB::register\_class<Demo1>()注册了Demo1这个类型,这样,就可以在i3d中使用了。
清理函数通常用于清除动态申请的内存空间或者在插件退出时执行一些配置操作,这里不涉及到这些操作所以清理函数就是一个空函数。
为了能够在i3d工程中使用插件文件,需要在i3d工程文件夹下定义一个”\*.s3extension”文件用来声明插件的位置。
如图所示,我们的插件文件”libs3afsim.windows.editor.x86\_64.dll”放在i3d工程文件夹的bin目录下。这样就可以在i3d中像其它任意节点一样使用Demo1。