💽 游戏开发中的编程理论基础


2024-04-07

Game Dev

Dev

💡
前沿:本篇文章为社团分享,作者为宏楼(我) (Koiro) 和 拉斯普(Aspirin) 两人。本文包含游戏开发中一些重要理论的基本概念,包括编程范式、面向对象、组合与继承、面向数据、ECS 的概念和优点以及一些现在市面上游戏引擎的使用的架构。

基础知识

编程范式

让我们从简单一点的内容开始。程序语言不仅仅给我们提供了强大的工具,还帮助我们组织我们关于一件事情“如何做”的想法。为此,程序语言需要赋予我们三种能力:表达最基本的表达式的能力,它们组成了这个程序语言最基本的概念,例如整数、浮点数等基本数据类型(以及锁、通道等等);组合的能力,我们需要把最基本的内容组合起来构建复杂的东西;抽象的能力,就是把组合起来的复杂结构当作一个整体并赋予名字的能力。

基本数据类型大家都了解,我们具体介绍一下组合的能力和数据的能力。例如现在我们现在要表示一个三维空间中的一个点,我们可以用三个数例如 x y z 来表示。但是这样我们每声明一个点,就需要三个变量,同时我们需要用命名的方式给不同的点的 x y z 区分开来。下面就是声明两个点的例子。

c
int main(){
  //第一个点
  float x1 = 2.0f;
  float y1 = 3.0f;
  float z1 = 0.1f;
  //第二个点
  float x2 = 1.0f;
  float y2 = 2.0f;
  float z2 = 0.6f;
}

一方面,我们要用命名的方式来声明每个变量的关联性,另一方面,声明多个变量让我们的代码变得很难管理,例如如果我们声明三个点,就需要声明九个变量。可见这样表达非常麻烦。

于是我们想,能不能将有关联的对象组合在一起,同时把它赋予一个新的名字呢?C 语言赋予了我们这样的能力,这个能力也就是 C 语言中的 struct (结构体)。

c
struct Vector3 {
  float x;
  float y;
  float z;
};

这里我们把三个数据类型组合成了一个新的数据类型,并给它起名为 Vector3 (三维向量)。这就是程序语言赋予我们组合和抽象能力的体现之一。我们把 x y z 组合在了一起,并抽象成了 Vector3 .

我们通过程序语言提供的这些能力构建我们想要的程序,程序语言也反过来通过它们塑造我们解决问题的风格。这类风格一般被称作编程范式。能叫得出名字的编程范式有很多,比如指令式编程、面向过程编程、面向对象编程、函数式编程之类的。值得注意的是,不同的编程范式不应该被看作是彼此独立甚至对立的,但有些编程范式确实会产生冲突,这点我们后面聊面向数据编程的时候会接着讲。

走近面向对象编程,之前

在本文中,“对数据的操作”、“操作”和“函数”的含义差不多,在本模块的语境中请当作一个概念看待。

回忆一下我们从 C 语言课上学到的东西,C 语言允许使用我们使用基本类型的变量来存放我们想要的数据,这是它赋予我们的第一个能力。同时,它也允许我们通过语句来操作这些变量,我们还可以把多条语句放在一个函数里面,这样我们就能把一个复杂的操作当作一个操作看待,这也是 C 语言赋予我们组合和抽象能力的体现之一。

C 风格(准确地说是面向过程)的编程范式将程序的重心放在函数上,也就是我们程序要完成的操作上。在这种观点下, 程序实际上是一系列的函数声明,我们把我们会用到的操作全都以函数的形式写下来,而变量是函数之间传递信息的方法。这种方法在维护某些大型项目的时候会遇到一些很特别的问题:首先是执行流过长会导致程序的运行难以分析和排障,虽然我们写代码的时候能够将程序的执行流看作一个不可分割的整体,但是我们在调试的时候仍就得将它们展开。其次是变量和关于它们的操作在代码逻辑上没有关联性,这会带来很多困惑。举例子来说:左偏树和二叉搜索树都被存储成数组(大家不需要知道这两个东西是什么,只要知道它们是数组就可以),而它们的区别仅在于对数组的操作不同。比如它们的插入函数就彼此不同。这种情况下,如果我们不用名称或者注释等方法注明,我们不知道一个数组是左偏树还是二叉搜索树。因此我们会说这两个数据结构和维护它们的操作在代码逻辑上没有关联性,但其在数据结构的逻辑上是有关联性的。因此我们需要一种将数据和对数据的操作的关联性表达在代码中的能力。也就是将数据和对数据的操作组合和抽象起来的能力。

