第2章 框架设计原则

良好的框架设计是游戏项目成功的基石。本章将介绍如何在 Godot C# 开发中应用经典的软件设计原则,建立可维护、可扩展的代码架构。

2.1.1 SOLID原则在游戏开发中的特殊性

SOLID原则源自企业软件开发,但在游戏开发中需要根据实时性、性能要求和架构特点进行适配。

单一职责原则的灵活性

游戏开发中的SRP需要平衡以下因素:

职责粒度的选择

  • 过细粒度:每个微小组件单独成类,导致节点数量爆炸
  • 过粗粒度:一个类承担过多职责,难以维护
  • 游戏实践:以"一个完整功能"为界限,而非"一个操作"

示例:玩家控制器可以包含移动、跳跃、蹲伏等输入处理,但不应该包含血条UI显示或存档逻辑。

开闭原则的权衡

原则定义:对扩展开放,对修改关闭。

在游戏开发中的应用:

场景应用方式注意事项
武器系统通过继承WeaponBase扩展新武器避免过度抽象导致性能损耗
敌人AI组合行为树节点而非修改基类运行时动态组装行为
技能效果策略模式+数据驱动配置新技能无需修改代码

性能考量:虚函数调用在游戏循环中可能有开销,关键路径考虑使用接口或委托。

里氏替换与组件模式

Godot的节点系统鼓励组合而非继承:

传统继承:Player -> Character -> Entity -> Node
Godot组合:Player (Node)
          ├── MovementComponent
          ├── HealthComponent
          └── InventoryComponent

组件模式自然地实现了LSP,因为组件之间是平行关系而非继承关系。

接口隔离与Godot信号

Godot的信号系统实现了松耦合:

  • 传统接口:类需要实现接口的所有方法
  • Godot信号:类只订阅关心的事件
  • 优势:无需强制实现,动态连接和断开

依赖倒置的实践

高层模块不应该依赖低层模块,两者都应该依赖抽象:

  • 使用场景:游戏管理器不应该直接创建具体系统
  • Godot实现:通过@Export暴露依赖,运行时注入
  • 服务定位器:GameManager作为中央注册表

2.1.2 组合优于继承的Godot实践

“组合优于继承"是Godot设计的核心理念。

为什么继承在游戏开发中存在问题

1. 继承层次过深

// 问题:深度继承链
class Unit { }
class Character : Unit { }
class Player : Character { }
class Warrior : Player { }
// 修改基类会影响所有子类

2. 钻石继承问题

// C#不支持多重继承,但接口组合仍然复杂
interface IAttacker { }
interface IMovable { }
interface ICollectable { }
// 需要同时实现多个接口

3. 运行时灵活性差

继承关系在编译期确定,无法动态调整。

Godot的组合模式

Godot的节点树就是组合模式的最佳实践:

// 推荐:使用节点组合功能
public partial class Player : CharacterBody3D
{
    // 移动能力
    [Export] public MovementComponent Movement { get; set; }

    // 战斗能力
    [Export] public CombatComponent Combat { get; set; }

    // 交互能力
    [Export] public InteractionComponent Interaction { get; set; }

    // 可以在编辑器中配置或运行时更换组件
}

组件间的通信

组件之间需要通过事件或消息通信:

  • 信号:Godot原生的事件系统
  • 消息总线:全局事件分发
  • 直接引用:父节点协调子组件
// 父节点协调示例
public override void _Ready()
{
    // 连接组件间的通信
    _combatComponent.OnDamageTaken += _healthComponent.TakeDamage;
    _healthComponent.OnHealthChanged += _uiComponent.UpdateHealthBar;
}

何时使用继承

继承在以下场景仍然适用:

  • is-a关系明确:Enemy继承Character
  • 共享基础功能:所有Entity共享位置、旋转
  • 多态需求:统一的接口处理不同类型

2.1.3 依赖注入容器设计思路

依赖注入(DI)是解耦代码的有效手段,在大型游戏项目中尤为重要。

为什么游戏需要DI

传统方式的痛点

// 紧耦合:直接创建依赖
public class PlayerController
{
    private AudioManager _audio = new AudioManager(); // 无法更换实现
    private InputSystem _input = InputSystem.Instance; // 全局单例难以测试
}

DI的优势

  • 易于单元测试(可注入Mock对象)
  • 支持运行时替换实现
  • 系统间的依赖关系清晰

简单DI容器实现

public class ServiceContainer
{
    private Dictionary<Type, object> _services = new();
    private Dictionary<Type, Func<object>> _factories = new();

    // 注册单例
    public void Register<T>(T instance) where T : class
    {
        _services[typeof(T)] = instance;
    }

    // 注册工厂
    public void Register<T>(Func<T> factory) where T : class
    {
        _factories[typeof(T)] = () => factory();
    }

    // 解析依赖
    public T Resolve<T>() where T : class
    {
        if (_services.TryGetValue(typeof(T), out var instance))
            return (T)instance;

        if (_factories.TryGetValue(typeof(T), out var factory))
        {
            var newInstance = factory();
            _services[typeof(T)] = newInstance; // 缓存为单例
            return (T)newInstance;
        }

        throw new Exception($"Service {typeof(T)} not registered");
    }
}

Godot中的DI实践

1. 导出属性注入

public partial class Player : CharacterBody3D
{
    [Export] public IInventoryService Inventory { get; set; }
    [Export] public IQuestService QuestSystem { get; set; }
}

2. 初始化时注入

public override void _EnterTree()
{
    // 从服务容器获取依赖
    _gameState = ServiceProvider.Resolve<IGameState>();
    _eventBus = ServiceProvider.Resolve<IEventBus>();
}

3. 场景树查找注入

[Export] public NodePath InventoryPath { get; set; }

public override void _Ready()
{
    _inventory = GetNode<IInventory>(InventoryPath);
}

服务生命周期管理

不同服务有不同的生命周期需求:

生命周期说明示例
单例(Singleton)全局唯一,游戏期间持续存在AudioManager, GameState
场景(Scene)随场景加载创建,卸载销毁LevelManager, EnemySpawner
瞬态(Transient)每次请求创建新实例DamageCalculator, PathFinder
作用域(Scoped)特定游戏会话期间存在PlayerSession, MatchState

2.0.4 代码组织与命名规范

良好的代码组织是框架可维护性的基础。

项目结构建议

项目目录结构:

Scripts/
├── Core/                    # 核心框架
│   ├── ServiceProvider.cs
│   ├── EventBus.cs
│   └── GameManager.cs
├── Entities/                # 游戏实体
│   ├── Player/
│   ├── Enemy/
│   └── NPC/
├── Systems/                 # 游戏系统
│   ├── Audio/
│   ├── Input/
│   └── Save/
├── UI/                      # 界面
│   ├── HUD/
│   ├── Menus/
│   └── Components/
└── Utils/                   # 工具类
    ├── Extensions/
    └── Helpers/

命名规范

类型命名规则示例
类名PascalCasePlayerController
接口PascalCase + I前缀ISaveable, IDamageable
方法PascalCaseTakeDamage(), MoveTo()
属性PascalCaseHealth, MaxSpeed
私有字段_camelCase_currentHealth, _isDead
常量UPPER_SNAKE_CASEMAX_HEALTH, DEFAULT_SPEED
枚举PascalCase + 名词GameState, WeaponType
信号PascalCase + EventHandler后缀HealthChangedEventHandler

