Unity C#面向对象编程:继承与多态在游戏开发中的实战应用
2026/7/25 9:54:40 网站建设 项目流程

1. 项目概述:为什么Unity开发者必须掌握继承与多态?

如果你刚开始用Unity做游戏,或者从其他语言转过来学C#,可能会觉得“继承”和“多态”这两个词听起来很学术,离实际的游戏开发有点远。我以前带新人时也常听到这样的疑问:“我写个脚本,让角色移动、跳跃不就行了吗?为什么要学这些?” 但我要告诉你,一旦你开始构建稍微复杂一点的系统,比如一个有十几种不同敌人的关卡、一个包含多种交互道具的背包,或者一套可扩展的技能树,你就会发现,不理解继承和多态,你的代码会迅速变成一团难以维护的“意大利面条”。

简单来说,继承让你能建立清晰的“家族关系”。比如,你可以创建一个Enemy(敌人)基类,定义所有敌人都有的生命值、移动速度等属性和TakeDamage(受到伤害)方法。然后,Goblin(哥布林)和Dragon(龙)这两个类去继承Enemy。哥布林有自己偷窃的行为,龙有喷火的能力,但它们都“是”一种敌人,共享基础的敌人逻辑。这避免了你在哥布林和龙的脚本里重复写生命值管理的代码。

多态则赋予了代码“灵活应变”的能力。它允许你通过一个统一的“接口”(比如基类引用)来操作不同的子类对象,而程序在运行时能自动识别出具体是哪个子类,并调用其正确的方法。想象一下,你有一个Enemy类型的数组,里面既装了哥布林对象,也装了龙对象。当你遍历数组,对每个元素调用Attack()方法时,哥布林会上去砍一刀,而龙则会喷出火焰。你不需要写if (enemy is Goblin) ... else if (enemy is Dragon) ...这样冗长的判断,系统会自动处理。这就是多态的魅力,它让增加新的敌人类型变得异常简单,只需新建一个类并继承Enemy,旧的攻击循环代码完全不用修改。

这个“Unity C#入门:继承与多态的简单应用”项目,就是带你绕过枯燥的理论,直接动手在Unity编辑器里,构建一个能直观看到效果的迷你游戏原型。我们将创建一个简单的战斗模拟,你会亲手写出基类、子类,体验多态如何让代码简洁而强大。这不仅是面试常考的基础,更是你日后设计复杂游戏系统的基石。无论你是完全的编程新手,还是有一定基础想巩固面向对象思想的开发者,跟着做一遍,你会有种“原来如此”的顿悟感。

2. 核心概念拆解:从“是什么”到“为什么用”

在直接敲代码之前,花几分钟把核心概念理清楚,能让你后续的实操事半功倍。很多初学者卡住,不是因为语法不会,而是没理解这些概念要解决的实际问题。

2.1 继承:构建清晰的代码家族树

继承的核心思想是“派生”与“扩展”。在C#中,使用冒号:来表示继承关系。

public class Enemy // 基类(父类) { public string name; public int health; public void TakeDamage(int damage) { health -= damage; Debug.Log(name + "受到了" + damage + "点伤害,剩余生命:" + health); } } public class Goblin : Enemy // Goblin继承自Enemy { public int stealPower; // 哥布林特有的属性:偷窃能力 public void Steal() { Debug.Log(name + "发动偷窃!"); } }

在这里,Goblin类自动获得了Enemy类的namehealth字段和TakeDamage方法。这就是代码复用,是继承最直接的好处。你可以创建几十种敌人,而它们共同的基础逻辑只需要在Enemy类里维护一份。

注意:继承代表一种“是一个(is-a)”的关系。Goblin是一个Enemy,这逻辑上是成立的。不要为了复用代码而滥用继承,比如让Car(汽车)继承Engine(发动机),这就不符合“是一个”的关系(汽车不是发动机),这种情况下使用组合(将发动机作为汽车的一个部件)更合适。

2.2 多态:同一接口,不同实现

多态建立在继承之上。它通常通过“重写”基类的虚方法或抽象方法来实现。

