惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

B
Blog
The Cloudflare Blog
J
Java Code Geeks
Apple Machine Learning Research
Apple Machine Learning Research
T
Tailwind CSS Blog
L
LangChain Blog
Recent Announcements
Recent Announcements
Hugging Face - Blog
Hugging Face - Blog
Microsoft Security Blog
Microsoft Security Blog
F
Fortinet All Blogs
Microsoft Azure Blog
Microsoft Azure Blog
V
V2EX
I
InfoQ
博客园 - 司徒正美
T
The Blog of Author Tim Ferriss
G
Google Developers Blog
云风的 BLOG
云风的 BLOG
aimingoo的专栏
aimingoo的专栏
小众软件
小众软件
H
Help Net Security
博客园 - 三生石上(FineUI控件)
S
SegmentFault 最新的问题
B
Blog RSS Feed
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

机核

游戏性能旗舰最强之选,一加 Ace 6 至尊版国补到手价2999元起 6元钱自己更换电动车刹车线 《生化危机9:安魂曲》编剧Haris Orkin专访 摸金游戏?音乐游戏!暗区新赛季这把能弹的琴有何来历? 好评国产武侠SRPG《息风谷战略》免费DLC现已推出 | 机核 GCORES 碎片 《生化危机:安魂曲》将于5月实装首个免费更新内容 | 机核 GCORES 新锐东方游戏,谱写世界新章! 沉浸式恋爱视觉小说游戏《心象演算》免费试玩版现已正式上线 | 机核 GCORES 互动影游《代号三国:龙起》上线!穿越三国与曹操并肩、与佳人同行、与权谋博弈! 《老头收集梦想生活》,游戏酒桌会6,录音笔VOL.689 | 机核 GCORES 破界·共生——《白日梦:无限世界》五大核心游戏特点解析 价格已到史低,锐龙5 9600X/锐龙7 9700X正适合抄底 时间循环之旅即刻启程!《归环》一周目测试今日开启 热门在线韩游变魂游,洛奇衍生作能否打破“花瓶”魔咒? LG UltraGear evo 全新高端显示器系列:当“5K”遇见“AI”,不止强大,更懂热爱 反套路三国互动影游《代号三国:龙起》今日上线! 愿望单登记人数突破10万!备受瞩目的“女儿养成游戏” 《梦幻魔法公主》今日于Steam平台上线!限时八折优惠中 《黑神话:悟空》全球音乐会2026巡演将于4月29日12时开票 | 机核 GCORES “Snowguelike”生存肉鸽挖矿新作《蛙穿雪境》公布发售日期,5月7日正式上线 | 机核 GCORES 重塑移动办公、AI创作新境!全新华硕灵耀Air系列、ProArt 骁龙版震撼首发,创芯未来 均分88:《Saros》媒体评分汇总 | 机核 GCORES 上海烛龙公布合作遗迹探险游戏《吉时已到》首支预告片 | 机核 GCORES 首个独立游戏《萝薇日记》已上线Steam! 烛龙新IP《吉时已到》首曝,打造国内首款中式合作遗迹探险游戏 喜加一:《暗黑破坏神Ⅳ》国服现已开启限时免费领取本体活动 | 机核 GCORES 新版《生化危机》电影定于9月18日上映,官方网站现已上线 | 机核 GCORES 《冲就完事模拟器2》“星球大战”联动DLC正式宣布 | 机核 GCORES SteamController将于5月4日发售,售价99美元 | 机核 GCORES 事已至此,内存这么用也算省钱了,“2+1”非对称双通道应用实测
C# with Godot:祖传秘方
cameLcAsE · 2025-08-02 · via 机核

谈了类,就肯定要谈继承。毕竟GDScript脚本在很多时候也会继承一个节点类。

继承一个类

在GDScript中,我们在脚本文件的开头写上extends Node这样的语句来表示此脚本(类)的基类是谁。

在C#中定义一个类时,在类名后面加上一个冒号,再写上另一个类名就表示此类继承了后者:

class Item { public int Id {set; get;} public string Name {set; get;} } ​ class Weapon : Item { public int Atk { set; get; } }

一般来说,基类是更抽象的概念,而子类是更具体的概念。

protected级别

除了之前提到的public和private,还有一个常用的访问级别是protected。