命名建议

  1. 清晰优先CalculateDistance 优于 CalcDist
  2. 避免缩写PlayerController 优于 PlayerCtrl
  3. 布尔属性:使用 Is/Can/Has 前缀,如 IsDead, CanMove
  4. 方法动词:使用动作动词,如 Load(), Save(), Initialize()
  5. 避免魔法数字:使用常量替代裸数字

2.1 SOLID 原则在 Godot 中的应用

SOLID 是面向对象设计的五大基本原则,它们同样适用于游戏开发。

2.1.1 单一职责原则 (SRP)

原则定义:一个类应该只有一个引起它变化的原因。

在游戏开发中,这意味着每个节点或类应该只负责一个明确的功能。

// 文件路径: Scripts/Player/PlayerController.cs
// 功能说明: 玩家控制器 - 只负责输入处理和移动逻辑

using Godot;

namespace GameFramework.Player
{
    /// <summary>
    /// 玩家控制器类 - 遵循单一职责原则
    /// 只处理玩家输入和移动逻辑
    /// </summary>
    public partial class PlayerController : CharacterBody2D
    {
        // 步骤1: 定义移动相关常量
        [Export] public float MoveSpeed { get; set; } = 300.0f;
        [Export] public float JumpForce { get; set; } = 500.0f;

        // 步骤2: 注入依赖(通过节点引用)
        private PlayerAnimation _animationHandler;
        private PlayerHealth _healthSystem;

        // 步骤3: 初始化时获取依赖
        public override void _Ready()
        {
            // 注意: 使用依赖注入而非直接创建实例
            _animationHandler = GetNode<PlayerAnimation>("AnimationHandler");
            _healthSystem = GetNode<PlayerHealth>("HealthSystem");
        }

        // 步骤4: 处理物理更新和输入
        public override void _PhysicsProcess(double delta)
        {
            // 获取输入方向
            Vector2 direction = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");

            // 应用移动
            Velocity = direction * MoveSpeed;
            MoveAndSlide();

            // 通知动画系统状态变化
            _animationHandler?.UpdateAnimationState(direction, IsOnFloor());
        }
    }
}
// 文件路径: Scripts/Player/PlayerAnimation.cs
// 功能说明: 玩家动画系统 - 只负责动画播放和管理

using Godot;

namespace GameFramework.Player
{
    /// <summary>
    /// 玩家动画类 - 只负责动画相关逻辑
    /// 遵循单一职责原则,与移动逻辑分离
    /// </summary>
    public partial class PlayerAnimation : Node
    {
        // 步骤1: 获取动画播放器引用
        private AnimatedSprite2D _sprite;

        public override void _Ready()
        {
            _sprite = GetParent().GetNode<AnimatedSprite2D>("Sprite");
        }

        // 步骤2: 根据移动状态更新动画
        public void UpdateAnimationState(Vector2 direction, bool isOnFloor)
        {
            // 注意: 这里只处理动画逻辑,不涉及移动计算
            if (direction.Length() > 0)
            {
                _sprite.Play("run");
                // 根据方向翻转精灵
                _sprite.FlipH = direction.X < 0;
            }
            else
            {
                _sprite.Play("idle");
            }
        }
    }
}

反例 - 违反 SRP

// 文件路径: Scripts/AntiPatterns/PlayerGodClass.cs
// 功能说明: 反例 - 上帝类,违反单一职责原则

using Godot;

namespace GameFramework.AntiPatterns
{
    /// <summary>
    /// 反例: 玩家上帝类 - 不要做这样的设计!
    /// 这个类处理了太多职责:移动、动画、音效、UI、存档...
    /// </summary>
    public partial class PlayerGodClass : CharacterBody2D
    {
        // 错误: 一个类处理了所有事情
        public override void _PhysicsProcess(double delta)
        {
            // 处理移动
            Vector2 direction = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
            Velocity = direction * 300;
            MoveAndSlide();

            // 错误: 直接在这里处理动画
            var sprite = GetNode<AnimatedSprite2D>("Sprite");
            if (direction.Length() > 0)
                sprite.Play("run");
            else
                sprite.Play("idle");

            // 错误: 还直接处理音效
            if (Input.IsActionJustPressed("jump"))
            {
                GetNode<AudioStreamPlayer>("JumpSound").Play();
            }

            // 错误: 甚至直接更新UI
            GetNode<Label>("/root/Main/UI/HealthLabel").Text = "HP: " + _health;

            // 错误: 还有存档逻辑
            if (Input.IsActionJustPressed("save"))
            {
                SaveGame();
            }
        }

        private void SaveGame()
        {
            // 存档实现...
        }
    }
}

2.1.2 开闭原则 (OCP)

原则定义:软件实体应该对扩展开放,对修改关闭。

在 Godot 中,我们通过抽象和多态来实现这一原则。

// 文件路径: Scripts/Abilities/IAbility.cs
// 功能说明: 技能接口定义 - 开闭原则的基础

using Godot;

namespace GameFramework.Abilities
{
    /// <summary>
    /// 技能接口 - 定义所有技能必须实现的方法
    /// 开闭原则:新的技能类型通过实现接口来扩展,而非修改现有代码
    /// </summary>
    public interface IAbility
    {
        // 步骤1: 定义技能基本属性
        string AbilityName { get; }
        float CooldownTime { get; }

        // 步骤2: 定义技能行为
        void Execute(Node2D caster);
        bool CanExecute();
    }
}
// 文件路径: Scripts/Abilities/BaseAbility.cs
// 功能说明: 抽象基类,提供通用实现

using Godot;

namespace GameFramework.Abilities
{
    /// <summary>
    /// 抽象技能基类 - 提供通用实现
    /// 子类可以扩展而不需要修改基类
    /// </summary>
    public abstract partial class BaseAbility : Node, IAbility
    {
        // 步骤1: 实现接口属性
        public abstract string AbilityName { get; }
        public abstract float CooldownTime { get; }

        // 步骤2: 冷却计时
        protected double _currentCooldown = 0;

        // 步骤3: 更新冷却
        public override void _Process(double delta)
        {
            if (_currentCooldown > 0)
            {
                _currentCooldown -= delta;
            }
        }

        // 步骤4: 检查是否可以执行
        public virtual bool CanExecute() => _currentCooldown <= 0;

        // 步骤5: 抽象执行方法,由子类实现
        public abstract void Execute(Node2D caster);

        // 步骤6: 启动冷却
        protected void StartCooldown()
        {
            _currentCooldown = CooldownTime;
        }
    }
}
// 文件路径: Scripts/Abilities/FireballAbility.cs
// 功能说明: 火球术 - 通过扩展实现新功能

using Godot;

namespace GameFramework.Abilities
{
    /// <summary>
    /// 火球术技能 - 扩展现有功能而不修改已有代码
    /// </summary>
    public partial class FireballAbility : BaseAbility
    {
        // 步骤1: 实现抽象属性
        public override string AbilityName => "火球术";
        public override float CooldownTime => 2.0f;

        [Export] public PackedScene FireballProjectile { get; set; }
        [Export] public float ProjectileSpeed { get; set; } = 500.0f;

        // 步骤2: 实现执行逻辑
        public override void Execute(Node2D caster)
        {
            if (!CanExecute()) return;

            // 步骤3: 实例化投射物
            var projectile = FireballProjectile.Instantiate<Projectile>();
            caster.GetParent().AddChild(projectile);

            // 步骤4: 设置投射物方向和速度
            projectile.GlobalPosition = caster.GlobalPosition;
            projectile.Direction = GetCastDirection(caster);
            projectile.Speed = ProjectileSpeed;

            // 步骤5: 启动冷却
            StartCooldown();

            // 注意: 这里可以添加火球特有效果,如屏幕震动
        }