什么是面向对象 OOP

来看看面向对象的编程范式对上述的问题做出了什么解决的尝试。面向对象的编程范式把编程从一系列函数声明变成了一系列对象(更准确地说,是类)的声明。它认为应当把对象看作是程序执行功能的基本单位,或者说,对象是工厂里的工人,而功能是工人要做的事情。面向对象的编程范式是怎么实现这个黑魔法的呢?大部分面向对象的程序语言构造变量和表达式的方式跟我们在 C 语言里习惯的并不不同,那我们得思考以下面向对象的程序语言给我们提供的组合和抽象的能力有什么不同了。

一部分的面向对象的程序语言会提供一个叫做“类”(class)的基本概念,这是一种组合基本单元的方式。不过不同于我们在 C 语言的结构体里面做的那样,类不仅仅能组合数据,还可以把操作也一起组合进来。下面是一个类的示例。这里的 public 关键字大家先忽略,我们下面会介绍。

c#
public class Vector3 {
  //成员字段
  public float x;
  public float y;
  public float z;

  //成员方法
  public void Clear() {
    x = 0f;
    y = 0f;
    z = 0f;
  } 
}

在类的语境里,类包括两部分:第一部分是类的成员字段,也就是持有的变量,比如上面的 x y z;第二部分是类的成员方法,也就是类持有的函数,比如上面的 Clear() ,这个函数没有什么特别的作用,就只是把这个向量的 x y z 都设置成 0。成员字段是这个类应当存储的数据,而成员方法就是对这些数据可行的操作。

在这个示例中,我们声明了一个叫 Vector3 的类,同时我们将一个操作也放了进来也就是 Clear 函数,或者说 Clear 方法。方法与函数的不同在于在方法中,我们天然地可以访问类中的其它变量,我们看到,上面的 Clear 方法可以直接访问 x y z 变量,而不需要把他们当作参数传进来。我们的 Clear 方法实际上类似于下面这样一个函数,这个函数在类之外。下面的函数中,我们传了一个名字为 self Vector3 ;而在上面的类的函数中我们并不需要传一个 self 进去,实际上我们把这个 self 隐藏了,我们隐藏地认为类中的函数可以使用这个类中的其它变量,所以我们并不用写出这个 self . 我们把这样的函数称作方法。

c#
void Clear(Vector3 self) {
  self.x = 0f;
  self.y = 0f;
  self.z = 0f;
}

同时,优秀的面向对象的程序要求每个类设计得足够小,小到只能完成一个逻辑上的功能。

这是一种理解组合和抽象的新方式,在类的范畴下,我们要思考的内容不再是整个程序在什么情况下要做什么,而是这些类应当具备什么功能,为了让某个类具备某个功能,我们应当在其中存储什么数据,实现什么方法。

另外,面向对象的编程范式认为,过多地考虑外部代码的正确性并不是好事,所以它希望程序员能够放弃做这件事情。因此,在定义类的时候,我们可以只让外部看到我们希望让别的类知道的细节,比如我们可以只对外表明我们有 A 方法和 B 方法,而除了 A 和 B 方法外我们内部还有一些其他的方法。这种技术被称为封装。

回到上面的代码,我们发现每个变量和类的声明前都有一个 public 关键字,这个关键字其实就是声明整个类和变量是对其它类都可见的。如果我们不声明,在很多编程语言中,这个字段默认对其它类是不可见(也就是说,字段默认是 private 的)。

那么,依靠类,我们是怎么解决先前的那些问题的呢。我们先回顾一下之前的问题是什么。

  • 执行流过长会导致程序的运行难以分析和排障
  • 变量和关于它们的操作在代码逻辑上没有关联性

