GD Script学习笔记(教程 by Brackeys)

对照Brackeys的GD Script视频教程做的个人学习笔记

本来写在星屑页面上,但觉得搭个博客来写会更好阅读一些,于是就搭了这个博客(?饺子醋)

教程来源:

YouTube-Brackeys: How to program in Godot - GDScript Tutorial

B站搬运熟肉

Hello, World!

  • func _ready()
    节点第一次进入场景时会调用此函数。可以在此放置需要游戏运行时立刻执行的代码。

  • pass
    意味着什么都不做,出现在尚未填写的函数中

  • print("Hello, World!")
    控制台会打印输入的信息

  • gdscript使用tab缩进来确定代码的结构,并对大小写敏感

Modifying nodes 1.0(修改节点1.0)

  • 调整字体大小:右侧栏-control-theme overrides-font sizes

  • 通过脚本来编辑标签:1.引用label;2.获取label中的属性(右侧栏上有,鼠标停留会显示代码)
    引用:可以将左侧栏的laber拖拽到脚本中,通过.text来获取text属性
    也可以用这种方法改变其他的东西,如颜色
    有一个叫modulate的属性,可以用来修改立绘和UI的颜色(右侧栏-CanvasItem-Visibility-Modulate)

    1func _ready() -> void:
    2    $Label.text = "Hello, World!"
    3    $Label.modulate = Color.GREEN

Input

  • 项目-项目设置-输入映射
    在此可以添加动作,动作允许我们将键位绑定到某些事件上
    命名并添加动作,点击加号输入需要绑定的键位

  • 在脚本中,需要创建一个输入函数,输入func _input回车,会自动补全
    这个函数会在每次游戏接收任何输入时调用。需要检查触发输入的事件是否是我们按下的操作:
    if event.is_action_pressed():括号会弹出可选的目标动作
    假设操作对象是label,需要在按键时将其颜色改为红色,则引用label,
    $Label.modulate = Color.RED

  • 检查动作何时结束
    if event.is_action_released():
    利用相同的代码将颜色改回绿色
    $Label.modulate = Color.GREEN

Variables 1.0

  • 变量:保存信息的容器
    例如使用变量保存玩家角色的信息

  • 将默认血量设置为100
    var health = 100
    然后可以在_ready()函数中打印这个变量
    print(health)

  • 可以对血量进行计算

    1health = 40
    2health = 20+30
    3health += 20
    4health -= 10
    5health *= 4
    6health /= 2
  • 写一个每次按键减少血量的脚本

    1extends Node
    2
    3var health: int = 100
    4
    5func _input(event: InputEvent) -> void:
    6    if event.is_action_pressed("my_action"):
    7        health -= 10
    8        print("Health: %d" % health)

If-statements(条件语句)

  • if语句检测一个条件是否被满足

  • 可以用if语句对变量做出反应,如希望玩家在血量为0时死亡

    1if health <= 0:
    2    health = 0
    3    print("Game Over")

    此代码中使用if health <= 0:将血量与0比较。
    其他的比较还有:

    1x == y 等于  
    2x > y 大于  
    3x >= y 大于等于  
    4x != y 不等于  
    5x < y 小于  
    6x <= y 小于等于  
  • 可以使用and关键词添加另一个条件并确保两者都需要满足
    使用or确保只需要满足其中之一条件
    if x == y or y > z

  • 还可以使用else关键词来定义当条件不满足时发生什么

    1else:
    2    print("You are still alive")
  • elif合并else和if语句

    1if health <= 0:
    2    health = 0
    3    print("Game Over")
    4elif health < 50:
    5    print("Warning: Low Health")
    6else:
    7    print("You are still alive")

Comments

  • 在一行的上方或后方对代码进行注释
    # This is a comment

  • 可以通过在代码前面加"#“暂时删除部分代码的执行
    编程规范:如果注释掉的是代码的话,不要在”#“后加空格
    #print()

  • 可以选择多行代码,右键单击切换注释(godot内有效,vs code没找到这个功能)
    但:不能有一个完全空白的函数,需要添加关键字pass避免报错