        // 步骤6: 获取施法方向
        private Vector2 GetCastDirection(Node2D caster)
        {
            // 默认向右,可以根据输入或目标调整
            return Vector2.Right;
        }
    }
}
// 文件路径: Scripts/Abilities/HealAbility.cs
// 功能说明: 治疗术 - 另一个扩展示例

using Godot;

namespace GameFramework.Abilities
{
    /// <summary>
    /// 治疗术技能 - 无需修改其他代码即可添加新技能
    /// </summary>
    public partial class HealAbility : BaseAbility
    {
        public override string AbilityName => "治疗术";
        public override float CooldownTime => 5.0f;

        [Export] public int HealAmount { get; set; } = 20;

        public override void Execute(Node2D caster)
        {
            if (!CanExecute()) return;

            // 步骤1: 获取生命值组件
            var health = caster.GetNode<HealthComponent>("Health");
            if (health != null)
            {
                // 步骤2: 执行治疗
                health.Heal(HealAmount);

                // 步骤3: 播放治疗特效
                SpawnHealEffect(caster);
            }

            StartCooldown();
        }

        private void SpawnHealEffect(Node2D caster)
        {
            // 特效生成逻辑
        }
    }
}
// 文件路径: Scripts/Player/AbilityManager.cs
// 功能说明: 技能管理器 - 使用开闭原则管理所有技能

using Godot;
using Godot.Collections;
using System.Collections.Generic;

namespace GameFramework.Player
{
    /// <summary>
    /// 技能管理器 - 管理所有技能,支持动态添加新技能
    /// 开闭原则的体现:添加新技能时不需要修改此类
    /// </summary>
    public partial class AbilityManager : Node
    {
        // 步骤1: 使用接口集合存储技能
        private List<GameFramework.Abilities.IAbility> _abilities = new();

        // 步骤2: 通过导出变量在编辑器中配置技能
        [Export] public Array<NodePath> AbilityNodes { get; set; } = new();

        public override void _Ready()
        {
            // 步骤3: 收集所有技能节点
            foreach (var path in AbilityNodes)
            {
                var abilityNode = GetNode<GameFramework.Abilities.IAbility>(path);
                if (abilityNode != null)
                {
                    _abilities.Add(abilityNode);
                }
            }
        }

        // 步骤4: 执行指定技能
        public void ExecuteAbility(int index, Node2D caster)
        {
            if (index >= 0 && index < _abilities.Count)
            {
                var ability = _abilities[index];
                if (ability.CanExecute())
                {
                    ability.Execute(caster);
                }
            }
        }

        // 步骤5: 动态添加新技能 - 扩展性
        public void AddAbility(GameFramework.Abilities.IAbility ability)
        {
            _abilities.Add(ability);
        }
    }
}

2.1.3 里氏替换原则 (LSP)

原则定义:子类型必须能够替换其基类型而不改变程序的正确性。

// 文件路径: Scripts/Enemies/BaseEnemy.cs
// 功能说明: 敌人基类 - 定义通用契约

using Godot;

namespace GameFramework.Enemies
{
    /// <summary>
    /// 敌人基类 - 定义所有敌人必须遵循的契约
    /// </summary>
    public abstract partial class BaseEnemy : CharacterBody2D
    {
        // 步骤1: 定义通用属性
        [Export] public int MaxHealth { get; set; } = 100;
        public int CurrentHealth { get; protected set; }

        // 步骤2: 虚方法 - 子类可以重写但必须保持行为一致
        public virtual void TakeDamage(int damage)
        {
            // 前置条件:伤害值必须为正
            if (damage <= 0) return;

            CurrentHealth -= damage;
            if (CurrentHealth <= 0)
            {
                Die();
            }
        }

        // 步骤3: 抽象方法 - 子类必须实现
        protected abstract void Die();

        // 步骤4: 虚方法 - 子类可以扩展
        public virtual void Initialize()
        {
            CurrentHealth = MaxHealth;
        }
    }
}
// 文件路径: Scripts/Enemies/SkeletonEnemy.cs
// 功能说明: 骷髅敌人 - 正确实现 LSP

using Godot;

namespace GameFramework.Enemies
{
    /// <summary>
    /// 骷髅敌人 - 正确遵循里氏替换原则
    /// 不改变基类方法的前置条件和后置条件
    /// </summary>
    public partial class SkeletonEnemy : BaseEnemy
    {
        [Export] public int Armor { get; set; } = 5;

        public override void _Ready()
        {
            Initialize();
        }

        // 步骤1: 重写时保持契约 - 仍然接受正数伤害
        public override void TakeDamage(int damage)
        {
            // 注意: 可以添加新的行为(护甲减伤),但不能违反契约
            int actualDamage = Mathf.Max(1, damage - Armor);
            base.TakeDamage(actualDamage); // 调用基类实现
        }

        // 步骤2: 实现抽象方法
        protected override void Die()
        {
            // 播放死亡动画
            var anim = GetNode<AnimationPlayer>("AnimationPlayer");
            anim.Play("die");

            // 延迟销毁
            QueueFree();
        }
    }
}
// 文件路径: Scripts/Enemies/GhostEnemy.cs
// 功能说明: 幽灵敌人 - 另一种实现

using Godot;

namespace GameFramework.Enemies
{
    /// <summary>
    /// 幽灵敌人 - 免疫物理伤害
    /// 仍然遵循基类契约
    /// </summary>
    public partial class GhostEnemy : BaseEnemy
    {
        [Export] public bool IsPhysicalImmune { get; set; } = true;

        // 步骤1: 通过新增参数类型扩展,而非违反契约
        public void TakeDamage(int damage, DamageType damageType)
        {
            if (IsPhysicalImmune && damageType == DamageType.Physical)
            {
                // 免疫物理伤害,但不抛出异常或违反契约
                ShowImmuneEffect();
                return;
            }

            base.TakeDamage(damage);
        }

        // 步骤2: 保持原有方法签名兼容
        public override void TakeDamage(int damage)
        {
            // 默认调用物理免疫版本
            TakeDamage(damage, DamageType.Physical);
        }

        protected override void Die()
        {
            // 幽灵消散效果
            var tween = CreateTween();
            tween.TweenProperty(this, "modulate:a", 0.0f, 1.0f);
            tween.TweenCallback(Callable.From(QueueFree));
        }

        private void ShowImmuneEffect()
        {
            // 显示"免疫"文字效果
        }
    }

    public enum DamageType
    {
        Physical,
        Magical,
        Pure
    }
}
// 文件路径: Scripts/Combat/EnemySpawner.cs
// 功能说明: 敌人生成器 - 展示 LSP 的应用

using Godot;

namespace GameFramework.Combat
{
    /// <summary>
    /// 敌人生成器 - 依赖基类,可以处理任何子类型
    /// </summary>
    public partial class EnemySpawner : Node
    {
        // 步骤1: 依赖基类而非具体子类
        [Export] public PackedScene EnemyScene { get; set; }

        // 步骤2: 生成敌人
        public GameFramework.Enemies.BaseEnemy SpawnEnemy(Vector2 position)
        {
            // 注意: 可以生成任何 BaseEnemy 的子类
            var enemy = EnemyScene.Instantiate<GameFramework.Enemies.BaseEnemy>();
            AddChild(enemy);
            enemy.GlobalPosition = position;

            // 步骤3: 使用多态调用 - 无需知道具体类型
            enemy.Initialize();

            return enemy;
        }

        // 步骤4: 对敌人造成伤害 - 适用于所有子类型
        public void DamageEnemy(GameFramework.Enemies.BaseEnemy enemy, int damage)
        {
            // 里氏替换原则保证:任何子类都能正确处理此方法
            enemy.TakeDamage(damage);
        }
    }
}