首先,我们规定了类的功能之后,在封装的帮助下,就只需要考虑这个功能本身的正确性,如果一个类本身工作正常,那么它在与别的类合作的时候也一定工作正常。因为我们只需要把一个类中承诺能实现的功能的部分暴露给其他人,其它部分被限制在类内部。其次,在封装的帮助下,我们不能也不会过分地考虑程序的细节,因为别的库的大部分代码都被其它开发者隐藏了。这两点把我们思考中要容纳的范畴缩小到了一个相对轻松的范畴。面向对象范式的支持者认为,应用面向对象的思想去设计程序会让程序变得更好维护。

我们以 Minecraft 的方块 Block 为例,给大家具体介绍一下类的概念。之前我们介绍了类,它拥有组合变量和操作的能力。MC 中方块的类简化后是代码中这样的。为了方便大家阅读,在后面我们所有的 public 关键字都省略掉,大家认为所有的字段都是 public 就好了。

java
class Block {
  String name;
  float explosionResistance; //爆炸抵挡能力
  float destroyTime; //破坏这个方块需要的时间
  Item item;
  ...
}

我们在 Block 类中声明了许多所有方块几乎都会有的信息,例如破坏需要的时间等等。这时,要创建一个方块,例如泥土,我们只需要把 Block 类实例化出来就好了。

java
Block dirt = new Block("dirt",...);

等下,什么是实例化?我们之前介绍了 struct ,我们创建一个 struct 时就是同时创建了其中所有的声明的变量,这些变量被“打包”到一个变量里。几乎一样的,我们创建一个类的时候,其实就是同时将一堆类中声明过的变量和函数创建出来,“打包”在一个变量里,我们把这个变量称作“对象”,我们称这个过程为“实例化”。这个被创建出的 dirt 变量就是被我们称作的“对象”。各位可能好奇我们没有设置过这些变量的初始值,那我们创建出的泥土里的变量的初始值是什么呢,在编程语言中我们有很多声明和设置对象的初始值的方法,大家可以在学编程语言时了解。这里我们方便大家理解概念,把这些代码省略了,这样看起来会更好懂一点。

好了,现在我们创建了一个泥土方块,我们可以类似地创建石头方块、树叶方块等等。

java
Block dirt = new Block("dirt",...);
Block stone = new Block("stone",...);
Block leaf = new Block("leaf",...);
...

大家注意我们上面代码里的括号,这是我们在 Java 语言设置对象初始值的一种方式。我们不同的方块有不同贴图、名字、破坏时间,都可以在我们声明这个方块的括号中设置。

我们刚刚举例介绍了类是如何组合变量的,现在我们来看看“操作(函数/方法)”,举个例子,我们每个方块都会有在被破坏时掉落物品的行为,我们在 Block 类里写了一个 spawnDrops (生成掉落物)方法,在方块被破坏时生成我们的掉落物。

java
//Block class  
void spawnDrops(){

}

但是在 Minecraft 中,不同的方块掉落物品的行为是不同的,比如泥土是掉落自己的方块物品,树叶是概率掉落树苗。但是我们 Block 类只有一个,只能实现一种逻辑。实际上,不止掉落物品,我们每个方块的各种行为都是不同的,那么有没有一种既可以保持它们有相同的行为声明和变量声明,又能实现每个方块特别行为的架构呢?

继承这个概念出现了。它允许我们在给我们的类扩充新功能的同时,保留旧的那一个类的内容。

java
class LeafBlock extends Block {
  void spawnDrops() {
    //有概率掉落树苗的逻辑
  }
}
java
class DirtBlock extends Block {
  void spawnDrops() {
    //掉落方块物品的逻辑
  }
}

上面我们从 Block 派生了两个新的类,两个类继承自 Block ,它们拥有 Block 类的内容,同时类允许我们修改其中某个方法的逻辑。这就解决了我们之前的遇到的问题。

用继承模型实现游戏

我们刚刚了解过了继承,用继承去设计游戏是一件符合直觉的事情。以 Minecraft 为例,Minecraft 是一个使用继承模型实现的游戏。在 Minecraft 中,我们用实体 Entity 这个基类来实现各种不是方块的物体,例如掉落物、生物等。我们看看 Minecraft 是怎么实现鸡和牛的,因为所有生物是有共性的,例如生物有血量系统、会受伤,于是开发者派生出 LivingEntity 这个类作为所有生物的基类。每个生物的行为是不一样的,因此开发者要派生出每个具体生物的类。