标注为protected的成员除了在定义它的类中访问之外,还可以在由它派生出来(继承它)的类中访问。在同一个继承链条上的类都可以访问,即子类、子类的子类都可以访问它。

“继承的”级别

如果你是C++程序员,你可能发现C#的继承语法和C++几乎一样,但是C#在继承类时不区分public、protected、private。C#的继承就相当于public继承,父类中的成员在子类中的访问级别不会发生变化。

类型兼容

子类的实例可以放到需要一个基类类型的值的地方:

Item i = new (); Weapon w = new Weapon(); i = w; F(w); ​ void F(Item i){}

这些行为是合情合理的,因为我们在继承时就是为了表达“is-a”关系。WeaponItem的子类,就意味着所有的Weapon都也应该是Item。所有需要Item的地方放入一个类型实为Weapon的实例也是没有问题的。

不过,在通过类型为基类的变量、参数引用实为更具体的类的实例时,我们只能访问基类中定义的成员。

GDScript中也是类似的。但还是由于GDScript本质上是动态类型的语言,即使尝试在类型为基类的变量上访问子类中定义的成员,只要此成员存在,那么在运行时就不会报错:

class A: var n = 1 class B extends A: var m = 2 var a: A = B.new() print(a.m)

类似的代码在C#中编译时就会报错。当然你会发现,按照目前的Godot编辑器的行为来看,这里实际上无论把a标记成啥类型或者不标类型,编辑器都会识别到a拿到了一个类型为B的对象,然后在下面的代码中会自动提示B的成员。

万物之始——object

C#中所有的类都隐式地继承了object类——你可以随便找个对象然后用blahblah is object去试。“一切皆对象”那确实不是说着玩的。

前面提到,所有的类实际上都可以调用ToString方法转换为字符串,这个方法实际上就定义在object上。因此你定义的一个新类的实例也可以调用这个方法,只不过它不一定是你想要的效果,默认情况下它会直接输出你的类名。

在Godot中,也存在一个叫Object的类型,引擎中的所有“类”都继承了它。但实际上按照Godot的设计,并不是所有的东西都是一个类的实例。例如整数、信号这些内置类型都不属于Object类型。Godot中真正可以引用任意类型的值的类型实际上是Variant,如果你不给一个变量标注类型,就相当于它是Variant类型。本质上Godot将值的类型分成了Variant.Type中定义的这些类别

类型判别

你的教科书可能会讲,我们通过继承和派生来表达各个类之间、实例和类之间的is-a关系。你可以在C#中使用is关键字来判断某个对象是不是某个类型的:

object o = new Item(); Console.WriteLine(o is Weapon); if (o is Item i) { Console.WriteLine($"It's an item called {i.Name}"); }

这实际上是C#模式匹配语法的一部分,类型放在这里会作为所谓的类型模式,即当给定的对象属于某个类型时就会匹配成功。整个表达式的返回值是bool,所以你可以很自然地把它用到if的条件中。

o is Item i这种写法也很好用。它的意思就是如果o成功和Item类型匹配,那么就把它的值绑定到变量i。这样一来就可以在if后面的代码块中通过类型为Item的变量i来访问定义在Item上的成员了。

GDScript中也有is运算符,可以用类似的语法进行类型判别。

类型转换——casting

正如前面所说,我们可以通过一个类型为基类类型的变量去引用一个实为子类的对象,但是通过基类类型的变量只能访问基类中定义的成员。

没毛病,因为所有汉堡都是食物,但是并不是所有食物都是汉堡。我们不能假定一个食物可以提供和面包胚相关的属性。

但是有些时候我们可能比编译器知道得更多,我们可以断定某个通过基类类型变量引用的对象实际上属于某个子类:

Item i = new Weapon(); Console.WriteLine((i as Weapon).Atk);

as可以将一个类型的对象“转换”成另一个类型——大家都爱这么说,但是实际上什么转换都没有发生,对象本身并没有变。通过不同的(但兼容的)类型去引用某个对象,实际上只是观察角度的改变。

从基类类型转换为子类类型的过程叫downcast,即向下转换。由于这个过程可能失败,as转换失败时会返回null

object o = new Player(); Console.WriteLine(o as Weapon);

另一个方向的转换叫upcast,但是由于语言本身的设计,这种转换一般是不需要的。

GDScript中也存在as运算符,和C#中的as行为基本一致。

显式转换