2.1.4 接口隔离原则 (ISP)

原则定义:客户端不应该被迫依赖它们不使用的接口。

// 文件路径: Scripts/Interfaces/IMovable.cs
// 功能说明: 移动接口 - 细粒度的接口设计

using Godot;

namespace GameFramework.Interfaces
{
    /// <summary>
    /// 移动接口 - 只包含移动相关功能
    /// </summary>
    public interface IMovable
    {
        float MoveSpeed { get; set; }
        void Move(Vector2 direction);
        void Stop();
    }
}
// 文件路径: Scripts/Interfaces/IAttackable.cs
// 功能说明: 攻击接口

using Godot;

namespace GameFramework.Interfaces
{
    /// <summary>
    /// 攻击接口 - 只包含攻击相关功能
    /// </summary>
    public interface IAttackable
    {
        int AttackDamage { get; set; }
        float AttackRange { get; set; }
        void Attack(Node2D target);
        bool CanAttack(Node2D target);
    }
}
// 文件路径: Scripts/Interfaces/IDamageable.cs
// 功能说明: 受伤接口

using Godot;

namespace GameFramework.Interfaces
{
    /// <summary>
    /// 受伤接口 - 只包含生命值相关功能
    /// </summary>
    public interface IDamageable
    {
        int MaxHealth { get; set; }
        int CurrentHealth { get; }
        void TakeDamage(int damage);
        void Heal(int amount);
        bool IsAlive { get; }
    }
}
// 文件路径: Scripts/Entities/Warrior.cs
// 功能说明: 战士类 - 实现多个接口

using Godot;

namespace GameFramework.Entities
{
    /// <summary>
    /// 战士 - 需要移动、攻击和受伤能力
    /// 只实现需要的接口,而非一个大而全的接口
    /// </summary>
    public partial class Warrior : CharacterBody2D,
        GameFramework.Interfaces.IMovable,
        GameFramework.Interfaces.IAttackable,
        GameFramework.Interfaces.IDamageable
    {
        // IMovable 实现
        [Export] public float MoveSpeed { get; set; } = 200.0f;

        public void Move(Vector2 direction)
        {
            Velocity = direction.Normalized() * MoveSpeed;
            MoveAndSlide();
        }

        public void Stop()
        {
            Velocity = Vector2.Zero;
        }

        // IAttackable 实现
        [Export] public int AttackDamage { get; set; } = 15;
        [Export] public float AttackRange { get; set; } = 50.0f;

        public void Attack(Node2D target)
        {
            if (!CanAttack(target)) return;

            if (target is GameFramework.Interfaces.IDamageable damageable)
            {
                damageable.TakeDamage(AttackDamage);
            }
        }

        public bool CanAttack(Node2D target)
        {
            return GlobalPosition.DistanceTo(target.GlobalPosition) <= AttackRange;
        }

        // IDamageable 实现
        [Export] public int MaxHealth { get; set; } = 100;
        public int CurrentHealth { get; private set; }
        public bool IsAlive => CurrentHealth > 0;

        public void TakeDamage(int damage)
        {
            CurrentHealth -= damage;
            if (!IsAlive)
            {
                QueueFree();
            }
        }

        public void Heal(int amount)
        {
            CurrentHealth = System.Math.Min(CurrentHealth + amount, MaxHealth);
        }
    }
}
// 文件路径: Scripts/Entities/Projectile.cs
// 功能说明: 投射物 - 只实现需要的接口

using Godot;

namespace GameFramework.Entities
{
    /// <summary>
    /// 投射物 - 只实现移动和造成伤害
    /// 不需要攻击接口,因为投射物本身不"攻击"
    /// </summary>
    public partial class Projectile : Area2D,
        GameFramework.Interfaces.IMovable
    {
        // IMovable 实现
        [Export] public float MoveSpeed { get; set; } = 500.0f;
        public Vector2 Direction { get; set; } = Vector2.Right;

        [Export] public int Damage { get; set; } = 10;

        public override void _PhysicsProcess(double delta)
        {
            // 步骤1: 移动
            Move(Direction);
        }

        public void Move(Vector2 direction)
        {
            Position += direction.Normalized() * MoveSpeed * (float)GetProcessDeltaTime();
        }

        public void Stop()
        {
            // 投射物击中后停止
            QueueFree();
        }

        // 步骤2: 碰撞检测
        public void OnBodyEntered(Node2D body)
        {
            if (body is GameFramework.Interfaces.IDamageable damageable)
            {
                damageable.TakeDamage(Damage);
                Stop();
            }
        }
    }
}

2.1.5 依赖倒置原则 (DIP)

原则定义:高层模块不应该依赖低层模块,两者都应该依赖抽象。

// 文件路径: Scripts/Services/IInputService.cs
// 功能说明: 输入服务接口 - 抽象依赖

using Godot;

namespace GameFramework.Services
{
    /// <summary>
    /// 输入服务接口 - 游戏逻辑依赖此抽象而非具体实现
    /// </summary>
    public interface IInputService
    {
        Vector2 GetMovementInput();
        bool IsActionPressed(string action);
        bool IsActionJustPressed(string action);
        Vector2 GetMousePosition();
    }
}
// 文件路径: Scripts/Services/GodotInputService.cs
// 功能说明: Godot 输入服务实现

using Godot;

namespace GameFramework.Services
{
    /// <summary>
    /// Godot 内置输入系统实现
    /// </summary>
    public class GodotInputService : IInputService
    {
        // 步骤1: 实现接口方法
        public Vector2 GetMovementInput()
        {
            return Input.GetVector("move_left", "move_right", "move_up", "move_down");
        }

        public bool IsActionPressed(string action)
        {
            return Input.IsActionPressed(action);
        }

        public bool IsActionJustPressed(string action)
        {
            return Input.IsActionJustPressed(action);
        }

        public Vector2 GetMousePosition()
        {
            return Input.GetMousePosition();
        }
    }
}
// 文件路径: Scripts/Services/AIInputService.cs
// 功能说明: AI 输入服务 - 另一个实现

using Godot;

namespace GameFramework.Services
{
    /// <summary>
    /// AI 输入服务 - 用于 AI 控制的实体
    /// 同样的接口,完全不同的实现
    /// </summary>
    public class AIInputService : IInputService
    {
        private Node2D _owner;
        private Node2D _target;

        public AIInputService(Node2D owner, Node2D target)
        {
            _owner = owner;
            _target = target;
        }

        public Vector2 GetMovementInput()
        {
            // 步骤1: 计算朝向目标的方向
            if (_target == null) return Vector2.Zero;
            return (_target.GlobalPosition - _owner.GlobalPosition).Normalized();
        }

        public bool IsActionPressed(string action)
        {
            // AI 根据逻辑决定是否"按下"某个动作
            if (action == "attack")
            {
                return _target != null &&
                       _owner.GlobalPosition.DistanceTo(_target.GlobalPosition) < 100;
            }
            return false;
        }

        public bool IsActionJustPressed(string action)
        {
            // AI 可以实现特定的触发逻辑
            return IsActionPressed(action);
        }

        public Vector2 GetMousePosition()
        {
            // AI 不需要鼠标位置,返回目标位置作为替代
            return _target?.GlobalPosition ?? Vector2.Zero;
        }
    }
}
// 文件路径: Scripts/Player/PlayerControllerDIP.cs
// 功能说明: 玩家控制器 - 依赖注入示例

using Godot;

