第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/
命名规范
| 类型 | 命名规则 | 示例 |
|---|---|---|
| 类名 | PascalCase | PlayerController |
| 接口 | PascalCase + I前缀 | ISaveable, IDamageable |
| 方法 | PascalCase | TakeDamage(), MoveTo() |
| 属性 | PascalCase | Health, MaxSpeed |
| 私有字段 | _camelCase | _currentHealth, _isDead |
| 常量 | UPPER_SNAKE_CASE | MAX_HEALTH, DEFAULT_SPEED |
| 枚举 | PascalCase + 名词 | GameState, WeaponType |
| 信号 | PascalCase + EventHandler后缀 | HealthChangedEventHandler |
命名建议
- 清晰优先:
CalculateDistance优于CalcDist - 避免缩写:
PlayerController优于PlayerCtrl - 布尔属性:使用
Is/Can/Has前缀,如IsDead,CanMove - 方法动词:使用动作动词,如
Load(),Save(),Initialize() - 避免魔法数字:使用常量替代裸数字
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
}
}
小结
本章介绍了游戏框架设计的核心原则:
SOLID 原则 提供了设计可维护代码的理论基础:
- SRP 确保类职责单一
- OCP 支持功能扩展
- LSP 保证多态正确
- ISP 避免接口臃肿
- DIP 降低模块耦合
组合优于继承 是 Godot 开发的核心理念:
- 使用节点组合构建功能
- 组件化设计提高复用性
- 灵活应对需求变化
规范的组织结构 是团队协作的基础:
- 清晰的文件夹结构
- 统一的命名约定
- 一致的代码风格
在下一章中,我们将基于这些原则,深入探讨 Godot C# 的具体实现技术。