ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、视频、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
[TOC] ## 类 class 类是一个比较特殊的概念,我自己也是花了很长时间才理解透彻。 记得MEA群里有个编程大神是这样给我解释的,类可以理解成蓝图,或者说模板。说实话当时仍然没有太理解,直到后来自己把它从头到尾探索了一遍。 在面向对象的编程语言中,几乎都有类这个概念。类也是面向对象这种思路的具体体现。 ~~~ void Main(){ } class Student { } ~~~ 这里的Student就是一个类,class是它的定义,Student是它的类名。类在定义的时候是不需要()的,只需要紧跟一个花括号。 ### 一、类的成员变量(类的属性) 类是对现实中实际物体的模拟,设想游戏中的怪物。我们给它设计一个名字、生命值、位置坐标、移动速度几个属性。并且所有的怪物都有这些属性,这时我们就可以使用一个类来封装这个怪物 ~~~ class Monster { public string Name; //名字 public int Health; //生命值 public Vector2I Position; //位置坐标 public Vector2I Velocity; //移动速度 } ~~~ 这些都是这个类的成员变量,意味着这个类有这些属性。 >[danger] 这个概念一定要先背下来,类可以有成员变量,这些都是变量,完全符合前文对变量的解释和用法。 > > 这里在变量类型前面还增加了一个public 关键字,它是必须的,它表示这个成员变量,在类的外面可以访问到。如果改为private表示私有变量,只允许在类里访问。 > >类中可以看做一个新的编程区域,在类的内部,也有公共变量和局部变量。 > >类中的变量都被保护在类之中,与外部是两个不同的编程空间。 > >因此当类中存在一个变量与类外面的公共变量重名,在类里你使用的时候,使用的是类里面这个变量而不是外部的公共变量。 ### 二、类的成员函数(类的方法) 其实上面我们已经创造好了一个怪物的模板,所有的怪物都有名字、生命值、坐标、速度这几个属性。接下来我们还可以定义一些所有怪物都通用的方法,或者说函数。比如移动 ~~~ class Monster { public string Name; //名字 public int Health; //生命值 public Vector2I Position; //位置坐标 public Vector2I Velocity; //移动速度 public void Move(){ this.Position = this.Position + this.Velocity; } public bool isDie(){ return (this.Health <= 0); } } ~~~ 我们给怪物增加了两个函数,实际上把函数说成方法,也是在类的基础上。因为类是某种实体的抽象,而函数就是对这个实体进行操作的方法。 上面我们定义了两个方法 Move()方法,它通过怪物当前的速度来改变它的位置,这就实现了移动。 isDie()方法,它是一个bool类型的方法,可以返回一个true或false结果,用来判断这个怪物是否死亡。我们规定生命值<=0就算死亡。 为什么要把这些类中呢?往下看 ### 三、类的实例化 ~~~ void Main(){ //实例化两个Monster类,并赋值给变量M_A和M_B Monster M_A = new Monster(); Monster M_B = new Monster(); //这时我们可以理解为,M_A和M_B是使用Monster模板创造出来的两个怪物,它们都有名字、生命值这些属性,并且它们的属性是不互相影响的 M_A.Name = "普通怪物"; M_B.Name = "精英怪物"; M_B.Health = 1000; //让B作为精英怪物,血量改为1000。此时M_A的血量依然是默认的100,它们互不影响。 Monster Winer = Fight(M_A, M_B); Echo(Winer.Name); //此时会显示 “精英怪物” } //定义一个函数,返回Monster类型变量,传入两个Monster类型变量。判断谁的血量高就返回谁 Monster Fight(Monster A, Monster B){ if(A.Health > B.Health){ return A; }else{ return B; } } class Monster { public string Name; //名字,声明同时不赋值,默认为""空字符串。 public int Health = 100; //生命值,默认为100 public Vector2I Position; //位置坐标 public Vector2I Velocity; //移动速度 public void Move(){ this.Position = this.Position + this.Velocity; } public bool isDie(){ return (this.Health <= 0); } } ~~~ 在主函数中,我们可以使用new关键字来实例化一个类,这是个固定的写法。当你实例化了一个类,它就会获得独立的一个内存空间,它的所有成员变量都会得到储存。当你需要访问它的某个成员变量时,使用M_A.Name来访问。访问它的成员方法,也是类似,区别在于需要加个括号。 >[danger] 注意这几点 >1. 使用new关键字来实例化一个类,并赋值给这个变量。这个变量在实例化的同时就自动产生了一些成员属性,这是由你自己来定义的。 >2. 使用Monster.Name来访问它的成员变量。使用Monster.Move()来访问它的成员函数。 >3. 同一个类可以实例化出多个变量,它们之间是相互独立互不干扰的。 >4. 在类中使用this关键字来指代它自己。例如Move()函数,M_A.Move()会让M_A这个怪物移动,但不会让M_B移动。因为在对M_A使用Move()的时候,里面的this代表了M_A这个实体,和其他的M_B、M_C都没有半点关系。 实际上这和之前说的变量与函数是完全相似的。 ### 四、构造函数 上面的例子中,我们都没有写类的构造函数。构造函数听起来很复杂,实际上就是一个普通的函数,它在类被实例化的时候会执行一次。同时,构造函数允许你将一些初始值传入类中,从而更灵活的实例化类。 ~~~ class Monster{ public string Name; //名字,声明同时不赋值,默认为""空字符串。 public int Health; //生命值,默认为100 public int Attack; //攻击力数值 public Vector2I Position; //位置坐标 public Vector2I Velocity; //移动速度 //这就是构造函数 public Monster(int health = 100){ this.Health = health; //生命值等于传入的参数,如果不传入就默认为100 this.Attack = this.Health - 5; //攻击力等于生命值-5 } } ~~~ 这就是构造函数的写法,函数名必须和类名完全一致,它相当于一个函数,当你 ~~~ Monster M_A = new Monster(10); ~~~ 的时候,这个函数会被执行,并且接收一个int型参数,这个怪物的生命值就可以在实例化的时候控制。 ~~~ void Main(){ Monster M_A = new Monster(50); //M_A的生命值为50,攻击力为45 Monster M_B = new Monster(); //M_B的生命值为10,攻击力为5 } ~~~ ### 五、类的继承 实际上我考虑了很久才决定写这一小段内容。 举了这么多例子,我想让你明白类是有多方便。类这个东西,就是用来封装属性和方法的。实际上现实中的物体,都可以分解成属性和方法。 当你像上面的例子中,用一个类来封装一个怪物对象时,要求所有的怪物都有这些成员属性和成员方法。即,所有的怪物都必须有名字,可如果有些属性是一部分怪物才有的,怎么办? 下面我们来解决这个问题,下面的内容,简单了解一下即可。 类是可以继承的,来,看看它有多神奇。 当一个类继承了另一个类,它会继承它的所有成员属性,成员函数,甚至是构造函数。 ~~~ //我们来设想一个平面游戏场景 //怪物基类 public class Monster { public string Name; //怪物的名字 public Vector2I Position; 怪物的坐标,Vector2I也是一个类,包含了int X和int Y两个成员变量。 public Vector2I Velocity; 怪物的速度矢量。 public int Health;//怪物的血量 //构造函数 public Monster(){ this.Name = "怪物"; this.Position = new Vector2I(0,0); //初始坐标 0 0 this.Velocity = new Vector2I(0,0); //初始速度0 0 this.Health = 100;//血量默认100 } public void Move(){ this.Position = this.Position + this.Velocity; } public bool isDie(){ return (this.Health <= 0); } } /* 当一个class在声明的时候在类名后面使用 : 并紧跟另一个类的类名,即表示这个类继承了另一个类。 这个类称为子类,它继承的类叫做它的父类。继承是可以无限制继承下去的,所以可能还会有爷爷、太爷爷类 */ //火焰怪,它可以攻击别人 public class FireMonster : Monster { public int AttackPower = 2; //攻击力 2 public FireMonster(){ this.Position = new Vector2I(-10, 0); //火焰怪初始位置在-10,0 this.Velocity = new Vector2I(2, 0); //火焰怪初始速度是2,,0 } //子类可以创造新的成员变量或新的成员函数 //这个函数传入一个Monster类变量,并按照火焰怪的攻击力减少它的生命值后返回出去 public Monster Attack(Monster BeAttackMonster){ BeAttackMonster.Health -= this.AttackPower; return BeAttackMonster; } } //僵尸怪,生命值为-50才判定为死亡 public class ZombieMonster : Monster { public ZombieMonster(){ this.Position = new Vector2I(10, 0); //僵尸怪初始位置是 10,0 } //子类可以重载从父类里继承的函数。重载以后,在调用子类实例化的变量的这个函数时,会运行新的函数里的内容,而不是继承下来的父类的内容。 public override bool isDie(){ return (this.Health <= -50); } } /* 下面我们来写主函数,创建几个怪物并让他们互相攻击 */ int tick; void Main(){ tick += 1; //用这个来记录当前是第几个循环 ZombieMoster Z_M = new ZombieMoster(); FireMonster F_M = new FireMonster(); //让两个怪物自由移动 Z_M.Move(); F_M.Move(); //这个方法可以判断两个Vector2I类型坐标是否重合 if((Z_M.Position - F_M.Position).Length() == 0){ F_M.Velocity = new Vector2I(0, 0); //速度设为0,停止运动。可以遇见,在下一帧里,由于两个怪速度都为0,僵尸怪和火焰怪的位置还是会重合的 Z_M = F_M.Attack(Z_M); //把僵尸怪拿给火焰怪打一下,再重新赋值给僵尸怪。 if(Z_M.isDie()){ Echo("游戏结束,僵尸怪挂了"); } } Echo(tick.ToString()); Echo(Z_M.Health.ToString()); //可以预见在tick=10的时候,火焰怪预见了僵尸怪,并停了下来,然后一直攻击僵尸怪,直到僵尸怪死亡。 } ~~~