namespace GameFramework.Player
{
    /// <summary>
    /// 玩家控制器 - 依赖倒置原则的应用
    /// 通过构造函数注入依赖,而非直接创建
    /// </summary>
    public partial class PlayerControllerDIP : CharacterBody2D
    {
        // 步骤1: 依赖抽象接口而非具体实现
        private GameFramework.Services.IInputService _inputService;

        [Export] public float MoveSpeed { get; set; } = 300.0f;

        // 步骤2: 默认构造函数 - 使用默认实现
        public PlayerControllerDIP()
        {
            _inputService = new GameFramework.Services.GodotInputService();
        }

        // 步骤3: 依赖注入构造函数
        public void SetInputService(GameFramework.Services.IInputService inputService)
        {
            _inputService = inputService;
        }

        public override void _PhysicsProcess(double delta)
        {
            // 步骤4: 使用抽象接口,不关心具体实现
            Vector2 direction = _inputService.GetMovementInput();
            Velocity = direction * MoveSpeed;
            MoveAndSlide();

            // 处理攻击输入
            if (_inputService.IsActionJustPressed("attack"))
            {
                PerformAttack();
            }
        }

        private void PerformAttack()
        {
            // 攻击逻辑
        }
    }
}
// 文件路径: Scripts/DI/ServiceLocator.cs
// 功能说明: 服务定位器 - 管理依赖注入

using Godot;
using System;
using System.Collections.Generic;

namespace GameFramework.DI
{
    /// <summary>
    /// 服务定位器 - 简单的依赖注入容器
    /// </summary>
    public static class ServiceLocator
    {
        private static Dictionary<Type, object> _services = new();

        // 步骤1: 注册服务
        public static void RegisterService<TInterface>(TInterface implementation) where TInterface : class
        {
            _services[typeof(TInterface)] = implementation;
        }

        // 步骤2: 获取服务
        public static TInterface GetService<TInterface>() where TInterface : class
        {
            if (_services.TryGetValue(typeof(TInterface), out var service))
            {
                return service as TInterface;
            }
            throw new Exception($"Service {typeof(TInterface).Name} not registered");
        }

        // 步骤3: 清空服务
        public static void Clear()
        {
            _services.Clear();
        }
    }
}

2.2 组合优于继承的设计哲学

2.2.1 Godot 节点组合示例

Godot 引擎本身就是组合设计的典范。节点通过父子关系和信号系统进行组合。

// 文件路径: Scripts/Composition/HealthComponent.cs
// 功能说明: 生命值组件 - 可复用的功能模块

using Godot;

namespace GameFramework.Composition
{
    /// <summary>
    /// 生命值组件 - 通过组合而非继承添加生命值功能
    /// </summary>
    public partial class HealthComponent : Node
    {
        // 步骤1: 定义信号
        [Signal] public delegate void HealthChangedEventHandler(int currentHealth, int maxHealth);
        [Signal] public delegate void DiedEventHandler();
        [Signal] public delegate void HealedEventHandler(int amount);
        [Signal] public delegate void DamagedEventHandler(int damage);

        // 步骤2: 属性定义
        [Export] public int MaxHealth { get; set; } = 100;
        [Export] public int CurrentHealth { get; private set; } = 100;
        [Export] public bool Invulnerable { get; set; } = false;

        public bool IsAlive => CurrentHealth > 0;
        public float HealthPercent => (float)CurrentHealth / MaxHealth;

        // 步骤3: 初始化
        public override void _Ready()
        {
            CurrentHealth = MaxHealth;
        }

        // 步骤4: 受伤方法
        public void TakeDamage(int damage)
        {
            // 注意: 检查无敌状态
            if (Invulnerable || damage <= 0) return;

            CurrentHealth = Mathf.Max(0, CurrentHealth - damage);
            EmitSignal(SignalName.Damaged, damage);
            EmitSignal(SignalName.HealthChanged, CurrentHealth, MaxHealth);

            if (!IsAlive)
            {
                EmitSignal(SignalName.Died);
            }
        }

        // 步骤5: 治疗
        public void Heal(int amount)
        {
            if (amount <= 0 || !IsAlive) return;

            CurrentHealth = Mathf.Min(MaxHealth, CurrentHealth + amount);
            EmitSignal(SignalName.Healed, amount);
            EmitSignal(SignalName.HealthChanged, CurrentHealth, MaxHealth);
        }

        // 步骤6: 重置
        public void Reset()
        {
            CurrentHealth = MaxHealth;
            EmitSignal(SignalName.HealthChanged, CurrentHealth, MaxHealth);
        }
    }
}
// 文件路径: Scripts/Composition/MovementComponent.cs
// 功能说明: 移动组件

using Godot;

namespace GameFramework.Composition
{
    /// <summary>
    /// 移动组件 - 通过组合添加移动功能
    /// </summary>
    public partial class MovementComponent : Node
    {
        [Signal] public delegate void MovementStartedEventHandler();
        [Signal] public delegate void MovementStoppedEventHandler();
        [Signal] public delegate void DirectionChangedEventHandler(Vector2 newDirection);

        // 步骤1: 移动参数
        [Export] public float MaxSpeed { get; set; } = 300.0f;
        [Export] public float Acceleration { get; set; } = 1500.0f;
        [Export] public float Friction { get; set; } = 1200.0f;

        // 步骤2: 当前状态
        public Vector2 Velocity { get; private set; } = Vector2.Zero;
        public Vector2 Direction { get; private set; } = Vector2.Zero;
        public bool IsMoving => Velocity.Length() > 0.1f;

        private CharacterBody2D _body;
        private bool _wasMoving = false;

        // 步骤3: 获取父节点
        public override void _Ready()
        {
            _body = GetParent() as CharacterBody2D;
            if (_body == null)
            {
                GD.PushError("MovementComponent requires a CharacterBody2D parent");
            }
        }

        // 步骤4: 应用移动输入
        public void ApplyMovementInput(Vector2 inputDirection, double delta)
        {
            if (_body == null) return;

            // 步骤5: 处理方向变化
            if (inputDirection != Direction)
            {
                Direction = inputDirection;
                EmitSignal(SignalName.DirectionChanged, Direction);
            }

            // 步骤6: 计算速度
            if (inputDirection != Vector2.Zero)
            {
                // 加速
                Velocity = Velocity.MoveToward(inputDirection * MaxSpeed, Acceleration * (float)delta);

                if (!_wasMoving)
                {
                    EmitSignal(SignalName.MovementStarted);
                    _wasMoving = true;
                }
            }
            else
            {
                // 减速
                Velocity = Velocity.MoveToward(Vector2.Zero, Friction * (float)delta);

                if (_wasMoving && Velocity.Length() < 0.1f)
                {
                    EmitSignal(SignalName.MovementStopped);
                    _wasMoving = false;
                }
            }

            // 步骤7: 应用到物理体
            _body.Velocity = Velocity;
            _body.MoveAndSlide();
        }

        // 步骤8: 强制设置速度
        public void SetVelocity(Vector2 velocity)
        {
            Velocity = velocity;
        }

        // 步骤9: 瞬移
        public void Teleport(Vector2 position)
        {
            if (_body != null)
            {
                _body.GlobalPosition = position;
                Velocity = Vector2.Zero;
            }
        }
    }
}
// 文件路径: Scripts/Composition/WeaponComponent.cs
// 功能说明: 武器组件

using Godot;
using System.Collections.Generic;

namespace GameFramework.Composition
{
    /// <summary>
    /// 武器组件 - 通过组合添加战斗功能
    /// </summary>
    public partial class WeaponComponent : Node2D
    {
        [Signal] public delegate void AttackStartedEventHandler();
        [Signal] public delegate void AttackEndedEventHandler();
        [Signal] public delegate void HitTargetEventHandler(Node2D target);