例如:只有鸡在空中会缓慢降落,因此开发者只需把缓落逻辑写在 Chicken 类中。类似的,开发者把用桶接牛奶的逻辑写在 Cow 类中。

java
public void aiStep() {
      super.aiStep();
      this.oFlap = this.flap;
      this.oFlapSpeed = this.flapSpeed;
      this.flapSpeed += (this.onGround() ? -1.0F : 4.0F) * 0.3F;
      this.flapSpeed = Mth.clamp(this.flapSpeed, 0.0F, 1.0F);
      if (!this.onGround() && this.flapping < 1.0F) {
         this.flapping = 1.0F;
      }

      this.flapping *= 0.9F;
      Vec3 vec3 = this.getDeltaMovement();
      if (!this.onGround() && vec3.y < 0.0D) {
         this.setDeltaMovement(vec3.multiply(1.0D, 0.6D, 1.0D));
      }

      this.flap += this.flapping * 2.0F;
      if (!this.level().isClientSide && this.isAlive() && !this.isBaby() && !this.isChickenJockey() && --this.eggTime <= 0) {
         this.playSound(SoundEvents.CHICKEN_EGG, 1.0F, (this.random.nextFloat() - this.random.nextFloat()) * 0.2F + 1.0F);
         this.spawnAtLocation(Items.EGG);
         this.gameEvent(GameEvent.ENTITY_PLACE);
         this.eggTime = this.random.nextInt(6000) + 6000;
      }
   }
鸡的滑翔逻辑(氛围代码,不需要看懂)

在继承结构中,Chicken 和 Cow 都继承自 Animal 类,而 Animal 则经过多层继承来自 LivingEntity。

牛 Cow 类的继承关系

但是各位思考一个问题,如果我们要做一只既可以接奶又可以缓落的生物,我们该怎么做?

这个问题在现实中很好解决,既然我们无法同时继承鸡的类或者牛的类,那我们创造一个新类,把鸡的滑翔逻辑和牛的接奶逻辑都放进去就好了。但各位应该发现一些问题了:我们复制了代码。复制代码是个很棘手的问题,因为在未来我们修改一个代码时要把所有的复制代码也都改了,这降低了代码的可维护性。在继承模型中,为了缓解复制代码的问题,我们只能将各种行为抽象成很更多类。例如我们看牛的类的继承关系:从 Animal 到 Mob 中有很多层类。但如果我们想到一个生物不能兼容现有的任何类,那么我们又要重新调整继承关系,或者选择复制之前的代码,造成代码复制的问题。如果我们能预知未来,我们就够构造一个完美的继承结构;但现实是我们不能,我们要么在每次遇到新需求的时候重整继承结构,要么就复制代码。

所以,在继承模型中,我们很难把不同的功能组合在一起。

组合模型大于继承模型

于是人们实践出了组合模型。

设想一下如果我们把物体的功能都分散成每一个能力会怎样。对于一只鸡来说,我们可以这样拆分它的行为(需要怎样的能力):

  • 生命能力
  • Ai 能力
  • 走路能力
  • 缓落能力
  • 接受药水效果能力
  • ……

类似的,对于牛,我们完全可以写一个“产奶能力”;这样先前那个生物的问题就很好解决了,我们只要把“缓落能力”和“产奶能力”组合在一起,都给一个生物就可以了。组合还有很多其它好处,假如我们要写攻击逻辑,我们不用在乎这个物体是什么,只要其存在生命能力,那我们就执行生命能力的受伤逻辑。更简单的,如果一个物体没有生命能力,那它就是无法受伤的物体;没有接受药水效果能力,就是免疫药水效果的物体,因为药水效果不会对其执行。

组合模型和继承并不冲突

继承是编程语言的特性,它和组合模型并不冲突。例如:不同生物的 AI 行为是不同的,但是他们有共享的部分,因此它们可以从同一个基类中派生出来。

容易接触到的组合模型