public class Enemy { // virtual关键字表示该方法可以被子类重写 public virtual void Attack() { Debug.Log("敌人发动了普通攻击!"); } } public class Goblin : Enemy { // override关键字表示重写父类的Attack方法 public override void Attack() { Debug.Log("哥布林挥舞短刀砍击!"); } } public class Dragon : Enemy { public override void Attack() { Debug.Log("巨龙喷吐出灼热的火焰!"); } }

多态的魔力在以下场景中展现:

List<Enemy> enemies = new List<Enemy>(); enemies.Add(new Goblin() { name = "卑鄙哥布林" }); enemies.Add(new Dragon() { name = "炎狱巨龙" }); foreach (Enemy enemy in enemies) { // 这里enemy的编译时类型是Enemy,但运行时类型分别是Goblin和Dragon enemy.Attack(); // 输出:“哥布林挥舞短刀砍击!” 和 “巨龙喷吐出灼热的火焰!” }

编译器在编译时只知道enemyEnemy类型,但它允许调用Attack方法。到了运行时,CLR(公共语言运行时)会查找对象的实际类型(GoblinDragon),并调用该类型下重写的Attack方法。这就是运行时多态动态绑定

为什么这如此重要?假设你没有用多态,你的代码可能长这样:

foreach (var unit in enemies) { if (unit is Goblin) ((Goblin)unit).GoblinAttack(); else if (unit is Dragon) ((Dragon)unit).DragonBreath(); // ...每增加一种新敌人,这里就要加一个else if }

这种代码极其脆弱,难以维护。而使用多态,增加一个新的Archer(弓箭手)敌人,你只需要新建Archer : Enemy类并重写Attack,上面的foreach循环一行都不用改!这极大地提高了系统的可扩展性可维护性

2.3 关键关键字:virtual, override, base 与 abstract

理解这些关键字是正确运用多态的关键:

  1. virtual(虚方法):在基类中声明,表示该方法可以被派生类重写。它本身可以有实现(方法体)。
  2. override(重写):在派生类中声明,用于提供基类虚方法或抽象方法的新实现。签名必须与基类方法完全一致。
  3. base:在派生类中,用于调用基类中被重写的方法或构造函数。例如,在GoblinAttack方法中,你可以先调用base.Attack()执行敌人的基础攻击逻辑,再追加哥布林的特殊效果。
  4. abstract(抽象方法与抽象类):这是更严格的约定。
    • abstract class:抽象类不能实例化(你不能new AbstractEnemy()),它存在的意义就是被继承。
    • abstract method:抽象方法只有声明,没有实现(没有方法体)。任何继承自抽象类的非抽象子类,都必须用override实现所有抽象方法。这强制了子类必须提供某些关键行为。
public abstract class Enemy // 抽象类 { public int health; public abstract void Attack(); // 抽象方法,无实现 public void TakeDamage(int damage) // 普通方法,有实现 { health -= damage; } } public class Goblin : Enemy { public override void Attack() // 必须实现Attack { Debug.Log("哥布林攻击!"); } } // 如果Goblin类不实现Attack,编译会报错。

使用abstract相当于和所有子类签订了一份“契约”,确保它们都具备某种能力。在游戏开发中,对于核心行为(如攻击、死亡),使用抽象类来定义接口是非常好的实践。

3. 项目实战:构建一个简易战斗模拟系统

理论说得再多,不如动手做一遍。我们将在Unity中创建一个简单的场景,通过代码直观地感受继承与多态如何工作。

3.1 环境准备与项目设置

  1. 打开Unity Hub,创建一个新的3D核心项目,命名为“InheritancePolymorphismDemo”。
  2. 在场景中,简单创建几个3D物体作为视觉参考即可,比如几个Cube或Sphere,分别命名为Player、EnemySpawnPoint等。因为我们主要关注脚本逻辑,所以美术资源从简。你也可以从Asset Store导入一些免费的低多边形模型让场景更好看。
  3. 在Project窗口的Assets文件夹下,创建一个名为“Scripts”的文件夹来存放我们即将编写的所有C#脚本。

3.2 设计基类:抽象敌人(Enemy)

我们的系统将从设计一个稳固的基类开始。考虑到敌人必须能攻击和受伤,但具体攻击方式未知,我们将Enemy设计为抽象类。

在Scripts文件夹下,右键创建新的C#脚本,命名为Enemy

using UnityEngine; // 声明为抽象类,无法直接创建Enemy实例 public abstract class Enemy : MonoBehaviour { // 公共字段,可在Inspector中编辑 public string enemyName = "默认敌人"; public int maxHealth = 100; public int attackPower = 10; // 当前生命值,对外只读,内部可修改 [SerializeField] // 序列化字段,在Inspector可见可调,但其他脚本不能直接修改 private int currentHealth; // 属性:用于安全地访问和设置当前生命值,并触发死亡事件 public int CurrentHealth { get { return currentHealth; } set { currentHealth = Mathf.Clamp(value, 0, maxHealth); // 确保生命值在0到最大值之间 Debug.Log($"{enemyName} 生命值变为:{currentHealth}"); if (currentHealth <= 0) { Die(); } } } // 初始化 protected virtual void Start() { CurrentHealth = maxHealth; // 使用属性赋值,触发Clamp和日志 Debug.Log($"{enemyName} 已生成,最大生命:{maxHealth},攻击力:{attackPower}"); } // 抽象方法:攻击行为。每个具体敌人都必须实现自己的攻击逻辑。 public abstract void Attack(); // 虚方法:受到伤害。子类可以重写此方法来增加特殊效果(如格挡、反弹)。 public virtual void TakeDamage(int damage) { Debug.Log($"{enemyName} 受到了 {damage} 点伤害!"); CurrentHealth -= damage; // 通过属性减少生命值 } // 受保护的虚方法:死亡逻辑。子类可以重写以播放独特的死亡动画或特效。 protected virtual void Die() { Debug.Log($"{enemyName} 被击败了!"); // 通常这里会播放死亡动画、产生掉落物、销毁对象等。 Destroy(gameObject, 0.5f); // 0.5秒后销毁游戏对象 } }

代码解析与设计思路:

  • 抽象类abstract classEnemy被声明为抽象类,因为它代表一个不完整的“敌人”概念。你永远不会创建一个纯粹的“敌人”,你创建的是哥布林、龙等具体敌人。这从设计上防止了误用。
  • 抽象方法abstract void Attack():这是本设计的核心“契约”。它强制所有子类必须定义“攻击”这个行为,但具体怎么攻击,由子类决定。
  • 虚方法virtual void TakeDamage()virtual void Die():它们提供了默认实现,但子类可以根据需要重写。例如,一个Boss敌人可能在死亡时触发关卡胜利事件。
  • 属性CurrentHealth:使用属性而非公共字段来封装currentHealth。这允许我们在设置生命值时加入逻辑(如数值钳制、触发死亡),这是面向对象中“封装”思想的体现。[SerializeField]让私有字段在Unity编辑器中可见,便于调试。
  • protected virtual void Start():标记为protectedvirtual,允许子类在需要时扩展初始化逻辑(通过override),同时也能调用基类的Start(通过base.Start())。

3.3 实现具体子类:哥布林(Goblin)与龙(Dragon)

现在,我们来创建两种具体的敌人,它们将继承Enemy类并实现其抽象方法。

创建Goblin脚本:

using UnityEngine; public class Goblin : Enemy { // 哥布林特有的属性:偷窃成功率 [Range(0f, 1f)] public float stealChance = 0.3f; public int stealAmount = 5; // 重写(实现)抽象攻击方法 public override void Attack() { // 哥布林的攻击:普通攻击并概率偷窃 Debug.Log($"{enemyName} (哥布林)挥舞生锈的短刀,造成 {attackPower} 点伤害!"); // 这里可以调用一个假设的玩家受伤方法:Player.Instance.TakeDamage(attackPower); // 尝试偷窃 TrySteal(); } // 哥布林特有的方法 private void TrySteal() { if (Random.value < stealChance) // Random.value 返回0-1之间的随机数 { Debug.Log($"<color=yellow>{enemyName} 在攻击中顺手牵羊,偷走了 {stealAmount} 枚金币!</color>"); // 这里可以修改游戏的金币数值:GameManager.Instance.Gold -= stealAmount; } } // 可选:重写受伤方法,增加哥布林敏捷闪避的设定 public override void TakeDamage(int damage) { float dodgeChance = 0.1f; // 10%闪避几率 if (Random.value < dodgeChance) { Debug.Log($"<color=cyan>{enemyName} 敏捷地躲开了攻击!</color>"); return; // 闪避成功,不受伤害 } base.TakeDamage(damage); // 闪避失败,调用基类的受伤逻辑 } }

创建Dragon脚本:

using UnityEngine; public class Dragon : Enemy { // 龙特有的属性:火焰伤害和范围 public int flameDamage = 25; public float flameRange = 5f; private bool isBreathingFire = false; // 重写(实现)抽象攻击方法 public override void Attack() { // 龙有两种攻击模式:普通爪击或火焰喷射 if (CurrentHealth < maxHealth * 0.5f && !isBreathingFire) // 生命低于50%且未在喷火 { BreatheFire(); } else { ClawAttack(); } } private void ClawAttack() { Debug.Log($"<color=red>{enemyName} (巨龙)用利爪猛击,造成 {attackPower} 点伤害!</color>"); } private void BreatheFire() { isBreathingFire = true; Debug.Log($"<color=orange>{enemyName} 愤怒地喷出火焰,对范围内所有目标造成 {flameDamage} 点伤害!</color>"); // 这里可以实现范围伤害逻辑,例如通过Physics.OverlapSphere查找范围内的玩家或物体 Invoke(nameof(ResetBreath), 3f); // 3秒后重置喷火状态 } private void ResetBreath() { isBreathingFire = false; Debug.Log($"{enemyName} 的火焰吐息冷却完毕。"); } // 重写死亡方法,增加巨龙死亡的震撼效果 protected override void Die() { Debug.Log($"<color=red>轰隆!!!{enemyName} 庞大的身躯轰然倒地,大地为之震颤!</color>"); // 可以在这里触发屏幕震动、播放爆炸特效等 base.Die(); // 调用基类的销毁逻辑 } }

实操要点:

  1. override关键字:在子类中重写基类的抽象或虚方法时,必须使用override。这是编译器检查多态正确性的关键。
  2. base关键字的使用:在Goblin.TakeDamage中,我们首先尝试闪避,如果失败,则通过base.TakeDamage(damage)调用基类(Enemy)的受伤逻辑。这避免了重复编写扣血和死亡检查的代码。
  3. 子类特有逻辑GoblinTryStealDragonBreatheFireClawAttack都是子类独有的行为,它们丰富了敌人的差异性。
  4. 状态管理Dragon中使用isBreathingFire布尔值来管理喷火状态,这是一个简单的状态标志,防止攻击逻辑混乱。

3.4 创建测试控制器与场景组装

为了在游戏中看到效果,我们需要一个简单的脚本来生成敌人并模拟战斗。

创建GameManager脚本:

using UnityEngine; using System.Collections.Generic; public class GameManager : MonoBehaviour { // 单例模式,便于其他脚本访问 public static GameManager Instance; // 敌人生成点 public Transform[] spawnPoints; // 敌人预制体列表(在Inspector中拖拽赋值) public GameObject[] enemyPrefabs; // 生成的敌人列表 private List<Enemy> activeEnemies = new List<Enemy>(); void Awake() { if (Instance == null) Instance = this; else Destroy(gameObject); } void Start() { SpawnEnemies(); StartCoroutine(SimulateBattle()); // 开始模拟战斗 } void SpawnEnemies() { if (spawnPoints.Length == 0 || enemyPrefabs.Length == 0) { Debug.LogError("请设置生成点或敌人预制体!"); return; } for (int i = 0; i < spawnPoints.Length; i++) { // 随机选择一个敌人预制体 GameObject prefabToSpawn = enemyPrefabs[Random.Range(0, enemyPrefabs.Length)]; GameObject enemyObj = Instantiate(prefabToSpawn, spawnPoints[i].position, Quaternion.identity); // 获取Enemy组件并添加到列表 Enemy enemy = enemyObj.GetComponent<Enemy>(); if (enemy != null) { activeEnemies.Add(enemy); // 可以在这里设置敌人独特名字 enemy.enemyName = $"{enemy.GetType().Name}_{i}"; } } Debug.Log($"已生成 {activeEnemies.Count} 个敌人。"); } System.Collections.IEnumerator SimulateBattle() { Debug.Log("=== 战斗模拟开始 ==="); yield return new WaitForSeconds(1f); // 等待1秒 while (activeEnemies.Count > 0) { // 1. 所有敌人行动(攻击) foreach (Enemy enemy in activeEnemies.ToArray()) // 使用.ToArray()避免在遍历时修改集合 { if (enemy != null) { enemy.Attack(); // 多态的核心体现!这里调用的是具体子类(Goblin或Dragon)的Attack方法。 yield return new WaitForSeconds(0.5f); // 每个攻击间隔0.5秒 } } // 2. 模拟玩家反击:对每个敌人造成随机伤害 Debug.Log("--- 玩家回合 ---"); foreach (Enemy enemy in activeEnemies.ToArray()) { if (enemy != null) { int playerDamage = Random.Range(15, 35); enemy.TakeDamage(playerDamage); // 多态!调用的是可能被重写的TakeDamage方法(如Goblin的闪避)。 yield return new WaitForSeconds(0.3f); } } // 3. 清理已死亡的敌人 activeEnemies.RemoveAll(enemy => enemy == null || enemy.CurrentHealth <= 0); yield return new WaitForSeconds(1f); // 回合间隔 Debug.Log($"剩余敌人:{activeEnemies.Count}"); } Debug.Log("=== 战斗结束,所有敌人被击败! ==="); } }

场景组装步骤:

  1. 在场景中创建一个空物体,命名为GameManager,将GameManager脚本挂载上去。
  2. 创建几个空物体作为SpawnPoints,摆放在场景中不同位置,拖拽给GameManager脚本的Spawn Points数组。
  3. 在Project窗口中,分别用GoblinDragon脚本创建两个预制体(Prefab)。可以给它们挂上不同的3D模型或颜色材质以便区分。
  4. 将这两个预制体拖拽到GameManager脚本的Enemy Prefabs数组中。
  5. 运行游戏。你将在Console控制台中看到完整的战斗日志,清晰地展示出不同敌人执行其独特的攻击和行为(哥布林偷窃、龙喷火),并且它们对受伤的反应也不同(哥布林可能闪避)。

4. 深入解析:多态在Unity游戏架构中的应用

通过上面的小项目,你已经看到了多态在具体类型行为差异上的威力。但在实际的大型Unity项目中,继承与多态的应用更加深入和广泛,它们构成了许多核心设计模式的基础。

4.1 状态模式:用多态管理复杂状态机

游戏中的角色(玩家或AI)经常有多个状态,如闲置、移动、攻击、受伤、死亡等。使用一堆bool标志和冗长的if-elseswitch语句来管理会非常混乱。

// 不使用多态的混乱写法(伪代码) void Update() { if (isDead) { /* 死亡处理 */ return; } if (isAttacking) { /* 攻击处理 */ return; } if (isTakingDamage) { /* 受伤处理 */ return; } // ... 更多的if }

使用多态的状态模式则清晰得多:

public abstract class PlayerState { protected PlayerController player; public PlayerState(PlayerController player) { this.player = player; } public abstract void EnterState(); public abstract void UpdateState(); public abstract void ExitState(); } public class IdleState : PlayerState { public override void EnterState() { player.animator.Play("Idle"); } public override void UpdateState() { if (Input.GetKeyDown(KeyCode.Space)) player.ChangeState(new JumpState(player)); if (Input.GetMouseButtonDown(0)) player.ChangeState(new AttackState(player)); } public override void ExitState() { } } public class AttackState : PlayerState { /* ... */ } public class JumpState : PlayerState { /* ... */ } // 在PlayerController中 public class PlayerController : MonoBehaviour { private PlayerState currentState; public void ChangeState(PlayerState newState) { currentState?.ExitState(); currentState = newState; currentState.EnterState(); } void Update() { currentState?.UpdateState(); } }

这样,每个状态都是一个独立的类,通过多态,PlayerController只需要调用currentState.UpdateState(),具体执行哪个状态的逻辑由运行时对象决定。增加新状态只需新建一个类,符合开闭原则。

4.2 策略模式:灵活配置AI或技能行为

假设你的游戏有不同类型的敌人AI:追击型、巡逻型、远程狙击型。你可以定义一个IAIStrategy接口或AIStrategy基类。

public abstract class AIStrategy { public abstract void Execute(Enemy enemy); } public class ChaseStrategy : AIStrategy { public override void Execute(Enemy enemy) { // 寻找玩家并移动向玩家 Vector3 direction = (Player.Instance.transform.position - enemy.transform.position).normalized; enemy.transform.Translate(direction * enemy.moveSpeed * Time.deltaTime); } } public class PatrolStrategy : AIStrategy { /* 沿路径点巡逻 */ } public class SniperStrategy : AIStrategy { /* 保持距离,远程攻击 */ } // 在Enemy类中 public class Enemy : MonoBehaviour { public AIStrategy currentStrategy; void Update() { currentStrategy?.Execute(this); // 多态调用 } public void SetStrategy(AIStrategy newStrategy) { currentStrategy = newStrategy; } }

你可以在运行时动态改变敌人的AI策略(例如,受伤后从“巡逻”切换到“追击”),而Enemy类的Update代码无需任何修改。这极大地提升了AI行为的可配置性和可扩展性。

4.3 工厂模式:用多态创建对象

当你需要根据条件创建不同类型的对象时,工厂模式配合多态可以消除复杂的创建逻辑。

public abstract class EnemyFactory { public abstract Enemy CreateEnemy(); } public class GoblinFactory : EnemyFactory { public override Enemy CreateEnemy() { GameObject go = new GameObject("Goblin"); var goblin = go.AddComponent<Goblin>(); goblin.enemyName = "工厂生产的哥布林"; // ... 其他初始化 return goblin; } } public class DragonFactory : EnemyFactory { /* 类似 */ } // 使用工厂 public class Spawner : MonoBehaviour { public EnemyFactory enemyFactory; // 在Inspector中分配具体的工厂(如GoblinFactory) void Spawn() { Enemy enemy = enemyFactory.CreateEnemy(); // 多态:不知道具体创建哪种,但知道创建的是Enemy // 使用enemy... } }

如果要增加新的敌人类型,你只需要新建一个对应的工厂类,而负责生成敌人的Spawner代码完全不用动。

5. 常见陷阱、性能考量与最佳实践

即使理解了概念,在实际编码中也会踩坑。下面是一些我总结的常见问题和经验。

5.1 继承的误用与“组合优于继承”原则

问题场景:你想让FlyingEnemy(飞行敌人)和GroundEnemy(地面敌人)都具备FireAttack(火焰攻击)的能力。新手可能会让FlyingEnemyGroundEnemy都继承自一个FireEnemy类。

// 不推荐的设计:使用继承来共享“能力” public class FireEnemy : Enemy { public void FireAttack() { /* 火焰攻击逻辑 */ } } public class FlyingEnemy : FireEnemy { /* 现在FlyingEnemy有了FireAttack,但它可能根本不需要! */ } public class GroundEnemy : FireEnemy { /* 同上 */ }

这导致了僵化的类层次。如果后来需要一种会IceAttack(冰霜攻击)的FlyingEnemy怎么办?难道要再创建一个IceFlyingEnemy类?这会引发“类爆炸”。

解决方案:使用组合将“攻击能力”抽象为组件。

public interface IAttackBehavior { void Attack(); } public class FireAttack : IAttackBehavior { public void Attack() { Debug.Log("火焰攻击!"); } } public class IceAttack : IAttackBehavior { public void Attack() { Debug.Log("冰霜攻击!"); } } public class Enemy : MonoBehaviour { public IAttackBehavior attackBehavior; // 持有攻击行为组件 void Update() { if (Input.GetKeyDown(KeyCode.Space)) { attackBehavior?.Attack(); // 委托给具体的行为组件 } } } // 配置敌人时,可以动态组合 Enemy dragon = new Dragon(); dragon.attackBehavior = new FireAttack(); // 龙使用火焰攻击 Enemy yeti = new Yeti(); yeti.attackBehavior = new IceAttack(); // 雪人使用冰霜攻击

这就是著名的“组合优于继承”原则。它通过将变化的部分(攻击方式)封装成独立的、可互换的组件,使得类更加灵活,更容易应对需求变化。Unity自身的GetComponent系统就是组合模式的典范。

5.2 多态与性能:虚方法调用的开销

虚方法(virtual)和接口方法调用,由于需要在运行时查找方法表(vtable),其性能开销略高于非虚方法(普通方法)的直接调用。对于在Update等每帧调用的高频函数,这个开销需要关注。

优化建议:

  1. 不要过度使用虚方法:只在确实需要子类改变行为的地方使用virtual。对于内部辅助方法,尽量使用非虚方法或private方法。
  2. 考虑使用策略模式(组合):如上所述,将行为委托给组件。组件的方法可以是非虚的,通过持有不同的组件引用来改变行为,有时比深层次的继承树更高效、更清晰。
  3. 对于性能极度敏感的代码(如渲染循环、物理计算),可以考虑使用其他模式,如数据导向设计(DOD),但这属于高级优化范畴,在大部分游戏逻辑中,多态的开销是可接受的。

5.3 设计陷阱:脆弱的基类问题

问题:当你修改一个被广泛继承的基类(如Enemy)时,可能会无意中破坏所有子类的功能。例如,你在Enemy.TakeDamage方法中增加了一个参数,那么所有重写了该方法的子类(如Goblin.TakeDamage)都必须修改签名,否则会导致编译错误或行为异常。

缓解策略:

  1. 对基类的修改保持谨慎,特别是对公共和受保护成员的修改。尽量通过添加新方法而非修改现有方法来扩展功能。
  2. 使用密封类(sealed:如果你确定某个类不会被继承,或者其继承会带来问题,可以将其标记为sealed。这也能带来微小的性能提升(因为运行时知道它没有子类,可以进行某些优化)。
  3. 优先使用组合:这能减少类之间的深度耦合,基类的变化影响范围会更小。

5.4 Unity特有的注意事项

  1. MonoBehaviour生命周期方法的重写Start(),Update(),Awake()等是Unity引擎调用的特殊方法。在子类中重写它们时,必须调用基类方法base.Start()),除非你明确知道不需要基类的初始化逻辑。忘记调用base.Awake()是导致组件初始化失败的常见原因。
    public class SpecialEnemy : Enemy { protected override void Start() { base.Start(); // 务必先调用基类Start,确保基类初始化完成 // ... 子类特有的初始化 } }
  2. 序列化与Inspector:公共字段和标记了[SerializeField]的私有字段会显示在Unity Inspector中。当使用继承时,子类的Inspector会同时显示自身和父类的可序列化字段。注意,如果修改了基类中某个序列化字段的名称,可能会破坏已有预制体或场景中对该字段的序列化引用。
  3. GetComponent与多态GetComponent<Enemy>()会返回挂载在GameObject上的Enemy组件或其任何派生类(Goblin,Dragon)的引用。这在需要统一处理所有敌人类型时非常方便。

掌握继承与多态,就像是拿到了编写可维护、可扩展游戏代码的钥匙。从今天这个简单的战斗模拟开始,尝试在你自己的项目里应用这些思想。下次当你发现自己在复制粘贴大段代码,或者写满一屏幕的if-else判断类型时,停下来想一想:“这里是不是该用多态了?” 思考和实践得越多,你的代码设计能力就会越强。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询