        // 步骤1: 武器配置
        [Export] public int Damage { get; set; } = 10;
        [Export] public float AttackCooldown { get; set; } = 0.5f;
        [Export] public float AttackRange { get; set; } = 50.0f;
        [Export] public PackedScene HitEffect { get; set; }

        // 步骤2: 状态
        public bool CanAttack => _cooldownTimer <= 0 && !IsAttacking;
        public bool IsAttacking { get; private set; } = false;

        private double _cooldownTimer = 0;
        private List<Node2D> _hitTargets = new();

        public override void _Process(double delta)
        {
            // 步骤3: 更新冷却
            if (_cooldownTimer > 0)
            {
                _cooldownTimer -= delta;
            }
        }

        // 步骤4: 执行攻击
        public bool Attack(Vector2 direction)
        {
            if (!CanAttack) return false;

            // 步骤5: 开始攻击
            IsAttacking = true;
            _cooldownTimer = AttackCooldown;
            _hitTargets.Clear();

            EmitSignal(SignalName.AttackStarted);

            // 步骤6: 使用Tween或动画系统处理攻击动画
            // 这里简化为立即结束
            EndAttack();

            return true;
        }

        // 步骤7: 结束攻击
        public void EndAttack()
        {
            IsAttacking = false;
            EmitSignal(SignalName.AttackEnded);
        }

        // 步骤8: 检测命中
        public void OnHitAreaEntered(Node2D target)
        {
            if (!IsAttacking) return;
            if (_hitTargets.Contains(target)) return;

            // 步骤9: 应用伤害
            if (target.GetNode<HealthComponent>("HealthComponent") is HealthComponent health)
            {
                health.TakeDamage(Damage);
                _hitTargets.Add(target);

                EmitSignal(SignalName.HitTarget, target);
                SpawnHitEffect(target.GlobalPosition);
            }
        }

        // 步骤10: 生成命中特效
        private void SpawnHitEffect(Vector2 position)
        {
            if (HitEffect == null) return;

            var effect = HitEffect.Instantiate<Node2D>();
            GetTree().CurrentScene.AddChild(effect);
            effect.GlobalPosition = position;
        }
    }
}
// 文件路径: Scripts/Entities/PlayerWithComposition.cs
// 功能说明: 使用组合方式构建的玩家

using Godot;

namespace GameFramework.Entities
{
    /// <summary>
    /// 组合式玩家实体 - 通过组合各种组件构建功能
    /// 比继承方式更加灵活
    /// </summary>
    public partial class PlayerWithComposition : CharacterBody2D
    {
        // 步骤1: 引用各组件
        private MovementComponent _movement;
        private HealthComponent _health;
        private WeaponComponent _weapon;

        public override void _Ready()
        {
            // 步骤2: 获取组件引用
            _movement = GetNode<MovementComponent>("Components/Movement");
            _health = GetNode<HealthComponent>("Components/Health");
            _weapon = GetNode<WeaponComponent>("Components/Weapon");

            // 步骤3: 连接信号
            if (_health != null)
            {
                _health.Died += OnDied;
                _health.HealthChanged += OnHealthChanged;
            }
        }

        public override void _PhysicsProcess(double delta)
        {
            // 步骤4: 使用移动组件
            if (_movement != null)
            {
                Vector2 input = Input.GetVector("ui_left", "ui_right", "ui_up", "ui_down");
                _movement.ApplyMovementInput(input, delta);
            }

            // 步骤5: 使用武器组件
            if (_weapon != null && Input.IsActionJustPressed("attack"))
            {
                _weapon.Attack(Vector2.Right);
            }
        }

        private void OnDied()
        {
            // 处理死亡逻辑
            QueueFree();
        }

        private void OnHealthChanged(int current, int max)
        {
            // 更新UI等
            GD.Print($"Health: {current}/{max}");
        }
    }
}

2.2.2 组件化设计模式

// 文件路径: Scripts/Composition/ComponentBase.cs
// 功能说明: 组件基类 - 定义组件通用行为

using Godot;

namespace GameFramework.Composition
{
    /// <summary>
    /// 组件基类 - 所有组件的基类
    /// </summary>
    public abstract partial class ComponentBase : Node
    {
        // 步骤1: 获取组件所有者
        public Node OwnerEntity { get; private set; }

        // 步骤2: 组件启用状态
        [Export] public bool Enabled { get; set; } = true;

        public override void _Ready()
        {
            OwnerEntity = GetParent();
            OnInitialize();
        }

        public override void _Process(double delta)
        {
            if (Enabled)
            {
                OnUpdate(delta);
            }
        }

        public override void _PhysicsProcess(double delta)
        {
            if (Enabled)
            {
                OnPhysicsUpdate(delta);
            }
        }

        // 步骤3: 生命周期方法 - 子类重写
        protected virtual void OnInitialize() { }
        protected virtual void OnUpdate(double delta) { }
        protected virtual void OnPhysicsUpdate(double delta) { }
        protected virtual void OnDestroy() { }

        // 步骤4: 获取同实体上的其他组件
        protected T GetComponent<T>() where T : ComponentBase
        {
            return OwnerEntity?.GetNodeOrNull<T>(typeof(T).Name);
        }

        public override void _ExitTree()
        {
            OnDestroy();
        }
    }
}
// 文件路径: Scripts/Composition/Entity.cs
// 功能说明: 实体类 - 管理组件

using Godot;
using System.Collections.Generic;
using System.Linq;

namespace GameFramework.Composition
{
    /// <summary>
    /// 实体类 - 组件的容器和管理者
    /// </summary>
    public partial class Entity : Node
    {
        // 步骤1: 组件字典
        private Dictionary<string, ComponentBase> _components = new();

        public override void _Ready()
        {
            // 步骤2: 自动收集所有子组件
            foreach (var child in GetChildren())
            {
                if (child is ComponentBase component)
                {
                    RegisterComponent(component);
                }
            }
        }

        // 步骤3: 注册组件
        public void RegisterComponent(ComponentBase component)
        {
            string typeName = component.GetType().Name;
            _components[typeName] = component;
        }

        // 步骤4: 获取组件
        public T GetComponent<T>() where T : ComponentBase
        {
            string typeName = typeof(T).Name;
            if (_components.TryGetValue(typeName, out var component))
            {
                return component as T;
            }
            return null;
        }

        // 步骤5: 获取所有特定类型的组件
        public IEnumerable<T> GetComponents<T>() where T : ComponentBase
        {
            return _components.Values.OfType<T>();
        }

        // 步骤6: 检查是否有组件
        public bool HasComponent<T>() where T : ComponentBase
        {
            return GetComponent<T>() != null;
        }

        // 步骤7: 移除组件
        public void RemoveComponent<T>() where T : ComponentBase
        {
            string typeName = typeof(T).Name;
            if (_components.TryGetValue(typeName, out var component))
            {
                component.QueueFree();
                _components.Remove(typeName);
            }
        }
    }
}

2.2.3 继承 vs 组合的对比

// 文件路径: Scripts/Comparison/InheritanceExample.cs
// 功能说明: 继承方式 - 僵化且难以扩展

using Godot;

namespace GameFramework.Comparison
{
    // 继承层次:Entity -> LivingEntity -> CombatEntity -> Player
    // 问题:如果想让NPC可以战斗但不能被玩家控制,怎么办?
    // 问题:如果想让陷阱可以造成伤害但没有生命值,怎么办?

