公司动态

Godot 4 C# 2D游戏开发实战:从节点系统到物理交互的七个项目

📅 2026/8/18 20:21:44
Godot 4 C# 2D游戏开发实战:从节点系统到物理交互的七个项目
在上一篇文章中我们搭建了 Godot 4 的 C# 开发环境并初步探索了引擎界面和 GDScript 脚本。本篇我们将深入核心聚焦于 C# 脚本编程、节点系统的灵活运用以及物理交互的实现并通过七个由浅入深的实战项目手把手带你从零到一掌握 Godot 4 的 2D 游戏开发全流程。无论你是 Unity 开发者想尝试新引擎还是游戏开发新手这篇详尽的指南都将为你铺平道路。1. 环境准备与项目创建回顾在开始编码之前确保你的开发环境已就绪。Godot 4 对 .NET 的支持是其一大亮点让熟悉 C# 的开发者能更顺畅地过渡。1.1 环境确认Godot 版本请确保你安装的是 Godot 4.2 或更高版本稳定版。你可以在 Godot 官网或 Steam 上获取。.NET SDKGodot 4 需要 .NET 6.0 或 .NET 7/8 的 SDK 支持。建议安装最新的 .NET 8 SDK以获得最佳兼容性和性能。安装后在命令行输入dotnet --version确认。IDE/编辑器虽然 Godot 内置了脚本编辑器但对于 C# 开发强烈推荐使用Visual Studio Code配合 C# 扩展或JetBrains Rider。它们能提供代码补全、调试、重构等强大功能。1.2 创建你的第一个 C# 项目打开 Godot 编辑器点击“新建项目”。输入项目名称如MyFirstGodotCSharpGame并选择空文件夹作为项目路径。在“渲染器”选项中对于 2D 游戏选择“兼容性”或“移动端”渲染器通常能获得更好的兼容性“向前”渲染器功能更强大但对硬件要求稍高。初学者可选择“兼容性”。关键步骤在“项目配置”区域找到“.NET”选项务必勾选“启用 .NET”。这是使用 C# 的前提。点击“创建并编辑”Godot 会生成项目文件其中包含一个project.godot配置文件和scenes、scripts等初始文件夹。项目创建后你会在文件系统面板中看到一个MyFirstGodotCSharpGame.csproj文件这表明 .NET 项目已成功初始化。2. Godot 节点系统与场景树核心概念Godot 采用独特的“场景Scene”和“节点Node”架构这是理解其一切工作的基础。2.1 节点一切皆节点在 Godot 中所有事物都是节点。一个节点可以代表一个精灵Sprite2D、一个碰撞体CollisionShape2D、一个摄像机Camera2D、一个声音播放器AudioStreamPlayer甚至是一个逻辑控制器。节点通过继承树组织功能。2.2 场景节点的容器场景是一个或多个节点组成的树状结构保存为一个.tscn文件。一个场景可以是一个角色、一个关卡、一个 UI 界面。游戏就是由这些场景实例化并组织起来的。2.3 场景树运行时的层次结构当游戏运行时当前活动的场景会被加载到“场景树”中。节点在树中的位置决定了它们的处理顺序如_Process调用顺序和变换位置、旋转、缩放的继承关系。子节点的变换是基于父节点的。理解示例一个“玩家”场景可能由以下节点树构成CharacterBody2D (根节点负责移动和物理) ├── Sprite2D (显示外观) ├── CollisionShape2D (定义碰撞形状) └── Camera2D (摄像机跟随)在 C# 脚本中我们可以通过GetNode()或使用[Export]特性并拖拽的方式来获取和操作这些子节点。3. C# 脚本编程基础与 Godot API 集成Godot 的 C# API 与 GDScript 的 API 高度对应但遵循 C# 的命名规范PascalCase。3.1 创建并附加 C# 脚本在场景中选中一个节点例如一个CharacterBody2D。在右侧检查器面板中找到“脚本”属性点击“新建”选择“C# Script”。输入脚本名称如PlayerControllerGodot 会自动生成一个 C# 类文件。生成的脚本模板如下using Godot; public partial class PlayerController : CharacterBody2D { // 在编辑器中可调整的变量 [Export] public int Speed { get; set; } 400; // 当节点进入场景树时调用类似构造函数但用于Godot对象初始化 public override void _Ready() { GD.Print(Player controller is ready!); } // 每帧调用一次。‘delta’是上一帧到当前帧的时间秒。 public override void _Process(double delta) { } // 每物理帧调用一次。‘delta’是固定的物理步长时间。 public override void _PhysicsProcess(double delta) { } }partial classGodot 会自动生成另一部分代码来处理与引擎的集成。继承自CharacterBody2D表示这个脚本控制一个 2D 角色体。[Export]特性使变量出现在 Godot 编辑器的检查器中方便设计和调整无需修改代码。_Ready()用于初始化例如获取子节点引用、加载资源。_Process(double delta)用于与帧率相关的逻辑如输入检测非物理、UI 动画。_PhysicsProcess(double delta)用于与物理相关的逻辑如移动、力施加。delta是固定的默认为 1/60 秒保证物理模拟的稳定性。3.2 获取节点引用操作其他节点是游戏逻辑的核心。有两种主要方式方式一使用GetNodeT()路径查找private Sprite2D _mySprite; private Camera2D _camera; public override void _Ready() { // 获取当前节点下的名为“Sprite2D”的子节点 _mySprite GetNodeSprite2D(Sprite2D); // 获取兄弟节点或更复杂路径 _camera GetNodeCamera2D(../Camera2D); // “..”表示父节点 }方式二使用[Export]特性与编辑器拖拽推荐[Export] private Sprite2D _mySprite; // 在编辑器中拖拽赋值 [Export] private Camera2D _camera;在编辑器中脚本附加到节点后这些属性会出现。你可以直接从场景树中将对应的节点拖放到检查器的属性框内。这种方式解耦了代码和节点路径更灵活。3.3 处理输入Godot 提供了强大的输入映射系统。建议在“项目设置 - 输入映射”中定义抽象的动作如move_left,jump然后在代码中检测这些动作。public override void _PhysicsProcess(double delta) { // 获取输入向量-1 到 1 Vector2 inputDirection Input.GetVector(move_left, move_right, move_up, move_down); // 计算速度 Velocity inputDirection * Speed; // 应用移动。MoveAndSlide()会自动处理碰撞。 MoveAndSlide(); }Input.GetVector()是处理 2D 方向输入的便捷方法。4. 实战项目一可移动的方块基础移动与碰撞目标创建一个受键盘控制的方块能在屏幕内移动并与墙壁碰撞。步骤创建场景新建一个 2D 场景。根节点类型选择CharacterBody2D命名为PlayerBox。添加子节点添加一个Sprite2D节点。在检查器中点击“纹理”选择“新建 SpriteFrames”然后可以添加一个简单的矩形纹理或使用 Godot 内置的图标。添加一个CollisionShape2D节点。在检查器中为其“形状”属性新建一个RectangleShape2D并调整大小匹配 Sprite。附加 C# 脚本选中PlayerBox根节点附加一个新的 C# 脚本命名为PlayerBoxController。编写移动逻辑using Godot; public partial class PlayerBoxController : CharacterBody2D { [Export] public int Speed { get; set; } 300; public override void _PhysicsProcess(double delta) { Vector2 inputDirection Input.GetVector(move_left, move_right, move_up, move_down); Velocity inputDirection * Speed; MoveAndSlide(); } }创建墙壁新建一个StaticBody2D场景作为墙壁为其添加CollisionShape2D矩形。实例化多个该场景摆放在玩家方块周围。配置输入映射打开“项目设置 - 输入映射”添加四个动作move_left,move_right,move_up,move_down。为每个动作分配键盘按键如 A/D/W/S 或方向键。运行将PlayerBox场景设为主场景拖到“主场景”按钮上按 F5 运行。你的方块应该可以移动并与墙壁碰撞了。5. 实战项目二平台跳跃角色重力、跳跃与状态目标创建一个可以跑跳的 2D 角色实现重力加速度、跳跃和落地检测。步骤创建角色场景根节点为CharacterBody2D添加Sprite2D和CollisionShape2D胶囊形或矩形。编写角色控制器脚本using Godot; public partial class Player : CharacterBody2D { // 导出变量方便在编辑器中调整 [Export] public float Speed 300.0f; [Export] public float JumpVelocity -400.0f; [Export] public float Gravity 980.0f; // 重力加速度 // 获取动画播放器引用后续添加动画时用 [Export] private AnimatedSprite2D _animatedSprite; public override void _PhysicsProcess(double delta) { Vector2 velocity Velocity; // 1. 添加重力 if (!IsOnFloor()) velocity.Y Gravity * (float)delta; // 2. 处理跳跃 if (Input.IsActionJustPressed(jump) IsOnFloor()) { velocity.Y JumpVelocity; GD.Print(Jump!); } // 3. 处理水平移动 float direction Input.GetAxis(move_left, move_right); // GetAxis 返回 -1 到 1 if (direction ! 0) { velocity.X direction * Speed; // 可选翻转精灵朝向 if (_animatedSprite ! null) _animatedSprite.FlipH direction 0; } else { velocity.X Mathf.MoveToward(Velocity.X, 0, Speed); // 平滑停止 } Velocity velocity; MoveAndSlide(); } }关键点解析IsOnFloor()CharacterBody2D提供的方法用于检测角色是否站在“地板”上即与下方的碰撞体接触。这是实现“仅在地面可跳”的关键。Input.IsActionJustPressed(“jump”)检测动作是否在当前帧刚刚被按下避免长按连续跳跃。Mathf.MoveToward()一个非常有用的函数用于将当前值平滑地向目标值移动常用于实现惯性或平滑停止。创建平台使用StaticBody2D或TileMap节点创建一些平台。运行测试配置好jump输入动作后运行角色应能受重力下落在平台上行走和跳跃。6. 实战项目三发射子弹实例化场景与信号目标让角色按下按键时向鼠标方向发射子弹。步骤创建子弹场景新建场景根节点为Area2D用于检测击中或RigidBody2D如果需要物理模拟。添加Sprite2D和CollisionShape2D。为根节点附加脚本Bullet.cs控制其移动和生命周期。// Bullet.cs using Godot; public partial class Bullet : Area2D { [Export] public float Speed 600f; [Export] public float Lifetime 2f; // 2秒后自动消失 private Vector2 _direction Vector2.Right; public void Initialize(Vector2 direction) { _direction direction.Normalized(); // 让子弹朝向移动方向 Rotation _direction.Angle(); } public override void _Ready() { // 设置一个定时器生命周期结束后删除自己 Timer timer new Timer(); AddChild(timer); timer.WaitTime Lifetime; timer.Timeout () QueueFree(); // QueueFree()是安全的删除节点方法 timer.Start(); } public override void _PhysicsProcess(double delta) { Position _direction * Speed * (float)delta; } // 当子弹进入另一个Area2D或PhysicsBody2D时触发 private void OnBodyEntered(Node2D body) { GD.Print($Bullet hit: {body.Name}); QueueFree(); // 击中后消失 } }别忘了在子弹场景的Area2D节点上连接其body_entered信号到脚本的OnBodyEntered方法。修改玩家脚本以发射子弹// 在Player类中添加 [Export] private PackedScene _bulletScene; // 在编辑器中拖入子弹场景 [Export] private Node2D _firePoint; // 一个Marker2D节点作为子弹生成位置 public override void _Process(double delta) { if (Input.IsActionJustPressed(shoot) _bulletScene ! null _firePoint ! null) { // 实例化子弹 Bullet newBullet _bulletScene.InstantiateBullet(); // 计算朝向鼠标的方向 Vector2 mousePos GetGlobalMousePosition(); Vector2 direction (mousePos - _firePoint.GlobalPosition).Normalized(); // 初始化子弹 newBullet.Initialize(direction); newBullet.GlobalPosition _firePoint.GlobalPosition; // 将子弹添加到场景树中通常添加到当前场景的根节点下 GetTree().CurrentScene.AddChild(newBullet); } }设置在玩家场景中添加一个Marker2D节点作为_firePoint。在玩家脚本的检查器面板中将子弹场景.tscn文件拖到_bulletScene属性将Marker2D节点拖到_firePoint属性。运行现在点击鼠标或按下射击键角色应该会向鼠标位置发射子弹。7. 实战项目四简单 UI 与分数系统Label、信号通信目标创建一个显示分数和生命值的 UI当子弹击中敌人时更新分数。步骤创建 UI 场景新建一个CanvasLayer场景CanvasLayer确保 UI 渲染在最上层。在其下添加Label节点来显示分数和生命值。编写 UI 脚本// UIManager.cs using Godot; public partial class UIManager : CanvasLayer { private Label _scoreLabel; private Label _healthLabel; private int _score 0; private int _health 100; public override void _Ready() { _scoreLabel GetNodeLabel(ScoreLabel); _healthLabel GetNodeLabel(HealthLabel); UpdateUI(); } public void AddScore(int points) { _score points; UpdateUI(); } public void TakeDamage(int damage) { _health - damage; if (_health 0) { GD.Print(Game Over!); // 触发游戏结束逻辑 } UpdateUI(); } private void UpdateUI() { _scoreLabel.Text $Score: {_score}; _healthLabel.Text $Health: {_health}; } }使用信号进行通信松耦合与其让子弹或敌人直接查找UIManager不如使用 Godot 强大的信号系统。在Bullet.cs中定义信号public delegate void HitSomethingEventHandler(int points);并声明事件[Signal] public event HitSomethingEventHandler HitSomething;Godot 4 C# 中更常用[Signal]特性。但更 Godot 风格的方式是在敌人或得分管理器上定义信号。例如创建一个全局的GameEvents单例Autoload来中转信号这是更清晰的做法。使用 Autoload单例简化全局通信创建脚本GameEvents.cs// GameEvents.cs using Godot; public partial class GameEvents : Node { [Signal] public delegate void ScoreUpdatedEventHandler(int newScore); [Signal] public delegate void PlayerHealthChangedEventHandler(int newHealth); private static GameEvents _instance; public static GameEvents Instance _instance; public override void _Ready() { if (_instance ! null) { QueueFree(); // 防止重复 return; } _instance this; } // 提供静态方法方便调用 public static void EmitScoreUpdated(int score) Instance?.EmitSignal(SignalName.ScoreUpdated, score); public static void EmitPlayerHealthChanged(int health) Instance?.EmitSignal(SignalName.PlayerHealthChanged, health); }* 在“项目设置 - Autoload”中将 GameEvents 脚本添加为自动加载的单例路径为 /root/GameEvents。 * 在 UIManager 的 _Ready 中连接信号public override void _Ready() { _scoreLabel GetNodeLabel(ScoreLabel); _healthLabel GetNodeLabel(HealthLabel); GameEvents.Instance.ScoreUpdated OnScoreUpdated; GameEvents.Instance.PlayerHealthChanged OnPlayerHealthChanged; UpdateUI(); } private void OnScoreUpdated(int newScore) { _score newScore; UpdateUI(); } private void OnPlayerHealthChanged(int newHealth) { _health newHealth; UpdateUI(); }* 当子弹击中敌人时调用 GameEvents.EmitScoreUpdated(100);。这样UI 管理器会自动更新而子弹和敌人之间没有任何直接引用耦合度极低。8. 实战项目五敌人 AI状态机与寻路目标创建一个简单的敌人在玩家进入一定范围后追逐玩家。步骤创建敌人场景根节点使用CharacterBody2D或Area2D添加外观和碰撞体。编写简单 AI 脚本使用一个简单的状态机枚举和RayCast2D或Area2D进行感知。// SimpleEnemy.cs using Godot; public partial class SimpleEnemy : CharacterBody2D { public enum State { Idle, Chase } private State _currentState State.Idle; [Export] private float _speed 150f; [Export] private float _detectionRadius 200f; // 检测半径 [Export] private Area2D _detectionArea; // 一个圆形Area2D用于检测玩家 private Node2D _targetPlayer; public override void _Ready() { if (_detectionArea ! null) { _detectionArea.BodyEntered OnDetectionAreaBodyEntered; _detectionArea.BodyExited OnDetectionAreaBodyExited; } } public override void _PhysicsProcess(double delta) { switch (_currentState) { case State.Idle: Velocity Vector2.Zero; break; case State.Chase: if (_targetPlayer ! null) { // 计算朝向玩家的方向并移动 Vector2 direction (_targetPlayer.GlobalPosition - GlobalPosition).Normalized(); Velocity direction * _speed; MoveAndSlide(); } break; } } private void OnDetectionAreaBodyEntered(Node2D body) { if (body.IsInGroup(player)) // 给玩家节点添加到“player”组 { _targetPlayer body; _currentState State.Chase; GD.Print(Enemy detected player!); } } private void OnDetectionAreaBodyExited(Node2D body) { if (body _targetPlayer) { _targetPlayer null; _currentState State.Idle; GD.Print(Enemy lost player.); } } }设置在敌人场景下添加一个Area2D为其添加一个CollisionShape2D圆形调整大小作为检测范围。将该节点拖到脚本的_detectionArea属性。给玩家根节点在检查器中添加一个“组”Groups命名为player。进阶寻路对于更复杂的关卡可以使用 Godot 内置的NavigationRegion2D和NavigationAgent2D节点来实现网格导航寻路这是制作 RTS 或 RPG 游戏敌人 AI 的利器。9. 实战项目六音效与背景音乐AudioStreamPlayer目标为跳跃、射击等动作添加音效并播放背景音乐。步骤准备音频文件将.wav或.ogg格式的音频文件导入项目建议放在res://assets/audio/文件夹。添加音效播放器一次性音效如跳跃、射击使用AudioStreamPlayer节点。将其添加到需要播放音效的场景中如玩家场景。背景音乐通常添加到主场景或一个全局的AudioStreamPlayer节点可设为 Autoload并设置为循环播放。在代码中播放// 在玩家脚本中 [Export] private AudioStreamPlayer _jumpSoundPlayer; private void TryJump() { if (IsOnFloor() Input.IsActionJustPressed(jump)) { velocity.Y JumpVelocity; _jumpSoundPlayer?.Play(); // 播放跳跃音效 } } // 在游戏主场景或一个管理器中 [Export] private AudioStreamPlayer _backgroundMusicPlayer; public override void _Ready() { _backgroundMusicPlayer.Play(); }音量与总线Godot 有强大的音频总线系统。你可以在“音频”面板中创建不同的总线如SFX、Music、Master并调整音量、添加效果如混响。然后在代码或编辑器中将AudioStreamPlayer分配到对应的总线。10. 实战项目七打包与发布导出项目目标将你的游戏打包成可执行文件分享给他人。步骤准备图标准备一个 1024x1024 或 512x512 的 PNG 图标命名为icon.png放在项目根目录Godot 会自动识别。配置导出预设点击顶部菜单“项目” - “导出...”。点击“添加...”选择目标平台如“Windows 桌面”、“macOS”、“Linux/X11”、“Android”、“Web”等。对于每个平台你需要配置一些选项比如应用程序名称、版本、图标等。对于 Windows你可能需要下载一个rcedit工具来嵌入图标Godot 会提示下载。处理依赖确保所有资源文件如图片、声音、字体的路径正确并且已包含在项目中。Godot 默认只会导出在项目中引用的资源。执行导出选择好预设点击“导出项目”。Godot 会生成一个可执行文件如.exe和一个_data文件夹或一个独立的.pck文件它们需要放在一起。测试在非开发环境的机器上运行导出的可执行文件确保所有功能正常。11. 常见问题与排查思路在开发过程中你可能会遇到一些典型问题。以下是一个快速排查指南问题现象可能原因解决思路C# 脚本无法附加或编译错误.NET SDK 未安装或版本不匹配项目未启用 .NET。1. 确认安装 .NET 6/7/8 SDK。2. 检查project.godot中[mono]部分配置。3. 在 Godot 编辑器中点击“工具”-“C#”-“创建解决方案”并重新加载。GetNode()返回null节点路径错误节点尚未添加到场景树脚本在_Ready()之前访问节点。1. 使用onready注解GDScript或在_Ready()中获取节点C#。2. 使用[Export]特性并在编辑器中拖拽赋值。3. 打印或调试检查路径字符串。物理碰撞不工作碰撞层Layer和掩码Mask未设置碰撞形状大小/位置不对节点类型错误如用了Area2D而非PhysicsBody2D。1. 检查碰撞体节点的“层”和“掩码”属性确保发生碰撞的双方在对方的掩码中。2. 在场景编辑器中可视化碰撞形状按 F3 进入调试模式。3. 确保移动使用的是MoveAndSlide()或MoveAndCollide()。输入无响应输入映射未正确配置在错误的回调中处理输入如应在_Process中处理射击在_PhysicsProcess中处理移动。1. 检查“项目设置”-“输入映射”中的动作和按键绑定。2. 使用Input.IsActionJustPressed()处理单次触发Input.IsActionPressed()处理持续按下。3. 区分逻辑帧和物理帧的处理位置。游戏运行缓慢每帧绘制调用Draw Call过多物理计算过于复杂脚本中存在性能瓶颈如频繁实例化、未释放资源。1. 使用TileMap替代大量单独的Sprite2D。2. 使用Occluder进行遮挡剔除2D。3. 在_Process中避免复杂计算使用_PhysicsProcess处理物理。4. 使用 Godot 的性能分析器调试器 - 分析器。12. 最佳实践与工程建议掌握基础后遵循一些最佳实践能让你的项目更健壮、更易维护。场景组织模块化将角色、敌人、道具、UI 元素都做成独立的、可复用的场景.tscn。场景继承Godot 支持场景继承。可以创建一个“基础敌人”场景然后创建特定类型的敌人继承并扩展它。使用组Groups给节点分配组如enemies,collectibles便于批量查找和操作GetTree().GetNodesInGroup(enemies)。代码结构信号驱动多用信号少用直接引用。这能极大降低场景和脚本之间的耦合度提高可复用性。Autoload 单例将游戏状态管理器、音频管理器、事件总线等全局对象设置为 Autoload。资源管理将常量如速度、血量定义为[Export]变量或在单独的Constants类中。使用Resource类来创建可配置的数据对象如武器属性、敌人属性。资源管理纹理图集将多个小图片打包成一个大的纹理图集可以减少 Draw Call提升渲染性能。音频格式使用.ogg格式用于音乐和长音效.wav用于短促音效。注意压缩质量。版本控制使用 Git 等版本控制系统管理你的项目。将scenes/,scripts/,assets/等文件夹纳入管理但忽略.import/文件夹和二进制导出文件。性能优化可见性通知对于屏幕外的对象可以利用VisibilityNotifier2D节点当其不可见时暂停处理逻辑或动画。对象池对于需要频繁创建和销毁的对象如子弹、粒子实现一个简单的对象池来复用实例避免频繁的内存分配和垃圾回收。合理使用物理不是所有东西都需要物理模拟。对于简单的移动直接修改Position可能更高效。Godot 4 与 C# 的组合为 2D 游戏开发提供了一个强大、高效且免费的开源选择。通过这七个实战项目的练习你应该已经掌握了从环境搭建、核心概念理解到完整功能实现的全流程。关键在于多动手、多实验。从模仿这些小项目开始逐步加入自己的想法你很快就能创造出属于自己的独特游戏世界。Godot 社区非常活跃遇到问题时官方文档、QA 社区和 GitHub 议题都是宝贵的资源。