1817 字
9 分钟
为 Godot 学 C# 04:类与对象

教程二 · 第 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); // 12

new = 按图纸造一个实物。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 节点的初始化规矩:

  1. 字段处给默认值 / _Ready() 里初始化
  2. 需要外部参数: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 作业#

  1. 写纯 C# 类 Health:max、current、Damage(int)、Heal(int)、IsDead,current 自动夹在 0..max;在 Playground._Ready 里 new 出来打几套拳(受伤→治疗→打死)打印结果。
  2. 给 Health 加 [Signal]?——先想想:纯 C# 类能不能发 Godot 信号?(答案:不能,信号是 GodotObject 的能力。那想让 UI 知道血量变化,纯类该怎么通知外界?带着这个问题读第 6 章的 C# event。)
  3. 把平台游戏玩家脚本里的”金币数”改成只能通过 AddCoin(n) 修改(private set),HUD 靠什么拿到新值?(提示:GameManager + 信号。)

第 5 章:继承、多态、接口——解开 override、_Ready、is Enemy 的全部秘密。


《Godot C# 零基础》系列导航

⬅️ 上一篇:为 Godot 学 C# 03:数组、字符串、空值与异常 📑 返回系列总目录(第 01 篇) ➡️ 下一篇:为 Godot 学 C# 05:继承、多态与接口

为 Godot 学 C# 04:类与对象
https://021028.xyz/posts/default/173/
作者
021028
发布于
2026-10-14
许可协议
CC BY-NC-SA 4.0