现代游戏引擎几乎都是可以使用组合模型的。在 Unity 中,每个物体的行为由各种各样的组件实现的。在 Godot 中,我们可以用 Node 来实现组合。

Unity
Godot

面向对象在游戏开发中的问题

软件工程里有一句经典的名言:没有银弹,亦即没有通用的解决方案。

面向对象在实际的工程实践中同样有很多的问题。首先是性能问题,这主要体现在两个方面:其一是我们在通过类获得我们想要的功能时,一定也不必要地获得了我们不需要的功能,而所有的这些功能都是有开销的;其二是“对象”这个概念对现在的机器并不友好,设想这么一个场景,我们需要获得大量对象的名字,但是为了它们的名字,我们需要将所有的对象都拉进我们的内存中,这通常包括我们在对象里存储所有其它数据。很快,内存里就会充满了我们不需要的数据。

实际上,这些数据并没有跟看起来的那样与我们要解决的问题无关,我们实际上将一个具有无法管理的内部状态带进了我们的上下文。我们无法得知某些操作是否跟它承诺的那样无害,因为我们看不到封装的另一头,直到某天一个神秘的内部错误毁了我们写的整段代码。

另一个问题与面向对象的思想有关,它总是鼓励程序员构造复杂的封装。类强大的功能总让程序员误以为所有的问题都可以通过构造一个新类来解决。出现新问题?实现新功能?没关系,我们都可以通过构造一个新的类来解决。于是我们将越来越多的类组合成新类,把这个不可控的内部状态变得越来越大,最后我们会为了实现一个简单的功能而写出一大堆没有必要的样板代码。

看起来面向对象的范式远远没有它看起来那么清爽无害,有没有能够解决这个问题的方法呢?

什么是数据驱动 Data-Driven

在下面的文章中 “数据驱动” 和 “面向数据” 表示同一个意思,当你看到其中的一个时,请将其换成你喜欢的那一个名词;

在开始介绍新范式之前

我们要再一次地指出:没有通用的解决方案,同时不同的编程范式也并不是互斥的。我们应当做的是在一个编程范式擅长的地方去发挥这个范式的长处,而不是固执地认为某个范式能解决所有的问题。

从数据开始

数据驱动的编程依赖与数据的模式,根据不同的模式执行不同的行为。这是一个很简单也很难的描述,它并不能像面向对象那样把程序描述成一个我们都能以直觉去理解的样子。要理解数据的模式,我们先要理解什么是相似的数据。

很多时候,其实我们并不是非常关心我们的数据到底是什么,我们其实只关心数据能完成的事情。比如,我们对一组数据做排序时,我们只关心这组数据之间是不是能排序,并不关心他们是否可加、可复制之类的事情。这种都具备一个通用的行为的数据我们可以认为是相似的数据。这给我们另一个提示,我们写下的行为,或者说操作函数,在很多时候是可以用于一大堆种类的数据的。而我们提炼出来的数据间的相似性,很多时候也表明了我们在使用这些数据时我们的关注重心。

haskell
min :: (Ord t) => t -> t -> t
min a b = case compare a b of
  LT -> a
  GT -> b
  EQ -> a
Haskell 语言中取最小值的函数,可以看到我们只限制了 a 和 b 是 Ord 的,也就是可排序的。

把相似的数据存储在一起,我们就可以在我们需要的时候只拿取出我们想要的数据,而我们对这些数据执行的操作又是相同的,因为这些数据有共同的特征,这就是数据的模式。

因此,数据驱动的编程范式更适合用于处理对大量相似的数据做同类操作的问题,它们天生就有更好的性能,并且能够更方便地使用多线程相关的优化技巧。但并不适合处理对某些特殊数据做某些操作的任务,因为这通常意味着我们得生造出那个特殊数据的“相似性”,并要求其他数据不满足这个特性,而这是反模式的。这种任务天生更适合用面向对象的方法完成。

什么是 ECS

本篇介绍的 Component 与 Unity 的 Component 并不相同,类似地 Entity 也并不是 GameObject,它们或许有些类似,但请大家不要带入过往的经验。