    public partial class EntityBase : Node2D
    {
        public virtual void Update(double delta) { }
    }

    public partial class LivingEntity : EntityBase
    {
        public int Health { get; protected set; }
        // 所有子类都必须有生命值,即使不需要
    }

    public partial class CombatEntity : LivingEntity
    {
        public int AttackDamage { get; protected set; }
        // 所有子类都必须能攻击,即使不需要
    }

    public partial class PlayerInheritance : CombatEntity
    {
        // 继承了一堆可能不需要的功能
        // 无法在不破坏继承链的情况下移除功能
    }
}
// 文件路径: Scripts/Comparison/CompositionExample.cs
// 功能说明: 组合方式 - 灵活且可复用

using Godot;

namespace GameFramework.Comparison
{
    /// <summary>
    /// 组合方式 - 按需添加功能
    /// </summary>
    public partial class GameEntity : Node2D
    {
        // 核心属性
        public string EntityId { get; set; }
        public string EntityName { get; set; }
    }

    // 玩家:有生命值、能战斗、能被控制
    public partial class PlayerComposition : GameEntity
    {
        // 步骤1: 在编辑器或代码中添加需要的组件
        // - HealthComponent (生命值)
        // - CombatComponent (战斗)
        // - InputComponent (输入)
        // - InventoryComponent (背包)
    }

    // NPC:有生命值、能战斗,但不能被控制
    public partial class NPC : GameEntity
    {
        // 步骤2: 添加需要的组件
        // - HealthComponent
        // - CombatComponent
        // - AIComponent (代替 InputComponent)
    }

    // 陷阱:能造成伤害,但没有生命值
    public partial class Trap : GameEntity
    {
        // 步骤3: 只添加需要的组件
        // - DamageComponent (造成伤害)
        // - TriggerComponent (触发器)
    }

    // 可破坏的箱子:有生命值,但不能战斗
    public partial class BreakableChest : GameEntity
    {
        // 步骤4: 只添加需要的组件
        // - HealthComponent
        // - LootComponent (掉落物品)
    }
}
// 文件路径: Scripts/Comparison/ComparisonTable.cs
// 功能说明: 对比说明(注释形式)

/*
 * 继承 vs 组合 对比
 *
 * ┌─────────────────┬────────────────────┬────────────────────┐
 * │     特性        │       继承         │       组合         │
 * ├─────────────────┼────────────────────┼────────────────────┤
 * │ 耦合度          │ 高(编译时绑定)   │ 低(运行时绑定)   │
 * │ 灵活性          │ 低(固定层级)     │ 高(动态添加)     │
 * │ 代码复用        │ 受限于继承链       │ 高度可复用         │
 * │ 扩展性          │ 需要修改基类       │ 添加新组件即可     │
 * │ 测试难度        │ 困难(依赖关系多) │ 容易(独立测试)   │
 * │ 性能            │ 略好(虚函数调用) │ 稍差(间接调用)   │
 * └─────────────────┴────────────────────┴────────────────────┘
 *
 * 使用建议:
 * - 当存在真正的"is-a"关系时使用继承(如:猫是一种动物)
 * - 当需要复用功能但没有语义上的继承关系时使用组合
 * - Godot 节点系统天然适合组合,优先使用 Node 组合
 * - 使用 C# 接口和抽象类定义契约,具体实现使用组合
 */

namespace GameFramework.Comparison
{
    // 这是一个文档类,用于说明对比
    public static class DesignComparison
    {
        public static void PrintComparison()
        {
            // 输出对比信息
        }
    }
}

2.3 场景组织规范

2.3.1 文件夹结构建议

项目根目录/
├── .godot/                    # Godot 引擎生成的文件
├── Assets/                    # 原始资源文件
│   ├── Audio/                 # 音频源文件
│   ├── Images/                # 图片源文件
│   ├── Models/                # 3D 模型源文件
│   └── Sprites/               # 精灵图源文件
├── Scenes/                    # 场景文件 (.tscn)
│   ├── Levels/                # 关卡场景
│   │   ├── Level_01.tscn
│   │   └── Level_02.tscn
│   ├── UI/                    # UI 场景
│   │   ├── MainMenu.tscn
│   │   ├── HUD.tscn
│   │   └── PauseMenu.tscn
│   ├── Characters/            # 角色场景
│   │   ├── Player.tscn
│   │   └── Enemy/
│   │       ├── Skeleton.tscn
│   │       └── Boss/
│   │           └── Dragon.tscn
│   └── Props/                 # 道具和可交互对象
│       ├── Chest.tscn
│       └── Door.tscn
├── Scripts/                   # C# 脚本文件
│   ├── Core/                  # 核心框架代码
│   │   ├── GameManager.cs
│   │   ├── SceneManager.cs
│   │   └── EventManager.cs
│   ├── Utils/                 # 工具类和扩展
│   │   ├── Extensions/
│   │   ├── Constants.cs
│   │   └── Helpers.cs
│   ├── Systems/               # 游戏系统
│   │   ├── Inventory/
│   │   ├── Quest/
│   │   └── Save/
│   ├── Entities/              # 实体相关
│   │   ├── Player/
│   │   └── Enemy/
│   ├── Components/            # 可复用组件
│   │   ├── HealthComponent.cs
│   │   ├── MovementComponent.cs
│   │   └── WeaponComponent.cs
│   ├── Interfaces/            # 接口定义
│   └── Services/              # 服务层
├── Resources/                 # 游戏内资源 (.tres)
│   ├── Data/                  # 数据资源
│   ├── Materials/             # 材质
│   ├── Shaders/               # 着色器
│   └── Themes/                # UI 主题
├── Plugins/                   # 插件
├── Tests/                     # 测试代码
├── Documentation/             # 文档
└── export_presets.cfg         # 导出配置

2.3.2 命名约定

// 文件路径: Scripts/Utils/NamingConventions.cs
// 功能说明: 命名规范示例

namespace GameFramework.Utils
{
    /// <summary>
    /// 命名规范参考类
    /// </summary>
    public static class NamingConventions
    {
        /*
         * C# 脚本命名规范:
         *
         * 1. 类名:PascalCase
         *    - 正确:PlayerController, GameManager
         *    - 错误:playerController, game_manager
         *
         * 2. 文件名:与类名保持一致
         *    - 正确:PlayerController.cs
         *    - 错误:playercontroller.cs, player_controller.cs
         *
         * 3. 接口名:以 I 开头,后接 PascalCase
         *    - 正确:IDamageable, IInputService
         *    - 错误:Damageable, InputService (无I前缀)
         *
         * 4. 抽象类:Base 后缀或前缀
         *    - 正确:BaseEnemy, EnemyBase
         *    - 也可以不加,通过 abstract 关键字识别
         *
         * 5. 场景文件:PascalCase
         *    - 正确:MainMenu.tscn, PlayerCharacter.tscn
         *    - 错误:main_menu.tscn
         */

        // 步骤1: 私有字段 - _camelCase 前缀下划线
        private int _privateField;

        // 步骤2: 受保护字段 - 同上
        protected float _protectedField;

        // 步骤3: 公共字段 - PascalCase(尽量避免,使用属性)
        public string PublicField;

        // 步骤4: 属性 - PascalCase
        public int Health { get; set; }
        public float Speed { get; private set; }

        // 步骤5: 方法 - PascalCase
        public void TakeDamage(int amount) { }
        public void Heal(int amount) { }

        // 步骤6: 私有方法 - _camelCase 前缀下划线
        private void UpdatePosition() { }

        // 步骤7: 参数 - camelCase
        public void Move(float deltaTime, Vector2 direction) { }