Variables 2.0

  • 创建和声明变量时,需要考虑在哪里这样做
    如果在if语句内部声明一个变量,就只能在该if语句里使用该变量。这叫做SCOPE(范围)
    如果希望在脚本的各个地方都能读取变量,应该将其放在代码的顶部,在任何函数之外

    1extends Node
    2
    3var script_variable = 100
    4
    5func _ready():
    6    var ready _variable = 100
  • gdscript的优点:可以声明变量而无需考虑数据类型

    1var godot_is_cool = true
    2var coolness = 9001
    3coolness = true

DATA TYPES

  • 在gdscript中,有四种经典数据类型:
    Boolean/bool(布尔变量):表示true和false
    integer/int:表示整数
    float:表示小数
    string(字符串):表示文字

  • 从一种类型转为另一种类型:casting(类型转换)

    1var number =42
    2var text = "Meaning of Life: " + str(number)
    3print(text)

    通过str()将其转化为字符串,输出:Meaning of Life: 42

    1var pi = 3.14
    2print(int(pi))

    将小数输出为整数。需要注意的是它只是去掉了小数点后的数字,不会进行四舍五入。

  • 两种常见数据结构:Vector2和Vector3
    Vector2存储两个浮点数:x和y,
    Vector3存储三个浮点数:x和y和z

    1var position = Vector3(3, -10, 5)
    2position.x += 2
    3print(position)
  • 默认情况下,gdscript是动态类型,这意味着创建变量时,不需要定义它可以储存什么类型的数据
    然而,它也更容易出现错误,并且它的性能低于静态类型
    但gdscript允许我们定义变量的类型
    var damage: int = 15 也可以通过写:= 15来让godot自动确认数据类型,这称为推断类型,得出的结果是一样的,godot意识到15是个整数,于是将变量设置为int
    这也意味着该变量不能更改为其他类型。如果尝试将其设置为一个字符串,

    1var damage := 15
    2
    3func _ready():
    4    damage = "A lot!"

    将会报错。

  • 在变量前添加@export可以将其暴露在检查器里。
    @export var damage := 15
    保存代码并点击左侧栏的节点(Node),可以在右侧栏检查器中设置变量

    在检查器中设置变量

    如果print(damage),可以看到通过检查器设置的值会在游戏中更新。可以按小圆圈恢复到默认值

  • constant(常量):定义一个不想改变的变量
    const GRAVITY = -9.81
    使用大写字母表示常量。
    常量不能被改写,否则会报错

Functions

  • 函数是编程的基础,它们允许您将代码捆绑在可重复利用的packages

  • func _ready() func _input() 下划线表明这些函数不是由我们激活或调用的,而是由引擎本身

  • 在godot中,可以创建自己的函数
    func jump() func die() func shoot() func respawn()

  • 开始创建一个函数

    1func jump():
    2    #添加向上的力
    3    #播放声音
    4    #播放跳跃动画
    5    print("JUMP!")

    为了不让每次按下空格就调用这个函数,于是在前面写input函数:

    1func _input(event):
    2    if event.is_action_pressed():
    3        jump()
    4
    5func jump():
    6    #添加向上的力
    7    #播放声音
    8    #播放跳跃动画
    9    print("JUMP!")
  • 在代码中,我们将给函数的输入称为参数(parameters),将输出称为返回值(returns)

  • 创建一个将两个数字相加的函数func add():,在括号中添加参数func add(num1, num2):。在函数中,我们可以将它们加在一起并将它们储存在一个叫result的变量中

    1func add(num1, num2):
    2    var result = num1 + num2
    3    print(result)

    可以在ready函数中调用add():

    1func _ready():
    2    add(3, 5)

    运行后会打印出8。

  • 但该函数目前并不返回结果,只是打印结果。为了在ready函数访问并调用add函数的结果,我们将add函数的print替换为return。

    1func add(num1, num2):
    2    var result = num1 + num2
    3    return(result)
    4
    5func _ready():
    6    var result = add(5, 10)
    7    print(result)
  • 利用return还可以做更多的事:

    1func add(num1, num2):
    2    var result = num1 + num2
    3    return(result)
    4
    5func _ready():
    6    var result = add(5, 10)
    7    result = add(result, 10)
    8    print(result)

    打印25。

  • 和声明变量时一样,也可以定义函数的参数和返回类型
    func add(num1: int, num2: int) -> int:
    使用箭头->来设置返回值类型

