教程二 · 第 4 章:类与对象——看懂 Godot 脚本的本质
前面章节你已经写了不少”挂在节点上的类”。这一章系统讲清面向对象(OOP):类、对象、字段、属性、构造、访问修饰符。学完你会彻底明白 public partial class Player : CharacterBody2D 这行字每个词的含义。
4.1 类是图纸,对象是实物
public class EnemyData{ public string Name = ""; public int Hp = 30;}EnemyData 是类(图纸)。按图纸造出来的变量叫对象/实例(实物):
var slime = new EnemyData { Name = "史莱姆", Hp = 30 };var bat = new EnemyData { Name = "蝙蝠", Hp = 12 };slime.Hp = 20; // 两只互不影响(回顾第 1 章引用语义:但两个引用指向不同对象)GD.Print(bat.Hp); // 12new = 按图纸造一个实物。Godot 节点例外:节点不要用 new 造,用 PackedScene.Instantiate() 造、AddChild 挂进场景树,引擎才能管理它的生命周期。普通数据类(存档、配置、运行时状态)放心 new。
4.2 成员三件套:字段、方法、属性
public class Weapon{ // 字段(field):存数据 private int _ammo = 10; private readonly int _maxAmmo = 10;
// 属性(property):带控制的"智能字段",外部读法像字段 public int Ammo => _ammo; // 只读属性(表达式体) public bool IsEmpty => _ammo <= 0;
// 方法(method):做事 public bool TryShoot() { if (_ammo <= 0) return false; _ammo--; return true; }
public void Reload() => _ammo = _maxAmmo;}字段 vs 属性,到底用哪个?
- 字段:内部存储,约定
private+_camelCase - 属性:对外暴露的接口,
PascalCase,可以在读写时插逻辑:
private int _hp;
public int Hp{ get => _hp; set { _hp = Mathf.Clamp(value, 0, MaxHp); // 赋值时自动夹取 GD.Print($"HP 变化:{_hp}"); }}外部世界 player.Hp = 999; 看起来在赋值,实际走了你的 set 逻辑。这是面向对象封装性的核心:数据对外是一个规整的门面,内部怎么存、有什么规矩是自己的事。
自动属性是最常见的简写(编译器帮你生成隐藏字段):
public string PlayerName { get; set; } = "";public int Coins { get; private set; } // 外部只读、内部可写public Vector2 RespawnPoint { get; init; } = Vector2.Zero; // 只能在构造时定在 Godot 里:[Export] 几乎都标在属性或字段上(官方推荐属性):
[Export] public float Speed { get; set; } = 300f;4.3 访问修饰符:谁能看见谁
public // 谁都能访问private // 只有本类内部能访问(不加修饰符默认就是 private)protected // 本类和子类(第 5 章)internal // 同一程序集内(Godot 项目里你基本用不到)设计纪律:默认 private,需要对外才 public。暴露越少,以后改内部实现越自由——外部代码没机会依赖你的细节。
对照 Godot 习惯:
public partial class Player : CharacterBody2D{ [Export] public float Speed { get; set; } = 300f; // 给编辑器/外部看:public [Signal] public delegate void DiedEventHandler(); // 信号必须 public
private int _hp = 100; // 内部状态:private private Label _nameLabel; // 子节点引用:private
public void TakeDamage(int amount) { } // 对外能力:public private void UpdateAnimation() { } // 内部步骤:private}“对外提供做什么(TakeDamage),藏起怎么做(播哪个动画、怎么扣)“——这就是封装。外部对象不该能直接 player._hp -= 999,它该通过方法或信号与玩家交互。
4.4 构造函数与 Godot 的”构造”
普通类的初始化方法,与类同名:
public class DamageInfo{ public int Amount; public string Source;
public DamageInfo(int amount, string source) { Amount = amount; Source = source; }}
var info = new DamageInfo(20, "陷阱");Godot 节点不要写带参构造函数,引擎实例化时调的是无参构造,那时节点还没进树、子节点不存在。Godot 节点的初始化规矩:
- 字段处给默认值 /
_Ready()里初始化 - 需要外部参数:
Instantiate()后、AddChild()前调一个 Setup 方法(教程一第 6 章的子弹模式):
var bullet = _scene.Instantiate<Bullet>();bullet.Setup(direction, speed, damage); // 代替构造参数AddChild(bullet);4.5 this、nameof、partial
public void Heal(int amount){ this.amount = amount; // this = 当前对象自己;参数名和字段撞名时区分用}nameof(SomeMethod) → 编译期变成字符串 "SomeMethod",拼错会编译报错。Godot 信号里偶尔用:
Connect(SignalName.HpChanged, new Callable(this, MethodName.OnHpChanged));// MethodName.Xxx 也是源生成器给的,和 SignalName 同理partial(部分类):Godot 类必须加它:
public partial class Player : CharacterBody2D原因:Godot 的源生成器在编译时自动为你生成这个类的另一半代码(信号的事件封装、SignalName、MethodName、[Export] 的胶水等)。partial 允许一个类拆在多个文件里,你的手写部分 + 生成部分合成完整的类。照抄即可,看到不要奇怪。
4.6 静态成员再认识
第 2 章见过静态方法。静态属性/字段属于类本身、全局唯一:
public class GameStats{ public static int TotalKills; // 所有地方共享一份}Godot 的 Autoload 单例是”场景树里的一个长期节点”,比纯静态状态更符合引擎模型(有生命周期、能连信号、能在远程树里检视)。跨场景数据优先 Autoload,不要滥用 static 字段(切场景后引用已释放节点是经典崩溃)。
4.7 组合:在对象里放对象
第 1 章教程一说过”节点树 = 组合”。C# 普通对象也一样:一个复杂类由几个职责单一的小类拼成:
public class Health{ public int Current { get; private set; } public int Max { get; } public Health(int max) { Max = max; Current = max; } public void Damage(int n) => Current = Math.Max(0, Current - n); public bool IsDead => Current <= 0;}
public partial class Player : CharacterBody2D{ private Health _health = new(100); // 组合一个生命值组件(纯 C# 对象)}把”血量规则”从玩家脚本里抽成独立小类,玩家、敌人、可破坏物都能复用它。能组合就别硬继承(下一章细讲)。这种纯 C# 逻辑类不挂节点、不用进树,是单元测试最友好的部分。
4.8 面向对象在 Godot 中的落地地图
| OOP 概念 | Godot 里的样子 |
|---|---|
| 类 | 一个脚本文件(挂节点)或纯数据类 |
| 对象/实例 | 场景里的节点实例、Instantiate() 的子弹 |
| 字段 | 私有状态 _hp、子节点引用 |
| 属性 | [Export] 参数、只读状态 IsDead |
| 方法 | TakeDamage、私有 UpdateAnimation |
| 构造 | 节点用 _Ready + Setup 方法代替 |
| 封装 | 内部 private,对外方法/信号 |
| 组合 | 节点子树 + 纯 C# 小组件类 |
| 静态 | GD/Mathf/Input 工具,Autoload 代替全局状态 |
4.9 作业
- 写纯 C# 类
Health:max、current、Damage(int)、Heal(int)、IsDead,current 自动夹在 0..max;在 Playground._Ready 里 new 出来打几套拳(受伤→治疗→打死)打印结果。 - 给
Health加[Signal]?——先想想:纯 C# 类能不能发 Godot 信号?(答案:不能,信号是 GodotObject 的能力。那想让 UI 知道血量变化,纯类该怎么通知外界?带着这个问题读第 6 章的 C# event。) - 把平台游戏玩家脚本里的”金币数”改成只能通过
AddCoin(n)修改(private set),HUD 靠什么拿到新值?(提示:GameManager + 信号。)
第 5 章:继承、多态、接口——解开 override、_Ready、is Enemy 的全部秘密。
《Godot C# 零基础》系列导航
⬅️ 上一篇:为 Godot 学 C# 03:数组、字符串、空值与异常 📑 返回系列总目录(第 01 篇) ➡️ 下一篇:为 Godot 学 C# 05:继承、多态与接口