ECS 指的是 Entities(实体)| Components(组件)| Systems(系统)模型。实体指一个特殊的单位,它将许多组件打包在一起,组件负责存储数据;而系统则负责处理数据。

ECS 是一种用面向数据思想设计的框架,我们下面也会介绍 ECS 系统如何用面向数据思想来实现的。

虽然上面是通俗简练的介绍,但还是很难懂。我们可以根据实际的游戏内容来理解。下面我们依然以 Minecraft 为例。

注:Minecraft 并不是用 ECS 模型实现的游戏,但我们以 Minecraft 的游戏内容为例来介绍 ECS,或者说如果我们以 ECS 模型来实现 Minecraft,我们会怎样设计。以下的 Entity(实体)定义与我们上面介绍过的 Minecraft 中的 Entity 没有关系。

先介绍一下什么是实体和组件

我们的游戏是由大量的物体组成的。以 Minecraft 为例:我们的每一个方块、物品、生物都可以看作一个实体。每个物体都有一个唯一标识符,而实体就是这个唯一标识符,在实现上它就是一个 unsigned int (在大部分机器上这是一个 32 位长无符号整数) 。而我们的数据存在组件中,我们可以声明每个组件属于哪个实体。

这样我们创建了一个组合数据的能力。我们将不同的组件,通过实体这个唯一标识符组合在了一起。

一个具体的例子,我们可以想一想,对于 Minecraft 中一个掉落在地上的物品来说,它需要有哪些组件:

kotlin
- Transform //位置、旋转、缩放信息
- ItemTexture //物品的贴图
- RegistryName //这个物品种类的唯一标识
- Count //掉落的物品的数量
- DroppedItem //不存储数据,只是一个用来标识这时掉落物的标签
..

各位可能好奇为什么我们不直接创建一个 Item 的组件,把物体相关的参数都放进去。

这确实是一种可以用的设计方法,但是,对于 ECS 模型,将组件做小,在 ECS 的 Systems(系统)中有性能优势,下面会介绍。

什么是系统(System)

系统是对数据的操作。在编程语言中,我们对数据操作的手段是函数,ECS 中的系统也是类似的,在实现上它们就是各种函数。我们要实现系统有两个问题:

  • 我们怎样获取需要的数据(主要为组件)?(了解组件的存储方式)
  • 我们怎样调用我们的系统?(了解系统的存储方式)

组件是怎样被存储的

我们刚刚说过 ECS 是一种面向数据的架构。在面向数据的思想中,我们会将同一类的数据放在一起。而在 ECS 中,组件就是这样的数据,在实现上我们将同一种组件放在一起存储。

将系统放在一起

基本数据类型可以被存储,函数是也可以被存储的,我们游戏通过循环来更新,系统或者说函数就是在这个过程中被执行的。现在大部分语言都拥有许多函数式编程特性,使得存储函数或者说操作的事情变得并不困难。函数式编程又是另一个天坑,我们未来再聊。大家只需知道我们可以像存储数据一样把函数存储在一起,然后在游戏循环中每帧调用。

🌰 例子

我们刚刚了解了如何解决处理数据遇到的两个问题,来看看具体的例子吧!

rust
fn rotate_drops(query: Query<&Transform, With<DroppedItem>>, time: Res<Time>){
    for transform in query{
        rotate_with_time(&mut transform, time.delta());
    }
}
一段用 Bevy 引擎实现的 System 函数(你不需要理解这段代码)

讲完这些这点之后,上面将不同数据分成更多细小的组件的原因也明晰了。假如我们一个组件写的很大,负责的数据多,如我们说过的 ItemStackDorp

为了获得 Transform ,我们就要获得所有的 ItemStackDrop ,这时那些用不上的数据也一并被获取了(texture, registryName…),这就产生了无用的开销。因此我们会倾向于将每个组件设计得小,使其只承担一个明确的任务。

ECS 的应用

ECS 有许多性能上的好处,但缺点也显而易见,它完全不如面向对象组织数据来的直观,在面向对象模型中我们将物体的数据和行为放在一起,而在 ECS 中却要把他们分开。另外,ECS 对编程语言特性也有要求,因此市面上大部分游戏引擎都不是纯 ECS 系统的,有的使用其它的范式,有的是和其它范式混合的 ECS 系统。

  • Unreal 的使用的就是混合的 ECS 范式。