Random numbers

  • 函数randf给出0-1之间的随机数,非常适合为代码分配概率。(抽卡)

    1func _ready():
    2    var roll = randf()
    3    if roll <= 0.8:
    4        print("Common item")
    5    elif roll <= 0.95:
    6        print("Rare item")
    7    else:
    8        print("Legendary item")

    (真的没问题吗用这个代码抽了十次出了四次rare )

    • (试着结合目前学的写了个按下按键抽卡的代码:
     1extends Node
     2
     3var roll: float = 0.0
     4var item_type: String = ""
     5
     6func _input(event: InputEvent) -> void:
     7    if event.is_action_pressed("my_action"):
     8        roll = randf()
     9        if roll <= 0.8:
    10            item_type = "Common item"
    11        elif roll <= 0.95:
    12            item_type = "Rare item"
    13        else:
    14            item_type = "Legendary item"
    15        print(item_type)
  • 还可以使用randf_range()randi_range()来得到一个指定范围内的随机整数或小数
    比如给角色随机生成身高可以用

    1var character_height = randi_range(140, 210)
    2print("Your character is " + str(character_height) + "cm tall.")
  • GD的官方文档和编辑器是连着的。
    这意味着按住ctrl并单击代码中想要了解更多信息的内容,它会在编辑器中直接打开官方文档

Arrays

  • 数组:可以容纳多个事物的变量。可以用于存储整个元素列表。

  • 制作一个用于保存玩家物品的列表:
    var items = ["Potion", 3, 6]
    GD Script的列表可以包含不同类型的变量。

    但如果想将数组限制为特定类型,我们可以进行定义:
    var items: Array[String] = ["Potion", "Feather", "Stolen harp"]

  • 使用索引来访问数组中的元素:当你向数组添加一个元素时,它会根据它在数组中的位置自动分配一个数字。
    如在var items: Array[String] = ["Potion", "Feather", "Stolen harp"]中,三个元素的索引依次为0、1、2。

    因此,要想访问并打印数组中的第一个元素:

    1var items: Array[String] = ["Potion", "Feather", "Stolen harp"]
    2print(items[0])

    打印出Potion

  • 更改元素:

    1var items: Array[String] = ["Potion", "Feather", "Stolen harp"]
    2items[1] = "Smelly Sock"
    3items[2] = "Staff"
  • 查找、删除或添加新元素:

    1items.remove_at(1) # 删除索引为1的元素
    2items.append("Overpowered Sword") # 添加新元素(将被加到列表的最后)
  • 数组有时会变得很长很难管理。可以用循环(Loops)来帮助解决这个问题。

Loops

  • 循环允许我们多次重复代码,可以用来逐个访问列表中的元素。

    比如打印var items: Array[String] = ["Potion", "Feather", "Stolen harp"]列表中的所有元素,可以用for循环:

    1for item in items:
    2    print(item)

    添加更多功能,如仅打印长度超过六个字母的物品:

    1for item in items:
    2    if item.length() > 6:
    3        print(item)

    这将只会打印Stolen harp

  • 创建会运行一定次数的代码循环:

    1for n in 8:
    2    print(n)

    打印出的变量n将从0开始一直增加到7(运行了8次)。我们说n是for循环的当前循环。

  • while循环:只要满足特定条件就会一直重复此过程的循环。

    如使用while循环使玻璃杯装满半杯水:

    1var glass := 0.0
    2
    3while glass < 0.5:
    4    glass += randf_range(0.01, 0.2)
    5    print(glass)
    6
    7print("The glass is now half full!")

    以下是一次运行的结果:

    10.18732265214986  
    20.30365408708942  
    30.37735687756539  
    40.46053994937302  
    50.6523261380621  
    6The glass is now half full!
  • 使用while循环时,注意不要创建无限循环。这容易导致程序崩溃。
    如注释掉上面的代码中往杯子里添加随机数的一行,运行后将无限输出0并报错,godot可能会无法响应。

  • 使用break continue关键字:
    break:跳出循环并继续执行后面的代码
    continue:立即跳到循环的下一次迭代

    如果想要检查杯子是否被倒了20%满:

     1var glass := 0.0
     2
     3while glass < 0.5:
     4    glass += randf_range(0.01, 0.2)
     5
     6    if glass > 0.2:
     7        break
     8
     9    print(glass)
    10
    11print("The glass is now half full!")

    运行结果:

    10.0389682989115  
    20.09942385229739  
    3The glass is now half full!  
  • 虽然数组非常适合储存元素列表,但有时使用索引访问每个元素会容易搞混,有时用字典更合适

Dictionaries

  • 字典会保存很多对的“索引(key)”和“数值(value)”。
    key:想要查找的词
    value:这个词的定义

  • 创建空字典:var my_dict = {}

    可以在大括号内添加“键值对(key-value pairs)”
    例如:游戏中有多名玩家,使用字典来跟踪他们
    key:用户名;value:等级

    1var players = {
    2    "Crook": 1, 
    3    "Villain": 35, 
    4    "Boss": 100,
    5}

    要想获取玩家的等级,只需输入用户名即可
    `print(players[“Villain”])

  • 分配新值或添加条目

    1players["Villain"] = 50
    2players["Dwayne"] = 999
  • 像数组一样,可以用for循环遍历字典。这样做时实际上是在循环所有字典中的key,即用户名。

    1for username in players:
    2    print(username + ": " + str(players[username]))

    这样会打印出整个字典:

    1Crook: 1
    2Villain: 50
    3Boss: 100
    4Dwayne: 999
  • 就像数组一样,可以在同一个字典中拥有多种数据类型的键和值
    甚至可以数组套数组,或者字典套字典

  • 比如不仅想存储玩家的等级,还想存储其他信息(如生命值),只需用另一个字典来替换值即可:

    1var players = {
    2    "Crook": 	{"Level": 1, "Health": 80}, # 使用缩进来保持代码简洁
    3    "Villain":	{"Level": 50, "Health": 150}, 
    4    "Boss": 	{"Level": 100, "Health": 500},
    5}

    现在我们可以使用两个键来访问一个值: print(players["Boss"]["Health"]) 打印出500。
    这样我们就可以想办法构建有关游戏中正在发生的情况的数据,例如玩家统计数据、库存、增益等。

Enums

  • 枚举:在游戏中定义标签和状态的便捷方法

    假设我们正在制作游戏,游戏里有一堆单位,我们需要一种方法将每个单位标记为敌对、中立或盟友。可以创建一个定义这些标签的枚举。
    在脚本顶部(进入节点和func_ready中间)写一个enum: enum { ALLY, NEUTRAL, ENEMY }
    现在我们可以在游戏中使用这些状态。

  • 例如,我们可以创建一个名为“单位势力(unit_alignment)”的变量,并设置它等于上述状态中的哪一个:
    var unit_alignment = ALLY

  • 给enum命名,让代码井井有条:
    enum Alignment { ALLY, NEUTRAL, ENEMY }
    这时要访问这个枚举必须要进入势力枚举:
    var unit_alignment = Alignment.ALLY

  • 选择Alignment.ALLY作为默认值。在ready内部,可以检查单位势力是否等于Alignment.ENEMY

     1extends Node
     2
     3enum Alignment { ALLY, NEUTRAL, ENEMY }
     4
     5var unit_alignment = Alignment.ALLY
     6
     7func _ready() -> void:
     8    if unit_alignment == Alignment.ENEMY:
     9        print("You are not welcome here.")
    10    else:
    11        print("Welcome.")

    打印出"Welcome.",因为我们的部队目前是盟友

  • 使用枚举要比使用字符串或整数来表示状态更安全,因为这样拼错了的话godot会报错

  • 可以把枚举用在@export变量里: 把var unit_alignment = Alignment.ALLY改为:@export var unit_alignment : Alignment,现在我们可以在右边栏检查器中设置我们的单位势力。
    1
    如果在检查器中将势力设置为敌人,将会打印出"You are not welcome here.”

  • 上述的幕后实际是godot为正在枚举的每一个状态创建了一个常量:

    1enum Alignment { ALLY, NEUTRAL, ENEMY }
    2
    3const ALLY = 0
    4const NUETRAL = 1
    5const ENEMY = 2
    6
    7@export var unit_alignment : Alignment

    所以枚举本质上是一堆值不断增加的常量。
    如果我们打印这些状态中的一个:

    1extends Node
    2
    3enum Alignment { ALLY, NEUTRAL, ENEMY }
    4
    5@export var unit_alignment : Alignment
    6
    7func _ready() -> void:
    8    print(Alignment.ENEMY)

    打印出2,即godot后台为第三个状态ENEMY设置的常量的值。

  • 如果需要的话,我们甚至可以覆盖godot设置的默认值。如: enum Alignment { ALLY = 1, NEUTRAL = 0, ENEMY = -1 }

  • 有了枚举之后,我们可以使用match语句。
    match语句方便我们为每一个枚举状态使用不一样的代码

Match

  • Match(匹配):相当于其他语言中的switch语句,允许我们根据变量的值执行不同的代码
  • 可以使用match语句为枚举的不同值添加一些代码:
     1extends Node
     2
     3enum Alignment { ALLY, NEUTRAL, ENEMY }
     4
     5@export var my_alignment : Alignment
     6
     7func _ready() -> void:
     8    match my_alignment:
     9        Alignment.ALLY:
    10            print("Hello, friend!")
    11        Alignment.NEUTRAL:
    12            print("I come in peace!")
    13        Alignment.ENEMY:
    14            print("TASTE MY WRATH!")
    15        _: # 设置默认值,即不是上述任何一种情况时
    16            print("Who art thou?")

Modifying nodes 2.0(修改节点2.0)

  • 到目前为止,当我们需要访问节点时,我们是通过以下方式完成的:
    将其拖动到脚本中,这会创建一个美元符号,后面跟着节点的路径
    23
    实际上,我们可以将此路径存储在变量中,只需将其拖至顶部并按住Ctrl键释放即可。这会自动创建一个带有节点名称和正确路径的变量:
    @onready var weapon = $Player/Weapon godot对节点的创建有非常严格的顺序。如果我们打开游戏并尝试在武器节点被创建之前找到它,将会报错。@onready确保godot会等待所有子节点都被创建后再访问,这样就不会报错了。(即预加载)

  • 美元符号$实际上是使用get_node函数的简写:
    @onready var weapon = $Player/Weapon = @onready var weapon = get_node(Player/Weapon)

  • 路径是相对的。我们的脚本位于主节点上,因此它在该节点之后立即启动。当然也可以在脚本里获取绝对路径:
    print(Weapon.get_path())
    这会打印从根节点开始到武器的绝对路径:/root/Main/Player/Weapon

  • 路径对很多事情来说很有用,但有时也有点不灵活:

    1. 如果重命名路径中的任何节点,路径就会失效。
    2. 路径一般只用于访问子节点。
      幸运的是,我们可以用@export关键字来引用其他节点:
      @export var my_node: Node
      然后在检查器中,我们可以为它分配我们想要的任何节点,或者只需要单击左侧栏中的节点名称并拖动到检查器上即可
      5
  • 我们还可以使用is关键字检查节点是否是某种类型:

    1extends Node
    2
    3@export var my_node: Node
    4
    5func _ready():
    6    if my_node is Node2D:
    7        print("Is 2D!")
  • 我们甚至可以声明我们希望能够引用什么类型的节点。
    例如我们只想引用立绘节点(sprite node),只需将类型改为Sprite2D
    @export var my_node: Sprite2D
    这时在检查器中重置变量,现在只能给它分配立绘节点了。
    这时运行游戏,仍然会打印出“Is 2D!”,这是因为Sprite2D继承自Node2D

Signals

  • 信号:节点可以互相发送的信息。用来通知发生了某个事件。

  • Godot有很多内置信号。在右侧栏节点可以查看和连接信号。
    信号连接成功时,脚本会多出一行函数并显示绿色箭头表示连接成功。
    点击绿色箭头可以查看信号源。

  • 可以为一个信号连接任意数量的函数,当信号发射时,所有函数都会被调用。
    这允许我们以一种无需相互了解的方式将节点链接在一起。
    按钮只需要发射信号,不需要了解哪些函数连接到了该信号。
    这使得信号很适合和用于模块化的游戏。(called Decoupling: 去耦 / 解耦)

  • 使用例:
    假设我们正在扮演一个能够获得Xp并升级的角色,当升级时可能会有很多游戏系统需要更新:UI、数值、咒语、成就系统
    从玩家脚本中调用这些内容会变得一团糟。
    相反,可以创建一个名为leveled_up的信号,所有的系统都可以连接到这个信号,玩家升级时发出这个信号:

    1signal leveled_up
    2
    3func level_up():
    4    # code here
    5    leveled_up.emit()

    在Node节点Main下添加子节点Timer,设置Timer的参数为Wait Time: 1;Autostart: on。
    这样它会计时1秒,并当秒数归零时发出_timed_out()信号。
    接入信号到Main节点:

    1extend Node
    2
    3var xp := 0
    4
    5func _on_timer_timeout():
    6    xp += 5
    7    print(xp)
    8    if xp >= 20:
    9        xp = 0

    运行时每秒经验+5,达到20后归零。

    现在我们创建一个其他节点可以连接的信号。在顶部写signal leveled_up(),保存,可以看到主节点多了一个leveled_up()信号。
    对于本示例,我们将其连接回Main节点。这会创建一个_on_leveled_up()函数,当信号发出时调用。

     1extend Node
     2
     3signal leveled_up()
     4
     5var xp := 0
     6
     7func _on_timer_timeout():
     8    xp += 5
     9    print(xp)
    10    if xp >= 20:
    11        xp = 0
    12        leveled_up.emit()
    13
    14func _on_leveled_up():
    15    print("DING!")

    现在,达到20Xp时会发出信号并打印"DING!"

  • 也可以通过代码来连接信号:

    1func _ready():
    2    leveled_up.connect(_on_leveled_up)

    断开连接:把connect改为disconnect即可。

  • 也可以通过信号传递参数:

     1extend Node
     2
     3signal leveled_up(msg) # 括号内添加参数名
     4
     5var xp := 0
     6
     7func _ready():
     8    leveled_up.connect(_on_leveled_up)
     9
    10func _on_timer_timeout():
    11    xp += 5
    12    print(xp)
    13    if xp >= 20:
    14        xp = 0
    15        leveled_up.emit("GZ!") # 发出信号时参数的信息
    16
    17func _on_leveled_up():
    18    print(msg) # 不再打印ding,而是打印参数

Get… Set… GO!

  • Getter和Setter允许我们在变量更改时添加代码。
    这意味着我们可以在修改或读取变量时做一些事情,例如将值限制在一定范围内或发出信号,让其它部分的代码知道变量发生了变化。

Setter

  • 常见例子:生命值
    1. 限制生命值变量的范围:
    1var health := 100: # 添加生命值变量,默认为100
    2    set(value): # 命名被传入的值为value,即试图将变量更改为这个value值
    3        health = clamp(value, 0, 100) # 使用clamp()函数将生命值限制在0-100之间
    1. 创建信号:
     1signal health_changed(new_health) # 创建生命值发生变化信号,输入新的生命值作为参数
     2
     3var health := 100: 
     4    set(value): 
     5        health = clamp(value, 0, 100) 
     6        health_changed.emit(health) # 信号释放时,改变生命值
     7
     8func _ready():
     9    health = -150 # 分配生命值-150
    10
    11func _on_health_changed(new_health): #连接信号并打印新的生命值
    12    print(new_health)
    运行后,会打印出0,
    这是因为-150被限制在0-100之间,然后发出信号,信号会调用_on_health_changed()函数并打印新的生命值。

Getter

  • 常用于转换值。

    1var chance := 0.2
    2var chance_pct: int: # 设置变量:机会百分比数
    3    get:
    4        return chance * 100
    5
    6func _ready():
    7    print(chance_pct)
    8    chance = 0.6
    9    print(chance_pct)

    打印出20、60。即机会百分比变量完全取决于机会变量是多少

    加入Setter:

     1var chance := 0.2
     2var chance_pct: int: 
     3    get:
     4        return chance * 100
     5    set(value):
     6        chance = float(value) / 100.0 # 注意由于chance是小数,因此要确保value也是小数
     7
     8func _ready():
     9    print(chance_pct)
    10    chance_pct = 40 # 现在可以直接通过修改机会百分比改变机会变量了。
    11    print(chance_pct)

    打印出20、40。
    由于加入了Setter,chance_pct的value修改时,触发Setter:重新计算chance。
    由于执行print(chance_pct),为了获取chance_pct的值,再走一遍Getter:返回chance的值乘以100,因此第二个打印的值是40。

Classes

  • GD Script是一门面向对象的编程语言,这意味着我们通常尝试在包含的对象内构建代码,然后让它们彼此相互作用。我们主要使用Classes来完成此操作。

  • 例:我们在制作一款RPG游戏,我们需要创建一堆可以互动的角色。因此我们创建一个角色类,这个类含有一些游戏中所有角色都应该具备的变量和逻辑:

    VARIABLES FUNCTIONS
    name talk()
    health die()
    dialogue

    我们获取这个类并创建实例(INSTANCES)。

  • 实例是该类的特定版本。我们可以创建一个叫POTION SELLER的实例,health = 50talk("You can't handle my strongest potions!")
    你还可以创建更多别的实例,所有实例拥有相同的变量,但值不同:

    INSTANCES POTION SELLER EX-ADVENTURER KNIGHT
    VARIABLES health = 50 health = 30 health = 130
    FUNCTIONS talk(“You can’t handle my strongest potions!”) talk(“I used to be an adventurer like you!”) die()
  • 实际上你已经在Godot中遇到一堆类了。这是因为Godot的内置节点是类。所有的节点都是具有一堆变量和逻辑的自包含对象(Self-contain object),我们可以创建它们的实例。 (这句机翻好奇怪但我也不知道怎么翻,后半句是从句- - All the nodes are… that we can create instances of. )
    如果我们添加一个SpriteNode,我们就实例化了Sprite类。所以我们创建一个脚本的同时,我们也是在创建一个类。(在技术上不是在创建一个类,但它的工作方式与类一样)

  • 在主节点下添加子节点Character。现在,为了更清楚地表明我们的脚本是一个定义角色的类,设置类名为"Character":

    1class_name Character #注意C大写
    2
    3extends Node
    4
    5@export var profession : String #在检查器中加入变量:职业(字符串)
    6@export var health: int #生命值:整数

    1
    我们可以通过复制此节点来创建更多实例。

    此处弹幕讨论
    弹幕   
    这么做的意义是什么?不能直接用字典吗?
    弹幕   
    字典只是一个索引,可以存放角色的基本信息但是功能性的逻辑还是要靠函数来实现啊。要写函数就得进脚本。
    弹幕   
    类是面向对象最重要的概念之一。类比下,类是蓝图,我们可根据蓝图,创造蓝图设计范围内的各式各样的实体。字典的主要功能仅是存储数据,而类则是创建新的实体(对象)。并且由于继承和多态的存在,有很多花活可整
    弹幕   
    字典内数据不方便改动和操作,所以倾向于放静态数据,类成员里面可以进行大量逻辑和数据的内容,放动态数据比较好。
  • 我们还可以给这个脚本一个函数:

     1class_name Character 
     2
     3extends Node
     4
     5@export var profession : String 
     6@export var health: int 
     7
     8func die():
     9    health = 0
    10    print(profession + "died.")

    这就是我们的角色类,但目前还没有任何东西触发添加的这个函数。

  • 让我们进入主节点,在这里我们可以设置引用角色并调用die函数。
    简单地把角色节点拖动到脚本中引用是可以的,但因为我们已经命名了角色类,因此可以使用@export关键词作为替代。
    这样我们就可以在编辑器中建立连接,而不用担心路径改变的问题。

    1extends Node
    2
    3@export var character_to_kill: Character
    4
    5func _ready():
    6    character_to_kill.die()

    保存后可以看到主节点检查器中可以选择哪个角色被杀死。

Inner classes

  • 内部类:存在于另一个类中的类。
    主要被用来将变量捆绑在一起,也可以添加一点函数
    内部类可以很好地替代字典,因为使用起来更安全

  • 假设往角色类中添加一些装备(equipment),我们可以创建一个名为Equipment的内部类。

    1class_name Character 
    2
    3extends Node
    4
    5class Equipment:
    6    var armor := 10
    7    var weight := 5

    现在我们可以在脚本中使用Equipment内部类,利用它创建一些变量:

     1class_name Character 
     2
     3extends Node
     4
     5var chest := Equipment.new() # 调用.new将创建一个Equipment类的实例
     6var legs := Equipment.new()
     7
     8class Equipment:
     9    var armor := 10
    10    var weight := 5

    _ready函数中可以访问这些类:

     1class_name Character 
     2
     3extends Node
     4
     5var chest := Equipment.new() # 调用.new将创建一个Equipment类的实例
     6var legs := Equipment.new()
     7
     8func _ready():
     9    chest.armor = 20
    10    print(chest.armor)
    11    print(legs.weight)
    12
    13class Equipment:
    14    var armor := 10
    15    var weight := 5

    这比使用字典更安全,因为GD Script会识别Equipment类中有一个weight变量,打错字可以在运行游戏之前报错。这被称为类型安全(Type Safe)

    保存并运行,可以看到每一个角色中的实例(即先前复制的角色节点)都将打印这两件装备。

Inheritance

  • 继承关系:从一个类中衍伸出另一个类的能力。

    实际上我们已经在这么做了:脚本中的extends Node,即该脚本衍伸自Node类(在上节已学过Godot的节点本质上是类),意味着Node类的函数和变量也可以在脚本中使用

    Godot的可视化:可以查看节点的继承信息,甚至可以在列表中找到刚创建的Character类。这是因为当创建Character类时,本质上是在定义一个新的节点类型。
    3

  • 在实际使用中,需要确保脚本继承自正确的类,这样才能确保我们实现想要的目的

    比如我们正在制作一个让玩家移动的脚本:
    创建CharacterBody2D节点并添加脚本,脚本会自动extends CharacterBody2D
    现在我们可以访问这个节点中的所有功能(functionality),例如:velocity move_and_slide()(这个函数可以让节点在空间中移动)

Composition

  • 组合结构:尽管Godot对它的节点使用了继承关系,但还有更好的构建代码的方法。
    Godot实际上非常倾向于使用另一种叫做组合结构的方式。

    Brackeys推荐的讲解Composition的频道: YouTube-Bitlytic: How You Can Easily Make Your Code Simpler in Godot 4

    B站搬运熟肉

    直接把笔记做在一起了吧

  • Inheritance(继承)和Composition(组合)是编程中常用的两种技巧,可以最大程度地复用代码。
    虽然继承用得比较多,但很多情况下使用组合会更好。

  • 例:创建Player和Enemy:

    Player Enemy
    Attack Attack
    Health Health
    Hitbox Hitbox
    User Input AI Stuff
  • 如果使用继承,我们会把attack、health、hitbox抽象出来封到独立的类中。
    暂且叫这个类Entity(实体),玩家和敌人都将继承这个类:

    Entity
    Attack
    Health
    Hitbox
    Player
    User Input
    Enemy
    AI Stuff
    这就将代码放在了一起,修改代码会同时影响玩家和敌人,而不需要在每一个类中慢慢改

    如果只是这样,继承简直是完美方案,
    但假如我们再定义一棵树,它也可以承受伤害,因此我们让它继承Entity类,然而这样会导致一些小问题:
    它确实是有生命值和碰撞箱了,但它也具有了攻击的能力

    更大的问题是,玩家和敌人都是CharacterBody2D节点,而树是StaticBody2D节点,而在Godot中是不允许同时继承多个类的。
    因为玩家和敌人是CharacterBody2D,那么Entity类也需要继承CharacterBody2D,这会导致树也得是CharacterBody2D

  • 如果使用组合,不抽象为一个大类(super class),而是使用多个组件(Components):

    Components
    AttackComponents
    HealthComponents
    HitboxComponents
    Player
    Attack
    Health
    Hitbox
    User Input
    Enemy
    Attack
    Health
    Hitbox
    AI Stuff
    Tree
    Health
    Hitbox
    组件对继承的类没有要求,可以各取所需。

    在这个例子中使用组件比使用继承好得多,
    但也有比较麻烦的事,比如怎么让碰撞箱检测到生命值组件以发送受伤信息,但这是值得的

    在Godot中,这些组件都可以被处理为子节点,可以任意地添加到需要的节点中 1

  • 例子
    以后再看

Call down, signal up

  • 往下用调用,往上用信号
    编写GD Script时,遵循良好的编程习惯。这是节点之间通信时的一个经验法则。

    Godot中的每个节点都是一棵节点树。起点被称为根节点。
    一个节点位于另一个节点上方,我们称它们有亲子关系。上面的是父节点,下面是子节点。

    1Main # PARENT
    2|--Player # CHILD
    3|  |--Graphics # Player的CHILD
    4|  |--Collider
    5|--Enemy
  • 往下用调用,往上用信号意味着父节点应该调用子节点的函数,反之则不然。
    相反,子节点应该用信号来与父节点交流,父节点可以根据接到这些信号的函数采取相应的行动。

  • 同一级别的两个节点:SIBLINGS
    这时,共同的PARENT负责将来自一个SIBLING发出的信号连接到另一个SIBLING的函数上。通常是在_ready()函数中完成的。 2

Styyyle

终于把这个视频看完了,短短一个小时让我啃了大半个月

本博客已稳定运行
发表了7篇文章 · 总计25.93k字
使用 Hugo 构建
主题 StackJimmy 设计