查看: 319|回复: 10

[教程] Godot —— 从入门到入土 教程(长期更新)

[复制链接]

40

主题

331

帖子

6

精华

副版主

☯ 博 丽 不 是 灵 梦 ☯

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

经验
3963
硬币
509 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章

发表于 2022-9-19 20:10:18 | 显示全部楼层 |阅读模式
本帖最后由 电童·Isamo 于 2022-9-25 22:23 编辑
伴随着版主dasasdhba的第一个Godot引擎暨国内Mario Forever圈内第一个Godot MF引擎的发布,MF制作进入了全新的多元化平台时代
过去,我们一直使用着经典的 Clickteam Fusion Developer 2.5 开发平台来制作自己的MF引擎。然而随着使用时间的不断增长,该平台的问题也日益凸显,且正在逐渐落后于日益增长的开发需要。这时,我们就不能将目光仅局限于CTF之上了。于是,一大批非CTF的引擎呼之欲出——Game Maker版的MF on GM(by dasasdhba),Godot版的Storm Engine(by TeamCE)和Berry Editor(by dasasdhba)成为了这些引擎中的典型。
而本人则选择了更具有开发潜力的后者——Godot
随着本人对该引擎的日趋了熟,本人更有把握以尽可能易吸收、易理解的语言来描述引擎的用法。
当然,由于部分操作不经常使用,教程中也会出现瑕疵,届时也望熟习者能够在本贴下方补充,鄙人将不胜感激。

回归正题,开本帖的目的,就是从入门到入土地讲解Godot的基本用法、操作和代码编写。其中基本用法和操作在本贴中只会讲一些比较常用的,重点会放在代码的讲解上。故如果你已有一定的编程基础(尤其是python),那么本贴中的代码部分对你来说将会得心应手。

本帖中会经常用到的链接:
官网点我进入
官方doc文档(内容非常丰富,学有余力可以来此学习,自带中文):点我进入

接下来是本贴的目录(不定时更新),其中标蓝的表示已完成的小节
  • Godot是什么?Godot的部署及其基础操作介绍
  • 节点、场景和场景的实例化
  • 资源简介与节点的属性
  • GDScript其一:GDScript入门
  • GDScript其二:GDScript的导出、一些常用关键字
  • GDScript其三:函数(方法)与
  • GDScript其四:虚函数、节点的简单导入和onready变量
  • GDScript其五:信号
  • GDScript其六:数组
  • GDScript其七:字典
  • GDScript其八:Setget方法
  • GDScript其九:帧步处理,延迟处理
  • GDScript其十:tool脚本
  • %型节点
  • 节点路径——NodePath
  • 数学——线性代数(简单学习)
  • 数学——随机数(简单学习)
  • 单例的介绍与应用
  • 计时器节点——Timer
  • GDScript高级篇其一:协程yield
  • GDScript高级篇其二:利用代码使场景实例化,NodePath在GDScript中的运用
  • GDScript高级篇其三:伪封装
  • GDScript高级篇其四:动态调用——call()和call_deferred()
  • GDScript高级篇其五:虚函数深入——构造函数、节点树函数和就绪函数
  • GDScript高级篇其六:is检查实例与get_class()比较
  • GDScript高级篇其七:静态方法、方法库
  • GDScript高级篇其八:高级导出
  • GDScript高级篇其九:导出数组的局限性
  • GDScript高级篇其十:Object类——所有对象的根类
  • GDScript高级篇其十一:信号深入
  • GDScript拓展篇其一:注解
  • GDScript拓展篇其二:隐函数(匿名函数,Lambda表达式)和Callable类
  • GDScript拓展篇其三:Set隐函数和Get隐函数
  • GDScript拓展篇其四:信号作为类、await取代yield
  • GDScript拓展篇其五:export的新用法
  • GDScript实践篇其一:CollisionShape2D
  • GDScript实践篇其二:StaticBody2D和KinematicBody2D
  • GDScript实践篇其三:Area2D和RayCast2D
  • GDScript实践篇其四:Sprite和AnimatedSprite
  • GDScript实践篇其五:AnimationPlayer的简单应用
  • GDScript实战篇其一:制作一个RemoterCollisionShape2D和RemoteeCollisionShape2D
  • GDScript实战篇其二:制作一个简易的敌人


评分

参与人数 3经验 +9 硬币 +6 收起 理由
Jira_Suyoru + 3 + 2 tzq
2333ty + 3 + 2 这么肝??????
zyc233 + 3 + 2 好!

查看全部评分

>攒着石头准备把草神抱回家【【<

40

主题

331

帖子

6

精华

副版主

☯ 博 丽 不 是 灵 梦 ☯

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

经验
3963
硬币
509 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章

 楼主| 发表于 2022-9-19 20:15:30 | 显示全部楼层
本帖最后由 电童·Isamo 于 2022-9-20 21:14 编辑

Godot是什么?Godot的部署及其基础操作介绍
  • 什么是Godot?