        // 步骤8: 局部变量 - camelCase
        public void Example()
        {
            int localVariable = 0;
            string playerName = "Player";
        }

        // 步骤9: 常量 - 全大写,下划线分隔
        public const int MAX_HEALTH = 100;
        public const float DEFAULT_SPEED = 5.0f;

        // 步骤10: 枚举 - PascalCase,值用PascalCase或全大写
        public enum GameState
        {
            MainMenu,
            Playing,
            Paused,
            GameOver
        }

        public enum DamageType
        {
            PHYSICAL,
            MAGICAL,
            FIRE,
            ICE
        }

        // 步骤11: 事件/信号 - PascalCase,以 EventHandler 结尾
        // 在 Godot 中:[Signal] public delegate void HealthChangedEventHandler();

        // 步骤12: 泛型类型参数 - T 前缀
        public T GetComponent<T>() where T : class { return null; }
    }
}
// 文件路径: Scripts/Utils/FolderNaming.cs
// 功能说明: 文件夹命名规范

namespace GameFramework.Utils
{
    /*
     * 文件夹命名规范:
     *
     * 1. 使用 PascalCase
     *    - 正确:PlayerScripts, UIComponents
     *    - 错误:player_scripts, ui-components
     *
     * 2. 按功能组织而非类型
     *    - 推荐:Player/, Enemy/, UI/
     *    - 避免:Scripts/, Scenes/ (除非这是顶层)
     *
     * 3. 子文件夹命名
     *    - 简短明了
     *    - 复数形式表示集合:Enemies/, Items/
     *    - 单数表示单个实体:Boss/, Player/
     *
     * 4. 命名空间应与文件夹结构对应
     *    - 文件夹:Scripts/Player/
     *    - 命名空间:GameFramework.Player
     *
     * 5. Godot 特定约定
     *    - 节点脚本文件名与节点名一致
     *    - 组件脚本以 Component 结尾
     *    - Manager 类以 Manager 结尾
     */

    public static class FolderNaming
    {
        // 示例结构说明
        public static void PrintStructure()
        {
            // 参考项目根目录的文件夹结构
        }
    }
}

2.3.3 代码风格指南

// 文件路径: Scripts/Utils/CodeStyleGuide.cs
// 功能说明: 代码风格指南示例

using Godot;

namespace GameFramework.Utils
{
    /// <summary>
    /// 代码风格指南 - 完整示例
    /// </summary>
    public partial class CodeStyleGuide : Node
    {
        #region 区域划分

        // 步骤1: 使用区域划分代码块
        // #region Fields
        // #endregion

        #endregion

        #region 字段和属性

        // 步骤2: 常量放在最前面
        public const float DEFAULT_SPEED = 100.0f;
        private const float INVULNERABILITY_TIME = 1.0f;

        // 步骤3: 导出变量分组
        [ExportGroup("Movement Settings")]
        [Export] public float MoveSpeed { get; set; } = DEFAULT_SPEED;
        [Export] public float JumpForce { get; set; } = 400.0f;

        [ExportGroup("Health Settings")]
        [Export] public int MaxHealth { get; set; } = 100;

        // 步骤4: 私有字段
        private int _currentHealth;
        private bool _isInvulnerable;
        private double _invulnerabilityTimer;

        // 步骤5: 属性
        public int CurrentHealth
        {
            get => _currentHealth;
            private set
            {
                _currentHealth = Mathf.Clamp(value, 0, MaxHealth);
                HealthChanged?.Invoke(_currentHealth, MaxHealth);
            }
        }

        #endregion

        #region 事件

        // 步骤6: 事件定义
        public event System.Action<int, int> HealthChanged;
        public event System.Action Died;

        #endregion

        #region Godot 生命周期

        // 步骤7: 生命周期方法按顺序排列
        public override void _EnterTree()
        {
            // 进入树时初始化
            base._EnterTree();
        }

        public override void _Ready()
        {
            // 准备就绪
            base._Ready();
            Initialize();
        }

        public override void _Process(double delta)
        {
            // 每帧处理
            UpdateInvulnerability(delta);
        }

        public override void _PhysicsProcess(double delta)
        {
            // 物理更新
            HandleMovement(delta);
        }

        public override void _ExitTree()
        {
            // 清理
            base._ExitTree();
        }

        #endregion

        #region 公共方法

        // 步骤8: 公共方法要有完整文档注释
        /// <summary>
        /// 对实体造成伤害
        /// </summary>
        /// <param name="damage">伤害数值</param>
        /// <param name="damageSource">伤害来源,用于计算方向</param>
        /// <returns>是否成功造成伤害</returns>
        public bool TakeDamage(int damage, Node2D damageSource = null)
        {
            // 步骤9: 前置条件检查
            if (_isInvulnerable || damage <= 0)
            {
                return false;
            }

            // 步骤10: 执行伤害逻辑
            CurrentHealth -= damage;
            ApplyDamageEffects(damageSource);

            // 步骤11: 启动无敌时间
            if (CurrentHealth > 0)
            {
                StartInvulnerability();
            }
            else
            {
                Die();
            }

            return true;
        }

        #endregion

        #region 私有方法

        // 步骤12: 私有方法使用 _ 前缀
        private void Initialize()
        {
            _currentHealth = MaxHealth;
        }

        private void HandleMovement(double delta)
        {
            // 移动逻辑
        }

        private void UpdateInvulnerability(double delta)
        {
            if (!_isInvulnerable) return;

            _invulnerabilityTimer -= delta;
            if (_invulnerabilityTimer <= 0)
            {
                _isInvulnerable = false;
            }
        }

        private void StartInvulnerability()
        {
            _isInvulnerable = true;
            _invulnerabilityTimer = INVULNERABILITY_TIME;
        }

        private void ApplyDamageEffects(Node2D source)
        {
            // 击退效果等
        }

        private void Die()
        {
            Died?.Invoke();
            QueueFree();
        }

        #endregion

        #region 格式化规则

        /*
         * 代码格式化规则:
         *
         * 1. 缩进
         *    - 使用 4 个空格(不要混用 Tab)
         *
         * 2. 大括号
         *    - 类/方法的大括号在新行
         *    - if/for/while 的大括号在新行
         *    - 单行语句可以省略大括号,但不推荐
         *
         * 3. 空行
         *    - 方法之间空一行
         *    - 逻辑块之间空一行
         *    - #region 前后空一行
         *
         * 4. 行长度
         *    - 建议不超过 120 字符
         *    - 长参数列表适当换行
         *
         * 5. 空格
         *    - 运算符两侧加空格:a + b
         *    - 逗号后加空格
         *    - 冒号后加空格(类型声明)
         *
         * 6. 注释
         *    - XML 文档注释用于公共 API
         *    - // 用于实现注释
         *    - /* */ 用于多行注释或禁用代码
         */

        #endregion
    }
}

小结

本章介绍了游戏框架设计的核心原则:

  1. SOLID 原则 提供了设计可维护代码的理论基础:

    • SRP 确保类职责单一
    • OCP 支持功能扩展
    • LSP 保证多态正确
    • ISP 避免接口臃肿
    • DIP 降低模块耦合
  2. 组合优于继承 是 Godot 开发的核心理念:

    • 使用节点组合构建功能
    • 组件化设计提高复用性
    • 灵活应对需求变化
  3. 规范的组织结构 是团队协作的基础:

    • 清晰的文件夹结构
    • 统一的命名约定
    • 一致的代码风格

在下一章中,我们将基于这些原则,深入探讨 Godot C# 的具体实现技术。