比较纯粹同时较流行的使用 ECS 范式游戏引擎我知道的有:

  • 一个是 Bevy,这是一个基于 rust 的纯 ECS 的面向数据的游戏引擎。
  • 另一个就是 Unity 于今年(2023)刚刚更新的 DOTS 框架,Unity 特别强调了该框架 ECS 和面向数据的特性。

不过,我们了解 ECS 更重要的是了解其中的思想,也就是面向数据的思想。ECS 只是其中的一种实现。我们不断思考架构的原因是为了创造出更好的软件。

以上,祝大家玩得愉快。Happy game dev and happy hacking!

附录

再好的学习资料也不能解决一切问题,学习的过程中希望大家广泛地运用搜索引擎、ChatGPT、New Bing 等来解决自己的问题。 在开发领域,在国际搜索引擎(Bing 国际版、Google 等)用英文搜索能得到更有帮助的答案。

程序设计基础方向

CS50: Introduction to Computer Science,Harward University, https://pll.harvard.edu/course/cs50-introduction-computer-science (中文版字幕 - CS50:计算机导论, https://www.bilibili.com/video/BV1DA411Y7jk/

适合的人群:希望了解程序设计基础知识的人; 你可以从中获得:作为程序员,我们在编程时到底在做什么和应该思考什么。程序设计的基本理念。

Structure and Interpretation of Computer Programs, MIT, https://mitp-content-server.mit.edu/books/content/sectbyfn/books_pres_0/6515/sicp.zip/full-text/book/book.html (中文版 -《计算机程序的构造与解释》, https://www.bilibili.com/video/BV1Xx41117tr/

适合的人群:希望了解编程语言如何赋予我们组合和抽象的能力,以及希望了解如何设计出良好程序的人群; 你可以从中获得:Lisp 语言的基础知识,设计良好程序的能力,分析编程语言结构的能力。

面向对象的编程范式方向

C# (C Sharp)

《C# 图解教程》,丹尼尔·索利斯,卡尔·施罗坦博尔,人民邮电出版社

适合的人群:无编程语言经验的编程入门人群、只有非面向对象编程语言经验的人群; 你可以从中获得:C# 程序设计语言的基础和进阶知识及实践指导,面向对象基本理论在 C# 编程中的实践。

Java & Kotlin

Kotlin 是一个可以用于编写 Java 程序、跨平台应用、Web开发、安卓应用等的编程语言。 Kotlin 更加现代并且完全兼容 Java,所以越来越多的 Java 项目开始使用 Kotlin 作为主要开发语言(例如安卓系统)。

《Head First Java(中文版)》Kathy Sierra , Bert Bates,中国电力出版社

适合的人群:无编程语言经验的编程入门人群、只有非面向对象编程语言经验的人群,想要学习 Java 的人群; 你可以从中获得:理解面向对象这种编程范式并学习如何用 Java 语言实践。

Kotlin 语言中文指南: https://hltj.gitbooks.io/kotlin-reference-chinese/content/txt/index0.html

适合的人群:有面向对象语言编程经验、想学习 Kotlin 的人群。

游戏引擎

GAMES 104 - 现代游戏引擎导论: https://www.bilibili.com/video/BV1oU4y1R7Km/

适合人群:对游戏和游戏引擎如何运作好奇的人群,即使大部分听不懂,也能收获颇多 你可以从中获得:现代游戏引擎的理论知识

Bevy 引擎

适合的人群:好奇 Bevy 是如何设计出干净的 ECS 结构的人 你可以从中获得:使用 Bevy 引擎的能力;ECS 的游戏引擎是如何被实践出来的知识;对数据驱动更深刻的认识。

Bevy 官网: https://bevyengine.org/

Rust Game Development with Bevy: https://taintedcoders.com/

Rust Bevy Entity Component System: https://blog.logrocket.com/rust-bevy-entity-component-system/

进阶

Beau­tiful Racket: an intro­duc­tion to language-oriented program­ming using Racket, https://beautifulracket.com/ (使用 Racket 设计属于你的编程语言)