DGScript ---c++基础学习笔记
因此,虽然有 C++ 基础可以让学习 GDScript 更容易一些,但仍然需要花一些时间来熟悉 GDScript 的特性和 Godot 引擎的使用方法。:C++ 和 GDScript 都是面向对象的语言,因此你对 C++ 中的类、对象、继承、多态等概念已经有一定了解,这些概念在 GDScript 中同样适用。
GDScript 是一种专门为 Godot 引擎设计的脚本语言。
Godot 是一款开源的、跨平台的游戏引擎,它使用 GDScript 作为首选的脚本语言来开发游戏逻辑、场景和其他功能。
GDScript 受到 Python 的启发,在语法和结构上与 Python 很相似,使得它易于学习和上手。与其他语言相比,GDScript 在 Godot 引擎中运行的性能良好,并且能够与引擎的 API 紧密集成,使得开发者可以方便地创建复杂的游戏逻辑和交互效果。
使用GDScript,开发者可以编写游戏中的脚本、节点的行为、UI 界面的逻辑等,并且能够直接在 Godot 编辑器中运行和测试代码。因此,GDScript 是学习和使用 Godot 引擎进行游戏开发的重要工具之一。
如果你已经有了 C++ 的基础,学习 GDScript 应该会相对容易一些,因为 GDScript 与 C++ 在语法和概念上有一些共通之处。以下是为什么学习 GDScript 对于有 C++ 基础的人来说可能会比较容易的几个原因:
1. 面向对象编程概念相似:C++ 和 GDScript 都是面向对象的语言,因此你对 C++ 中的类、对象、继承、多态等概念已经有一定了解,这些概念在 GDScript 中同样适用。
2. 语法接近:GDScript 的语法与 Python 更接近,但与 C++ 也有一些相似之处,比如使用大括号 `{}` 表示代码块、定义变量时指定类型等。这些相似之处可能会让你更容易上手。
3. 轻量级脚本语言:GDScript 是一种脚本语言,不需要像 C++ 那样需要关心内存管理、编译过程等底层细节,这样可以让你更专注于游戏逻辑和功能的实现。
尽管 GDScript 和 C++ 在某些方面有相似之处,但也有一些差异,例如类型系统、性能等方面。因此,虽然有 C++ 基础可以让学习 GDScript 更容易一些,但仍然需要花一些时间来熟悉 GDScript 的特性和 Godot 引擎的使用方法。通过不断练习和尝试,你将能够更加熟练地运用 GDScript 进行游戏开发。
综上,作为有c++基础的,本文记录博主在学习Godot引擎过程中,遇到的GDSCript的需要记的知识点。
可能有点混乱……
博主的学习方式:
1 视频:1【已完结】Godot4零基础入门游戏开发制作教程_哔哩哔哩_bilibili (注意加群)
2 godot 4.x 教程 100集_哔哩哔哩_bilibili (up另开了一个200集的)
2 图文:1 前言 — Godot Engine (4.x) 简体中文文档 官方中文版文档
2 GDScript零基础图文入门.pdf(从视频1的群里下载到的,谢谢群友分享)(这个文章里,作者列出了很多学习资源网站,不想麻烦找的看本文末尾)
3 工具查询: GODOT帮助文档
ChatGPT3.5
正文:
1 float转int,四舍五入
Variant round(x: Variant)
将 x 舍入到最接近的整数,中间情况远离 0 舍入。支持的类型:int、float、Vector2、Vector2i、Vector3、Vector3i、Vector4、Vector4i。
round(2.4) # 返回 2
round(2.5) # 返回 3
2 浮点数比较
bool is_equal_approx(a: float, b: float)
如果a和b彼此接近相等,则返回true。
这里,“近似相等”意味着 a 和 b 在彼此的一个小的内部 epsilon 内,该 epsilon 与数字的大小成比例。相同符号的无穷大值被认为是相等的。
if(is_equal_approx(0.1+0.2,0.3)):
print("相等") #执行
3 逻辑表达式
not 非(否)
and 与(同时为 true)
or 或(任意为 true)
4 强类型变量
在定义变量时,在变量名后加上冒号和类型来明确变量类型,例如:
var lifeV: int = 100
var name: String = "Conan"
5 推导类型
每次都加上一个冒号和类型会比较麻烦,所以 GDScript 提供了一种语法: :=
var player_Life := 100 # 等于 player_Life: int
var player_Name := "Conan" # 等于 player_Name: String
6 获取节点 $
$ 语法本质上是一种简写,它的完整写法是一个方法调用,写做 get_node("节点路径"),不过还是 $ 写法更简单实用。
类似c++中获取控件指针
var 输入框: LineEdit = $LineEdit
var 按钮: Button = $Button # 顺手把按钮也拿
节点名可能包含一些奇怪的符号,直接把名字写在 $ 后面会出现语法错误,比如有个节点叫 做 外. 币 巴-伯,这时就可以使用字符串来表示节点名,变成 $"外.币 巴-伯" 即可。
准确来说,$ 符号后面填写的并不是节点名,而是节点路径,例如我们可以使用两个点 .. 表示上一级,或者使用 /root/ 开头表示场景根节点,下面来看几个例子: $"../ABC" 获取和当前脚本所在节点同级的 ABC 节点 $"../../../" 获取自己的父节点的父节点的父节点 $"/root/BFG" 获取场景中最外层的 BFG 节点。
①获取直接子节点
▪ get_child()
②获取直接父节点
▪ get_parent()
▪ get_node("..")
③获取多级子 / 多级父节点
▪ /
④获取所有的直接子节点
▪ get_children()
唯一名称
某些节点需要在其他位置反复通过 为累赘。 $"XXX" 语法访问,这时候 $ 符号后面长长的路径就会成 如果这个节点的名称在场景中是唯一的,那么就可以给这个节点勾选上 唯一名称:
此时即可在代码中通过 %"节点的唯一名称" 语法来获取这个节点,在这种语法中就只填写节 点名即可,不需要节点的路径。
如果节点名称没有空格或者其他特殊符号、没有造成语法歧义的话,可以去除引号,也就是 %节点的唯一名称
7 添加节点 add_child
调用哪个节点的 add_child 就是给那个节点添加子节点。
$ABC.add_child(新节点) # 给 ABC 节点添加子节点
8 删除节点 free 和 queue_free
一般情况下更建议使用 queue_free 来删除节点,方法名中的 queue 是队列的意思,可 以理解成排队,也就是说这是让节点排队删除,而不是立刻删除。 而 free 则是立刻删除节点,在调用 free 时,Godot 就会立刻删除这个节点
# 这是举例用的错误代码
free()
print(position) #已经删除,这里就会出错
如果我们将 free 换成 queue_free 则可以避免这个报错,Godot 会先将调用 queue_free 的节点记录下来,等咱们的代码执行完毕后,在空闲时间时再将它们删除。
queue_free() 等同于 call_deffer("free") 在空闲帧中调用free
9 Match 条件分支语句
match 语句在判断变量与大量固定值是否相等时,比 if 分支更简洁,不过由于 if 可以实 现同样的功能,所以用的较少。
比如下面的示例判断三月份有几天。
var 月份 = 3
match 月份:
2:
print("非闰年该月份有28天")
4,6,9,11:
print("该月份有30天")
_:
print("该月份有31天")
更多用法可以参阅官方文档相应介绍,可以先认识一下这个语句,但不一定能用到。
10 三元运算符(又称三目运算符)
比如下面的例子,判断玩家在 x 轴的正半轴还是负半轴。
var 玩家的x轴位置 = 100
var 玩家方向 = 1 if 生命值 >=0 else -1
夹在 if 和 else 中间的为条件语句 生命值 >= 0,如果条件语句为真,返回 if 前的 值,否则返回 else 后的值。
11 随机整数 randi_range
int randi_range(from: int, to: int)
它会根据括号里填入的数字生成一个随机 整数,包含这两个数以及两数之间的数
bingoA = randi_range(1,9) #产生1`9之间的随机数
12 数组 Array (或叫集合)
数组就是一堆数据构成的组,在 GDScript 中使用一对方括号表示数组,在方括号中填入要保 存的数据,数据之间用逗号分隔,例如使用数组制作背包:
var 背包: Array = ["水瓶", "钥匙", "金币"]
print(背包[2]) # 显示:金币
print(背包) # 显示:["水瓶", "钥匙", "金币"
获取数组长度 len
var 背包 := ["水瓶", "钥匙", "金币"]
print(len(背包)) # 显示一个数字
使用 .remove_at() 删除指定位置的元素
var 背包 := ["水瓶", "钥匙", "金币"]
背包.remove_at(1)
print(背包) # 显示:["水瓶", "金币"
一个数组中可以存在不同类型的数据
var 一个数组 := [1, "你好", false
甚至数组内在再含一个数组
var 又数组 := [[1, 2, 3], [1, 2, 3]]
数组遍历: 可以自行写循环遍历,也可用专门为遍历而生的语法:
这个东西本质上还是个循环,循环次数就是遍历目标的长度,每一轮循环中,都将从遍历目标 里取出一个元素放到元素变量中。
for <元素变量名> in <遍历目标>:
<代码块>
var 背包 = ["水瓶", "钥匙", "金币"]
print("背包中有:")
for 物品 in 背包:
print(物品)
有时候我们想直接指定循环次数,例如显示 50 个 hello,这样直接用一个变量和 while 也可 以搞定,但我们可以结合 range 方法和 for 来实现同样的效果:
range 这个方法会根据括号里的数字产生一个数组,里面分别是 0、1、2、3、4...直到括号 里的数字,但不包括那个数,也就是最后一个数字是 49。
for 当前次数 in range(50):
print("Hello")
不过 GDScript 还给咱们提供了一种简写方式,直接把 和上面的 range(123) 写成 123 即可,例如 range(50) 效果相同的 for 可以写成
for 当前次数 in 50:
13 str 把任何Variant类型转换为一个String
尽可能以最佳方式将一个或多个任何 Variant 类型的参数转换为一个 String。
var a = [10, 20, 30]
var b = str(a)
print(len(a)) # 输出 3(数组中元素的数量)。
print(len(b)) # 输出 12(字符串“[10, 20, 30]”的长度)。
14 函数返回值 强类型
返回值也支持强类型语法,在参数列表的括号后面使用 -> 来表示返回值的类型
func 输出拼接结果(左边的输入框, 右边的输入框) -> String :
...
return ...
15 表示坐标向量 Vector2
对应的整数版本 Vector2i。
16 引用类型数据
数字、字符串、布尔值都是这值类型数据,而数组是引用类型数据
var a = [1, 2]
var b = a
a.append(10)
print(a)
print(b)
这段程序会输出两个 [1, 2, 10]。这是因为数组是引用类型的数据,当执行 并不是这个数组,而是这个数组的引用。
这种引用类型的数据,除了数组外还有很多,例如绝大多数类的实例都是引用类型,自然也就 包括各种节点。
17 节点的生命周期方法
生命周期方法不需要我们手动调用,Godot 会在内部自动调用它们。
_enter_tree :在节点进入到场景树时执行
注意不要和下面的 _ready 搞混,在执行 _enter_tree 生命周期方法时可能还没有子节 点,因为子节点还没有加入到场景树中。
_ready :当节点完全准备好时执行。
完全准备好是指子节点都执行完毕 _enter_tree 方法。
_exit_tree : 节点离开场景树
顾名思义,当节点离开场景树时执行,且子节点优先执行。
生命周期 - 循环执行周期。下面这两个周期方法会在节点存在时反复执行。
_process 和 _physics_process
_process(空闲帧) 是每个画面帧时执行 。这个方法还需要有个参数,Godot 给的默认参数名是 delta,它表示当前帧和上一帧之间间 隔的时长,单位是秒。
_physics_process 类似画面帧,游戏中进行物理效果模拟时也是一帧一帧进行的,不过这个帧不等于画面帧, Godot 默认是每秒 60 物理帧,同样他也有个 delta 参数,表示上一个物理帧与当前物理帧之间的间隔时长。
画面帧和物理帧就是指 _process 和 _physics_process。
当我们处理一些画面显示相关的逻辑,例如按钮动画、视角移动等,建议使用 _process, 这能保证每次画面刷新时都能看到流畅的画面变化。
如果要处理一些物理相关的逻辑,例如玩家移动、开门关门等,一定要使用 _physics_process,因为物理碰撞、摩擦等运算都是在物理帧进行的,如果某个物体在画面帧 中移动,可能会导致物理帧中处理不到这次移动信息,从而影响物理模拟的真实性。
两个 process 生命周期都有个 delta 参数,使用这个方法可以平衡不同帧率对游戏的影响。
最常用的生命周期就这三个: _ready 、_process 、 _physics_process 。
18 获取输入 Input类
Input 类专门用于获取玩家的输入信息
比如,方法 bool is_key_pressed(keycode: Key) const 获取玩家按键
另: 与 is_key_pressed() 相比,is_physical_key_pressed() 被推荐用于游戏内的动作,因为无论用户的键盘布局如何,它都会使 W/A/S/D 布局有效。
Godot 定义了一堆 KEY_??? 这样的变量来表示每一个按键,KEY_W 是键盘上的 W 键。具体见 godot的搜索帮助 Key 。
引擎主界面菜单中的 项目 -> 项目设置 -> 输入映射 选项卡,即可看到类似上图的界 面,我们可以在这里添加咱们的按键映射
线性输入
游戏手柄上有一些可以“输入一半”的键,比如摇杆和扳机,这时候就可以使用 Input.get_action_strength("动作名称") 来获取一个小数数值,范围是 0 ~ 1,表示按键 移动的强度。
成对输入
get_axis 指定两个动作来获取轴的输入,一个是负的,一个是正的。
例如操控船只的加速和减速,我们可以使用 Input.get_axis("反方向动作","正方向动作") 来获取一个 -1 ~ 1 的值。
get_vector 通过指定正负 X 和 Y 轴的四个动作来获取输入向量。 这个方法在获取向量输入时很有用,比如从操纵杆、方向盘、箭头或 WASD。向量的长度被限制为 1,并且有一个圆形的死区,这对于使用向量输入进行运动很有用。
例如玩家的上下左右移动,可以使用 Input.get_vector("-x动 作","+x动作","-y动作","+y动作") 来获取到一个 Vector2 类型的值,其中的 x 和 y 的范围是-1 ~ 1。
19 鼠标输入
可以使用 get_global_mouse_position() 获取鼠标在 2D 世界中的坐标。
如果要获取鼠标的移动速度:
func _input(event):
if is_instance_of(event,InputEventMouseMotion):
print(event.velocity)
20 _input 生命周期
void _input(event: InputEvent) virtual
只有在启用输入处理时才会被调用,如果该方法被重写则会自动启用,可以使用 set_process_input() 进行切换。
21 常用节点
- Node2D: 2D 场景中的基本节点类型,用于表示 2D 空间中的对象。
- Control: 用户界面元素的基本节点类型,用于构建游戏 UI。
- Sprite: 用于在 2D 场景中显示 2D 图像或纹理。
- AnimatedSprite: 用于播放带有动画的精灵。
- Camera2D: 用于定义 2D 场景的相机属性,控制视图的位置和缩放等。
- CollisionShape2D: 用于 2D 碰撞检测的节点,可以定义物体的碰撞形状。
- Area2D: 用于触发区域检测的节点,可以检测其他节点是否进入或离开区域。
- KinematicBody2D: 用于实现基本的 2D 运动和碰撞检测。
- RigidBody2D: 用于实现具有物理属性的 2D 物体,例如重力、碰撞反应等。
- TileMap: 用于创建 2D 地图,可以快速构建复杂的地图场景。
一些常见节点的一般属性和常用成员:
Node2D:
- 位置属性:
position
、rotation
、scale
。- 父节点和子节点管理:
get_parent()
、add_child(node)
、remove_child(node)
。Control:
- 用户界面属性:
rect_min_size
、rect_size
、anchor_left
、anchor_top
。- 样式:
theme
、custom_styles
。Sprite:
- 显示的纹理:
texture
、flip_h
、flip_v
。- 动画:
frame
、frame_coords
.AnimatedSprite:
- 动画播放:
frame
、animation
、speed
。- 动画控制:
play()
、stop()
、set_frame()
.Camera2D:
- 视图控制:
zoom
、offset
、rotation
。- 视图限制:
limit_left
、limit_right
、limit_top
、limit_bottom
.CollisionShape2D:
- 碰撞形状类型:
shape
、shape_type
。- 碰撞检测:
is_colliding()
、get_collider()
。Area2D:
- 区域属性:
shape
、monitoring
、collision_layer
、collision_mask
。- 区域检测:
_on_area_entered()
、_on_area_exited()
。KinematicBody2D:
- 运动控制:
move_and_slide()
、move_and_collide()
、is_on_floor()
。- 碰撞检测:
move()
、collide_and_slide()
。RigidBody2D:
- 物理属性:
mass
、friction
、bounce
。- 物理控制:
apply_central_impulse()
、set_linear_velocity()
。TileMap:
- 地图属性:
tile_set
、cell_size
、custom_tiles
。- 编辑地图:
set_cell()
、get_cell()
、tile_exists()
。
一些常见节点类型之间的部分继承关系:
Node ├── CanvasItem │ ├── Control │ │ ├── Button │ │ ├── Label │ │ └── TextureRect │ ├── Sprite │ ├── TextureRect │ └── ... ├── Spatial │ ├── Node3D │ │ ├── Camera │ │ ├── MeshInstance │ │ └── Light │ └── Node2D │ ├── Sprite │ ├── Area2D │ └── ... ├── PhysicsBody2D │ ├── RigidBody2D │ │ ├── KinematicBody2D │ │ └── StaticBody2D │ └── CollisionObject2D │ ├── Area2D │ └── ... ├── Resource └── ...
Control 节点及其子类节点:
Control ├── AcceptDialog ├── Button ├── CheckBox ├── ConfirmationDialog ├── Container ├── CenterContainer ├── HBoxContainer ├── MarginContainer ├── Menu ├── OptionButton ├── Panel ├── PopUp └── ...
Node2D 节点及其子类节点:
Node2D ├── Camera2D ├── CollisionObject2D │ ├── Area2D │ ├── KinematicBody2D │ ├── StaticBody2D │ └── RigidBody2D ├── Position2D ├── RayCast2D ├── Sprite ├── TileMap └── ...
Sprite 节点及其子类节点:
Sprite ├── AnimatedSprite ├── Particles2D ├── NinePatchRect ├── Polygon2D ├── RectangleShape2D ├── RayCast2D └── ...
AudioStreamPlayer 节点及其子类节点:
AudioStreamPlayer ├── AudioStreamPlayer2D └── AudioStreamPlayer3D
上面的示例图表展示了一些节点类型之间的基本继承关系。节点类层次结构在 Godot 引擎中非常庞大和多样化,以满足不同类型的游戏开发需求,因此继承关系也相对复杂。
22 move_and_collide 移动物体
KinematicCollision2D move_and_collide(motion: Vector2, test_only: bool = false, safe_margin: float = 0.08, recovery_as_collision: bool = false)
沿着运动向量 motion 移动该物体。为了在 Node._physics_process() 和 Node._process() 中不依赖帧速率,motion 应该使用 delta 计算。
返回 KinematicCollision2D,包含停止时的碰撞信息,或者沿运动向量接触到其他物体时的碰撞信息。
23 模板 PackedScene (打包场景)
创建 PackedScene 方法有三:
1. 点击菜单栏[场景] -> [新建场景]后,开始制作你的模板,并保存当前场景。
2. 在任意场景对着节点列表中的某个节点右键,点击 [将分支保存为场景]。
3. 在节点列表中拖拽节点到下面的文件列表中。
Godot 中的一个场景就是个 PackedScene
想要把一个 PackedScene 使用代码创建出来,就需要先在代码中获取到 PackedScene 这个 文件。
使用 load("文件路径") 来读取一个 Godot 资源,这里的文件路径使用 项目中的资源。
load("res://物体/某个packed_scene.tscn")
创造一个 PackedScene
只需要先实例化一个 PackedScene 实例,并调用它的 pack 方法 即可:
var 打包包 := PackedScene.new()
打包包.pack(被打包的节点)
这个 pack 方法就是将某个节点放到这个 PackedScene 中,所以结合节点元数据,我 们就能把分数信息保存到一个 PackedScene 中了
var 节点 := Node.new()
# set_meta 就是添加一条元数据
节点.set_meta("分数", 123)
var 打包包 := PackedScene.new()
打包包.pack(节点)
不需要 add_child 此处的 Node 节点只是存个数据,不需要添加到场景中去。
保存资源
需要使用 ResourceSaver.save 方法:
ResourceSaver.save(打包包, "user://存档.tscn")
这样,那个包含分数元数据的Node就被以 PackedScene 的方式保存到用户目录的 存档.tscn 文件中了。 完整的代码如下:
var 节点 := Node.new()
# set_meta 就是添加一条元数据
节点.set_meta("分数", 123)
var 打包包 := PackedScene.new()
打包包.pack(节点)
ResourceSaver.save(打包包, "user://存档.tscn")
# 删除这个用完了的节点
节点.free()
不要保存引用 不要保存任何实例的引用,在读取时这些引用都会失效。 建议只保存值类型数据,例如 数字、字符串、Vector3、Color 等。
读取存档
因为咱们保存的是个 PackedScene,所以读取存档就是实例化 PackedScene
var 节点 = load("user://存档.tscn").instantiate()
# 显示之前保存的分数
print(节点.get_meta("分数"))
节点.free()
24 instantiate 实例化该场景的节点架构
实例化该场景的节点架构。触发子场景的实例化。在根节点上触发 Node.NOTIFICATION_SCENE_INSTANTIATED 通知。
当我们想要根据PackedScene 创建新物体时,可以调用它的 instantiate 方法,这个方法会返回创建好的节点。
var 保存好的场景 = load("res://物体/某个packed_scene.tscn")
var 新物体 = 保存好的场景.instantiate()
get_parent().add_child(新物体)
25 代码实现信号连接
有点类似于Qt,connect和disconnect
例如:
extends Control
func _ready():
$Button.pressed.connect(当点击按钮) #关联到函数
func 当点击按钮():
$Label.text = str(int($Label.text) + 3)
$Button.pressed 是 Signal 类型
需要断开连接时
$Button.pressed.disconnect(当点击按钮
自定义信号
在脚本中使用 signal 关键字定义信号,具体格式和定义方法差不多:
signal <信号名>([参数列表])
触发信号,手动触发 emit 关键字
signal 开机完成()
func 开机():
print("加载中...")
已经开机 = true
print("开机完成")
开机完成.emit()
信号可以向外界反应自身的状态,但这不是节点之间的唯一通信途径,别忘了我们可以直 接使用 <节点变量>.属性或方法 这种形式修改其他节点的属性或是调用其他节点的方法。
使用Callable
#test signal
signal abc
func _ready():
connect("abc",Callable(slotabc))
emit_signal("abc")
func slotabc():
print("abc")
传递参数
signal abc
func _ready():
connect("abc",Callable(self,"slotabc")) #或者这么写
emit_signal("abc","123")
func slotabc(str):
print(str)
26 组
使用节点对象的 is_in_group 方法判断节点是否属于某个组
func _ready():
print($Zombie.is_in_group("亡灵生物"))
虽然应该不常用,但如果你想要使用代码操纵节点的组,可以使用 节点添加到一个组中,或使用 add_to_group 方法把 remove_from_group 从组中移除节点
$Zombie.remove_from_group("亡灵生物")
$Zombie.add_to_group("怪物")
print($Zombie.is_in_group("亡灵生物")) # 输出 false
print($Zombie.is_in_group("怪物")) # 输出 true
还有个 get_groups 方法可以获取节点的全部组,一般用不到
27 节点自定义属性
给脚本中的属性变量加上 @export 前缀即可
@export var 玩家名: String = "没名字吗?"
@export var 钱包: int = 5
func _ready():
# 注意,成员变量是指脚本最外层的变量,不要定义在方法里面!
pass
给节点加上上面代码后,即可在属性面板看到效果
很多时候,属性导出可以代替掉 load 方法。
属性变量必须使用强类型指定类型或指定上初始值
28 字典 Dictionary
键值对 存储数据
var 玩家信息: Dictionary = {
"名字": "Rika",
"年龄": 22,
"职业": "赤魔法师",
}
获取元素值的方式也很类似数组,方括号里直接填写键即可,例如显示玩家名字:
print(玩家信息["名字"])
如果不能保证某个键是否存在,可以使用in 关键字(或者叫运算符)进行判断,in的左边 是键,右边是字典,结合 if 关键字:
if "武器" in 玩家信息:
print("玩家手持 " + 玩家信息["武器"])
else:
print("玩家没有武器")
如果只是简单的获取值,每次都加这个 if xxx in 也太麻烦了,所以 GDScript 为字典对象 提供了一个方法,叫做 get。
print(玩家信息.get("名字"))
# 等同于
print(玩家信息["名字"])
如果玩家信息中不包含名字键,则 get 方法会返回一个 并停止游戏。 null,而 ["名字"] 索引则会报错 同时,get 方法的第二个参数可以指定一个默认值,若键不存在,则返回这个默认值:
print(玩家信息.get("名字", "无名"))
字典的增加删除
若要向字典中添加数据,直接使用元素赋值语句即可: 玩家信息["武器"] = "小棍子"。如果字典中存在武器键,则会修改对应的值,若没有武器键则会创建这个键并赋值。
若要删除,则调用 erase 方法并传入一个键,例如 玩家信息.erase("武器") 就会删除武 器键值对。
29 类
写在节点上的脚本就是一个类
extends Node2D
var 移动速度: int = 100
func _physics_process(delta):
var 移动 := 获取横向移动()
if 移动 != 0:
position.x += 移动 * delta * 移动速度
func 获取横向移动() -> int:
if Input.is_action_pressed("Left"):
return -1
if Input.is_action_pressed("Right"):
return 1
return 0
在其他代码中,也可以获取当前节点,从而获取其中的属性和方法。
$"/root/玩家".移动速度 = 300 #这样就可以修改玩家的移动速度了
$"/root/玩家".获取横向移动() #在节点上定义的方法也可以被上述方式访问
命名类: class_name 关键字
内部类:定义类
某些情况下,我 们需要一些小巧的类,我们懒得去再创建一个新的脚本文件了,此时就可以用内部类语法:
class <类名>:
<类成员(方法、属性、信号等)>
例如我们用这种形式定义一个 伤害来源 类:
class 伤害来源:
var 攻击者 = null
var 伤害值 = 0
var 是魔法吗 = false
这样定义的类被称作内部类,就是说这个伤害来源类是位于当前脚本文件类内部的 类,所以外部脚本想要使用这个伤害来源类时,就需要使用 外部类.内部类 的形式来访问。
假设刚刚定义 伤害来源 类的文件中有一行 class_name 伤害相关,那么外部代码在使用 伤 害来源 类时则需要:
var 伤害 = 伤害相关.伤害来源.new() # 实例化这个类
30 向量归一化
normalized()
返回该向量缩放至单位长度的结果。等价于 v / v.length()。另见 is_normalized()。
31 封装
也就是不直接访问变量,而是通过函数修改 (set/get事物)
使用<节点>.<属性变量>的方式引用其他节点的属性变量并修改,但这样其 实很危险,例如我们可能会不小心把玩家速度设置成负数,或将玩家的生命值设置过大导致超出生 命上限。
var 移动速度: int = 100
# 假设玩家最低移动速度是 10
func 设置移动速度(新速度:int):
if 新速度 < 10:
新速度 = 10
移动速度 = 新速度
func 获取移动速度() -> int:
return 移动速度
随后,我们只要保证每次用到 移动速度 时都通过调用 设置移动速度 或 获取移动速度 方法 即可。
肯定有一天我们会忘记 移动速度 属性还有两个对应的访问方法,这时候就需 要用到 GDScript 为我们提供的 set、get 关键字来指定变量的访问方法了:
var 移动速度: int = 100: set = 设置移动速度, get = 获取移动速度
在我们需要使用 移动速度 变量时,依旧按照普通变量的方式使用即可。就是说当执行 $"玩 家".移动速度 = 400 这句代码时,就会自动调用 设置移动速度 方法并将 400 作为参数传入其 中,相应的,执行 print($"玩家".移动速度) 时,实际输出的就是 获取移动速度 方法的返回 值。
var 属性:int = 10:set = _设置属性, get = _获取属性
func _设置属性(新的值:int):
if 新的值 > 0:
属性 = 新的值
else:
属性 = 新的值 * 新的值
func _获取属性() -> int:
if 属性 == 0:
print("巧了,属性值竟然是零")
return 属性
_设置属性 和 _获取属性 方法作为属性的访问方法,咱一般不希望别人随便使用,所以 起名字的时候给加上下划线前缀来告诉别人没事别用我。
简写形式:
var 属性 = 100:
set(新的值):
if 新的值 > 0:
属性 = 新的值
else:
属性 = 新的值 * 新的值
get:
if 属性 == 0:
print("巧了,属性值竟然是零")
return 属性
32 继承
extends 关键字指定了当前类的父类
GDScript 仅支持单继承,就是说子类只能继承自一个父类。 不过,父类还可能继承自另一个父类(爷爷类),所以继承关系可以形成一条长链。
当我们的脚本继承自某个节点类时,我们实际 上就是在一种节点的基础上创造了一种新的节点,并添加了我们的功能,例如玩家移动等效果。
super关键词,引用父类实例并从中调用方法
# 在子类中
func 打电话(电话号码:String):
print("为什么不试试发短信呢?")
# 调用父类的打电话方法
super.打电话(电话号码)
在子类的函数中调用父类同名的函数,直接写super()即可
33 多态
把子类实例存放到父类类型的变量中,并且可以根据父类中的成员名称访 问子类的成员
var 某人的手机:手机 = 新手机.new()
某人的手机.打电话("10086")
# 虽然 某人的手机 是手机类型,但其值是新手机,所以调用的是新手机的打电话方法。
# 某人的手机.发短信("10086","Hello")
# 上面这句注释掉的代码是错误的。
# 虽然新手机能发短信,但是 某人的手机 是老手机类型,不包含发短信方法。
使用多态的目的不是让我们的变量变得花里胡哨,而是规范一类操作
34 全局定义
同时注意自动加载列表中有一个全局变量按钮,当勾选了这个东西时即可在代码中的任意位置 通过前面的名称使用这个节点或脚本的实例,例如现在在任意代码处即可使用:
print(player.global_position)
如果学过其他编程语言中的设计模式,就会明白“自动加载”就是起到了单例模式的作用
35 load
PackedScene 部分提到 load 方法,知道它可以从文件中读取一个 PackedScene。
但实际上,load 方法可以读取任何 Godot 认识的文件,例如脚本、图片、声音以及 PackedScene等等。
例如加载图片并显示在 TextureRect 节点上:
$"TextureRect".texture = load("res://你的图片路径")
或者加载一段声音并播放:
$"AudioStreamPlayer".stream = load("res://你的音频路径")
$"AudioStreamPlayer".play()
36 类型判断
有时候我们需要判断一个实例是否是某个类的实例,此时可以使用 is 关键字。
func _on_body_entered(body):
if body is 敌人:
print("敌人进来了")
else:
print("进来的不是敌人")
37 静态方法
使用 static 关键字标注:
static func 获取生命值高(敌人1, 敌人2):
if 敌人1.生命值 > 敌人2.生命值:
return 敌人1
return 敌人2
如果上面代码所在的文件中定义了类名 class_name 工具,即可在任意代码处使用 工具.获取生命值高 来调用这个方法。
38 格式化字符串
<模版字符串> % <填入值>
例如:
发消息("获得了 %d 点经验,你升到了 %d 级!" % [经验, 等级])
前面字符串中的 %d 我们称之为占位符,此处会被替换成百分号后面的内容,上例中, 变量的值填入到了第一个 %d 的位置, 等级变量的值填入到了第二个 %d 的位置。
39 存档读档
Godot 拥有对 Json 数据的完美支持,如果你学习过 Json,可以尝试使用 Json 保存游戏存 档。
介绍一种纯 Godot 的数据保存方式
元数据
可以理解成给节点添加一些额外的信息,这些信息类似属性,也是由名称和 值来表示的
代码当中,使用 set_meta 函数添加
39 函数式编程 Lambda表达式
函数式编程允许我 们在变量中存放一个方法的引用,并可通过这个变量调用对应的方法
func _ready():
var f = A
f.call() # 输出 123
func A():
print("123")
func B():
print("666")
同时我们也可以简写,直接将方法定义到变量中,而不用定义新的方法
var hello = func():
print("Hello")
hello.call()
这种写法一般称为 Lambda 表达式,或者匿名方法。 注意,调用变量中的方法必须要使用 .call,如果方法有参数,则填入到 中即可。
40子节点操作
查找 find_child
用 find_child 来获取藏在子节点内部甚至子子子节点中的某个节点
节点名参数还可以使用 * 和 ? 这种通配符
# 在当前节点的子节点中查找名为 "MyChildNode" 的子节点
var child_node = find_child("MyChildNode", recursive = false)
# 检查是否找到对应的子节点
if child_node != null:
# 找到了子节点,做进一步处理
print("找到子节点:", child_node)
else:
# 没有找到对应的子节点
print("未找到子节点")
遍历子节点
get_child_count 可以获取当前节点的子节点个数。 get_child 可以根据下标获取一个子节点。 结合 for 语句可以遍历全部子节点:
for i in range(get_child_count()):
print(get_child(i))
41 字符串操作
-
字符串连接:
+
运算符:用于连接两个字符串。append()
:将一个字符串附加到另一个字符串的末尾。
-
字符串查找:
find()
:在字符串中查找指定的子字符串,返回第一次出现的索引位置。rfind()
:在字符串中反向查找指定的子字符串,返回最后一次出现的索引位置。findn()
:在字符串中查找所有指定的子字符串。
-
字符串替换:
replace()
:替换字符串中的指定子字符串。
-
字符串切片:
substr()
:截取字符串的子串。left()
和right()
:截取字符串的左边或右边一定长度的子串。substr_replace()
:替换字符串中的子串。
-
字符串大小写转换:
to_lower()
:将字符串转换为小写。to_upper()
:将字符串转换为大写。
-
字符串分割:
split()
:将字符串分割为子字符串。
-
字符串去除空格:
trim_prefix()
和trim_suffix()
:去除字符串开始或结尾的空格。strip_edges()
:去除字符串两端的空格。
-
字符串格式化:
format()
:使用参数替换字符串中的格式说明符。- 使用
%
和format
函数进行格式化字符串。
42 Object类
Object 是 Godot 中所有类的父类,所有类都继承自 Object。
Object 的重要函数
▪ _init() virtual :初始化虚函数,当一个对象生成时,此函数立即 自动调用
▪ _notification(what: int) virtual:接收通知虚函数,当此节点接收到 notification 的通 知时此函数自动被调用。what 以数字的形式来告知此函数具体是接收到了那一个 notification。
▪ connect/disconnect/is_connect: 信号连接、取消连接、是否存在对应的信号连 接。信号出现在 Object 内部,这代表 Godot 中所有的对象都可以进行信号连接,信 号不是节点的专利。
▪ emit_signal :发射信号。
▪ call 与 call_deffer :调用或在空闲帧中调用函数。对于空闲帧的解释会在后续教程 中进行解释。 ▪ set_script(script) : 设置此对象的 GDS 脚本。Godot 中大部分对象都可以设置脚 本。
▪ free () :从程序中删除此对象,也就是将此对象从内存中移出。Godot 中所有对象都可以执行 free 功能。
43 RefCounted 节点之外的对象
非常适合用于制作游戏中数目庞大的数据对象。如仙侠游戏中 的成百上千的随机 NPC 数据对象。
RefCounted 继承自 Object,它以及它的子类同样具有 Object 的所有功能。 ▪ 它拥有众多的子类。RefCounted 内置了一个独特的引用计数器,要了解掌握这些子 类,就必须先了解掌握 RefCounted 的引用计数器机制。
RefCounted 对象的生成
▪ 类名 .new() (类名由 class_name 进行自定义)
▪ load(“路径”).new()(也可以 preload("路径").new())
RefCounted 的特征及作用
▪ 特征 1:内置引用计数器,当引用计数器归 0 时,此对象自动被程序删除。
▪ 特征 2:在 GDS 中不包含任何内置属性,仅有四个内置函数,也不必像节点一样要频 繁的受场景树的控制,参与服务器的计算工作,相比于节点更加轻量。
▪ 作用 :RefCounted 不但比 Node 占用更少的内存节约更多的电脑的算力、还可以 进行内存的自动管理。非常适合用于制作游戏中数目庞大的数据对象。如仙侠游戏中 的成百上千的随机 NPC 数据对象。
RefCounted 的重要子类
最重要:
▪ Resource——游戏文件与游戏程序的中转站,所有的图片、音频、视频文件都必须 先转为 Resource,然后才可以被节点等对象使用。
了解即可
▪ Astar2D/Astar3D 等可以用于构建游戏中的寻路数据。
▪ Thread/Mutex/SemaPhore 可以用于构建多线程。
▪ FileAcess/DirectoryAcess 可以用于加载读取计算机内的文件。
▪ 还可以使用某些子类进行 UDP、TCP 或Websocket 的网络通信。
▪ StreamPeer 可以以字节单位对网络传输中收发的数据进行处理。
▪ 此外,它的子类还涉及了正则、JSON、游戏物理、自定义引擎编辑器等领域
44 Resource 资源类
Resource 是游戏文件与游戏程序的中转站。游戏项目内的大部分文件都要先转为 Resource 对象,然后才可以被游戏程序中的节点等对象使用。
需要转为 Resource 的常见文件
• 图片、3D 模型、音频、视频文件
• 场景文件
• 脚本文件
Object
│
├─ RefCounted
│ │
│ └─ Resource
资源供对象的使用的方式
• 多对象使用同一资源本身、适用于图片、3D 模型、音频、视频文件等。
• 资源经过再处理后,生成新对象,供不同对象使用。
特例:• 脚本资源再处理生成的对象在 GDS 中不可见,这个新对象被编写在内置代码中, 当我们使用 set_script 或绑定脚本的对象生成时它也将自动生成。
程序中 Resource 对象的创建
通过路径加载文件生成资源。
①资源随场景启动而自动创建
▪ 将资源文件(可以转化为资源的文件)拖入游戏项目中。
▪ 编辑器对资源文件进行导入,生成特殊文件以及资源文件对应的 import 文件。 import 文件会指向特殊文件。
▪ 游戏启动后借由 import 文件加载特殊文件生成资源对象。
②load 与 preload
如果加载的文件已经被转化为资源,且此资源引用计数器不为 0,则在此加载此文件不会产 生新的资源对象,而是返回一个原先资源对象的引用。
▪ load("文件路径")或 preload("文件路径")
创建新对象。
③由类名
.new()创建,不会产生相同引用。
▪ 内置资源类:某些内置资源类也可以使用 new 以外的函数来创建资源对象。部分内置 资源类内部拥有读取与保存文件的函数。
▪ 自定义资源类
load 与 preload 的区别
▪ load 与 preload 的参数都是文件路径,都是字符串形式,但 load 的参数可以是字符 串变量。而 preload 则无法使用变量,这是由于二者执行的时机不一样所导致的。
▪ 当脚本文件转化为脚本资源时,转换部分的程序会自动翻找文件中是否出现了 preload 函数,若出现,则在脚本资源生成的同时进行 preload 资源的加载。这时候 变量还没有出现,因此只能以文本字符串的形式来告知 preload 加载的内容。
▪ load 后可跟随字符串变量,字符串变量的值可以在程序运行时改变,如果改变变量, load 可以加载不同的文件。
▪ preload 后只能跟随固定的字符串,它不会在程序运行时改变。
资源保存的格式
资源对象是文件和程序的中转站,它不是简简单单的文件本身,一个资源对象可能是对一个 或对个文件处理后产生的数据对象。这些处理可以是对若干图片的裁剪、压缩、拼接;一个 资源对象,也可能是对节点某部分功能的记录与描述,比如 Pong 中区域检测时 shape 节点 的形状就也是一个资源对象。要保存这些复杂的信息,Godot 必须准备单独的文件。
▪ tres :文本资源格式。资源的内容以文本的形式储存,资源文件具有可读性。
▪ res :二进制资源格式。简单来说就不是人话,但是程序加载速度更快。
▪ 某些资源会提供特殊格式来保存自己的数据。
Resource 对象的特征
▪ 加载路径产生资源时,如果原先的资源不消失,则不会产生这个文件的新资源对象, 而只返回引用。在任意一处对此资源的修改,都将影响到所有使用此资源的对象
(更新中)
附录:
这里罗列一下文章《GDScript零基础图文入门.pdf》中提到的参考资料。(在此感谢作者分享)
开始
Godot 官网:Godot Engine - Free and open source 2D and 3D game engine
最新正式版下载:Download for Windows - Godot Engine
所有版本下载:Index of /godotengine/ (downloads.tuxfamily.org)
视频教程
完整教程
十分钟制作横版动作游戏|Godot 4 教程《勇者传说》#0_哔哩哔哩_bilibili (B 站视频)
【中字】Godot 4 吸血鬼生存复刻教程【新手】P0:简介_哔哩哔哩_bilibili(B 站视频)
RTS即时战略游戏 (B 站视频)
【Godot教程搬运】2D平台游戏快速入门指南 ~ Godot 4 游戏开发新手教程_哔哩哔哩_bilibili(B 站视频)
【Godot教程搬运】速成班 ~ 如何在 Godot 4 中制作资源收集游戏!_哔哩哔哩_bilibili(B 站视频)
【Godot4】从零开始制作土豆兄弟 #1 | 创建背景_哔哩哔哩_bilibili(B 站视频)
Godot 俯角射击游戏教程(jmbiv) (bilibili.com)(B 站视频)
Godot4.0 2D游戏全要素全技能速成教程(B 站视频)
Godot 制作 Roguelike 游戏系列_哔哩哔哩_bilibili(B 站视频)
经典的俄罗斯方块游戏(B 站视频)
godot 4.x 教程 100集_哔哩哔哩_bilibili(B 站视频)
知识点
【Godot】Control节点实例—背包系统_哔哩哔哩_bilibili(B 站视频)
【转载】【Godot】超简单的2D可破坏地形 - SUPER EASY 2D DESTRUCTIBLE TERRAIN in Godot_哔哩哔哩_bilibili(B 站视频)
将 VS Code 连接到 Godot(B 站视频)
文档资源
godot官方简体文档
Home :: Godot 4 Recipes (kidscancode.org) :英文网站,本网站收集了各种解决方案和示例,可帮助您制作所需的任何 游戏系统。
A Vegetables Bird's Blog (liuqingwen.me) Liuqingwen 的个人博客:包含众多 Godot 中文资料
资源网站
开源素材的整合站:
国内的开源素材的整合站,制作游戏时不再因为找可商用素材花费大量时 间
godot工坊:国内的 Godot 论坛,包含文档、教程、资源、答疑等板块。
(不知道为什么都没打开)
Download the latest indie games - itch.io:国外的独立游戏交流平台,有很多资源和工具,还能玩其他作者的游戏。
Home · Kenney:国外的资源网站,上千免费资源任你使用,大多是 low poly 或像素风。
Game UI Database | Welcome :国外的游戏 UI 网站,主要用来学习和参考。
Godot Asset Library - The Best Assets, All Free:一个国外的 Godot 资源网站,看起来比 Godot 自带的资源网站高级一些。
Godot Assets Marketplace – All you need in one place (godotmarketplace.com):国外的 Godot 资源市场,类似上一个,包含付费内容
补充:
OpenGameArt.org 一个游戏素材社区,你可以提交素材或在上面找到想要的免费纹理、人物、3D模型、音乐、音效等,非常良心。各素材的许可证可能不同,使用时请遵循许可证。
Home · Kenney 来自国外工作室Kenny,包含100多个免费素材和教程,例如UI,材质,人物,背景,也可以学习教程自己绘制素材。并且他家也开发游戏,有兴趣可以看看。
free3d.com 你可以在Free 3D上找到你想要的3D模型,并且支持下载Blender模型,3DMAX模型,FBX模型等。模型非常精致,但是打开速度有点慢(几乎打不开,建议科学)。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)