对象也可以像数字那样用括号进行显式转换:

Item i = new Weapon(); Console.WriteLine((Weapon)(i).Atk);

和使用as的区别在于,显式转换在失败时会直接引发异常

虚方法和重写

当一个方法标记为virtual,就表明此方法可以在其派生类中被重写(override)。“可以”的意思是,你可以重写也可以保持原样。

恰好,ToString就是这样一个方法。其默认的行为在很多时候用起来在调试时并不方便,更别提真正地把一个对象转换成有用的字符串了。如果我们要重写一个方法,我们要这样写:

class Item { // 略去其它定义 public override string ToString() { return $"{Id} {Name}\nDescription: {Description}\n"; } }

和定义一般的方法唯一的区别在于多了个override关键字。这个关键字就表明这个方法的定义就是在重写一个基类中对应的方法。

重写方法时,方法的签名必须和基类的定义保持一致。即方法的名称、参数列表、返回值类型、访问级别都必须和基类中的定义保持一致。在实际编写代码的过程中,你也不用老老实实地从头打到尾。一般你写一个override之后,你的IDE和编辑器就会提示可以重写的方法。

属性重写

我们已经了解到,属性的定义在很大程度上也就是定义了对应存取的两个方法。属性也可以标记为virtual,也可以重写:

class A { protected int n; public virtual int N { protected set => n = value; get => n; } } ​ class B : A { public override int N { protected set { Console.WriteLine($"N set to {value}"); n = value; } get => n; } }

借用基类实现

如果你不需要完全覆盖虚方法在基类的实现中所做的工作,你也可以按需调用基类实现的版本:

// 继承Item的Weapon类 public override string ToString() { return $"{base.ToString()}Atk: {Atk}"; }

通过base关键字可以调用各种在基类中定义的成员。

GDScript中使用的是super关键字,作用类似。

多态

继承和重写方法的好处在于,我可以在有必要的时候忽略父类和子类的区别,又在有必要的时候让它们的成员自发地有不同的行为。

假设我把一系列道具放在一个数组(背包)里面。由于C#的数组要求保存的元素必须都属于同一个类型,所以我们只能把数组的元素类型定义为基类。但是,由于继承关系的存在,我们实际上可以放入从Item类派生出来的各个类型的实例:

Item i = new Item(); // 假设这些类存在且都继承了Item Weapon w = new Weapon(); SkillScroll s = new SkillScroll(); ​ Item[] items = [i, w, s];

很好,所有东西都一股脑地装进去了,数组也不管它们到底是啥,只要它们“是道具”就行了。

foreach(var i in items) { items.Use(); }

如果Use是定义在Item上的一个虚方法,且各个类都实现了自己的版本。那么在上面这样的代码中,在运行时它会“非常聪明地”调用具体的道具实例对应真正的类型上定义的具体的版本,而不是调用Item上的版本。

这就是所谓多态(polymorphism)的一种体现。

抽象基类

有些时候一个基类是真的很抽象,以至于没必要、甚至不应该直接构造基类的实例。

比如说“动物”。我们有很多种方式去定义动物的分类、去构造它的各种子类。食蚁兽、西伯利亚雪橇犬都是很具体的动物,但是并不存在一种动物就是“动物”。

这种基类可以定义为抽象类:

abstract class Animal { public stting Name {set; get;} }

使用关键字abstract指明一个类是抽象类。

抽象类不能被实例化,但是我们依然可以定义类型为抽象类的变量。正如我反复强调的,变量不是对象也不存储对象,它只是引用一个对象,并通过它的类型来观察这个对象。

抽象方法

抽象类中可以定义抽象方法。抽象方法不允许有方法主体,用传统一点的术语来说就是它只能有声明,不能有定义。抽象类中定义的抽象方法,就是要求子类在继承此类时必须实现此方法。一般适用于一些基类无法实现或者没什么意义的方法。

abstract class Animal { // ... public abstract void Bark(); } ​ class Dog : Animal { public override void Bark() { Console.WriteLine("Woof"); } } ​ class Husky : Dog { public override void Bark() { Console.WriteLine("Woooooooooo"); } }

在子类中,抽象方法的实现会用到和重写虚方法一样的语法即使用override关键字。

Dog类实现了Animal的抽象方法,Husky本身是不用重新实现的。但是如果有必要也可以进行重写,抽象方法天生就是virtual的。