Godot(全称Godot Engine)是Juan Linietsky和Ariel Manzur开发一款制作游戏的软件,可以制作2D和3D游戏。通过基于节点的架构来设计游戏,3D渲染器设计可以增强3D游戏的画面。具有内置工具的2D游戏功能以像素坐标工作,可以掌控2D游戏效果。面向团队的设计从架构和工具到VCS集成,Godot专为团队中的每个人设计。
编辑器可在Windows、Mac OS和Linux系统中运行,支持导出游戏到Windows、Mac OS、Linux、Android、iOS、UWP和HTML5等平台。
(注:从3.5版本编辑器也支持在Android端上运行
  • 引擎有官网吗?


  • 怎么下载Godot引擎?

前往下载地址后,找到

                               
登录/注册后可看大图

点击后,你会跳转到如下页面

                               
登录/注册后可看大图

(本帖中主要以3.X为主,待4.1发布后本贴将会以4.1版本为主)
我们很容易发现右侧有四个下载按钮,其中每两个按钮上方都有一行字来描述,这行字便是你要下载的子版本:
Godot 3.X分为Standard版本和Mono版本,如图,后者追加对C#脚本的支持。所以,如果你选择下载后者,那么在用编辑器写代码时,你就可以直接利用Mono版本自带的C#语言库编写脚本,而不需要其他额外操作。

                               
登录/注册后可看大图
上图是对于你下的godot对你的电脑配置的需求,其中对于Mono版本,你还需要安装.NET SDK才能正常使用Mono版的C#功能
本帖只讨论Standard版本
然后,根据你电脑的操作系统的位数,点击带有对应位数的按钮下载。
  • 如何查看自己的电脑是32位的还是64位的?
以Windows11为例,右键任务栏的windows图标,找到“系统”并点击

                               
登录/注册后可看大图
之后你可以在此看到你的系统的位数:

                               
登录/注册后可看大图
如果是x86,则为32位系统(注:Windows11没有x86位!);如果是x64,则为64位系统
  • 安装
点击下载按钮后,你下载的将会是一个zip压缩包

                               
登录/注册后可看大图
下载完成后,打开该zip压缩包,里面会有两份文件(有些版本则是一个,具体还请以你的压缩包内的实际情况为准),都不要删除,将其解压到一个指定的文件夹下

                               
登录/注册后可看大图
我这里因为下的安装包只有exe所以就只有这一个文件。
  • 项目管理器
打开软件后,会有一个加载界面,加载界面结束后,你将会见到如下界面,该界面便是项目管理器

                               
登录/注册后可看大图
如果你是第一次打开本软件的话,则会弹出一个弹窗:“当前管理器内没有工程,可从右侧按钮中新建一个工程”,点ok关闭即可
软件本身有中文和地区检测,故如果你在国内,你无需担心默认语言的问题
这里先介绍一下右侧的按钮:
  • 编辑:需要选中一个已有工程才能生效,点击后将打开该工程的编辑器,等价于双击一个已有工程
  • 运行:需要选中一个已有工程才能生效,点击后直接从主场景(后面会讲到)处开始运行当前工程(相当于直接从头运行一个没有被导出【后面会讲】为exe的程序)
  • 扫描:点击后,会在你所选的文件夹(路径)(如:

                                   
    登录/注册后可看大图
    )中扫描所有含.project的文件并将其导入至项目管理器
  • 新建工程:点击后,将会弹出该弹窗:

                                   
    登录/注册后可看大图
    项目名称填完后,点击右侧的“创建文件夹”后即可在下面的项目路径下新建一个名为你填的项目名称的空文件夹,渲染器这里推荐选择OpenGL ES 3.0(简称GLES3.0,如果你的电脑配置实在是太老太老,则建议选择右侧的OpenGL ES 2.0【GLES2】)
  • 导入:点击后,在你给定的路径

                                   
    登录/注册后可看大图
    下找到.project文件并双击即可将选中的.project文件导入到项目管理器内
  • 重命名:需要选中一个已有工程才能生效,重命名选中的工程
  • 移除:需要选中一个已有工程才能生效,从项目管理器中移除选中的工程,也可以在弹出的弹窗中勾选“同时移除项目文件”来将你不需要的工程从你的电脑中彻底移除
  • 移除缺失项:由于有时候你可能会执行的蜜汁操作,会导致项目管理器你存在缺失项工程,这些工程大多数都是因为从资源管理器内直接删除工程源文件而遗留下来的。点击该按钮后,所有缺失项都将会从你的项目管理器内清除

考虑到开发者开发的参考需要,Godot特意提供了Asset Lib,只需点击

                               
登录/注册后可看大图

即可切换到素材库界面。
注:
1.素材库中只会显示示例工程(下面会讲到素材库的另一个版本,它不会显示示例工程)
2.部分地区的网络可能连接不上素材库而导致素材库内无法显示内容。出于不可抗力,请自行百度搜索该解决方法
  • 编辑器界面
这里以Berry Editor 为例,双击打开,会弹出一个加载界面,加载界面结束后,你会看到如下界面,这便是该工程的编辑器界面

                               
登录/注册后可看大图

我们先讲解位于左上、左下、右侧和中间的内容:
左上角的窗口有两个选项:场景和导入
  • 场景:当前场景的节点树(后面会讲到),你可以在这里操作该场景里的节点(后面会讲到)
  • 导入:只有在选择了资源(后面会讲到)后该选项卡下的界面才会显示,这个我们会在讲资源的时候会详细讲解

左下角的窗口即为文件系统窗口,它显示的就是你当前的工程文件下的文件,res://为当前工程文件夹,比如说我把Berry Editor的所有文件放在了D:\Projects\BerryEditor目录下,那么res://就是D:\Projects\BerryEditor。你可以从文件系统窗口中将一个文件(夹)拖动到其余三个部分内

右侧的窗口有两个选项,分别为检查器选项卡和节点选项卡。检查器选项卡可以显示一个选中的节点(后面会讲)和资源的基础属性,同时支持在检查器里修改这些属性(有些属性是实时修改的,意味着你修改了这个属性后,对应的节点将会立即对该属性作出反应)。节点选项卡我们后面再讲。
最后就是中间的这个大窗口,他就是编辑器的主界面,任何呈现在游戏/程序里的东西,也就是你希望玩家/使用者看到的东西,都需要放在主界面里,如果不放在主界面里,那么它在导出成exe文件(后面会讲到)后也就不会在游戏/程序中显示出来。

接下来我们讲解一下主界面上方的四个按钮:

                               
登录/注册后可看大图
从左到右分别为:2D编辑模式,3D编辑模式,脚本编辑模式 和 素材库
  • 2D编辑模式,该编辑模式下的场景只有横、纵两个维度,这个模式也是本帖主要使用的编辑模式,所有2D节点(后面会讲)都只能在该编辑模式下显示

                                   
    登录/注册后可看大图
  • 3D编辑模式,该编辑模式下的场景只有横、纵、竖三个维度,如果你有做3D游戏或借用3D效果的需要,可以使用该模式,但本帖不会对该模式下的内容进行任何讲解,如需学习还请自行查阅官方doc(见1L)。所有的3D节点(后面会讲)都只能在该编辑模式下显示

                                   
    登录/注册后可看大图
  • 脚本编辑模式,在此模式下编辑脚本

                                   
    登录/注册后可看大图
  • 素材库,同项目管理器中的素材库,不过这里的素材库只能显示插件等素材,不会显示示例工程

  • 开始开发前你需要做的准备
首先我们打开

                               
登录/注册后可看大图


                               
登录/注册后可看大图

将上图中的“限制编辑器视图”取消勾选,如若保持勾选,则在主界面移动视图时,你可移动的视图范围将会受到限制
接下来我们找到并点击

                               
登录/注册后可看大图

                               
登录/注册后可看大图
将窗口宽高根据情况进行调整(这里调为了默认的640*480),建议勾选使用垂直同步(否则会出现部分代码高帧率执行的问题,后面会讲到)

                               
登录/注册后可看大图
将拉伸按上图进行修改即可

                               
登录/注册后可看大图
将帧缓冲分配调为2D

                               
登录/注册后可看大图
必须开启GPU像素吸附,否则图像在拉伸后会出现裂缝

                               
登录/注册后可看大图
这里将UV收缩勾选以防在导出到部分windows平台上后程序内的图像出现问题

以上就是本节的全部内容了,接下来我会介绍关于项目编辑器里一些比较常用的选项卡/窗口


>攒着石头准备把草神抱回家【【<

40

主题

331

帖子

6

精华

副版主

☯ 博 丽 不 是 灵 梦 ☯

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

经验
3963
硬币
509 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章

 楼主| 发表于 2022-9-19 22:26:21 | 显示全部楼层
本帖最后由 电童·Isamo 于 2022-9-20 00:39 编辑

第一节·补充环节
  • 图层名称
在后面的章节中我们会学习PhysicsBody2D的有关内容,这里先简单介绍一下Godot中的图层:
Godot中的图层是一个很宽泛的概念,有Canvas图层渲染图层物理图层导航图层等四种主要的图层,其中后三者的名称可以在项目设置里直接设置,而Canvas图层只能在使用Canvas节点(后面会讲到)时才能使用。
除Canvas图层外,渲染图层、物理图层和导航图层都只有32个

                               
登录/注册后可看大图
你可以在上图的界面中给每个图层赋予一个名称,这些名称的作用我们将会在讲PhysicsBody相关的内容后再做说明。
  • 键位映射


                               
登录/注册后可看大图
上图为键位映射编辑选项卡,你可以在这里编辑给定的动作键,甚至你还可以自行添加/删除动作键,如能合理使用该功能将会呈现出十分强大的效果。关于这些键位映射的详细用法,我们到后面讲解Input单例时再做说明。
  • 自动加载

                               
登录/注册后可看大图
上图即为自动加载的选项卡,这里存放的东西就叫做该工程的单例(Singleton)或全局对象(Global Object),本帖中我们习惯用前者称呼这些内容。至于什么是单例,以及其用途用法,我们会在专门的一节中讲解单例。
  • 插件

                               
登录/注册后可看大图
上图即为插件选项卡。插件是Godot中十分强大且恐怖的存在,它们小到可以提醒你当前时间,大到直接修改你的编辑器工作界面,甚至是菜单栏,可以说这是其他引擎所未有的一个优势。你可以从Asset Lib(即素材库)中下载对应的插件并安装后,可在此控制其开关状态。如果代码能力十分优秀,你甚至可以直接编辑相关代码,更甚者可以创建一个属于自己的Godot插件。不过制作插件暂时不在本帖的教程范围内,故感兴趣的同学可以前往官方doc(见1L)进行学习
>攒着石头准备把草神抱回家【【<

40

主题

331

帖子

6

精华

副版主

☯ 博 丽 不 是 灵 梦 ☯

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

经验
3963
硬币
509 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章

 楼主| 发表于 2022-9-19 22:58:49 | 显示全部楼层
本帖最后由 电童·Isamo 于 2022-9-21 12:12 编辑

节点、场景和场景的实例化
  • 什么是节点
节点(Node)是Godot中组成程序功能的最基本单位。小到一个按钮、一个苹果的精灵图、一个角色,大到一整个场景乃至一整个程序(需要实例化,下面会讲),都算是节点。
节点也是构成一个场景的最基本单位(下面会讲)

Godot中,节点主要分为以下几类(标斜体的表示无法被直接使用,本帖常用的几类将标红加粗):
  • Node最基本的节点,是其他所有节点的最基础单位,也可以说,其他任何节点都是这个节点所派生出来的拓展节点,它们均具有这个最基本节点的属性
  • Spatial:在4.0中叫做Node3D,即3D编辑模式下需要使用到的节点。如果你有3D游戏的需求,那么建议使用这类节点
  • CanvasItem2D节点最基本的节点,即2D编辑模式下需要使用到的节点都是这个节点的拓展节点。Control节点Node2D节点都是拓展自这个节点
  • Control:图形用户控制界面(GUI或者UI)类节点的最基本节点,诸如按钮、滑条等组件均为该节点的拓展节点
  • Node2D:2D编辑模式下需要使用到的节点的最基本的节点
  • AnimationPlayer系列的节点:负责处理动画的一系列节点,具有一定的高级功能
  • AudioStreamPlayer:负责处理音频的节点
  • CanvasLayer:负责处理视觉图层(Canvas图层)的节点的最基本节点
  • HTTPRequest:负责发送HTTP(S)请求的节点
  • Navigation系列的节点:负责处理节点导航的一系列节点
  • SkeletonIK:骨骼IK节点
  • Timer:负责倒计时的节点
  • Tween:负责处理插值(平滑效果)的节点
  • Viewport:负责处理视图,名为root的Viewport节点是所有节点的最基本的根节点
  • WorldEnvironment:负责处理渲染环境

  • 如何新建一个节点
方法一:

                               
登录/注册后可看大图
选中一个节点,然后点击箭头所指向的“+”按钮,会弹出如下界面

                               
登录/注册后可看大图
上述界面便是添加节点界面,根据自己的需求双击对应类型的节点即可
方法二:

                               
登录/注册后可看大图
选中一个节点,右键点击,找到“添加子节点”并点击即可弹出如方法一图二所示的窗口
注:上述节点都是在创建你所选的节点的子节点(下面会讲)
  • 操作节点
节点可以进行如下操作:

                               
登录/注册后可看大图
其中的【实例化子场景】和【将分支保存为场景】我们下面会讲到
  • 场景
在讲节点树之前,我们先来学习一下什么是场景。
提到场景(Scene),我们学过用过Clickteam Fusion都知道:场景不就是一个容纳若干对象的大容器嘛。
但是,在Godot里,场景的概念就与CTF截然不同了。
这么说吧,CTF里的场景等同于Game Maker里的Room,他们都属于是一种容纳小对象的容器(Container),就好比一个房子,每个房间都是场景,每个房间里的物品就是这个场景里的对象(Object),而这些对象又包含了许许多多不同的东西,包括这个对象的不同组件(Component)和属性(Property)。
然而CTF和GM的场景都有一个缺陷,那就是:开发者只能按照独体式思维来思考问题,即不同场景就是不同场景,你不容我,我不容你,A场景不可以塞到B场景里作为B场景的子场景(Subscene),反之亦然。同时,这种思维只承认显然宏观的容器为场景,诸如玩具车、玩偶、桌子这些放在房间里的小宏观物件,则不算做场景。
面对这个问题,Godot做出了大胆的尝试:那不妨就让这些房间里的物品也变成场景吧!
于是,Godot的场景的概念的轮廓就诞生了——它不再局限于传统的独体式思维,而是利用了全新的一种思维来解决这个问题,即:这是一幢房子,房子本身就是一个最大的房间,这就是最大的场景。房子里又分为若干个房间,这些房间也是一个个场景,只不过它们变成了整个房子这个大场景的子场景。每个房间里有着不同的物品,这些物品里面也肯定包含一些东西。你像房间A里有一辆玩具车,而玩具车包含骨架、轮子、马达……照着思路套娃,这样一想,玩具车岂不也成了包含东西的“场景”了吗?而且还是这个玩具车所在的房间的子场景。再往下分析:马达里有什么?磁铁和线圈,对吧,这样一来,马达也包含东西了,也是个“场景”……不仅如此,玩具车可以移动,那么移动这个动作就是玩具车所包含的属性(的表达),对吧?这么看,好像玩具车这个东西也能包含一些抽象的事物,而且如果包含抽象的事物,且作用在具象的物体上后有所表现的话,那么这个玩具车岂不是可以容纳之?换句话说,如果是一个抽象的事物,它本身也包含抽象的东西,但是它所包含的抽象的东西作用到它本身身上能够支持这个抽象事物的存在的话,那这个抽象事物岂不也是个“场景”?只不过这个场景是抽象的罢了。
这样一思考下来,你也许就在冥冥之中已经掌握了Godot开发中最基本的一种思维了:含析式思维
用人话讲就是:只要物体A包含事物B,不管事物B是什么(如速度、力、材料、材质、零件等),物体A都可以叫做场景
换句话说,Godot的场景的概念十分广泛,大到一整个程序、一个游戏房间(舞台),小到一个对象、一个经常复用组件,乃至一个节点,都可以说是一个场景
然而需要注意的是,在Godot中,含析式思维描述正确的前提是:物体A、B必须都是节点,但B可有可无
因此,在Godot中,只要物体A、B均是节点,且节点A包含节点B(或者说,节点A派生出子节点B)或不含节点B,那么节点A就叫做场景
  • 如何新建场景
方法一:

                               
登录/注册后可看大图

在菜单栏中找到场景——新建场景并点击即可
方法二:

                               
登录/注册后可看大图

在编辑器主界面上方的选项卡右侧找到“+”并点击即可
  • 节点树
创建了新的场景后,我们将学习什么是节点树(SceneTree)
先来看一个例子:

                               
登录/注册后可看大图

上面这张图取自BerryEditor的level场景,可以看到,这个场景里包含了若干个节点。但很有趣的是,这些节点被有序化地安置了,可以看到它们左侧有一些像枝条一样的折线,连接着不同的节点,像是构成了一棵以节点为主体的树一样。这些折线和其所连接的节点就构成了这个场景的节点树(SceneTree),而最上方的那个节点便是这个场景的根节点(Root Node)。
每个场景不能没有根节点,且有且只有一个根节点。因此,根节点就代表着这个场景
我们还能看到,有一些节点的枝杈是连接到其他节点上的,枝杈两端的节点就构成了父(Parent)子(Child)节点的关系,我们以枝杈上面所连接的节点叫做父节点(Parent),枝杈下方所连接的所有节点称为这个父节点的子节点(Children)。
在Godot中,一个父节点可以存在若干个子节点,但这些子节点只能有一个共同的父节点这些子节点彼此称为兄弟节点(Siblings),且这些子节点也可以分叉出若干个子节点,叫做它们的父节点的孙节点(Grandchildren)
换句话说,任何一个节点只能存在一个父节点(可以与其他节点共用,也可以不共用),且可以包含若干个子节点,子节点可以不同地套子节点,子节点的子节点还可以派生出子节点……
但归根结底,这些节点都要位于根节点之下。换句话说,根节点是这些节点的祖节点(Ancestor)。
在一个场景中,根节点的所有子节点祖节点,等价于这个场景主节点(Owner)
然而,根据定义【每个场景不能没有根节点,且有且只有一个根节点。因此,根节点就代表着这个场景】,考虑到单个节点也能成为一个场景,即该场景只有根节点的情况,因此【这些折线和其所连接的节点就构成了这个场景的节点树】说法其实不够严谨,应该为:只要根节点存在,根节点与其下面的所有节点共同构建了根节点所代表的场景的场景树/根节点与其下面的所有节点共同构建了这些节点所在场景的节点树


                               
登录/注册后可看大图

我们仍然以上图为例,来说明这个节点的子节点
首先Level为这个场景的根节点,其子节点为Camera, Music, Tilemap和Brush2D。而TileMap又派生出了三个子节点:TileScenery, TileGeneral和TilePipe。Brush2D下则只有一个子节点:PlayerMario
反过来说,TileScenery, TileGeneral和TilePipe的父节点是TileMap,PlayerMario的父节点是Brush2D,Camera, Music, Tilemap和Brush2D的父节点是根节点Level,Level就是这个场景内所有节点的祖节点暨这个场景的主节点
上面的这些节点就构成了这个场景里以Level为根节点的节点树
  • 创建根节点
方法一:从新建场景中创建根节点

                               
登录/注册后可看大图

创捷场景后直接在场景面板里选择一个进行创建即可
方法二:将已有场景内的某个节点转化为根节点
这个方法适用于你不小心把一个本该是根场景的节点错加成子场景,亦或是反悔时使用

                               
登录/注册后可看大图

右键应作为根节点的节点,找到“设为场景根节点”并点击即可
  • 根节点的子节点的基本操作
将节点置于某一节点下
方法一:

                               
登录/注册后可看大图

将节点拖动到目标父节点上,使其出现矩形高亮框后松开鼠标左键即可
方法二:

                               
登录/注册后可看大图


                               
登录/注册后可看大图

选中目标子节点,右键找到“重设父节点”并点击,在弹出的窗口中选择目标父节点即可
给一个节点设新的父节点

                               
登录/注册后可看大图


                               
登录/注册后可看大图

选中目标子节点,右键找到“重设父节点为新节点”并点击,在弹出的窗口中选择你希望的新创建的父节点的类型即可
设置兄弟节点

                               
登录/注册后可看大图

将某个节点的子节点拖到下方节点处出现线形高亮后松开鼠标左键即可,亦或是右键找到“重设父节点”并点击,并在弹出的窗口中选中其父节点的父节点即可
  • 保存场景
在你做完一个关卡、一个敌人、一个角色、乃至一个物件后,我们需要将其保存以便于我们下面要讲到的实例化与日后的编辑。

                               
登录/注册后可看大图

在菜单栏的【场景】选项中有如图所示的三种保存方法,但前两种是最常用的,如果是新建的场景,保存时系统则认为是另存为。这里就直接以另存为的方式来讲解

                               
登录/注册后可看大图

如上图所示,场景默认以.tscn结尾,但是godot提供了三种后缀名:tscn、scn和res。对于这些后缀名,需要点击“所有可用类型”之后再选项框里选择对应的后缀才可以成功保存为带有对应后缀的场景。Godot的场景文件不可以直接改后缀,即使是改为给定的三种,只要不是原来的后缀均不行
由于后缀的多样性,规范性起见,我们规定:
  • 房间、舞台、游戏大厅、游戏主界面之类的大宏观场景均以tscn或scn结尾
  • 角色、敌人、道具等小宏观对象场景均以res结尾
  • 以组件节点为根节点的场景也均以res结尾
同时,为了进一步区分场景,我们规定:
  • 以tscn或scn的场景统称为房间场景
  • 以res结尾的场景,如果是一个完整的对象,则称为对象场景(如角色、敌人、道具、HUD等),如果只是以实例化(下面会讲到)的形式嵌入进对象场景内,并作为对象场景的一部分的场景则称为组件对象场景(简称组件或组件场景)


在保存完场景以后,我们就可以在我们的文件系统里找到你之前保存过的场景了,如图箭头所示

                               
登录/注册后可看大图


  • 场景实例化
在保存完了一个场景之后,我们就可以随时编辑这个已经保存好场景了。
但是,我们前面提到过,Godot中的场景大到一整个程序,一整个游戏舞台,小到一个对象乃至一个节点,而且也在讲节点的时候提到过:Godot是支持场景嵌套的。那么,有没有一种方式,把我已经保存过的场景直接塞入一个新的场景中,让他作为场景里的一个对象(节点)存在呢?
比如,我现在做好了一个名叫level.scn的游戏舞台场景,也做好了一个叫player.res的角色对象场景,那么我该怎么把player.res这个对象(场景)放入level.scn这个更大的舞台(场景)中呢?
这些问题的答案,便是我们这一小节要讲的场景实例化(Scene Instantiation)
  • 什么是(场景)实例化?什么是实例?
场景实例化,顾名思义,就是将场景作为对象应用到另一个场景当中。由于在计算机语言学中,对象有时也可称为实例,因此这一过程也就叫做场景的实例化,简称实例化。
Godot中,将一个(对象/房间)场景A放入目标(对象/房间)场景B中后实例化,这个实例化后的场景(A')就叫做场景B中的一个实例化场景(Instantiated Scene)。原来的那个场景A就叫做场景(A')的源场景(Source Scene),而场景(A')就叫做场景A的一个实例(Instance)
仔细揣摩一下上面【一个实例】中的【一个】一词。没错,场景A其实可以在场景B中被实例化若干次,那么如果我在场景B中多次实例化场景A,那么实例化后的场景(A1),(A2)……(An),都叫做场景A的实例(Instances)


                               
登录/注册后可看大图



                               
登录/注册后可看大图

如上图2,A、A2和A3都是上图1中场景A的实例
  • 实例化场景和根节点的关系
我们在上一小节学习节点树的时候提到过:【每个场景不能没有根节点,且有且只有一个根节点。因此,根节点就代表着这个场景】。由于根节点a代表了一个场景A,那么在场景A在场景B实例中实例化以后,场景A的实例在场景B中缩略成了场景A的根节点a
因此,一个场景在另一个场景中实例化,等价于这个实例化后的场景以其根节点缩略形式出现在目标场景中
用半人半鬼的话说:场景A有一个根节点a,场景B有一个根节点b,那么当场景A放入场景B中实例化为场景(A1)后,这个场景(A1)就会以根节点a的身份出现在场景B中,并作为场景B的子节点而存在
用纯人话讲,公司有一罐原版的、开了罐的番茄酱,罐身和盖子都是不透明的,需要工人生产出一模一样的番茄酱到出口架上,那么在工人做完以后,番茄酱就被摆到了出口架上,因为番茄酱是对外销售的,肯定不能开罐对吧,所以呢工人就把罐子盖上了,这样以来,我们也只能知道这是番茄酱,但里面具体是什么,我们无从知晓,还需要自己去拆开,或者有能力的话直接找老板要那罐开了罐的模板番茄酱。这里番茄酱本身就是根节点暨场景,内容物就是这个根节点的所有子节点。但是,当它们放到出口架上的时候,也即是一个场景实例化到另一个场景中的时候,番茄酱必须密封,只有罐壳可以被直接看到,罐壳也就是容器,就类比于根节点。
因此,场景的实例化,等同于该实例化场景的根节点及其被隐藏掉的所有子节点的实例化,本质上,场景等价于(一串/根)节点(即一棵节点树
同时需要注意的是:当源场景中的对应节点或属性发生变化后,该源场景的所有实例化场景的对应节点或对应属性均会发生对应的变化。反之,如果一个场景的(所有)实例化场景发生变化,则该源场景则不会发生任何变化
由于场景文件名与其内部根节点名可以不相同,因此建议在保存场景的时候将场景名命名为其内部根节点名的snake_case写法
  • 在一个场景中加入实例化场景/将一个场景在另一个场景中实例化
方法一:

                               
登录/注册后可看大图


                               
登录/注册后可看大图

在节点面板上方找到锁链形按钮,点击后在弹出的“实例化子场景”中双击选择一个你希望实例化的场景即可
方法二:

                               
登录/注册后可看大图

从文件系统管理器中直接将你希望实例化的场景拖拽到目标场景的目标节点下即可(此时实例化后的场景节点将作为目标节点的根节点)
有了场景的实例化,角色、敌人、金币等小宏观场景就可以通过实例化的形式来进入像游戏舞台这样的大宏观场景内了


                               
登录/注册后可看大图

实例化后的场景节点右侧都会有这个按钮,点击后即可在新打开的场景选项卡窗口内编辑源场景
  • 显示并允许编辑实例化后的场景的根节点下的所有子节点
前面提到,生产完的番茄酱,摆到了出口架时就已经把罐盖给封起来了,但我们肯定是要看看里面究竟是什么东西的对吧,那么我们就需要将其开盖来一探究竟。
类似地,场景实例化到另一个场景内后,实例化后的场景默认是以该实例场景的根节点显示并存在的,但其实,里面的东西只是被【隐藏】起来了,实际运行的时候,这些子节点仍然存在。但是,如果我们到后面需要打开看一看,甚至需要编辑这些子节点,该怎么办?
这个时候,我们就需要一个菜单选项来帮我们解决这个问题了:

                               
登录/注册后可看大图

如上图,只需要将右键菜单中的“子节点可编辑”勾选,即可显示该节点下方的所有子节点,如下图

                               
登录/注册后可看大图

注:显示出来的子节点的属性的文字均为金色
如果取消勾选上述右键菜单中的“子节点可编辑”,则已经编辑后的子节点的属性将会还原为在源场景内的默认值
  • 节点抽出成场景
前面或多或少也已经明示或暗示到,节点本身就是一个场景。
场景实例化,本质上是将场景转化为节点,那么反过来,节点是否也可以从节点树中抽离并保存为场景呢
答案是可以的

                               
登录/注册后可看大图


                               
登录/注册后可看大图

如上图1,右键一个不是实例化场景节点的节点,找到并点击“将分支保存为场景”,之后像另存为场景那样将场景保存到一个指定的位置即可。

需要注意的是:
  • 利用这个方法创建的场景,根节点是你选中的那个节点,其子节点均为该根节点的子节点
  • 利用这个方法创建的场景,其内部所有节点的属性的值均为其对应源节点的对应属性的对应值
  • 利用这个方法创建的场景,会将源节点直接实例化为新建场景的实例化场景节点,同时自动将该节点下的所有子节点隐藏

  • 父子节点的属性继承
注:本小节涉及到部分GDScript内容
我们前面提过,一个节点只能有一个父节点,这不仅是因为我们有“子无二亲父”这样一个人之常理,更重要的还是因为父子节点的一个重要特征——属性继承
之前的教程图片中有部分用的是Node,然而Node并不存在坐标和变换这些图形学概念。这时,我们就需要将眼光放在具有变换、Z索引等图形学概念的、功能更强大的Node2D身上了
Node2D本身具有位置[position]、旋转[rotation]和缩放[scale]等三个基本的变换元素,而这些就属于Node2D的属性(Property)。
所谓属性继承(Property Extension),就是说父节点的非脚本内导出变量的属性(后面会讲到)都会对其子节点产生影响(实际上是其子节点会对这些属性进行继承)。

                               
登录/注册后可看大图


                               
登录/注册后可看大图

这里我们就以两个Node2D节点所派生的Sprite节点为例,来视觉化展示一下父子对象的属性继承
注意:Sprite派生自Node2D这个类,意味着Node2D的所有属性,包括位置、旋转、缩放等,在Sprite节点上亦存在且有效
上图中大的图像为Sprite,小的图像为Sprite2
我们尝试更改一下子节点Sprite2的位置:

                               
登录/注册后可看大图

可以看到,子节点的位置发生了变化,而父节点的位置却丝毫未改
我们尝试更改一下父节点Sprite的位置:

                               
登录/注册后可看大图

可以看到,父节点Sprite向右移动了一段距离,子节点Sprite2也跟着向右移动了一段相同的距离
现在,我们更改一下子节点Sprite2的旋转:

                               
登录/注册后可看大图

同样地,父节点Sprite的旋转没有发生任何变化
同理,我们改一下父节点Sprite的旋转:

                               
登录/注册后可看大图

我们会惊奇地发现,子节点居然也跟着父节点旋转了,并且是绕着父节点的旋转中心而旋转的!
那么缩放这里就不再展示了,是同样的效果
由此观之,当父节点的某些属性(如坐标、旋转、缩放,三者合称变换)被修改以后,其子节点就会继承这些属性的影响。
如果一个节点A下面有两个子节点B和C,那么当A的属性改变时,B、C的属性也会随之改变,反之,当B或C的属性发生改变时,C或B、父节点A的属性则不会随之改变,因为B、C互为兄弟节点,互相独立,无法将属性直接共享。
但是,如果一个节点A下面有一个子节点B,而子节点B下面又有一个子节点C,那么当A的属性发生改变时,B、C的属性都会发生改变;当B的属性发生改变时,A的属性不会发生改变,但C的属性会随B的属性的改变而发生改变;如果C的属性发生改变,则A、B的属性均不会发生改变
所以,一个节点,无论其下面派生有多少级子节点,该节点的固有属性会被其所有子节点及其所有多级子节点继承
所谓固有属性,便是开头提到的“不是脚本导出出来的属性”,至于为何,我们会在讲资源时进行讲解

  • 场景继承
Godot虽然是一款游戏引擎,但严格地说,它是一款以节点树为基本架构的,面向对象的游戏/软件开发引擎。
对于学过编程的人来说,提到【面向对象】,就一定少不了这些概念:类、对象、成员、方法、继承、多态、is a,等。
这里需要介绍一个基本的、编程人员才能看懂的信息:场景是PackedScene对象的实例
上面那句断言的意思是:场景本身就是一个【对象】,既然是对象,他肯定是满足【面向对象】的那些要素的。事实上,如果你喜欢捣鼓引擎,你在单击一个在文件系统管理器里的场景文件后,会很容易在检查器面板里发现:检查器显示了场景文件的属性!
如果你看到这里还是一头雾水的话,那我就直接把答案告诉你吧:
场景是可以继承
  • 何谓“场景继承”
场景继承,指一个场景以自己为模板,派生出另一个场景,而作为模板的场景的属性等信息,在派生出的场景里均会继承自父节点。这里这个模板场景就被称为父场景,而派生出来的场景就被称为这个父场景的子场景
场景继承后,子场景内所有从父场景内拷贝过来的属性和节点均会被继承。父场景的相应节点或属性发生变更时,子场景相对应的节点或属性也会发生同样的变更。但子场景内的变化并不会影响到父场景。
举个例子,我有一个场景名叫“苹果”,我创建了个继承这个场景的子场景“红富士苹果”,那么红富士这个场景内的所有节点及其属性均会从“苹果”里相对应的地方拷贝进来,并且,即便红富士里的某些节点的属性发生了变化,其父场景“苹果”里对应节点的对应属性却不会发生任何变化,然而反过来,如果“苹果”内的一些节点或属性发生了变化,则“红富士苹果”的相应节点或属性也会发生对应的变化。无论我在红富士这个场景内加多少节点,或者把那些只加在红富士这个场景内的节点(全部)删除,都不会导致苹果这个场景内的节点树发生变化。
需要注意的的是:在子场景中无法删除父场景中存在的节点
  • 如何新建继承场景?
方法一:

                               
登录/注册后可看大图




                               
登录/注册后可看大图



                               
登录/注册后可看大图

如上图,选中一个需要作为父场景的场景,在菜单栏“场景”选项下找到“新建继承场景”,然后会弹出一个窗口(如上图2),之后打开需要作为父场景的场景文件,然后会弹出一个新的场景窗口,另存为一个新的场景即可
方法二:

                               
登录/注册后可看大图

在文件系统管理器内右键需要作为父节点的场景文件,找到并点击“新建继承场景”,剩余操作同方法一
创建后的继承场景,其根节点是一个实例化的场景节点,如下图:

                               
登录/注册后可看大图

这是因为,场景继承还有另一个名字:根节点的场景化
如果你尝试对根节点使用“将分支保存为场景”的话,会弹出这个报错:

                               
登录/注册后可看大图


仔细看途中红框的部分,第一个说明:根节点是不可以被抽离成场景的。但是第二个红框却说:可以使用场景继承来将根节点抽离成场景。

本质上,场景的继承,其实就是将根节点及其子节点抽离出所在场景之后保存成场景,再变为实例化场景加回源场景,然而我们知道:场景不能没有根节点。这显然是Godot所不允许的,因此,必须从父场景的根节点出派生出去一个子场景,而这个根节点本身不需要被抽离出它所在的场景,这才是保证这一前提的最佳方案。因此,编辑器是不允许开发者将根节点抽离成场景
而继承后的子场景,本质上就是父场景的根节点使用了一种不需要将自己抽离出去再加回来的“将当前分支保存为场景”,使创建出去的子场景的根节点,就是父场景的根节点的实例化场景节点。因此,子场景的根节点是实例化节点这事儿也就说得通了

  • 主场景
我们已经知道:Godot的场景,大到一整个程序、一个游戏舞台,小到一个对象乃至一个节点。但是,我们也只是把游戏舞台、对象和节点这些东西变成了场景并将其进行了实例化,而整个程序究竟是谁来负责启动、运行,我们仍然还无从得知。这个时候,我们就需要引入一个新的概念——主场景(Main Scene)
主场景是游戏/程序的入口,任何Godot工程要想从头到尾完整地运行,就不能没有主场景。它就是我上面所提到的【最大的场景】,是负责整个程序的场景。
  • 设置主场景

                               
登录/注册后可看大图

在文件系统管理器中,右键你需要设为入口场景的场景,找到并点击“设为主场景”,即可设为主场景

                               
登录/注册后可看大图

主场景的文件名会在文件系统管理器内显示为蓝色
设置主场景后,就需要把过渡场景或关卡场景实例化为该场景内根节点的子节点才能完整运行整个游戏

至此,第二节就全部讲完了

>攒着石头准备把草神抱回家【【<

40

主题

331

帖子

6

精华

副版主

☯ 博 丽 不 是 灵 梦 ☯

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

经验
3963
硬币
509 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章

 楼主| 发表于 7 天前 | 显示全部楼层
本帖最后由 电童·Isamo 于 2022-9-21 12:13 编辑

资源简介与属性
除了节点,Godot中还有一些物件,他们虽然无法像节点那样直接加入到节点树中,成为节点树的一部分,但它们可以以节点为载体,通过装载到节点这个载体上来参与程序的执行,并在一定程度上影响节点的效果,这样的物件就叫做资源(Resource)

  • 什么是资源?
资源(Resource),在Godot中指的是:不直接性地加入节点树中,却能够通过装载到节点内部,进而改变节点在节点树中的表现的这一类物件的统称。资源可根据其性质、影响的对象的不同而产生不同的分类。
按照性质的不同,资源可分为:
  • 基本资源
  • 图文资源
  • 音视频资源
  • 数理资源
  • 其他资源
按照影响的对象的不同,资源可分为:
  • 基础节点资源
  • 3D资源
  • 2D资源
  • UI资源
  • 动画资源
等等。
  • 几种常用的资源
在本贴中,我们将会经常用到以下几类资源:
  • Shape2D
  • AudioStream
  • Texture
  • SpriteFrames
  • Tileset
  • Font
  • Script
等资源,接下来我将对上述六种资源做一个简单的介绍,涉及到具体细节的,如其属性、方法等,我们会在相应的实践篇和实战篇中进行详解。
  • Shape2D
Shape2D是Godot中存储形状的资源,用于Area2D、PhysicsBody2D等节点中,用来定义这些对象的影响范围。其形状可以是经典的矩形、亦可以是椭圆、胶囊形、箭头形、线形等形状
  • AudioStream
AudioStream是Godot中存储音频的资源,用于AudioStreamPlayer(2D/3D)中,由后者将前者播放。AudioStream存储的音频文件可以是mp3、ogg、wav等音频格式。
  • Texture
Texture是Godot中存储图片图形的资源,用于Sprite和资源SpriteFrames中,由后者将前者呈现在主场景中。其又派生出14种子类型的资源,由于数量庞杂,且多数不为本帖所常用,故不再全部提出。
需要注意的是,Texture本身并不能被直接创建,但是.png、.jp(e)g等图片文件则是直接的Texture文件,可以被直接导入到Sprite等节点中
  • SpriteFrames
SpriteFrames是Godot中存储动画图集的资源,用于AnimatedSprite节点中,由后者将前者呈现、播放。SpriteFrames内部存有不同的动画图集,在使用AnimatedSprite的时候会被调用
  • Tileset
Tileset是Godot中存储图块的资源,用于TileMap节点中,由后者将前者呈现。Tileset本身具有许多属性,这里不再进行讲解,在实践篇中我们将会详细讲述这些属性中的常用属性
  • Font
Font是Godot中存储字体的一类资源,包括BitmapFont和DynamaticFont,用于具有文本显示功能的子节点,由后者将前者显现。在后期学习draw_string()方法的时候,这个资源也十分重要。
  • Script
Script是Godot中存储脚本的一类资源,包括GDScript、GDNative等。它们是Godot中极为重要的资源,没有脚本,整个软件就没有了动力。我们将会从下一节开始介绍脚本。
  • “导入”选项卡设置导入的图片和音频
在第一节里面我们简单介绍了“导入”这个位于场景节点树面板右侧的选项面板,在文件系统管理器中选择一份资源后,这个选项卡就会由一行字变成一系列信息。
本帖中我们最常用到的是Texture和AudioStream这两个资源

                               
登录/注册后可看大图

我们在文件系统管理器中选中一个图片,便能让上图所示的导入选项卡显示出详细信息
其中我们重点介绍一下“过滤”,如果你不取消勾选这个选项的话,你的素材导入进去后所呈现出的样子将会是比较模糊的,去掉后即可显示这个图片原先的样子

                               
登录/注册后可看大图

上图为勾选了【过滤】选项的情况

                               
登录/注册后可看大图

上图是取消勾选【过滤】的情况
其他选项本帖中不常用,故不再进行讲解

接下来讲解音频的【导入】面板
由于Godot中导入不同格式的音频格式,其【导入】面板也不尽相同,这里就只展示.wav音频和.ogg/.mp3音频的【导入】面板

                               
登录/注册后可看大图

上图为.wav格式的音频的导入面板

                               
登录/注册后可看大图

上图是.ogg/.mp3格式的导入面板,需要注意的是:.ogg/.mp3格式的音频在导入面板中默认开启【循环】,故如果你导入的.ogg/.mp3音频是游戏音效的话,请务必取消勾选【循环】!
以上就是资源的简单介绍了,接下来我们来学习节点的属性

  • 什么是属性
在第二节我们学习节点的时候,我们就已经听到这样一个概念了:节点的属性。那么,到底什么是属性呢?
属性(Property),从编程语言的角度来说,就是对象的成员变量。不过这个概念我们会在下一节中讲到。我们本节先以直观的认知来认识属性。
我们就以节点为准,来讲解节点的属性

                               
登录/注册后可看大图

如上图,当我们选中一个节点后,右侧的检查器就会显示很多信息,这些信息就是我们选中的节点的属性,
我们主要看一下这个检查器内的内容

                               
登录/注册后可看大图

我们会很明显地看到,检查器左边有一些文字,这些文字就叫做属性名,将鼠标悬停在这些属性名上,你就可以获得这些属性对应的成员属性名。右侧的数字、选项等就是这些属性所对应的属性值。
一个属性由(成员)属性名和属性值两个部分组成,属性名用于区分、寻找、引用、解释说明属性值,而属性值则赋予属性名代码性内容,是决定属性的关键
因此,当你理解了属性之后,请回到第二节再次学习后半部分的内容,你会有一层更深的体会。
  • 如何修改属性?
由于Godot过于人性化的属性修改设计,因此我们可以选择自行摸索。不过,出于教程起见,这里我还是需要说明一些输入规则:
  • 看到带x,y,z的,你可以输入小数
  • 看到有数字的,你可以输入小数或整数
  • 看到有一串字,旁边有个箭头的,可以点开选择其他给定的字(值)
  • 看到有空白栏,一个字也没有的,可以输入任何文字
  • 看到有【空】字样的,可以将一个资源放在这里
  • 看到有Size字样的,点开可以看得更多
  • ……
Godot里直接修改属性的操作还有很多,这里就不再一一列举了,各位同学可以自行摸索学习。

至此,资源简介和属性就到此结束了


>攒着石头准备把草神抱回家【【<

40

主题

331

帖子

6

精华

副版主

☯ 博 丽 不 是 灵 梦 ☯

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

经验
3963
硬币
509 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章

 楼主| 发表于 7 天前 | 显示全部楼层
本帖最后由 电童·Isamo 于 2022-9-23 15:52 编辑

GDScript其一:GDScript入门
从本节开始,我们就要学习整个Godot里最核心的内容——代码编程了。
由于Godot支持的脚本语言千千万,故我们本帖只以Godot原生的GDScript为范本进行学习与实操。
如果你对C#非常上手,那么你可以去1L的官网中下载并安装Mono版本,API可查看1L的官方doc。
回归正题,我们在开始学习写GDScript之前,我们先来讲解一些最基本的一些干货,以便于你对GDScript有一个更好的理解

  • 何为脚本
脚本(Script)一词,亦可称为“剧本”。这个词很形象,给一个对象写脚本,就好像给一个舞台剧写剧本,或者给一个演员写台本一样。因为你写的东西决定着整个舞台剧每个演员的动作、语言,所以剧本就起着规范、指导、驱动的作用。同样地,你写的脚本决定着一个对象的属性、这个对象的行为,驱动着这个对象。由于“脚”是人行走最基本的部位,因此形象地称之为“脚本”。
就像来自不同国家的编剧写不同语言的剧本一样,你使用的计算机语言不同,你的脚本也就不同,其文件后缀也就不同。
在Godot中,GDScript脚本以.gd结尾
  • 脚本在节点上
我们在学习场景节点的时候讲过他们的几个特征(权当复习一下):
  • 场景可以在另一个场景中实例化,作为另一个场景的子节点出现
  • 节点是整个程序里执行不同功能的最基本单位
  • 父节点的属性会影响其所有子节点的对应的属性,即其所有子节点会继承父节点的对应属性

但是,单独的一个简单节点,并不能起到多大的作用,甚至连最基本的位移,它都无法脱离脚本而自主实现。因此,我们在学习到【GDScript高级篇其十——Object】之前,我们暂时规定:所有的脚本均应应用于节点上(也就是下面会提到的挂载脚本到节点上
只有有了执行代码的脚本的节点,才算是有灵魂的节点。因此:节点是脚本的躯壳,脚本则是节点的灵魂

那么,在上一节我们提到,Script是一种资源,于是,我们就可以将脚本以资源的形式载入到节点中。
接下来,我将会讲解如何新建一个脚本,以及如何将脚本加载到一个节点上
  • 脚本第一步:创建脚本
首先我们切换到脚本编辑器

                               
登录/注册后可看大图

之后我们就会来到一个类似于下图所示的页面,如果没有任何东西,没有关系,因为这个工程里没有任何你打开过的脚本。

                               
登录/注册后可看大图

接下来,我们找到文件——“新建脚本”并点击,如下图所示

                               
登录/注册后可看大图

之后会弹出如下页面:

                               
登录/注册后可看大图

其中第一项我们不要改,第二项暂时不用管他,我们下面会讲到,第三项我们也暂时不用管他,第四项亦不须理会。
我们直接看到路径,这个是你的脚本所保存的位置,点击右侧的文件夹形状的按钮,会弹出如下窗口:

                               
登录/注册后可看大图

选择好一个指定的路径后,只需要点击打开即可确定你新建的脚本所需要保存到的位置。
别忘了给你的脚本改个名称哦~
之后点击“创建”,就会弹出如下界面:

                               
登录/注册后可看大图

那么恭喜你,你已经成功学会了如何创建一个新的脚本。
接下来,我们将学习如何将一个脚本加载到一个节点上(或者说,将脚本挂载一个节点,让这个节点挂载这个脚本)
  • 脚本第二步:挂载脚本
方法一:

                               
登录/注册后可看大图

选中目标节点,在其检查器内找到最底部的Script,点击空,将会弹出如下窗口:

                               
登录/注册后可看大图

当然,你可以从这里新建脚本,之后的操作还请见“脚本第一步:创建脚本”内的相关内容
下面我们有两个选项
点击“快速加载后”,会弹出如下窗口:

                               
登录/注册后可看大图

找到你新创建的那个脚本双击即可
方法二:

                               
登录/注册后可看大图

仍然是这个窗口,找到加载,会弹出一个窗口,在窗口内找到你新创建的那个脚本文件双击即可。
方法三:

                               
登录/注册后可看大图

在文件系统管理器内直接将新创建的脚本文件拖拽到目标节点上即可
方法四:

                               
登录/注册后可看大图

直接在脚本编辑器里左边的脚本筛选框中找到你新创建的脚本,并拖拽到目标节点上即可

我们成功将脚本挂载到了节点上,在需要保存脚本的时候,我们按下Ctrl Alt S三个键即可。
需要注意的是:
  • 每个节点只能挂载一个脚本,所以,如果你想尝试在一个脚本上挂载两个及以上的脚本,这是完全不可能的。请考虑在其下方添加子节点,并让其子节点来代行挂载这些脚本以实现一个节点挂载两三个脚本的效果
  • 每个脚本在修改后不能立即生效,需要保存后才能让新保存的脚本生效
同时,下图中的“继承”选项中的节点类型一定要与你挂载的目标节点的类型相一致,或目标节点的类型为“继承”中的类型的派生类型才可以让脚本安全地挂载在目标节点上(原因我们将会在【高级篇其十——Object】一节中进行讲解)

                               
登录/注册后可看大图


好了,基本的操作我们就讲到这里,接下来我们就正式开始讲解GDScript的入门编写了
  • GDScript 基础——extends限制脚本所需要挂载的节点
下面是两个简单的GDScript代码的范例:
  1. extends Node

  2. const T = 1

  3. var a = 10
  4. var b = "what"
  5. var c = null

  6. func foo() -> void:
  7.         pass

  8. func foo2() -> void:
  9.         c = Node.new()
复制代码
  1. extends Node2D

  2. func _draw() -> void:
  3.         draw_circle(Vector2.ZERO,3,Color.webgray)

  4. func _process(delta: float) -> void:
  5.         update()
复制代码
我们可以发现,这两行代码都有一个共同的特点:它们均以extends开头,在脚本编辑器中,它显示为红色。这些标红色的英文单词就是GDScript的关键字(Key words)
本帖中,所有关键字均以加粗标红的Verdana字体显示,以匹配编辑器内的代码文字显示,同时方便阅读。
我们接下来来学习这个放在脚本开头的extends
extends,为继承关键字,位于脚本开头时,表示这个脚本:
  • 继承自一个类或一个脚本
  • 继承自的类或脚本决定了这个脚本可以适用于哪个或哪些节点
extends的格式如下:
  1. extends <类或一个脚本的路径>
复制代码
其中extends后面所跟的类就决定了这个脚本可以挂载到哪个类型的脚本上,至于什么是类,以及为什么还可以跟脚本路径,我们会在后面讲到函数与类的时候会讲到,同时将会把extends的更一般的用法给一并讲解。这里就暂时理解为:
  1. extends <节点类型名>
复制代码
部分节点类型名可以回到第二节“节点、场景和场景实例化”一讲中进行查阅
如:
  1. extends Node
复制代码
上面的一行代码就表示该脚本只能挂载到类型为Node的节点及其派生类节点上,由于所有节点都派生自Node,故该脚本可以挂载到任何一个节点上
  1. extends Node2D
复制代码
上面的一行代码表示该脚本只能挂载到类型为Node2D的节点及其派生类节点上,由于Sprite、KinematicBody2D也是派生自Node2D的,因此这个脚本也可以挂载到Sprite节点和KinematicBody2D节点上
  1. extends Sprite
复制代码
上面一行代码表示该脚本之只能挂载到类型为Sprite的节点及其派生类节点上,由于Sprite没有原生派生节点,因此在不声明具名类并继承Sprite类(后面会讲到)的情况下,该脚本只能挂载在Sprite脚本上
在讲到【函数与类】这一讲之前,我们需要注意:
  • extends只能位于脚本开头(但也可以位于脚本其他地方,不过有关于这一情况的我们到后面会进行讲解)
  • extends只能存在一个,倘若出现了两个及以上的 extends,则会导致脚本编辑器报错
  1. extends Node
  2. extends Node2D

  3. func _do_something() -> void:
  4.         pass
复制代码
上述代码就会导致脚本编辑器报错,原因是 extends 出现了两次
以上就是关于 extends 这个关键字最基本的使用方法

  • GDScript 基础——声明量
编写代码,最基础最基础的莫过于声明量。
量(Values)是代码中存储数据信息和处理运算的最小单元,它可以参与一段代码的运算和读写。一旦失去了量这个概念,那么这个脚本的存在意义也就基本上尽失了。
所以,不管是学习哪个计算机语言,包括我们这个GDScript,声明量是我们最基本也是最基础的编写操作和环节
在GDScript非高级篇里,我不会涉及到计算机原理等相关知识,各位同学可以放心,我们只需暂时记住:声明变量是为了参与代码的运算就可以了。
代码中,任何一个代数运算的前提,就是要先声明相对应的变量。
  • 标识符——命名量的基础
在学习如何声明量之前,我们需要先学习GDScript中的标识符
所谓标识符(Identifier),就是声明变量、函数、信号这些量时这写变量和函数的名字,下文我们统一以<idf>来代指标识符
标识符以无变音纯拉丁字母a~z、A~Z、数字0~9和下划线"_"为合法字符,除此之外输入的标识符均为非法
以数字开头的标识符也是非法的标识符,且foo和FOO、FoO、FOo、fOO、foO、Foo等均不属于同一个标识符。
标识符本身就是一个特殊的字符串,只是在声明变量、函数、信号等定义量的时候,你无需也不应输入""(后面会讲到""表示字符串)
下面列举一些合法和非法的标识符,供各位同学学习参考
  • instance_maker(合法)
  • abilityCreator(合法)
  • _you_and_Me(合法)
  • up2three4U(合法)
  • 2UareMine(非法,以数字开头)
  • ^ssas(非法,含未规定为标识符的字符)
  • $lllk(非法,含不合规的符号)
注:一些在GDScript中也合法的符号,如@、$、#等,在作为标识符时不应算作合法字符
  • 数据类型——限定数据的基础
在学习声明变量和常量前,我们还需要学习数据类型:
由于Godot中数据类型繁多,我们只选择2D中比较常用的几个类型进行说明:
基本数据类型
  • int:Integer的缩写,叫做整数型,简称整型,这种类型表示的是不带小数点的整数,不论正负,如-4,0,7,11,129等
  • float:叫做浮点数型,简称浮点型,表示带小数点的数,不论正负如0.3,-3.1415926,0.7893,1.414514等
  • bool:Boolean的缩写,叫做布尔型真值型,简称布尔或真值,只有两个数:true(代表1)和false(代表0)
  • String:叫做字符串类型,简称字符串,用英文双引号""括住一串字符即为一个字符串,内部可以有空格及其他特殊字符,如"my family"、"同学"、"大好きだ"等
  • null:叫做空类型,简称空或无。它虽然是一个数据类型,也是一个特殊的数值,但它不包含任何信息,不能赋值为任何其他值
内置数学类型
  • Vector2:二维向量,它包含x和y两个基本元素,且均为float型元素
  • Vector3:三维向量,它包含x、y和z三个基本元素,且均为float型元素
  • Rect2:二维矩形,它包含x、y、w、h四个基本元素,且均为float元素
  • Transform2D:二维线性变换矩阵,它包含x、y和origin三个基本元素,且均为Vector2型元素
引擎内置类型
  • Color:颜色,可看作一个四维向量(Vector4),它包含r、g、b和a四个基本元素,且均为float型元素
  • NodePath:节点路径,它以@"xxx"格式的数据(后面会讲)或String类型为其基本元素类型
  • Object:对象,是所有Godot非内置类型的基类
容器内置类型
  • Array:数组,以[]为数组的直接定义,可容纳任何类型的数据
  • Dictionary:字典,以{}为字典的直接定义,可容纳任何类型的数据,且其内部的数据以键-值的形式相匹配
此外,所有的Node也都属于数据类型,但不是内置类型

  • 量的声明
GDScript中有四种量:
  • 常量
  • 变量
  • 枚举
  • 定义量
我们逐一讲解这些量
  • 常量(Constant):在脚本中始终不变的量,它不受其它因素的影响,永远都是一个固定的数值。
  • 变量(Variable):在脚本中可能会改变的量,它可能会受到其他因素的影响,其数值是不固定的。
  • 枚举(Enum):本质上是一系列常量的集合,在Godot中则只限定于一系列整型常量的集合
  • 定义量(Definition):这种量只用于定义一个特定的信息,并将这个信息赋予一定的属性,如函数、信号、具名类和内部类等等。
我们把常量和变量合称为数值量(Numeric value)
我们接下来讲学习如何声明这四种量
  • 常量的声明
常量用const关键字来声明,并规定要用全大写字母来书写常量名(如果遇到多个单词,用下划线"_"隔开)。格式如下:
  1. const NAME<idf> = <值>
复制代码
其中NAME就是你键入的全大写的常量名,一个等号表示给这个非定义量赋予一个初始值(Initial value),等号后面的<值>可以替换成其他任何一个在Godot所给的数据类型的数值
如下面这个常量:
  1. const ABC = 12
复制代码
表示声明了一个常量ABC,其值为12
不过,这也只是最基本的例子,根据前面学过的的数据类型,我也可以这么写
  1. const DEF = 12.76
  2. const STRING = "I love you"
复制代码
下面这种情况也是允许的
  1. const D = 32
  2. const E = D
  3. const F = E + 7
复制代码
只不过,这里常量E的值变成了常量D的值,常量F的值变成了常量E的值再+7,其结果就是:
  1. D = 32
  2. E = 32
  3. F = 39
复制代码
但是以下这种情况,脚本编辑器就会报错:
  1. const E = D
  2. const D = 32
  3. const F = E + 7
复制代码
这里我们先要澄清一个概念:代码的执行顺序
不管是哪个编程语言,计算机执行代码的顺序默认都是从上往下,即从第一行往最后一行执行的。GDScript亦是如此,从最开始的代码执行,一直执行到最下方才算结束。在后面我们讲到函数的调用的时候,这个结论依然成立。
声明常量也遵循从上往下声明与定义的原则。在上面这段错误代码中,我们先声明了E变量,给它以D变量的值,计算机执行到常量E这里,就叫做常量E被定义(Defined)了。但是在计算机执行这个代码的时候,它知道E变量是谁,但不知道D变量是谁,因为按照计算机从上往下定义变量的原则,D还没有被定义,所以这样子写,计算机因无法找到常量D的定义而导致报错。
我们在声明常量的时候,只需要常量的值在Godot所给的数据类型里存在就行了。但有些情况下,一些工程是靠团队开发的,每个成员之间可能并不熟悉你写的代码。假如真有这么一个成员,看到你声明了一个常量A,其值为32,然后这个成员就把A的初始值改成了"pee",你并不知道,然后你第二天如期地运行一下你写的程序,发现出现了bug或者闪退报错了。这可怎么办?
因此,我们需要给声明的数值量加以限定,以防止发生这类情况。格式如下所示:
  1. const <idf_VALUE>:<数据类型> = <对应数据类型下的值>
复制代码
在变量名后面加:<数据类型>就可以让一个指定的数值量的值限定在给定的数据类型范围内,如下所示:
  1. const A:int = 3
  2. const B:float = 5.0
复制代码
上面这段代码的声明是没有问题的,但下面这段就会导致编辑器报错:
  1. const TEXT:String = 114514
复制代码
原因在于114514是int型的变量,而TEXT要求给的值一定要是String类型的,这就导致类型不匹配而导致报错。
解决方法就是把int型的114514变成String类型的"114514"就行了
当然,如果你不知道你输入的数值是属于什么类型的话,可以这样写:
  1. const NAME: = <值>
复制代码
这种语法我们就叫做推断类型(Type inferrence)
计算机会根据你后面的数值来自动将变量定义为这个数值所在的数据类型
  1. const A: = 1
  2. const B: = "myself"
复制代码
上述代码中常量A会被计算机自动定义为int,常量B则会被自动定义为String

  • 变量的声明
在我们学会了声明常量以后,我们接下来就要学习如何声明变量了
变量的声明类似于常量,采用var关键字来声明变量,并规定采用下划线+全小写(即snake_case,蛇形命名法)标识符来命名变量:
  1. var value_name<idf> = <值>
复制代码
其中value_name为你的变量名,<值>可以取在Godot数据类型内的任何值
下面是几个声明变量的例子:
  1. var a = 14
  2. var b = 4.12
  3. var c = "MysSQL"
复制代码
跟常量一样,变量也支持量的引用
  1. const TEST = 1

  2. var is_reference = TEST
  3. var inheriter = is_reference
复制代码
这时,变量 is_reference 就引用了常量 TEST 的值,为1;而下面的变量inheriter也因为引用了is_reference的值而变成1
跟常量一样,变量也支持数据类型的限定语法:
  1. var <idf>:<数据类型> = <对应数据类型下的值>
复制代码
接下来的例子将展示何为变量的数据类型限定:
  1. var a:int = 14
  2. var b:float = 20
  3. var c:String = "TEST"
复制代码
可以看到,变量d是一个新建立的节点(新建语法我们后面会讲到),这时这个变量就不再是一个数值了,而是代表着一个对象(Object)(实际上节点也是一种对象)。任何一个变量,如果其值是一个对象,则我们称该变量为这个对象的引用变量(Reference variable),简称引用(Reference)。
关于引用的详细说明,我们在【函数与类】这一节中将会进一步学习
(注:声明变量时,如果变量的限定类型为float而右侧为整数,则右侧的整数可以不加.0,但是const和导出的export var【下节会讲】则必须要加上.0)
(注2:声明变量时,虽然计算机依然遵守从上往下定义每个变量这一规则,但是由于变量的一些特殊性,使得计算机在发现一个未定义的变量(的引用)后,会尝试在所有变量中寻找这个未定义的变量,若找到了这个未定义变量的声明,则将该未定义的变量提前定义并将引用指向这个变量;若翻遍了所有变量都没有找到这个变量的声明,则编辑器会报错)
同常量一样,变量也支持推断类型语法:
  1. var name: = <值>
复制代码
注意:推断类型语法和不带":"而直接赋值的动态类型(Dynamic type)语法不一样的地方在于:前者会在计算机时给这个数值量定义一个类型,这个变量的类型在运行时不能被修改,否则会报错;而后者的类型则是不固定的,意味着计算机在定义这个常量的时候,不会给它一个给定的类型,之后该变量的值可以从int修改为float,或者从int修改为String再修改为其他的类型。
当然,声明变量还可以使用隐式声明法(Default-value-omitted Claiming):
  1. var name<idf>
复制代码
这个时候,如果不规定其数据类型的话,它就等同于:
  1. var name<idf> = null
复制代码
如果隐式声明这个变量时限定了其数据类型,则它就等同于:
  1. var name<idf>:<数据类型> = <该数据类型的默认值>
复制代码
默认值如下:
  • int、float:默认值为0(0.0)
  • String:默认值为""
  • Vector2、Vector3:默认值为Vector2.ZERO(Vector3.ZERO)
  • Rect2:默认值为Rect2()
  • Transform2D:默认值为Transform2D()
  • Object(包括Node及其派生类型):默认值为null
  • Color:默认值为Color.white
  • NodePath:默认值为@""
  • Array:默认值为[]
  • Dictionary:默认值为{}

注意:隐式声明语法只能用于变量,常量则必须赋值

  • 枚举(常量)的声明
我们已经介绍了数值量(即常量和变量)的声明,接下来,我们将学习枚举(常量)的声明与引用
枚举常量用enum关键字来声明,其格式如下:
  1. enum EnumName<idf> {
  2.         VALUE_1<idf>,
  3.         VALUE_2<idf>,
  4.         ...
  5.         VALUE_N<idf>
  6. }
复制代码
其中EnumName为你枚举集合的名称,采用大驼峰命名法(也叫帕斯卡命名法,PascaCase)来命名枚举,枚举内容用一对花括号{}表示,里面的VALUE_1一直到VALUE_N都是该枚举的内容常量,且只能是整型常量,不需要也不能使用数据类型限定语法
枚举内默认将第一个常量的值赋为0,第二个为1,以此类推
下面是一个声明枚举的例子:
  1. enum TestEnum {
  2.         VALUE1,
  3.         VALUE2,
  4.         VALUE3,
  5.         VALUE4
  6. }
复制代码
上面的代码片段中声明了一个名为TestEnum的枚举,里面包括VALUE1~VALUE4四个变量,且其内部的四个常量的值分别为:VALUE1 = 0, VALUE2 = 1, VALUE3 = 2, VALUE4 = 3。
枚举的引用不同于数值量,它需要利用以下格式来进行引用:
  1. enum EnumA {
  2.         VALUE1,
  3.         VALUE2,
  4. }
复制代码
其中some_value就是需要引用枚举的变量,EnumA就是被引用的枚举,结合我们马上会讲到的运算符"."来获取其内容,在点后面键入你需要引用的具体的一个枚举内容常量名即可。同时,常量 VALUE 引用了枚举EnumA的VALUE2这个内容常量,这也是允许的,后面我们会讲解关于赋值中这个数值的问题。
除此之外,枚举的内容常量可以被赋予初始值
  1. enum TestEnum {
  2.         VALUE1 = 12,
  3.         VALUE2 = 24,
  4.         VALUE3 = -8,
  5.         VALUE4 = 8
  6. }
复制代码
只要保证这些内容常量均是整型即可

有关定义量的声明,如函数、信号、类 等,我们会在其对应的章节中进行学习
现在,布置一个作业:
  • 声明一个变量,名称为test1,赋值为12
  • 声明一个变量,名称为queue_checker,赋值为null
  • 声明一个常量,名称为PASCA,赋值为字符串Pasca
  • 声明一个变量,名称为que,限定类型为字符串,引用常量PASCA的值
  • 声明一个枚举,名称为FuturePlan,内容有四个常量:MY_LIFE, MY_COLLEGE, MY_BAG, MY_GROUP
  • 声明一个枚举,名称为ExamScores,内容有三个常量且具有初始值:STUDENT_A = 10, STUDENT_B = 9, STUDENT_C = 9
  • 声明一个变量,名称为enum_me,引用上一个枚举ExamScores中STUDENT_B的数值

  • 注释
注释(Comment)是GDScript中非常有用的辅助语法,注释可用于说明解释代码的作用,同时也可以当作一个你写小说随笔的好语法(才不是呢!!【恼】)
在计算机执行脚本时,注释会被计算机直接忽略
注释以井号#开头,#后面的一整行内容均会被视为注释
  1. # 这是一个注释
复制代码
后面的代码学习中,本帖都将会以注释的方式来解释一些代码中的问题。
(按键盘上的Ctrl和K可以将让代码和注释互换)

  • GDScript 基础——运算符
我们已经学了如何声明量(定义量除外),现在我们还需要学习运算符号才可以让我们的代码真正发挥出运算的功能。
接下来以代码块的形式来依次介绍GDScript中的运算符
一类运算符,运算结果为int或float:
  • +:加法
  • -:减法
  • *:乘法
  • /:除法
  • %:先做除法,然后用得到的商求余
  • -a:取a的相反数:a为正数时-a为负数,a为负数时-a为正数,a为0时则-a仍为0
  • +=:a += b 等价于 a = a + b
  • -=:a -= b 等价于 a = a - b
  • *=:a *= b 等价于 a = a * b
  • /=:a /= b 等价于 a = a / b
  • %=:a %= b 等价于 a = a % b

需要注意的是:如果是int除以int,若其结果中存在小数,则只保留结果中的整数部分(即去掉小数点后面的所有数字)

二类运算符,运算结果为bool
注意:true => 1, false => 0
  • >:大于
  • <:小于
  • >=:大于或等于
  • <=:小于或等于
  • ==:等于
  • !=:不等于
  • &&、and:与。两个bool均为true时,结果为true;只要有一个false,则结果为false
  • ||、or:或。两个bool只要有一个为true,则结果为true;若均为false,则结果为false
  • !、not:非。一个bool如果为true,则结果为false;如果为false,则结果为true

其中需要注意:单个等号“=”是赋值号,用于声明数值量;而双等号“==”则是比较两个值的相等关系

三类运算符,运算结果是二进制数
注意:1 => true, 0 => false,下列运算均是对一串二进制数中每个0和1进行运算
  • &:位与运算,0 & 1 = 0,1 & 1 = 1,0 & 0 = 0
  • |:位或运算,0 | 1 = 1, 1 | 1= 1,0 | 0 = 0
  • ~:位非运算,~1 = 0,~0 = 1
  • <<:位左移运算,二进制数中所有0和1向左移动一定数目的位,如 a << b就是让a中的所有0和1均向左移动b个位。移位后,新的二进制数的总位数必须与原来的二进制数的总位数保持一致。故高位算元(算元指0和1)需要舍弃,低位算元均需补为0。
  • >>:位右移运算,二进制数中所有0和1向右移动一定数目的位,如 c >> d 就是让c中的所有0和1均向右移动d个位。移位后,新的二进制数的总位数必须与原来的二进制数的总位数保持一致。对无符号数,高位算元补0。
  • <<=:a <<= b 相当于 a = a << b
  • >>=:a >>= b 相当于 a = a >> b

其他运算符,结果视情况而定
  • a if b else c:三目运算符,其中b的结果必须为bool,如果b为true,则整体结果为a,否则整体结果为c

  • 括号
GDScript中,括号(Brackets)是表示域或优先级的成对符号,在GDScript中,一共有三种括号:
  • 圆括号(),定义运算域
  • 方括号[],定义数组
  • 花括号{},定义枚举与字典
GDScript种的括号具有一个特点:括号内的内容允许跨行,但需要在保证语法完整的前提下才可以换行,此时括号内的多行代码仍然视作一行代码
下面的代码以圆括号为例,剩余两种括号同理
  1. var a = (
  2.         3 + 5 + (
  3.                 8 - 2 * 3
  4.         )
  5. )
  6. # 等同于
  7. var a = (3 + 5 + (8 - 2 * 3))
复制代码
GDScript中,圆括号可以定义运算域(Calculation Field)
  1. var b = (2 + 4) * 6  # 结果为36
复制代码
不管是计算机学上还是数学上,如果出现运算域,都是先计算运算域内的结果,再将运算域内的结果与其他数或运算域进行计算
如果出现了运算域嵌套,则先计算最内层的运算域,然后计算外层的运算域,由内向外依次运算,直至运算完最外层运算域为止
  1. var d = ((2 + 4) * 6) / 3  # 先计算2+4=6,再计算6*6=36,最后计算36/3=12,故结果为12
复制代码
以上就是括号的介绍了

  • 实践
接下来,我们来写一个实践作业:
  • 制作一个脚本,该脚本可以挂载在Node2D及其派生类型的节点上,需要准备一个名为CollisionType的枚举,内含WALL,CEILING和FLOOR这三个内容常量,不加默认值。准备三个常量:A、B、C,其中A为int类型,值为5;B引用A的值并加上3,C引用B的值并除以2。
  • 在上述脚本中,再声明三个变量:a、b、c:其中a引用常量B并除以2,b引用变量a并除以8,c引用常量C并除4求余
我们来逐一分析一下这个问题:
首先,脚本需要挂载到Node2D上,那么我们就需要在脚本开头写上(或者将开头一行修改为):
  1. extends Node2D
复制代码
这样,脚本就可以挂载到Node2D及其派生类型的节点上了。
然后,需要声明一个名为CollisionType的枚举,里面有三个内容常量。那么我们就很容易写出来这个枚举:
  1. enum CollisionType {
  2.         WALL,
  3.         CEILING,
  4.         FLOOR
  5. }
复制代码
接下来,我们要声明三个常量并赋予初始值,根据题意,我们有:
  1. const A:int = 5
  2. const B = A + 3
  3. const C = B / 2
复制代码
接下来,我们需要声明三个变量并赋予初始值,根据题意,我们有:
  1. var a = B / 2
  2. var b = a / 8
  3. var c = C % 4
复制代码
那么最终的脚本就是:
  1. extends Node2D

  2. enum CollisionType {
  3.         WALL,
  4.         CEILING,
  5.         FLOOR
  6. }

  7. const A:int = 5
  8. const B = A + 3
  9. const C = B / 2

  10. var a = B / 2
  11. var b = a / 8
  12. var c = C % 4
复制代码

补充
  • 当一个数值量引用另一个数值量时,改变这个引用者数值量的数值后,并不会影响被引用的数值量的数值;反之,如果被引用的数值量发生改变,则引用者数值量的数值也将发生改变
  • 声明常量时不能引用变量来进行赋值,但在声明变量时,可以引用变量或常量来进行赋值

以上就是本节的全部内容了,下一节我们将会讲解变量的导出以及其它的关键字
>攒着石头准备把草神抱回家【【<

23

主题

425

帖子

12

精华

版主

Rank: 24Rank: 24Rank: 24Rank: 24Rank: 24Rank: 24

经验
4509
硬币
491 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章第五届MW杯亚军数字君X68数字君X68对不起,小姐

发表于 5 天前 来自手机 | 显示全部楼层
本帖最后由 dasasdhba 于 2022-9-22 00:16 编辑

部分比喻过于奇妙反而更难理解,读者遇到这种情况可自行略过,无需深究。
此外,学习游戏引擎而言,理论只是铺垫工作,实践才是最有效的学习方式,切忌“学习”大量纯理论而不实践。
目前教程的内容在一些细节操作讲解方面相比官方文档比较详细,初学者可以参考以快速熟悉软件界面。
顺便,关于本帖部分未来计划更新内容,至少在 MF 开发中用的极少甚至根本不需要。其实泛泛而谈,我们学习任何一个软件,往往只是因为其中有一部分功能是我们想要的,软件有什么就学什么并不明智,我们只需要关注自己想要的东西。

评分

参与人数 1经验 +3 硬币 +2 收起 理由
电童·Isamo + 3 + 2 --------

查看全部评分

40

主题

331

帖子

6

精华

副版主

☯ 博 丽 不 是 灵 梦 ☯

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

经验
3963
硬币
509 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章

 楼主| 发表于 5 天前 | 显示全部楼层
本帖最后由 电童·Isamo 于 2022-9-22 23:46 编辑

GDScript其二:GDScript的导出变量、一些常用关键字
我们已经学习了如何声明数值量,为我们的代码编写打下了坚实的基础。
然而,我们声明的数值量,如果不加以处理,也仅仅是保存在脚本内部且写死的
有些时候,我们希望这些带着脚本的节点的对应变量能(在一定范围内)有不同的初始值,而且方便我们这些开发者能够不需要深入这些节点的脚本,就可以直接修改这个节点的参数(Parameter)。这时,我们就需要一个全新的概念:导出(Export)
不过在讲GDScript的导出之前,我需要对我们第三节学过的概念——属性(Property)进行一个新的讲解。
  • 属性和变量
我们在第三节就已经初步认识了属性,只不过那个时候只是以检查器这一直观的视角来定义所谓的属性。
实际上,在Godot中,一个不加脚本的节点的属性,等同于这个节点的内部脚本(Native Script,其实就是这个节点的C++源码【Source Code】)中所声明的数值量
(由于还没有学到Object,因此在学到高级篇【Object】之前,我们只考虑节点这一情况我们还以第三节讲到的Node节点为例

                               
登录/注册后可看大图

上图中,我将鼠标悬停在了Node的Editor Description上,就会显示出该节点的属性名:editor_hint。实际上,这就是Node在源码中声明的一个变量
同时我们可以在上面很容易看到清晰明了的“属性”,二字。难道说,editor_hint真的是个变量吗?
下面是该属性从C++源代码翻译成GDScript后的的代码:
  1. export(String, MULTILINE) var editor_hint = ""
复制代码
可以明显地看到有var关键字,这就是我们上一节学到的声明变量的关键字。
而窗口中“属性”二字的出现,也坐实了一个事实:一个节点的属性,就是一个节点的脚本(或其C++源代码)变量(数值量)
(注:这里我没有说GDScript脚本,是因为“变量”这个概念是通用的,你不管使用GDScript也好,还是C#也好,编程语言中的变量(严格地说应该是成员变量),在面向对象时就是所对应对象的属性。同时,我在“变量”二字后面补充了个“数值量”,是因为常量其实也是一个对象的属性,用目前所学过的知识来说,就是:常量和变量都是一个脚本所挂载到的节点的属性
注意:这里我说了“一个节点”,就意味着,同一个脚本,同一个属性,除非特殊处理,否则在每个节点中都是独立且互不干扰。我们假设有a、 b、c三个节点,挂载的都是E这一个脚本,E脚本里有一个名叫abcd的变量的声明,初始值为1。当你修改了a节点的abcd这个属性的时候(比如改为15),b、c两个节点的abcd的值是不会发生改变的。
同时,若无特殊说明,本帖今后所说的【一个节点的属性】,就代指这个节点所挂载的脚本内声明的变量(数值量),或其C++源代码中已经声明的变量,或其在检查器中可以被直接修改的变量


  • 变量的导出
既然我们已经理清了变量(数值量)和属性的关系,那么我们接下来就要尝试解决这样一个问题:
我写了一个名叫enemy_movement.gd的脚本,制作了三个对象场景(在高级篇【Object】前我们统一简称“对象”):板栗仔、乌龟、紫乌龟。本质上,它们都是敌人,都应该挂载这个脚本。脚本里我声明了这样一个变量:
  1. var speed:float = 1.0
复制代码
那么这些敌人的速度就是1.0了。
然而,紫乌龟的速度我们都知道是2.0而非1.0,如果我把上面的值改为2.0的话,不考虑给每个敌人单独写一份脚本,那么这些敌人的速度就全都是2.0了。
试想一下,假如一共有128种这样的敌人,如果每个敌人的速度都不一样,那么我就需要写128份不同的脚本,每一份脚本都要声明这个变量,还要一个个改初始值,想想都麻烦。
那么,有没有一种方法,既不需要给每个敌人单独写脚本,又可以给每个敌人不同的速度呢?
这个时候,我们就需要借助一个叫做导出变量(Exported value)的手段了:
导出变量使用关键字export,这个关键字只能用于变量声明关键字var,用在其他关键词前,或者直接使用,都会导致编辑器报错。
导出变量的格式是(以下三种的任意一种均可):
  1. export var name<idf>:<内置数据类型(可选)> = <数值>
  2. export(<内置数据类型>) var name<idf>: = <数值>
  3. export(<内置数据类型>) var name<idf>:<内置数据类型> = <数值>
复制代码
其中name为导出变量(属性)的名字,<内置数据类型>只能是上一节所提到的内置数据类型(不包括Node及其派生类型
注意:第三个导出变量的格式的两个<内置数据类型>必须是同一个内置数据类型
下面的导出变量是合法的:
  1. export var test1:String = ""
  2. export(int) var test2 = 5
  3. export(NodePath) var path:NodePath = @""
复制代码
而下面的几个则是非法导出:
  1. export var node:Node = null  # 报错原因:给定的数据类型不是内置数据类型
  2. export(int) var text:String = ""  # 报错原因:前后给定的内置数据类型不一致
复制代码
导出了一个变量之后,我们选中这个脚本所挂载到的节点,在其检查器栏最上方即可看到这个变量的操作栏。
以下图为例:

                               
登录/注册后可看大图

可以看到,声明了一个导出变量tt后,点击该脚本所挂载到的节点A时,A的属性显示在了右侧的检查器中,其中最上面的Tt就是这个导出变量tt
我们修改这一数值

                               
登录/注册后可看大图

这时后我们会在Tt这个编辑栏左侧看到一个旋转的箭头,点击它即可恢复到初始值1
我们刚刚提到:不同节点挂载了同一脚本,其属性是互相独立不共通的。这一点同样适用于导出变量。
我们还以那三个敌人和它们所挂载的脚本为例
如果我们把speed改为导出变量,那么这个时候,检查器里就出现了Speed这一属性。
我们只修改紫乌龟的Speed,将其改为2,那么结果如下图:

                               
登录/注册后可看大图


                               
登录/注册后可看大图


                               
登录/注册后可看大图

可以发现,除了紫乌龟的Speed是2以外,其他剩下的两个敌人的Speed属性的值均为1。
导出的变量,在检查器内修改其值以后,在程序运行时以修改过的值为准
这样一来,我们就可以用最省事的方式来“定制”我们的敌人了。
善用导出变量,有以下好处:
  • 降低了代码的工作量,只需要一个脚本,几个导出变量,就可以制作出具有不同属性的节点
  • 降低了代码的耦合度,提升了代码的灵活性。只需要修改导出属性,就能创造出无限的可能性

  • 导出变量的具体类型
前面我们提到过:导出变量可以限制一些数据类型。下面我们以一段代码来介绍导出变量可以限制的类型
  1. export var integer:int = 1
  2. export var float_num:float = 0.8
  3. export var string:String = "hi, world!"
  4. export var velocity:Vector2 = Vector2.ZERO
  5. export var cube_size:Vector3 = Vector3.ZERO
  6. export var rect:Rect2 = Rect2()
  7. export var transformer:Transform2D = Transform2D(0,Vector2.ZERO)
  8. export var color:Color = Color.blue
  9. export var nodepath:NodePath = @"."
  10. export var array:Array = []
  11. export var dictionary:Dictionary = {}
  12. export var any_resource:Resource = null  # Resource也可以被替换为其派生出的子类型
复制代码
(注:此外,还有一些内置数据类型是可以被导出的,学有余力的同学可以点击这里前往官方doc查看详细内容)
至此,我们就已经学习完导出变量的基本操作了,有关其更加高级的操作,我们会在后面的高级篇的【高级导出】中进一步学习导出变量


  • 一些常用关键字
我们在前面已经接触了extends、const、var、enum、export这五个关键字。实际上,Godot还有许多其他用途丰富的关键字,这里我们就挑几个最常用到的来进行说明。剩下的关键字,我们会在其专门的一节中进行介绍
在学习这些关键字前,我们需要先介绍一个很重要的概念:作用域(Scpoe)
  • 作用域
在我们执行一段代码的时候,我们不可能说会让这些代码全部从上到下,从头到尾执行一遍,我们需要给这段代码加一个限定范围,让这段代码只在这一个范围内执行,这个范围就叫做这段代码的作用域(Scope)。
GDScript不像其他编程语言(如C++、C#、Java等)那样带上花括号{}来表示作用域,它是以冒号加Tab缩进(按下Tab所形成的长空格,在左侧会以“>|”的形式出现)的形式来表示一个作用域。其中Tab缩进(若无特殊说明,本帖中的缩进均指Tab缩进)的次数(也就是 >| 的个数)表示这个作用域的层级
下面以一段伪代码来展示一个作用域:
  1. func _demo() -> void:
  2.         作用域1
  3.         作用域1
  4.         作用域1
  5.         if a:
  6.                 作用域2
  7.                 if b:
  8.                         作用域3
  9.                 作用域2
  10.                 作用域2
  11.         作用域1
  12.         作用域1
复制代码
由于本贴中代码块不支持Tab缩进,因此请在复制完本帖中的代码后自行将开头的空格改为Tab缩进

接下来,我们正式开始学习这些关键字。但有一个前提:接下来的所有关键字全部需要写在函数中,如果写在函数外,则会导致编辑器报错。

这里先给同学们呈现上一个函数,以方便后面的学习:
  1. func _ready() -> void:
  2.         <codes>
复制代码
同学们将<codes>替换成你要学习的代码片段即可
  • 条件关键字
我们在学习上一节的【运算符】一小节中曾经接触过一个运算符。叫做“三目(元)运算符”,也就是
  1. var value = a if b else c
复制代码
实际上,这个运算符是由两个条件控制关键字ifelse变化而来的。
接下来我们就来学习条件关键字(Condition keywords)。
条件关键字一共有三个:if, elseelif,其中elif关键字是else if两个关键字的缩写。
其运行格式如下:
  1. if a: # a的结果必须是bool型
  2.         <codes1>
  3. elif b: # b的结果也必须是bool型
  4.         <codes2>
  5. else:
  6.         <codes3>
复制代码
条件关键词的语法用人类语言可以表达为:
  1. 如果(if) a 的运算结果为true:
  2.         运行代码段1
  3. 否则,如果(elif) b 的运算结果为true:
  4.         运行代码段2
  5. ……
  6. 否则(else):
  7.         运行代码段3
复制代码
同时,if下面也可以不接elifelif下面也可以不接else,但elif上面必须承接一个ifelse上面必须承接一个if或者elif
下面的示例错误地使用了条件关键字:
  1. if a:
  2.         <codes1>

  3. else:
  4.         <codes2>
  5. # 编辑器报错,理由是if(elif)和else(elif)的作用域中间不能出现空行
复制代码
  1. else:
  2.         <codes1>
  3. elif:
  4.         <codes2>
  5. if a:
  6.         <codes3>
  7. # 编辑器报错,if必须在elif和else上,elif必须在if之下、else之上,而else只能位于if和elif之下
复制代码
条件关键字允许嵌套使用,如下:
  1. if a:
  2.         <codes1>
  3.         if b: # 只有在if a的结果位true,上面的codes1执行完毕后才会执行条件判断
  4.                 <codes1-1>
  5.         elif c:
  6.                 <codes1-2>
  7.         else:
  8.                 <codes1-3>
  9.         <codes1>
  10. elif d:
  11.         <codes2>
  12.         if e: # 只有在elif d的结果位true,上面的codes2执行完毕后才会执行条件判断
  13.                 <codes2-1>
  14.         else:
  15.                 <codes2-2>
  16. else:
  17.         <codes3>
  18.         if f: # 只有在上面的codes3被执行且完毕后才会执行条件判断
  19.                 <codes3-1>
复制代码

注意:一组条件关键词(if、elif、else)中,只要有一个成立,计算机就只会执行那个成立的条件后面的代码,而剩余的就不会再被计算机检测与执行!
因此,如果需要计算机连续检测一系列条件,请使用if并列句
  1. if a: # 计算机会检测该条件
  2.         <code1>
  3. if b: # 计算机会检测该条件
  4.         <code2>
  5. else:
  6.         # 如果上面的if后条件结果为true,则这段代码就不会被计算机执行
  7.         # 反之则上面的if后面的code1不会被执行而这个else后面的code3被执行
  8.         <code3>
复制代码
在某些特殊条件下,你还可以使用分支条件句来代替if并列句(见后面【分支条件句】小节)
  • 循环关键字
在实际的编写过程中,我们希望能够在一个作用域内重复执行多次同一段代码,这个时候我们就需要用到循环(Loop)来解决这个问题
在学习循环之前,我们需要先学习一下GDScript执行循环的原理:
当计算机执行代码到循环体后,计算机会根据循环关键字后给定的条件,从循环给定的作用域开头开始运行,一直往下运行到作用域末端,之后,只要循环的条件仍然能够满足,就会重复执行上述作用域内的代码若干次。每次执行作用域内的代码前,计算机都会检验循环关键字后给的条件是否满足,如果不满足条件,则立刻终止循环,并继续往下执行下面的代码
接下来我们学习GDScript中的两个循环关键字:whilefor
while关键字的用法如下:
  1. while a: # a的结果必须是bool
  2.         <looping codes>
复制代码
上述代码中,只要a的结果为true,就会重复执行looping codes中的代码,直到a的结果为false,才会跳过该段代码继续往下执行下面的代码
下面是一个while循环的示例:
  1. var a = 10
  2. while a > 0: # 只要a大于0就一直循环下面的代码,否则不执行
  3.         a -= 1 # 每循环一次,a就减1
  4.         print(a) # 把每次循环的a的值打印到控制台
复制代码
最终会把9 8 7 6 5 4 3 2 1 0依次打印到控制端,而我们肉眼看见的就是这九个数字被几乎同时全部打出来。
以上就是while的语法,接下来介绍for循环的用法。
for循环的格式如下:
  1. # i为整数:
  2. for i in <整数j>:
  3.         <looping codes>
  4. # 最基本的用法,i从0开始,循环执行<looping codes>j次。
复制代码
其中,最后两种循环我们会在学习数组时再进一步细讲
下面是一个循环的简单示例:
  1. for i in 10: # i从0开始循环到9
  2.         print(i) # 依次打印出 0 1 2 3 4 5 6 7 8 9
  3.         # 呈现时,这九个数字是几乎同时呈现在控制台上的
复制代码
关于forwhile的更多用法,我们会在日后的学习中逐步探索。
  • 分支条件句
我们前面学过了条件关键字所构成的结构(也就是所谓的条件句【Condition blocks】),但这种条件句在某些情况下也有一定的不足之处。
来看下面一段示例代码:
  1. enum Enum {
  2.         A, B, C, D, E, F, G, H, Z
  3. }

  4. var b = 0

  5. if b == Enum.A:
  6.         <codes>
  7. if b == Enum.B:
  8.         <codes>
  9. if b == Enum.C:
  10.         <codes>
  11. if b == Enum.D:
  12.         <codes>
  13. ...
  14. if b == Enum.Z:
  15.         <codes>
  16. else:
  17.         <codes>
复制代码
我们发现,这段条件句都有个共同的特点:它们都是以==作为条件判断的运算主体,而且还重复出现了枚举Enum。
试想一下,如果变量b换个长度超过28个字符的名字,那么这些条件关键字后面所跟的条件中的b就得一个个替换成这个超长的名字,而且,如果Enum里的内容常量的数量超过了10个,那么这一套换下来,就已经差不多过去了一分钟。
因此,对于这一类连等判断式,我们需要一种更加高效、一劳永逸的方法来解决这个问题。这就是分支条件句(Switch pattern)
GDScript中,分支条件句的关键字只有一个:match
match的基本用法如下:
  1. match a: # a为要被检测的变量,相当于"=="左边的变量
  2.         b: # 等价于 if a == b:
  3.                 <code1>
  4.         c: # 等价于 if a == c:
  5.                 <code2>
  6.         d,e,f: # 等价于if后面只要a == d、a == e和a == f这三个条件中有一个为true即可
  7.                 <code3>
  8.         _: # 上面的条件一个都不满足时执行:
  9.                 <code4>
复制代码
下面为分支条件句的一个例子:
  1. var a = 10
  2. match a:
  3.         3: # if a == 3:
  4.                 a = "I love you"
  5.         6: # if a == 6:
  6.                 a = "Show your love"
  7.         8: # if a == 8:
  8.                 a = "Yes, man"
  9.         _:  # 上面的都不满足时执行:
  10.                 a = "What?"
复制代码
以上便是分支条件句的基本用法,关于更加高级的用法,我们会在后面的学习中进行研究
  • 流程控制词
有时候,我们不希望循环或者分支条件句能够全部执行完,这时,流程控制词(Process-controlling keywords)就派上了用场
流程控制词一共有两种:breakcontinue
我们来同时学习一下breakcontinue的用法:
  1. for a in b:
  2.         <codes>
  3.         if c:
  4.                 break
  5.         elif d:
  6.                 continue

  7. while e:
  8.         <codes2>
  9.         if f:
  10.                 break
  11.         elif g:
  12.                 continue
复制代码
上述两段代码中,break会强制退出当前循环,并不再继续执行循环内未执行的代码,之后让计算机继续往下执行下面的代码;而continue则是强制进入下一轮次的循环,并不再继续执行当前这一轮此内的循环内未执行的代码。
从这两个词的英文词义break“中断”和continue“跳行”,我们也可以猜出这两个关键字在循环中的作用。
除了循环,continue还可以用在分支条件句中:
  1. match a:
  2.         1:
  3.                 <codes>
  4.                 if b:
  5.                         continue
  6.                 <codes_vir>
  7.         2:
  8.                 <codes>
复制代码
上述代码中,当条件句b内的continue被执行后,计算机将会停止执行 <code_vir> 这一段的代码,同时将进行a==2的检测
因此,continue在分支条件句中可以强制跳过一个条件所管辖的作用域的代码的执行,并让计算机继续检测下面的条件。
  • 作用域占位符
当一个作用域内没有任何代码执行时,如果什么都不写,会导致编辑器报错:
  1. if a:
  2.         # 注释不算代码
  3.         # 此处没有任何代码,但是编辑器报错了
复制代码
这个时候,我们就需要使用占位符pass来充当一个占位符,提示计算机:这是个空的作用域。
  1. if a:
  2.         pass
  3.         # 编辑器不报错,因为计算机已经意识到这是个空的作用域了
复制代码
如果作用域内有代码,则pass不会影响整个作用域内代码的执行,它只是一个占位符,提示计算机:如果这是空作用域的话,你就不要报错了
以上就是本节的全部内容了,下一节开始我们就要开始学习非常重要的概念——函数和类了。

>攒着石头准备把草神抱回家【【<

40

主题

331

帖子

6

精华

副版主

☯ 博 丽 不 是 灵 梦 ☯

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

经验
3963
硬币
509 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章

 楼主| 发表于 4 天前 | 显示全部楼层
本帖最后由 电童·Isamo 于 2022-9-24 13:39 编辑

GDScript其三:函数(方法)与类
我们已经在前面的两节中已经学习过GDScript中最基础的几个概念和操作,都是为了本节内容而打下的铺垫。从本节开始,我们将要正式步入GDScript中最为重要的领域——函数和类
  • 函数
实际上,我们在上一节里已经接触到了一个函数:
  1. func _ready() -> void:
  2.         <codes>
复制代码
那么,到底什么是函数呢?
我们在初二的数学课本上就已经学过关于函数(Function)的定义了:当一个自变量x发生变化时,因变量y也发生了变化,则我们称因变量y是自变量x的函数。到了高中,由于更多初等函数的加入(包括常函数y=a,a是一个常数),函数的定义又变为了:在集合X中,存在任意一个数x,使得在集合Y中,都有唯一一个数y与之对应,记它们的映射关系为f,则称y为x经过映射f而得到的函数。
计算机中(也包括GDScript),函数(Function)就是指一系列算法(有序)排列而成的算法或算法集
GDScript中,除声明变量以外,所有计算均应在函数所定义的定义域(以后统称这个函数的函数体【Function body】)内进行,否则会被编辑器报错
  • 函数的声明
函数的声明需要用到func关键字,格式如下:
  1. func function_name<idf>():
  2.         <codes>
复制代码
其中,function_name为你声明的函数的函数名称,采用snake_case命名法来命名。
下面就是一个简单函数的例子:
  1. func my_func():
  2.         a += 1
复制代码

  • 函数体内的变量调用
我们刚才提到:除声明变量外,所有计算均应在函数体内进行。那么接下来我们将会学习在函数体内使用已声明的数值量进行简单的计算
但在开始学习这个技术之前,我们先需要学习另一个概念:调用(Call)变量
调用变量,指的是将一个已经声明过的量进行直接引用,当这个引用后的量发生变化以后,其所引用的原变量也会发生相应的变化
它与引用(Reference)不同的是:引用是只引用原变量,但不改变原变量;而调用则不但引用原变量,同时还让会让原变量因引用后的变量发生变化而发生变化
下面是一个引用和调用的区别例子:
  1. var a = 1

  2. func tutorial():
  3.         var b = a # 变量b【引用】了变量a
  4.         b += 1
  5.         print(b, ",", a) # 打印结果是2,1

  6. func tutorial2():
  7.         var b = a
  8.         a += 1 # 调用了变量a
复制代码
可以看到,上面的两个函数中,局部变量b(接下来会讲局部变量,这里顺便提到了)引用了变量a的值。而在第一个函数中,变量b加上了1,因为是引用了a的值,因此只是b加上了1,但是a却丝毫没有发生变化。因此打印的结果是2,1。而第二个函数中,虽然局部变量b引用了变量a的值,但是我在这个函数中调用了a变量并且修改了它的值,这个时候这个调用的原变量a(其实就是在上面声明过的a变量)被计算机定义后,在执行这个函数时,这个变量a就会加上1而变成2,从而使打印结果为1,2。
我们还可以从赋值的角度来区分引用和调用:
  1. var a = <值> # 这是一个声明好的变量a,计算机在运行程序时将其定义

  2. var b = a # 位于单等号右侧的同名变量a就是指a被b所引用

  3. a = 1 + 2 # 位于单等号左侧的同名变量a就是指a被调用到某个作用域内
复制代码
同时需要注意的是:引用可以用于函数体外,但调用变量只能用于函数体内
还有一点需要注意:常量只能被引用,但不可以被调用,因为常量是不会也不能被改变的量!

  • 函数的局部变量和脚本变量
在上一小节的例子中,我们无意间接触了局部变量b。那么,什么是局部变量呢?
所谓的局部变量(Local variable),指的就是在一个指定的作用域内才可以使用的变量。声明局部变量后,这个变量只能在这个作用域及其更下级作用域内使用,而不能被其他函数或者上一级的作用域所引调(引用和调用)
在计算机执行代码时,若离开了这个局部变量所在的作用域,则局部变量会被计算机销毁而不能再被使用。且每次当计算机执行该作用域内的局部变量定义时,都会重新定义该局部变量的值。也就是说,每执行一次作用域,该作用域内的局部变量都会被赋为你在脚本编辑器内为它所赋予的值,之后再被计算机定义
局部变量的声明类似于脚本变量的声明,同样是使用var关键字来声明:
  1. func function():
  2.         var local_var = 1
复制代码
其中function就是你的目标函数,local_var就是你的局部变量的名字。
隐式声明、限定数据类型等声明脚本变量的方法,在声明局部变量时依旧有效
下面分别为一个正确的局部变量示范和一个错误的局部变量的示范:
  1. func correct():
  2.         var a = 1
  3.         var b = 2
  4.         if a == 1:
  5.                 var c = 3
  6.                 b += 1
  7.                 c -= b
  8.                 print(c + b + a)

  9. func wrong():
  10.         var a = 1
  11.         var b = 2
  12.         if a == 1:
  13.                 var c = 3
  14.                 b += 1
  15.         c -= b
  16.         print(c + b + a)  # 会被编辑器报错,原因是变量c只是在条件句这个作用域中被定义的,而出了这个作用域就无法使用c这个变量了
复制代码
而与局部变量相对的,就是脚本变量,也就是之前我们学习的那些在函数体之外的变量了,它们又称为这个脚本的全局变量(Global variable),只要它们已被声明,它们就可以在任何地方被引调。
需要注意的是:export关键字和onready(下一节会学到)关键字不可以用于局部变量

  • 函数的参数
有时候,我们希望向函数中预先输入一些数,就好像GDScript中函数像数学上的函数一样,这个时候,我们就需要给函数一些输入参数:
带参函数的声明方法就是在声明函数时,向函数名后的括号内填入参数名
  1. func function_params(param1, param2, ..., paramN):
  2.         <codes>
复制代码
上述代码中,函数名后面的参数可以有无限多个,但我的建议是不要超过10个,否则会很影响代码的可读性
每个参数之间用逗号","隔开
函数的参数与变量一样,可以在函数体内的任何作用域内被引用,同时也支持变量的数据类型限定语法
如果参数在声明时,后面接了个单等号 + 数值,这个时候这个参数就称为含值参数(Parameter with default value)
但含值参数的声明有一个前提:普通参数之后,连续多个参数均为含值参数。如果在这些连续的含值参数中插入了一个普通参数,则会导致编辑器报错。
下面是含值参数的声明对比示例:
  1. func compare(v1:int,v2:int=1,v3:int=5):
  2.         <codes>

  3. func miscompare(v1:int,v2:int=2,v3:int,v4:int=5): # 编辑器会报错,原因是含值参数v2和v4中间插入了普通参数v3
  4.         <codes>
复制代码

  • 函数的调用与返回
我们既然声明了一个函数,那么自然必须要让它有用武之处。这个时候,我们就需要【使用】这个函数,这一过程就叫做调用函数(Call functions)
调用函数的方法,就是直接将函数名和其后面的圆括号写出来
  1. func foo(): # 声明一个函数foo()
  2.         <codes>

  3. func main():
  4.         foo() # 调用函数foo()
复制代码
调用函数时,计算机会转到这个函数所在的函数体并依次执行函数体内的所有代码,当所有代码都执行完毕后,才会返回到这个函数被调用的地方继续往下执行
如果这个函数带有参数,则调用函数时,需要把参数的输入圆括号内。
  1. func foo(param1,param2):
  2.         <codes>

  3. func main():
  4.         foo(1,2) # 调用含参函数foo
  5.         foo(1) # 会报错,原因是没有输入足够的参数
复制代码
注意:调用含参函数时,你输入的参数的值和函数被声明时函数的参数的出现顺序是一一对应。上面的例子中,输入的数值1对应声明的参数param1,输入的2就对应着param2。
如果被调用的含参函数中有含值参数,则在调用时,可不需要对应输入含值参数的值。如果输入,则输入的值将覆盖该含值参数在函数体内的值
  1. func foo(i1,i2 = 3):
  2.         <codes>

  3. func main():
  4.         foo(1) # 合法,因为i2是含值参数,可以不用输入i2所对应的值,此时i2 = 3
  5.         foo(1,5) # 合法,但此时i2被输入的值所覆盖,在调用时,i2 = 5
复制代码

当然,也有一些函数,它们在进行运算之后会产生结果,我们希望在调用这个函数的时候可以获得这一结果,并将这个结果进行运算或赋值。这时我们就需要一个概念:函数的返回(Return)
所谓函数的返回,就是指当计算机运行完毕这个函数体,或者执行到函数体内的某一特定关键字处时,返回到函数被调用的地方的这一情况。但是,有些情况下我们希望函数能够在返回时,将原来调用这个函数的地方转换成一个值,这个时候,我么就需要用到返回关键字return

return只能作用于函数体内,且一个作用域内只能有一个return关键字,否则编辑器将会报错
当计算机执行到return关键字所在的那一行时,计算机将会停止运行return之后的所有代码,并离开函数体,返回到函数所在的地方。如果return后面带有数值量,则会将该数值量提取到函数被调用的地方,作为函数执行的结果。
下面是return的一个示例:
  1. func foo(a):
  2.         if a == 1:
  3.                 var b = 2
  4.                 return b
  5.         if a == 2:
  6.                 var c = 3
  7.                 return c
  8.         if a == 3:
  9.                 return
  10.         if a == 4:
  11.                 var d = 6
  12.                 return d

  13. func main():
  14.         print(foo(1)) # foo()中的局部变量 b将会被作为函数的结果返回到这里,其后的代码不再被执行
  15.         print(foo(2)) # foo()中的局部变量 c将会被作为函数的结果返回到这里,其后的代码不再被执行
  16.         print(foo(3)) # 由于return后面没有值,因此默认以null作为结果返回到这里,其后的代码不再被执行
  17.         print(foo(4)) # foo()中的局部变量 d将会被作为函数的结果返回到这里
复制代码

在GDScript中,声明函数时,你还可以强制指定函数的返回类型。语法如下:
  1. func function() -> <类型>:
  2.         <codes>
  3.         return result
复制代码
其中function为目标函数,<类型>就是你需要让函数输出(返回)的结果的类型。
如果一个函数被强制指定了返回类型,则函数体内的所有作用域(包括函数体本身),都需要return关键字来返回一个对应返回类型的值
  1. func foo() -> int:
  2.         var a:String = "hi"
  3.         return a # 报错,原因是a的类型与函数强制返回的类型int不匹配

  4. func foo2(k:bool) -> bool:
  5.         if k:
  6.                 return false
  7.         else:
  8.                 if !k:
  9.                         k = true
  10.                         return k
  11.                  # 报错,原因是有一个(或几个)作用域内缺少return关键字
复制代码

当然,也有一些情况下函数确实返回不出来数值,但就是为了防止【有开发成员不小心篡改成有返回数值的而导致出问题】这个问题。这个时候,我们可以将强制返回类型语法中箭头[->]后面的类型名改为关键字void。它表示这个函数什么值都不返回。此时return关键字后面不能跟任何数值量,只起到【中断函数运行并离开所在函数,让计算机返回函数被调用的地方继续往下执行代码】的作用。
  1. func foo(a) -> void:
  2.         if a == 1:
  3.                 return
  4.         if a == 2: # Mark A
  5.                 <codes1>

  6. func main():
  7.         foo(1) # 由于a等于1,因此函数foo()的Mark A处及以下的函数均被跳过执行,同时本函数继续往下执行<codes2>
  8.         <codes2>
复制代码

返回值函数和无返回值函数(以下分别称为有果函数和无果函数/功能函数)的共同作用:
  • 都是处理一系列算法/处理一系列事件的集合
  • 都是降低代码复用的结构
它们的不同作用:
  • 有果函数可以用来编写读取值的函数,如get_last_velocity(),am_I_mario(),is_block_hit(),等。也有少量有果函数可以同时负责主要事件/算法的处理同时输出结果
  • 功能函数主要用于处理一系列事件,或者设置值,如set_velocity(a,b,c),die()等等

至此,我们就学完了函数的内容,接下来我们将初步接触面向对象中一个非常重要的概念——(Class)
  • 【类】为何物?
在计算机编程语言中,类(Class)就是一个对象的基床。任何一个对象,都是要以类为模板来进行创建的
GDScript也不例外,任何一个对象,都是以类为模板来进行创建的,而这些以类为模板而被创建出来的对象,就叫做这个类的实例(Instance)
实际上,Godot中的节点、脚本、资源等等,它们都是Object类所派生出来的类的实例。本质上,这三者也都可以叫做对象(Objects)
我们这一小节就围绕类来讲解脚本的实质与extends的真实用法,对象的成员属性和成员方法,父子类的特性与父方法重写,调用类(节点)的成员,以及GDScript中几种类的声明。
  • 脚本的实质与extends的真实用法
我们在学习GDScript其一时就已经学习了一个叫做extends的关键字,那时候我们规定的是:
  • 这个关键字只能用于开头,后面只能跟节点的类型名
  • 这个关键字只能在该脚本中出现一次
实际上,extends的意思是:将一个类进行扩展,使得该类能够继承(Inherit)这个被扩展的类的所有东西
而本质上,GDScript中的脚本是类的基床,代表着一个类
所以换句话说:extends就是用于继承、扩展类的关键字,而其所在的脚本便创建了这个被继承类的一个子类(Child class),而这个被扩展的类也就理所当然地叫做父类(也叫做超类,Super class或Parent class)
我们以新的视角再来看下面这行代码:
  1. extends Node
复制代码
上述代码的意思就是说:这个脚本(所代表的类)继承了Node这个类里的所有东西,然后创建了Node这个类的一个子类
只不过,如果不加以特殊修饰,那么这个类就是一个没有名字的类,我们会在后面学习具名类时就可以为这个没有名字的类赋予名字。
既然脚本利用继承关键字extends继承了一个类的同时还创建了这个类的子类,那么是不是就意味着继承也可以用在脚本上?
答案是肯定的。
继承脚本,如果这个脚本所代表的类(注:不是所继承的类)没有名字,那么就需要在继承关键字extends后加上这个脚本的存放路径:
  1. extends "res://your_script.gd"
复制代码
这时就代表这个脚本继承了路径所指向的脚本所成的类,并创建了这个类的子类。
由于我们通常让脚本继承自节点,并将这个脚本赋予在节点上,因此也可以说是节点继承了这个脚本所创建的类。
(也可以说:这个节点就是这个脚本所代表的类的一个实例)
因此,只要是需要类继承的地方,都可以使用继承关键字extends
还要注意一点:GDScript只允许单继承,不能多重继承即不可以继承两个及以上的类
  • 类的成员
我们上面已经学过:脚本代表着一个类。接下来,我们就要学习面向对象中另一个重要的概念:类的成员(Member)
GDScript中,一个类的成员,就等于这个类所对应的脚本里所声明的所有量,包括常量、变量、枚举、函数、信号等(不包括函数里声明的所有局部变量
前三者则又统称为这个类的成员属性,简称这个类的属性;而后两者则又称为这个类的成员方法(Method),简称这个类的方法
而类又是创建对象实例的基床和模板,因此,这个类的实例的成员,就是这个类的成员
由于节点也是其所挂载的脚本所代表的类的一个实例,因此我们也可以说:一个节点的属性就是这个脚本所代表的类的属性,一个节点的方法就是这个脚本所代表的类的方法。
而我们又学过:每个节点的属性,除非是父子关系,否则是互相独立、互不干扰,这一点同样适用于其挂载的脚本所代表的类所声明的属性和方法。
  • 父子类的特性与父方法重写
前面提到过,一个类(子类)继承自另一个类(父类)后,父类中的所有东西都可以在子类中使用。这里的“所有东西”,指的就是父类的成员。
因此换句话说:父类中的成员会继承给子类,并使之成为子类的成员的一部分
我们看下面两个脚本的代码:
  1. # New script
  2. extends Node

  3. var a = 10
  4. var b = 15
复制代码
  1. # New script 2
  2. extends "res://new_script.gd"

  3. var c = 20

  4. func _ready() -> void:
  5.         print(a,",",b,",",c)  # 打印结果10,15,20
复制代码
很显然,new script2继承了new script,因此new script中的两个属性a、b均可以在new script2中直接使用
同样地,父类中声明的方法,在子类中也可以被直接调用
  1. # New script
  2. extends Node

  3. var a = 10
  4. var b = 15


  5. func foo():
  6.         print(a + b)
复制代码
  1. # New script 2
  2. extends "res://new_script.gd"

  3. func _ready() -> void:
  4.         foo() # 打印结果为25
复制代码
但反过来,父类不可以调用子类的成员,否则编辑器会报错
  1. # New script 2
  2. extends "res://new_script.gd"

  3. func foo():
  4.         print("I am here!")
复制代码
  1. # New script
  2. extends Node

  3. func _ready() -> void:
  4.         foo() # 报错,父类不能调用子类的成员
复制代码
导出变量也属于成员的范畴,不过在检查器中显示时,会先显示父类的导出变量,然后才会显示子类的导出变量
  1. # New script
  2. extends Node

  3. export var parent:int = 1
  4. export var something:float = 0.8
复制代码
  1. # New script 2
  2. extends "res://new_script.gd"

  3. export var child:int= 3
  4. export var something2:float = 0.78
复制代码
导出结果如下图所示

                               
登录/注册后可看大图

(勘误:应该是父类属性和子类属性)

当然,我们也会遇到这样一种情况:
我给父类写了个方法,名叫作killed(),但是父类的方法的内容在子类不能完全一样,可却又要保证方法名字不变。这个时候我们就需要用到一个叫做方法重写(Override)的手段来解决这个问题了。
这个手段很简单,只需要在子类中声明一个跟需要被重写的父类方法同名的方法即可
  1. # New script
  2. extends Node

  3. func foo():
  4.         pass
复制代码
  1. # New script 2
  2. extends "res://new_script.gd"

  3. func foo(): # 父类的方法foo()被子类重写
  4.         print("child")
复制代码
然而,在子类中重写后的方法会直接覆盖掉父类,这时如果你希望仍然可以使用父类的同名方法,只需要在调用foo()的时候在foo()前面加一个英文句点"."即可
  1. # New script
  2. extends Node

  3. func foo():
  4.         print("parent")
复制代码
  1. # New script 2
  2. extends "res://new_script.gd"

  3. func foo():
  4.         .foo() # 调用父类同名函数,打印"parent"
  5.         print("child") # 再执行下面的代码,打印"child"
复制代码
  • 调用类(节点)的成员
前面我们是在子类调用了父类的成员,但实际上,一个工程内的所有对象不可能全都由父子类来定义并让子类调用父类。这个时候,我们就需要学习如何从一个类中调用另一个类的方法(或者说,如何从一个节点的脚本中调用另一个节点的(相同)脚本的方法)
我们只需要按照下面的格式就可以成功调用外部类(节点)的成员:
  1. <对象(的引用)>.<目标对象的属性或者方法>
复制代码
在调用外部类或者成员时,我们只需要获取这个对象,然后打上英文的句点".",之后输入这个对象的属性或方法即可调用成功
下面的代码就是一个调用类的成员的例子:
  1. # New script,挂载在名为NewScriptNode的节点上
  2. extends Node

  3. var s = 1
  4. var t = 2


  5. func foo():
  6.         print(s + t)
复制代码
  1. # New script 2
  2. extends Node

  3. onready var target:Node = $NewScriptNode # 挂载new_script.gd的节点的引用


  4. func _ready() -> void:
  5.         target.s = 2 # NewScriptNode节点的属性s被调用,修改值为2
  6.         target.foo() # NewScriptNode节点的方法foo()被调用,打印值为4
复制代码
不过这里我们提前涉及到了导入节点,我们到下一节再将导入节点。
上面的代码就很好地示范了如何调用其他节点的方法,不过调用外部节点的成员是有一定限制的,具体是什么限制我们到下一节会细讲
  • 几种类的声明
GDScript中可以声明三种类:空白类(脚本)、内部类(Inner class)和具名类(也叫公开类,Named class或Public class)
声明空白类就是我们的创建脚本的操作,这里就不再多讲了。
接下来我们学习声明内部类和具名类
  • 声明内部类
声明内部类用class关键字来声明,其格式如下:
  1. class ClassName<idf> (extends Class):
  2.         <类体>
复制代码
其中ClassName就是你要声明的内部类的名字,采用PascalCase命名法命名,括号中的部分可不要,但默认会继承Object类
类体必须位于内部类的作用域内,且类体内的内容等同于写脚本时脚本中的内容
下面是一个内部类和内部类外的声明的范例:
  1. extends Node

  2. var t:int = 4
  3. var s:int = 8



  4. class TestClass extends Node: # 声明一个名叫 TestClass的内部类
  5.         var r = 9
  6.         var q = 12
  7.         
  8.         func _ready() -> void:
  9.                 print(r + q)
复制代码
内部类声明好后,你不可以直接从该脚本中调用内部类的成员。这虽然不会导致编辑器报错,但会导致程序在运行时出问题,如:
  1. extends Node

  2. var t:int = 4
  3. var s:int = 8



  4. class TestClass extends Node:
  5.         var r = 9
  6.         var q = 12
  7.         
  8.         func _ready() -> void:
  9.                 print(r + q)


  10. func _ready() -> void:
  11.         print(TestClass.r) # 直接调用内部类的成员,虽然不会报错,但是运行时会出现问题
复制代码
GDScript里要想调用一个内部类的成员,必须新建这个内部类的实例并引用之
  1. extends Node

  2. var t:int = 4
  3. var s:int = 8



  4. class TestClass extends Node:
  5.         var r = 9
  6.         var q = 12
  7.         
  8.         func _ready() -> void:
  9.                 print(r + q)

  10. onready var new:TestClass = TestClass.new() # 由于TestClass是节点,因此需要onready关键字限定,我们下一节会讲

  11. func _ready() -> void:
  12.         print(new.r) # 这样子才能成功打印该属性
复制代码
具名类在制作库的时候,配合单例使用会更有奇效,我们会在讲解单例的时候再进一步学习内部类。
  • 声明具名类
具名类需要用class_name关键字来声明,其格式如下:
  1. extends <类>
  2. class_name ClassName<idf>, "图标路径"(可选)
复制代码
其中ClassName为你要声明的具名类的名字,图标路径可有可无
一个脚本只能声明一个具名类,这一点很像Java中的【一个类文件中只能有一个公开类】
声明了具名类之后,我们便可以在节点管理器中找到我们的具名类
  1. extends Node
  2. class_name New
复制代码

                               
登录/注册后可看大图

需要注意的是:
  • 每个具名类都会在节点管理器中都会显示声明这个具名类的脚本,非常方便开发者溯源。
  • 如果你继承的是Node2D类,那么请在Node2D选项下面找到你声明的具名类节点,如果你继承的是Sprite类,那么请在Sprite下方找到你声明的具名类节点,其他节点亦是如此。
继承关键字extends后面可以加上你声明的具名类的类名,这个时候,脚本就会继承这个具名类
  1. # Script 1
  2. extends Node2D
  3. class_name New
复制代码
  1. # Script 2
  2. extends New # 继承了Script 1所代表的类New(Node2D)
  3. class_name New2
复制代码
这样,New2就是New1的子类、Node2D的孙类了。
如果具名类后面同时声明了"图标路径",那么节点管理器中就会直接显示这个图标而非所继承的类的图标

补充
  • 默认情况下,直接调用脚本内的变量和方法,都只是针对这个脚本所挂载到的那一个节点,并不会影响挂载了同一脚本的其他节点


以上就是函数(方法)与类的全部内容了,下一节我们将要开始学习虚方法、onready关键字和节点的导入

>攒着石头准备把草神抱回家【【<

40

主题

331

帖子

6

精华

副版主

☯ 博 丽 不 是 灵 梦 ☯

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

经验
3963
硬币
509 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章

 楼主| 发表于 3 天前 | 显示全部楼层
本帖最后由 电童·Isamo 于 2022-9-24 13:46 编辑

GDScript其四:虚函数、节点的简单导入和onready变量
前面三节我们学会了GDScript中的三个基本操作:数值量的声明、导出变量和声明函数。其中数值量(包括导出变量)和函数统一称为这个脚本所代表的类的成员,也是脚本所挂载到的那个节点的成员的一部分。
本节中,我们将学习虚函数(虚方法)、节点的简单导入和onready变量。

  • 虚函数
实际上,我们在上一节和上上一节的课程中就已经接触到了一个虚函数(Virtual method):
  1. func _ready() -> void:
  2.         <codes>
复制代码
虚函数是Godot中一类非常重要的函数,它们是唯一一类可以通过程序内固定的事件而被自行触发的函数。而开发者所声明的其他函数,是不可以被程序直接执行的,正如我们上一节所学,必须要经过调用才可以执行这些函数。同样地,如果你想因为一些程序中固定的事件而触发你声明的函数内的事件,那么就必须要在虚函数中调用这些声明的自定义方法
虚函数有一个共同的特点:它们的函数名都以下划线"_"开头,但有些情况下,我们也可以声明一个以下划线"_"开头的函数。因此,如果你想知道一个函数是否是虚函数,可以这么做:
按住Ctrl,将鼠标移到要检测的函数的函数名上,此时,该函数名下方会出现下划线

                               
登录/注册后可看大图

此时鼠标左键点击该函数名,这时将会跳转到一个页面:

                               
登录/注册后可看大图

这个页面就是Node的属性和方法大全页面,如果以后想要直到一个类到底有哪些成员,可以直接在这个页面里查询。
回归正题,如上图所示,如果你看到一个方法名右侧有"virtual"这个单词,就说明它是一个虚函数(虚方法),会因系统的某个或某些固定的事件的触发而被触发。具体的事件可以阅读该虚函数下方的文字说明。
为方便起见,这里先附上几个常用的虚函数及其触发说明。其中有四个虚函数(标红),我们会在高级篇进行更加深入的讲解。
虚函数
虚函数触发说明
_init() 该对象被载入内存中初始化时触发,为该对象的构造函数(Structure function)
_enter_tree()脚本所挂载到的节点进入当前场景的节点树时触发
_exit_tree()脚本所挂载到的节点离开当前场景的节点树时触发
_ready()脚本所挂载到的节点进入当前场景的节点树并就绪时触发
_process()每一个空闲帧(即电脑屏幕刷新率限制内的帧数)就触发一次,一秒内触发的次数与你电脑的显示器的屏幕刷新率有关
_physics_process()每一个物理帧(即程序运行时程序内已经设定好的帧数,等同于CTF中设置程序属性时的Frames Rate)就出发一次,默认一秒内触发60次
_input(event)有键位输入时触发
_unhandled_input(event)有键位输入但还未被_input(event)方法处理时触发


这些虚函数我们在后面会经常用到,这里我们暂时以_ready()虚函数为例:
  1. extends Node

  2. func _ready() -> void:
  3.         print("I'm ready!")
复制代码
根据虚函数表对_ready()函数的描述,我们需要把脚本挂载到一个节点上,并且要把该节点放入当前场景的节点树中(如果该节点就是该场景的根节点则请跳过这一步)。运行程序,之后会在控制台中看到:
  1. I'm ready!
复制代码
接下来,我们尝试以调用方法的形式把上述代码改写一下:
  1. extends Node

  2. func _ready() -> void:
  3.         foo()


  4. func foo():
  5.         print("I'm ready!")
复制代码
再运行一次,结果是一样的。
至此我们把虚函数的简单介绍就已经学习完了,我们会在后面的学习中逐步学习更多的虚函数及其用法。

  • 节点的简单导入
我们在上一小节中学习了_ready()虚函数的基本用法。实际上,我们在上一节已经接触到了一种导入节点的写法。这次,我们将学习如何导入节点。
导入节点(Import node)是获取一个节点并调用该节点最基本的手段和必须要经过的步骤。如果不导入节点,那么我们在上一节学习的【调用其它节点的成员】这一方法也就无能为力了。因此,要想调用其它节点的成员,我们必须先导入节点才行。
  • 导入节点的方法
导入节点一个叫做get_node()的函数,这个函数只能在Node类中使用。
使用方法如下:
  1. get_node(<NodePath>) # 是有果函数,返回一个Node,该Node就是通过NodePath而得到的
复制代码
其中的NodePath就是我们下面要讲的节点路径了。
节点路径的格式如下:
  1. @"节点路径"
复制代码
我们以下图所示的作为一个调用节点路径的一个例子

                               
登录/注册后可看大图

对于Root节点而言,Parent节点的路径是:
  1. @"Parent"
复制代码
而Child的节点路径是:
  1. @"Parent/Child"
复制代码
而Root的节点路径则是:
  1. @”/root/Root“
复制代码
实际上,节点路径的写法有很多,下面是从官网doc某处搬运过来的所有节点路径的格式与写法,可供各位同学学习参考:
  1. # 没有路径前缀表示此路径是相对于当前节点的
  2. @"A" #子节点A
  3. @"A/B" # A的子节点B
  4. @"." # 当前节点
  5. @".." # 父节点
  6. @"../C" # 兄弟节点C
  7. # 携带前缀路径表示场景树的绝对路径
  8. @"/root" # 等同于 get_tree().get_root()
  9. @"/root/Main" # 主场景——"Main"(假设主场景的名字为Main)
  10. @"/root/MyAutoload" # 自动加载脚本——MyAutoload(如果有)
复制代码
然而,在调用get_node()这个方法时,"@ "符号可不用输入。
仍然以上面的为例,以Root为准,在Root中获取Parent时可以这么写:
  1. get_node("Parent")
复制代码
对于Parent下的Child,我们有:
  1. get_node("Parent/Child")
复制代码
而对于Root本身,我们有:
  1. get_node(".")
复制代码
或者,在获取节点自身时,我们也可以将上述方法直接替换成关键字self,表示直接引用这个脚本所挂载到的节点。
然而,对于挂载该脚本的节点而言,get_node()也未免有些影响敲代码的效率了(?)。为此,Godot引入了一个字符“$”来直接替换get_node():
  1. # 对于Root而言
  2. $Parent
  3. # 等价于
  4. get_node("Parent")
复制代码
但是,如果这个语法的前面含有一个对象的调用,则该语法非法,需要替换成get_node()
下面是"$"语法糖的合法与非法示例,供各位同学参考学习
  1. 对于Parent节点而言
  2. $Child  # 合法
  3. $".."  # 合法
  4. $Child.$".."  # 非法,不可以在其它节点的引用后再次使用改语法。
  5. $Child.get_node("..") # 合法
复制代码
  • 如何成功导入节点
我们已经学会了导入节点的方法,但是,到底如何成功地导入节点,还是一个未知数。
接下来,我们将学习如何成功地导入节点。
导入节点需要遵守如下步骤:
  • 声明一个变量,并给它指定一个Node类及其子类,这个指定的类必须是你要导入的节点的类型或该节点的类型的父类
  • 在虚函数_ready中调用这个引用变量,并给这个变量赋值,值即为get_node()语法(也包括$语法糖)
下面我们仍然以Root-Parent-Child这个结构为例,将脚本挂载在Root节点上,来学习如何导入这些节点:
  1. extends Node

  2. var par:Node = null
  3. var child:Node = null
  4.         
  5. func _ready() -> void: # 注:导入节点必须写入_ready()变量中,不可以直接在声明变量时直接将值赋给变量
  6.         par = $Parent
  7.         child = $Parent/Child
复制代码
需要注意的是:导入的节点只能在当前场景的场景树中有效,超出这个范围则无法导入节点
至此,我们就已经学会如何导入一个节点了

  • onready变量(载入变量)
前面一小节我们学会了如何导入节点:先声明变量,再在_ready()函数中调用这个变量并将这个变量赋的值设为get_node或$语法糖。
然而,如果我们一次性要导入十几个变量,这样写未免会显得麻烦,同时还会占据代码的大部分篇幅。
为此,GDScript引入了一个关键字:onready
这个关键字跟导出变量关键字export一样,只能加在声明变量关键字var前,且不能用于局部变量。但同时,export关键字和onready关键字不可同时使用!
onready的作用,就是将其后面所声明的变量在节点加入当前场景的节点树并就绪时再由计算机定义,而在就续前,onready后所声明的变量则会使用默认值。
给一个声明的变量前加onready,等同于将这个变量先声明在函数体外,然后再在_ready()虚函数中调用并赋值。
  1. # 还是以Root节点为Parent的父节点,脚本挂载在Root节点上
复制代码
这样一来,在我们导入节点的时候,就可以直接给载入变量赋值为get_node()或$语法糖的结果了

至此,我们就已经学习完了虚函数、节点的简单导入和onready变量的用法。
下一节开始,我们将要学习GDScript中另一个比较重要的概念——信号(Signal)
>攒着石头准备把草神抱回家【【<

40

主题

331

帖子

6

精华

副版主

☯ 博 丽 不 是 灵 梦 ☯

Rank: 20Rank: 20Rank: 20Rank: 20Rank: 20

经验
3963
硬币
509 枚

赞助用户永吧十五周年建吧日纪念勋章永吧十五周年倒计时海报勋章

 楼主| 发表于 前天 13:13 | 显示全部楼层
本帖最后由 电童·Isamo 于 2022-9-25 22:03 编辑

GDScript其五:信号
我们在前四讲中已经学会了如何声明数值量,如何让脚本继承类并挂载到节点上,如何声明函数,以及虚方法和onready变量的简单应用。
在本节课开始之前,我们先来回忆一下如何调用其它节点的方法:
  1. <节点引用>.<方法>
复制代码
例如下图这棵节点树:

                               
登录/注册后可看大图

其中A的脚本为:
  1. extends Node

  2. onready var child:Node = $Child


  3. func _ready() -> void:
  4.         child.foo() # child为子节点的引用,foo()为Child节点的一个方法
复制代码
而Child的脚本为:
  1. extends Node


  2. func foo():
  3.         pass
复制代码
试想一下这个情况,假如我有三个Child,分别为Child1、Child2和Child3
其中,除了第一个Child1子节点的方法仍然为foo()外,其它两个分别为foo2()和foo3(),
为了一次性获取A的所有子节点,并调用A下所有子节点的某个方法,于是有了以下的代码片段:
  1. extends Node

  2. func _ready() -> void:
  3.         var children:Array = get_children()
  4.         for i in children: # 获取A的所有子节点
  5.                 i.foo() # 调用A的所有子节点的该方法
复制代码
看上去好像一劳永逸了,然而我们前面提到过,除了Child1的方法名是foo()以外,其余两个的方法名均不是foo(),这就会导致这段代码在程序运行时报错。
在实际的工程中,会有很多类似地情况发生。同时也会有一些情况是:我并不能确定目标节点的目标方法叫什么名,但我希望能够通过这个函数来调用这个目标节点的目标方法。当然,我们还可以思考这一一个情况:我现在有一个函数function(),我希望它在被调用时,能够发送(Emit)一个通知(Notice),然后我可以自由选择让一部分节点在接收到这个通知之后,能够迫使接收这个通知的节点的某个特定的方法被强制调用。而实现这个手段的工具,便是我们今天要学的,GDScript中另一个十分重要的概念——信号(Signal)

  • 何谓信号?
信号是Godot中将某些事件与脚本中的方法相连接的一种重要媒介(Media),它可以让若干个节点的方法响应于一个特定的信号,而这个信号又可以在某些事件被触发或某些函数需要对外发送事件通知时所产生。它是降低GDScript节点之间高耦合度的重要工具。
你可以简单地理解为:村长在广播里喊人,然后被村长点到的人都急忙忙地赶到了村长办公室门口。“喊人”这个动作就是信号,而“被喊到的人”就是接收这个信号的节点,而“这些人跑到村长办公室门口”就是节点在接收到了特定的信号之后调用的特定方法。
  • 在节点面板中查看信号
我们在第一节中讲基本界面时曾经提到了检查器右侧的节点面板,而该面板中的其中一个子面板便是节点的信号
我们在场景树面板中选中一个节点,就可以在节点面板的信号子面板中查看该节点的所有信号:

                               
登录/注册后可看大图

这些便是该选中的节点的信号
  • 如何将信号连接到某个方法上
我们前面提到过,信号发送时,是让特定的节点接收的,而要想沟通信号和这些节点,就必须要让信号发射源(Signal source)连接(Connect)到这些需要接收该信号的节点的特定方法上
我们有两种连接方式:手动连接(也叫信号面板初始连接)和自动连接(又称信号代码连接
手动连接的方法,就是双击你要连接的信号(这里我以ready()信号为例):

                               
登录/注册后可看大图

然后会弹出如下窗口:

                               
登录/注册后可看大图

在上面,你可以选择一个要连接到的带脚本的节点,然后在下方的“接收方法”中输入你需要接收该信号的方法名即可。
不过这里有个编程习惯:我们通常会将接收信号的方法以"_on"或者"_"+大写字母开头为方法名的开头,同时将它们声明在脚本的末尾部分,并与其他方法以两空行隔开。而这些方法我们又称之为信号方法(Signal method)
当然,我的建议是大部分情况下用"_"作为方法名开头即可,如果接收方法还有可能会被其它节点所调用,则不需要加"_"前缀
不过,你写的接收方法也可以是一个节点本身就自带的方法。如queue_free()。
确定好要连接的方法以后,点击连接,系统会自动跳转到脚本编辑器界面。
如果你填写的接收方法在目标节点的目标脚本中并不存在,则会在该脚本中自动声明一个同名方法:
  1. # 连接前
  2. extends node
复制代码
  1. # 连接后
  2. extends Node


  3. func _on_A_ready() -> void:  # 系统自动声明的方法
  4.         pass # Replace with function body.
复制代码
我们只需要在编写时将pass那部分替换成你的代码即可
手动连接完毕后,你会在该方法名最左侧看到一个绿色的图标:

                               
登录/注册后可看大图

点击这个图标,会弹出如下界面:

                               
登录/注册后可看大图

在这个界面中,你可以查看所有连接到这个方法的信号及其信号源节点和信号目标节点
自动连接则需要一个名叫connect的方法,该方法的使用格式如下:
  1. connect(<信号名:String>, <信号接收节点>, <信号接收方法名:String>)
复制代码
其中,信号名为你要连接的信号的名称,字符串的形式出现。信号接收的节点为目标节点,如果连接到的是当前脚本内的方法,可用self关键字代替。信号接收方法名为你要将该信号连接到的方法的名称,字符串的形式出现
注:当方法名以字符串的形式出现时,不可将括号带入
如果要使用connect()方法来连接信号,最好写在_init()虚方法或者_ready()虚方法内。
下面以Child为例,讲解自动连接信号的方法
  1. extends Node

  2. onready var A:Node = get_parent() # get_parent()方法可以获取该节点的直接父节点


  3. func _ready() -> void:
  4.         A.connect("ready",self,"_on_A_ready") # 注:如果要将别的节点的信号连接到自身,请引用该节点并调用其connect()方法
  5.         # 将父节点 A 的信号 ready 连接到下面的方法 _on_A_ready() 上


  6. func _on_A_ready() -> void:
  7.         pass
复制代码
上述_ready()方法可以替换为_init()。
ready()信号的作用等同于_ready()方法,因此,一旦A的_ready()方法被触发,则会发送ready信号,而ready信号连接到了节点Child的一个方法 _on_A_ready() ,因此就会调用该方法。
实际上,一个信号可以连接多个节点的多个方法,反之,一个方法也可以接收来自多个节点的多个信号
由此,信号实际上更加适合制作像组件节点这种可能会添加到不同节点,而这些节点在处理同一种事件时可能会出现不同操作的情况。当然,对于需要节点接耦的场合(比如制作一个玩家精灵对象和玩家主对象,需要把玩家精灵对象塞到玩家对象中,并同时由玩家主对象来操控玩家精灵对象,且在玩家精灵对象不存在的情况下,玩家主对象照常能用),信号依然是你的不二选择。
  • 如何解除信号连接
既然信号可以连接到方法上,那它也理应可以解除连接。
与连接信号一样,解除信号连接的方法也分为手动和自动两种。
手动解除方法:前往信号子面板,找到、选中已经建立连接的信号并右键,会弹出一个菜单:

                               
登录/注册后可看大图

在弹出的菜单中选择“断开连接”即可。
自动解除方法则使用disconnect()方法,其结构如下:
  1. disconnect(<信号名:String>, <信号接收节点>, <信号接收方法名:String>)
复制代码
其语法类同于connect()语法,只不过是断开目标信号和目标方法而已。
(注意:connect()和disconnect()方法不可以被重复调用,否则会导致调试器报错。虽然不会中断程序运行,但大量的调试器报错则会引起开发者不适。如需防止被二次调用,请配合is_connected()方法使用,该方法的结构等同于disconnect(),用于检查某个信号是否已经连接到某个方法上)
  • 查看节点中某个信号的触发条件
实际上,我们只从信号名称上来猜测信号被触发时的条件,在某些情况下会造成“只知其一,不知其二”的偏负面效果,如果想要确切地知道一个节点的信号的触发条件,就需要查看这个信号的描述。
方法也很简单,将鼠标悬停在对应的信号上一段时间即可:

                               
登录/注册后可看大图

上图就是查看ready信号被触发时的条件

  • 自定义信号
前面我们所提到的信号,都只是局限在节点自带的信号上。而实际上,我们也可能会希望我们有些方法在其被调用期间内也可以发送信号,让其它节点的方法也能够接收到这个信号。这时,我们就需要声明自定义信号(Custom signal)了
声明自定义信号需要用到signal关键字,其格式如下:
  1. signal my_signal<idf>
复制代码
其中,my_signal就是你要声明的自定义信号的名称
声明完成后,你就可以在挂载该脚本的节点的信号子面板中找到该自定义信号了。

                               
登录/注册后可看大图

如上图所示,你在脚本中所声明的自定义信号会显示所有节点自带信号的最上方,这就使得开发者可以非常直接方便地使用自己声明的信号。
之后,我们便可以在函数内部控制我们已声明好的自定义信号发送。发送信号需要emit_signal方法,其结构如下:
  1. emit_signal(<信号名:String>)
复制代码
其中信号名同connect()方法中的信号名一样,为字符串。该方法用于发送一个指定的信号,
下面仍然以本节课中的A节点为例,声明一个自定义信号,并将该自定义信号发送出去:
  1. extends Node

  2. signal my_signal

  3. func _ready() -> void:
  4.         emit_signal("my_signal")
复制代码
接下来,我们让子节点连接到这个信号my_signal,连接到的方法为_on_A_my_signal
  1. extends Node


  2. func _on_A_my_signal() -> void:
  3.         print("Signal received")  # 将会在A节点调用emit_signal("my_signal")时打印Signal received
复制代码
只要emit_signal()方法被调用,他就会立即发送其内部所代表的信号,同时也让其它连接到该信号的对应节点的对应方法也立即被调用。
  • 含有参数的自定义信号
但是,函数是可以带参数的,可要是一个信号连接到了带有参数的方法上,这可怎么办?
实际上,节点中有些信号和别的不太一样。

                               
登录/注册后可看大图

就拿上面的这两个为例,可以看到,这两个信号的括号里面是带有参数的。其实,如果这个方法通过手动连接的形式在某个节点中自动创建了一个信号方法,则这个信号方法也是带参数的:
  1. func _on_A_child_entered_tree(node: Node) -> void:
  2.         pass # Replace with function body.
复制代码
这就是带参信号(Signal with parameters),它支持一个参数的发送,并让目标方法的参数也成为这个带参信号的参数。
对于自定义节点的声明而言,带参信号需要如下声明:
  1. signal my_signal(param1,param2,...,paramN)
复制代码
只需要在信号名后面输入括号,然后括号内输入若干参数即可。
需要注意的是:声明的自定义带参信号是不可以对其参数使用数据类型限定语法的!也不可以给其每个参数设置初始值
带参信号所连接到的方法,其参数数量必须要和带参信号中的参数数量一致。并且,带参信号的每个参数,在目标方法上也是一一对应的
下面仍然以A作为父节点,Child作为子节点,来示范一个带参信号的例子:
  1. extends Node

  2. signal my_signal(param1,param2)
复制代码
子节点Child:
  1. extends Node


  2. func _on_A_my_signal(param1, param2) -> void:
  3.         pass # Replace with function body.
复制代码
由于我是手动将my_signal连接到子节点上的,并且子节点的脚本在连接该信号之前并不存在 _on_A_my_signal() 方法,因此,该方法内的参数是直接自动搬运自源信号的。
如果需要发射一个带参信号,我们仍然可以使用emit_signal()方法,此时它的用法如下所示:
  1. emit_signal(<信号名:String>,param1,param2,...,paramN)
复制代码
其中信号名后面的若干个参数param要与你发射的带参信号的参数保持一致。
上面的那个例子用emit_signal()改写就是:
  1. extends Node

  2. signal my_signal(param1,param2)

  3. func _ready() -> void:
  4.         emit_signal("my_signal",1,2) 发送带参信号,参数分别为1和2
复制代码
  1. extends Node


  2. func _on_A_my_signal(param1, param2) -> void:
  3.         print(param1 + param2)  # param1 = 1, param2 = 2,故打印结果为3
复制代码
注:由于声明带参信号时不能直接指定其参数的数据类型,因此在编写发送带参信号的代码前,请务必确定好你要连接到的方法的参数的数据类型,并根据这一数值类型来确定你要发送的带参信号的参数
  • 连接信号时给带参信号的目标方法绑定特定参数
实际上,在我们连接信号时,我们也可以将参数预留在我们声明的带参信号上,在发送到指定方法后将该方法的参数自行替换为这些预留的参数。
如果是手动连接带参信号,则需要先选中要发射带参信号的节点,然后在其信号子面板里找到并双击你要发射的带参信号,这时会弹出如下窗口:

                               
登录/注册后可看大图

我们找到右下角的“高级”开关并将其打开,会出现如下窗口:

                               
登录/注册后可看大图

在新出现的窗口右侧,我们就可以添加目标方法的给定参数了,注意:添加的参数于要连接到的目标方法的参数是一一对应的,且不能通过拖拽来调整其顺序。
我们还以A-Child这对父子节点为例:
  1. # A节点
复制代码
  1. # Child 节点
复制代码
注意,A节点中的emit_signal方法里并没有发送参数,如果我用手动连接的方法来绑定默认参数,则应如下设置:

                               
登录/注册后可看大图

回到如上图所示的节点,如果我给定的默认参数分别为1,2,则首先在“添加额外的参数”下方的选项框里选择第一个参数(1)的数据类型

                               
登录/注册后可看大图

在这个例子里,我们的第一个参数为1,可以是real(float),也可以是int,这里就以int为准。
然后点击右侧的“添加”按钮,就会出现如下所示的情况:

                               
登录/注册后可看大图

这个时候,我们把上面的0修改为1,即可完成第一个参数的设定。
类似地,我们也可以完成第二个参数的设定。完成后如下图所示:

                               
登录/注册后可看大图

然后点击“连接”即可,此时,将鼠标悬停在该信号连接上,会弹出如下提示:

                               
登录/注册后可看大图

运行程序,控制台将会打印结果“3”。
如果是自动连接,则connect()方法需要按如下格式编写:
  1. connect(<信号名>,<目标节点>,<目标方法名>,[param1,param2,...,paramN])
复制代码
在中括号内对应输入要绑定的默认参数即可。
如果使用自动连接来改写上述示例,则Child的示例如下:
  1. extends Node

  2. onready var a:Node = get_parent() # 引用父节点A


  3. func _ready() -> void:
  4.         a.connect("my_signal",self,"_on_A_my_signal",[1,2]) # 使得父节点A在emit_signal后不带任何参数的情况下而被调用时,目标方法的param1为1,param2为2


  5. func _on_A_my_signal(param1, param2) -> void:
  6.         print(param1 + param2)
复制代码
结果是一样的。
关于信号连接带默认参数的更详细的用法,我们会在高级篇【信号的深入】一篇中讲解。
以上就是本节的全部内容了,下一节我们将会讲到GDScript中的一种重要的数据类型——数组(Array)的用法
>攒着石头准备把草神抱回家【【<
您需要登录后才可以回帖 登录 | 创建账户

本版积分规则