2.2 面向对象编程 (OOP) (类, 对象, 继承, 多态, 接口) 2.2 面向对象编程 (OOP) 面向对象编程 (Object-Oriented Programming, OOP) 是一种强大的编程范式,它以对象作为程序的基本单元,强调数据和操作数据的行为封装在一起。OOP 的核心思想在于将复杂的问题分解为一系列相互作用的对象,从而提高代码的可重用性、可维护性和可扩展性。在 Unity3D 游戏开发中,C# 作为主要的脚本语言,充分利用了 OOP 的特性,使得游戏逻辑的构建更加高效和清晰。
面向对象编程 (Object-Oriented Programming, OOP) 是一种强大的编程范式,它以对象作为程序的基本单元,强调数据和操作数据的行为封装在一起。OOP 的核心思想在于将复杂的问题分解为一系列相互作用的对象,从而提高代码的可重用性、可维护性和可扩展性。在 Unity3D 游戏开发中,C# 作为主要的脚本语言,充分利用了 OOP 的特性,使得游戏逻辑的构建更加高效和清晰。
本章节将深入探讨 OOP 的四大核心概念:类 (Class), 对象 (Object), 继承 (Inheritance), 多态 (Polymorphism) 以及 接口 (Interface)。通过理论结合实践的方式,并辅以 Mermaid 图表,帮助你理解这些概念在 Unity/C# 环境下的应用。
类 (Class) 是 OOP 的蓝图或模板,它定义了一类对象的共同属性和行为。可以将类比作建筑设计图,它描述了建筑的结构和功能,但不代表实际存在的建筑本身。
对象 (Object) 是类的实例,是根据类这个蓝图创建出来的具体实体。对象拥有类定义的属性和行为。继续上面的比喻,对象就是根据建筑设计图建造出来的实际房屋。
代码实践 2.2.1:定义类和创建对象
using UnityEngine; // 定义一个名为 "Enemy" 的类 public class Enemy { // 类的属性 (字段) public string enemyName; public int health; public float speed; // 类的行为 (方法) public void Move() { Debug.Log(enemyName + " 正在移动,速度为: " + speed); } public void TakeDamage(int damage) { health -= damage; Debug.Log(enemyName + " 受到 " + damage + " 点伤害,剩余生命值: " + health); if (health <= 0) { Die(); } } private void Die() { Debug.Log(enemyName + " 死亡!"); // 在游戏中可以添加死亡逻辑,例如销毁对象、播放死亡动画等 } } public class ClassObjectExample : MonoBehaviour { void Start() { // 创建 Enemy 类的对象 (实例化) Enemy goblin = new Enemy(); goblin.enemyName = "哥布林"; goblin.health = 50; goblin.speed = 3f; Enemy slime = new Enemy(); slime.enemyName = "史莱姆"; slime.health = 30; slime.speed = 1.5f; // 调用对象的方法 goblin.Move(); slime.Move(); goblin.TakeDamage(20); slime.TakeDamage(10); goblin.TakeDamage(40); // 哥布林会死亡 } }
代码解释 2.2.1:
public class Enemy: 定义了一个公共类 Enemy。public 关键字表示这个类可以被其他脚本访问。
public string enemyName;, public int health;, public float speed;: 在 Enemy 类中定义了三个公共属性(字段):enemyName (敌人名称), health (生命值), speed (速度)。这些属性描述了 Enemy 对象的特征。
public void Move(), public void TakeDamage(int damage), private void Die(): 定义了三个方法(函数),描述了 Enemy 对象的行为。
Move(): 使敌人移动。
TakeDamage(int damage): 敌人受到伤害。
Die(): 敌人死亡(private 关键字表示该方法只能在 Enemy 类内部访问)。
new Enemy(): 在 ClassObjectExample 脚本的 Start() 方法中,使用 new Enemy() 创建了 Enemy 类的两个对象 goblin 和 slime。new 关键字用于实例化类,即创建类的对象。
goblin.enemyName = ..., goblin.health = ..., goblin.speed = ...: 通过点运算符 (.) 访问对象的属性,并为它们赋值。
goblin.Move(), slime.TakeDamage(10): 通过点运算符 (.) 调用对象的方法。
Mermaid 图表 2.2.1:类与对象的关系
图表解释 2.2.1:
Enemy Class 代表 Enemy 类,是蓝图。
goblin : Enemy Object, slime : Enemy Object, ... : Enemy Object 代表根据 Enemy 类创建的多个对象实例。
箭头表示 "实例化" 关系,即对象是由类创建出来的。
继承 (Inheritance) 是 OOP 的一个重要特性,它允许我们创建一个新的类 (子类/派生类),继承已有的类 (父类/基类) 的属性和行为。继承实现了代码的重用,并建立了类之间的层次关系。
"Is-a" 关系: 继承体现了 "Is-a" 关系。例如,"MeleeEnemy 是一种 Enemy", "RangedEnemy 也是一种 Enemy"。子类继承父类,意味着子类拥有父类的所有非私有特性,并且可以扩展或修改这些特性。
代码实践 2.2.2:继承的实现
using UnityEngine; // 父类: Enemy (基类) public class Enemy { public string enemyName; public int health; public float speed; public virtual void Move() // 使用 virtual 关键字允许子类重写 { Debug.Log(enemyName + " 以基础速度 " + speed + " 移动"); } public virtual void Attack() // 使用 virtual 关键字允许子类重写 { Debug.Log(enemyName + " 进行基础攻击"); } public void TakeDamage(int damage) { health -= damage; Debug.Log(enemyName + " 受到 " + damage + " 点伤害,剩余生命值: " + health); if (health <= 0) { Die(); } } protected virtual void Die() // 使用 protected virtual 关键字,允许子类访问和重写 { Debug.Log(enemyName + " 基础死亡"); } } // 子类: MeleeEnemy (派生类),继承自 Enemy public class MeleeEnemy : Enemy { public float attackRange; public override void Move() // 使用 override 关键字重写父类方法 { Debug.Log(enemyName + " 作为近战敌人,快速冲向目标"); speed = speed * 1.5f; // 近战敌人移动速度更快 base.Move(); // 调用父类 Move() 方法的基础移动逻辑 } public override void Attack() // 使用 override 关键字重写父类方法 { Debug.Log(enemyName + " 使用近战武器攻击,攻击范围: " + attackRange); } protected override void Die() // 使用 override 关键字重写父类方法 { Debug.Log(enemyName + " 近战死亡,掉落更多金币"); base.Die(); // 调用父类 Die() 方法的基础死亡逻辑 // 添加近战敌人特有的死亡逻辑,例如掉落更多金币 } } // 子类: RangedEnemy (派生类),继承自 Enemy public class RangedEnemy : Enemy { public float attackDistance; public override void Attack() // 使用 override 关键字重写父类方法 { Debug.Log(enemyName + " 进行远程攻击,攻击距离: " + attackDistance); } protected override void Die() // 使用 override 关键字重写父类方法 { Debug.Log(enemyName + " 远程死亡,爆炸效果"); base.Die(); // 调用父类 Die() 方法的基础死亡逻辑 // 添加远程敌人特有的死亡逻辑,例如爆炸效果 } } public class InheritanceExample : MonoBehaviour { void Start() { MeleeEnemy warrior = new MeleeEnemy(); warrior.enemyName = "战士"; warrior.health = 80; warrior.speed = 2f; warrior.attackRange = 1.5f; RangedEnemy mage = new RangedEnemy(); mage.enemyName = "法师"; mage.health = 60; mage.speed = 1f; mage.attackDistance = 10f; warrior.Move(); warrior.Attack(); mage.Move(); mage.Attack(); warrior.TakeDamage(30); mage.TakeDamage(20); warrior.TakeDamage(60); // 战士会死亡 mage.TakeDamage(50); // 法师会死亡 } }
代码解释 2.2.2:
public class MeleeEnemy : Enemy, public class RangedEnemy : Enemy: MeleeEnemy 和 RangedEnemy 类都使用冒号 (:) 语法继承自 Enemy 类。这意味着它们继承了 Enemy 类的所有公共和受保护的属性和方法。
virtual 关键字: 在 Enemy 类中,Move(), Attack(), Die() 方法使用了 virtual 关键字。virtual 关键字允许子类重写 (override) 这些方法,提供子类特有的实现。
override 关键字: 在 MeleeEnemy 和 RangedEnemy 类中,使用 override 关键字重写了父类 Enemy 的 Move(), Attack(), Die() 方法。override 关键字明确表示子类要修改父类方法的行为。
base.Move(), base.Die(): 在子类重写的方法中,可以使用 base 关键字调用父类被重写的方法的实现。这允许子类在扩展父类行为的同时,保留父类的基础逻辑。
protected 关键字: Die() 方法在父类中被声明为 protected virtual。protected 关键字表示该成员在父类及其子类中可以访问,但不能在类外部直接访问。这使得子类可以访问和重写 Die() 方法,但外部代码不能直接调用它。
Mermaid 图表 2.2.2:继承关系
图表解释 2.2.2:
EnemyClass 代表父类 Enemy。
MeleeEnemyClass 和 RangedEnemyClass 代表子类 MeleeEnemy 和 RangedEnemy。
is a 箭头表示继承关系,例如 "MeleeEnemy is a Enemy"。
多态 (Polymorphism) 字面意思是 "多种形态"。在 OOP 中,多态指允许使用父类类型的引用来调用子类对象的方法,从而实现不同的行为。多态性提高了代码的灵活性和可扩展性。
多态主要通过两种方式实现:
方法重写 (Override): 子类重写父类的方法,运行时根据对象的实际类型调用相应的方法实现 (运行时多态)。我们在继承的例子中已经看到了方法重写。
接口实现 (Interface Implementation): 多个类实现同一个接口,可以通过接口类型的引用调用不同类的方法实现 (接口多态)。我们将在接口部分详细介绍。
代码实践 2.2.3:多态的应用
using UnityEngine; using System.Collections.Generic; // 引入 List public class PolymorphismExample : MonoBehaviour { void Start() { // 创建不同类型的 Enemy 对象 MeleeEnemy warrior = new MeleeEnemy(); warrior.enemyName = "战士"; RangedEnemy mage = new RangedEnemy(); mage.enemyName = "法师"; Enemy baseEnemy = new Enemy(); // 父类对象 baseEnemy.enemyName = "基础敌人"; // 创建 Enemy 类型的列表,存储不同类型的 Enemy 对象 List<Enemy> enemyList = new List<Enemy>(); enemyList.Add(warrior); enemyList.Add(mage); enemyList.Add(baseEnemy); // 遍历列表,调用每个 Enemy 对象的 Move() 和 Attack() 方法 foreach (Enemy enemy in enemyList) { Debug.Log("--- " + enemy.enemyName + " ---"); enemy.Move(); // 多态调用: 根据对象的实际类型执行不同的 Move() 方法 enemy.Attack(); // 多态调用: 根据对象的实际类型执行不同的 Attack() 方法 enemy.TakeDamage(10); } } }
代码解释 2.2.3:
List<Enemy> enemyList = new List<Enemy>();: 创建了一个 Enemy 类型的列表 enemyList。由于多态性,我们可以将 MeleeEnemy, RangedEnemy, Enemy 对象都添加到这个列表中,因为 MeleeEnemy 和 RangedEnemy "is-a" Enemy。
foreach (Enemy enemy in enemyList): 遍历 enemyList 列表。在循环中,enemy 变量的类型是 Enemy (父类类型)。
enemy.Move();, enemy.Attack();: 通过父类类型的引用 enemy 调用 Move() 和 Attack() 方法。关键在于,由于多态性,程序在运行时会根据 enemy 实际指向的对象类型 (是 MeleeEnemy, RangedEnemy 还是 Enemy 对象) 来调用相应子类或父类的方法实现。 这就是运行时多态性的体现。
运行结果分析:
程序运行时,你会看到 warrior, mage, baseEnemy 分别执行了各自类型的 Move() 和 Attack() 方法,即使我们是通过父类 Enemy 类型的引用来调用的。这就是多态性的强大之处,它允许我们以统一的方式处理不同类型的对象,并让对象自身决定其行为。
Mermaid 图表 2.2.3:多态性
图表解释 2.2.3:
父类引用 (Enemy Reference) 代表 Enemy 类型的引用变量。
子类对象 (MeleeEnemy Object, RangedEnemy Object, Enemy Object) 代表不同类型的对象实例。
箭头表示父类引用可以指向不同子类对象。
当通过父类引用调用 Move() 或 Attack() 方法时,会根据实际指向的对象类型,执行不同的方法实现 (例如 MeleeEnemy.Move(), RangedEnemy.Attack(), Enemy.Move())。
接口 (Interface) 是一种定义契约的方式。它定义了一组方法、属性、事件或索引器,但不提供实现。实现接口的类必须提供接口中定义的所有成员的实现。
"Can-do" 关系: 接口体现了 "Can-do" 关系。例如,"Enemy 可以是可伤害的 (IDamageable)", "Player 也可以是可伤害的"。接口描述了类可以做什么,而不是它是什么。
接口的特点:
接口本身不包含任何实现代码。
一个类可以实现多个接口。
接口可以实现多态性 (接口多态)。
代码实践 2.2.4:接口的定义和实现
using UnityEngine; // 定义一个接口 IDamageable public interface IDamageable { // 接口成员 (方法) - 没有实现 void TakeDamage(int damageAmount); } // Enemy 类实现 IDamageable 接口 public class Enemy : MonoBehaviour, IDamageable // 使用冒号和接口名表示实现接口 { public int health = 100; // 实现接口方法 public void TakeDamage(int damageAmount) { health -= damageAmount; Debug.Log(gameObject.name + " 受到 " + damageAmount + " 点伤害,剩余生命值: " + health); if (health <= 0) { Destroy(gameObject); // 销毁游戏对象 Debug.Log(gameObject.name + " 死亡!"); } } } // Player 类也实现 IDamageable 接口 public class Player : MonoBehaviour, IDamageable { public int health = 150; // 实现接口方法 public void TakeDamage(int damageAmount) { health -= damageAmount; Debug.Log("玩家 " + gameObject.name + " 受到 " + damageAmount + " 点伤害,剩余生命值: " + health); if (health <= 0) { Debug.Log("玩家 " + gameObject.name + " 阵亡!"); // 可以添加玩家死亡逻辑,例如游戏结束、重生等 } } } public class InterfaceExample : MonoBehaviour { void Start() { // 获取实现了 IDamageable 接口的对象 IDamageable enemyDamageable = GetComponent<Enemy>(); // 获取 Enemy 组件,Enemy 实现了 IDamageable IDamageable playerDamageable = GetComponent<Player>(); // 获取 Player 组件,Player 实现了 IDamageable if (enemyDamageable != null) { enemyDamageable.TakeDamage(25); // 通过接口引用调用 TakeDamage 方法 } if (playerDamageable != null) { playerDamageable.TakeDamage(15); // 通过接口引用调用 TakeDamage 方法 } } }
代码解释 2.2.4:
public interface IDamageable: 定义了一个公共接口 IDamageable。接口使用 interface 关键字声明。接口名通常以 I 开头,以区分于类。
void TakeDamage(int damageAmount);: 在 IDamageable 接口中定义了一个方法 TakeDamage(int damageAmount),但没有提供实现。接口方法默认是公共的 (public)。
public class Enemy : MonoBehaviour, IDamageable, public class Player : MonoBehaviour, IDamageable: Enemy 和 Player 类都使用冒号 (:) 语法,并在 MonoBehaviour 后添加 , IDamageable,表示它们实现了 IDamageable 接口。
接口方法的实现: Enemy 和 Player 类都必须提供 IDamageable 接口中定义的 TakeDamage(int damageAmount) 方法的实现。它们的实现可以根据自身的需求而不同。
IDamageable enemyDamageable = GetComponent<Enemy>();: 在 InterfaceExample 脚本中,使用 IDamageable 接口类型的引用 enemyDamageable 和 playerDamageable 来引用 Enemy 和 Player 组件。由于 Enemy 和 Player 都实现了 IDamageable 接口,因此可以将它们赋值给 IDamageable 类型的变量。
enemyDamageable.TakeDamage(25);, playerDamageable.TakeDamage(15);: 通过接口引用 enemyDamageable 和 playerDamageable 调用 TakeDamage() 方法。程序会根据接口引用实际指向的对象类型 (是 Enemy 还是 Player),调用相应类中 TakeDamage() 方法的实现。 这就是接口多态性的体现。
Mermaid 图表 2.2.4:接口实现
图表解释 2.2.4:
IDamageable Interface 代表 IDamageable 接口。
Enemy Class 和 Player Class 代表实现了 IDamageable 接口的类。
implements 箭头表示类实现了接口。
IDamageable Reference 代表 IDamageable 接口类型的引用变量。
接口引用可以指向实现了该接口的不同类的对象。
当通过接口引用调用 TakeDamage() 方法时,会根据实际指向的对象类型,执行不同类中 TakeDamage() 方法的实现 (例如 Enemy.TakeDamage(), Player.TakeDamage())。
在 Unity 游戏开发中使用 OOP 具有以下显著优势:
模块化 (Modularity): OOP 将代码组织成独立的类和对象,每个对象负责特定的功能。这使得代码结构清晰,易于理解和维护。
代码重用 (Code Reusability): 通过继承,可以重用父类的代码,减少重复编写,提高开发效率。接口也促进了代码的重用,因为不同的类可以实现相同的接口,共享相同的行为规范。
可维护性 (Maintainability): 由于代码模块化和高内聚低耦合的特性,当需要修改或扩展功能时,只需要修改相关的类,而不会影响到其他部分的代码,提高了代码的可维护性。
可扩展性 (Extensibility): OOP 易于扩展新的功能。可以通过创建新的类、继承现有类或实现新的接口来添加新的功能,而无需修改原有代码,提高了系统的可扩展性。
团队协作 (Team Collaboration): OOP 促进了团队协作开发。不同的开发者可以负责不同的类或模块的开发,降低了代码冲突的风险,提高了开发效率。