ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

C#游戏开发实战:从零构建游戏引擎与核心机制

2026/8/9 12:25:26 拓冰建站 浏览量
C#游戏开发实战:从零构建游戏引擎与核心机制

1. 项目概述:为什么选择C#作为游戏开发的“白骑士”?

如果你正在寻找一门既能让你快速上手,又能支撑起从2D小游戏到3A级大作核心逻辑的语言,C# 绝对是一个被低估的“白骑士”。很多人一提到游戏开发,第一反应是C++或者Unity里的可视化脚本,但C#凭借其在.NET生态下的独特优势,正在成为独立开发者和大型工作室都青睐的利器。这个项目,我们就来实战拆解,如何用纯粹的C#,不依赖重型游戏引擎,从零开始构建一个可玩的游戏原型。这不仅仅是学习语法,更是理解游戏循环、资源管理、事件驱动这些核心概念的过程,无论你未来是使用Unity、Godot,还是自研引擎,这些底层知识都将让你受益匪浅。

C# 的强类型、优雅的语法和强大的.NET库,让它特别适合构建结构清晰、易于维护的游戏逻辑。从处理玩家输入、管理游戏状态、到实现简单的物理碰撞,我们都可以用C# 一步步实现。本次实战,我们将聚焦于一个经典的2D场景——比如一个简单的“飞机大战”或者“贪吃蛇”变体——来贯穿所有知识点。目标是让你在动手之后,能清晰地回答:一个游戏是如何一帧一帧跑起来的?数据如何驱动画面变化?以及,当你想为游戏添加一个新功能时,从代码层面应该如何思考。

2. 核心架构设计:理解游戏的主循环与模块分离

在动手写第一行代码之前,我们必须先搭好房子的骨架。一个结构混乱的游戏项目就像一团乱麻,添加新功能会举步维艰。核心在于理解并实现两个概念:游戏主循环(Game Loop)模块化分离

2.1 游戏主循环:一切动起来的引擎

游戏主循环是游戏程序的心跳。它的任务非常简单:在每一帧里,依次处理输入、更新游戏状态、渲染画面,然后等待下一帧开始。在C#控制台或简单的图形界面中,我们需要手动实现这个循环。

一个最基础的循环结构如下所示:

// 这是一个简化的概念代码,实际实现会因渲染方式不同而差异巨大 bool isRunning = true; while (isRunning) { // 1. 处理输入:读取键盘、鼠标事件 ProcessInput(); // 2. 更新游戏状态:计算物体位置、检测碰撞、更新分数等 UpdateGameLogic(DeltaTime); // DeltaTime是关键! // 3. 渲染:将最新的游戏状态绘制到屏幕上 Render(); // 4. 控制帧率:避免循环跑得太快耗尽CPU Thread.Sleep(16); // 粗略实现60帧/秒 (1000ms / 60 ≈ 16ms) }

这里的关键是DeltaTime(增量时间)。它代表上一帧到这一帧所经过的真实时间(通常以秒为单位)。为什么需要它?想象一下,如果你的游戏在一台很慢的电脑上运行,Update调用一次可能花了0.1秒,而在快的电脑上只花了0.01秒。如果不使用DeltaTime,那么慢电脑上的游戏对象移动速度会只有快电脑的十分之一,这显然不公平。通过将物体的速度乘以DeltaTime,我们确保物体的移动是基于真实时间,而非帧数,从而实现帧率无关的平滑运动

注意:上面用Thread.Sleep控制帧率的方式非常粗糙,会阻塞线程且不精确。在实际项目中,我们会使用更精确的计时器,例如.NET中的Stopwatch类来高精度计算DeltaTime,或者直接使用游戏框架(如MonoGame)内置的计时系统。

2.2 模块化设计:高内聚,低耦合

我们不能把所有代码都塞进主循环里。清晰的模块划分能让代码易于阅读、调试和扩展。对于一个最小化的2D游戏,我通常建议分为以下几个核心模块:

  1. GameEngine核心引擎类:负责驱动主循环,管理游戏状态(开始、运行、暂停、结束),并作为其他模块的协调中心。
  2. InputManager输入管理器:封装所有输入设备(键盘、鼠标)的查询逻辑。外部模块只需询问“A键是否被按下?”,而不需要知道底层是如何检测的。
  3. SceneManager场景管理器:游戏可能有开始菜单、游玩关卡、结束画面等不同场景。这个管理器负责加载、切换和卸载场景。
  4. Entity/GameObject游戏对象基类:游戏中的所有实体(玩家、敌人、子弹、道具)都应继承自一个共同的基类。这个基类至少包含位置、速度、更新(Update)和渲染(Draw)方法。
  5. PhysicsSystem简易物理系统(可选但推荐):处理碰撞检测与响应。即使是简单的矩形(AABB)碰撞检测,单独封装成一个系统也大有裨益。
  6. ContentManager资源管理器:负责加载和缓存图片、音效、字体等资源,避免重复加载。

这样的设计遵循了“单一职责原则”。当你想修改碰撞逻辑时,只需去PhysicsSystem;想添加新的输入设备支持,就修改InputManager。它们之间通过定义良好的接口(如事件、委托或简单的方法调用)进行通信,耦合度很低。

3. 实战环境搭建与基础框架构建

理论说再多不如动手。我们选择.NET 6/8 控制台应用作为起点,并使用Raylib-cs这个优秀的C#绑定库来进行图形渲染。为什么是Raylib?因为它轻量、跨平台(Windows, macOS, Linux)、API直观,且完全开源,能让我们专注于游戏逻辑而非复杂的图形API。

3.1 项目初始化与依赖安装

首先,确保你安装了最新版的.NET SDK。然后通过命令行创建项目并添加Raylib依赖:

# 创建一个新的控制台项目 dotnet new console -n CSharpGameDev cd CSharpGameDev # 添加 Raylib-cs 的 NuGet 包引用 dotnet add package Raylib-cs

接下来,我们需要修改项目文件(.csproj),确保生成的是原生可执行文件,这对于发布游戏很重要:

<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>net8.0</TargetFramework> <!-- 发布为独立应用,包含运行时 --> <PublishSingleFile>true</PublishSingleFile> <SelfContained>true</SelfContained> <!-- 重要:启用原生AOT编译预览,可以显著减小最终exe体积并提升启动速度 --> <PublishAot>true</PublishAot> </PropertyGroup> <ItemGroup> <PackageReference Include="Raylib-cs" Version="5.0.0" /> </ItemGroup> </Project>

3.2 创建游戏引擎骨架

现在,在Program.cs中,我们将创建最基础的游戏引擎骨架。这个骨架实现了主循环和模块化的雏形。

using Raylib_cs; using System.Numerics; namespace CSharpGameDev { // 游戏状态枚举 public enum GameState { Menu, Playing, Paused, GameOver } public class GameEngine { private bool _isRunning; private GameState _currentState; private float _deltaTime; private long _lastUpdateTime; // 核心模块的引用(这里先以属性占位,后续实例化) public InputManager Input { get; private set; } public Scene CurrentScene { get; private set; } public void Run() { Init(); GameLoop(); Cleanup(); } private void Init() { // 初始化窗口,设置一个简单的640x480窗口 const int screenWidth = 800; const int screenHeight = 450; Raylib.InitWindow(screenWidth, screenHeight, "白骑士的C#游戏实战"); Raylib.SetTargetFPS(60); // 设置目标帧率,Raylib会帮我们控制循环速度 // 初始化模块 Input = new InputManager(); // 初始化一个默认场景(后续会由SceneManager管理) CurrentScene = new GamePlayScene(this); _isRunning = true; _currentState = GameState.Playing; _lastUpdateTime = Raylib.GetTime(); // Raylib提供的高精度时间 } private void GameLoop() { while (_isRunning && !Raylib.WindowShouldClose()) // 主循环条件 { // 计算增量时间 DeltaTime (以秒为单位) long currentTime = Raylib.GetTime(); _deltaTime = (currentTime - _lastUpdateTime); _lastUpdateTime = currentTime; // 1. 处理输入 Input.Update(); // 更新输入状态 // 2. 更新游戏逻辑 Update(_deltaTime); // 3. 渲染 Raylib.BeginDrawing(); Raylib.ClearBackground(Color.RayWhite); // 清空背景色 Render(); Raylib.EndDrawing(); } } private void Update(float deltaTime) { // 根据游戏状态,决定更新什么 switch (_currentState) { case GameState.Playing: CurrentScene?.Update(deltaTime); // 示例:按ESC键暂停 if (Input.IsKeyPressed(KeyboardKey.KEY_ESCAPE)) { _currentState = GameState.Paused; } break; case GameState.Paused: if (Input.IsKeyPressed(KeyboardKey.KEY_ESCAPE)) { _currentState = GameState.Playing; } break; // ... 其他状态处理 } } private void Render() { CurrentScene?.Render(); // 可以在这里绘制UI,比如在暂停状态显示“PAUSED” if (_currentState == GameState.Paused) { Raylib.DrawText("PAUSED", 350, 200, 40, Color.DarkGray); } } private void Cleanup() { CurrentScene?.Unload(); // 卸载场景资源 Raylib.CloseWindow(); } public void Quit() { _isRunning = false; } } // 输入管理器简化版 public class InputManager { // 存储当前帧和上一帧的按键状态,用于检测“按下瞬间” private HashSet<KeyboardKey> _currentKeys = new HashSet<KeyboardKey>(); private HashSet<KeyboardKey> _previousKeys = new HashSet<KeyboardKey>(); public void Update() { // 将当前帧状态存入“上一帧” _previousKeys = new HashSet<KeyboardKey>(_currentKeys); _currentKeys.Clear(); // 获取当前所有被按下的键(这是一个简化处理,实际可能需要遍历所有关心的键) // 这里我们只检查几个示例键 var keysToCheck = new[] { KeyboardKey.KEY_W, KeyboardKey.KEY_A, KeyboardKey.KEY_S, KeyboardKey.KEY_D, KeyboardKey.KEY_SPACE, KeyboardKey.KEY_ESCAPE }; foreach (var key in keysToCheck) { if (Raylib.IsKeyDown(key)) { _currentKeys.Add(key); } } } // 判断按键是否在当前帧被按下(瞬间触发) public bool IsKeyPressed(KeyboardKey key) { return _currentKeys.Contains(key) && !_previousKeys.Contains(key); } // 判断按键是否被持续按住 public bool IsKeyDown(KeyboardKey key) { return _currentKeys.Contains(key); } // 判断按键是否被释放 public bool IsKeyReleased(KeyboardKey key) { return !_currentKeys.Contains(key) && _previousKeys.Contains(key); } } // 场景基类 public abstract class Scene { protected GameEngine Engine; public Scene(GameEngine engine) { Engine = engine; } public abstract void Update(float deltaTime); public abstract void Render(); public virtual void Unload() { } } // 游戏主场景(示例) public class GamePlayScene : Scene { private Vector2 _playerPosition; private const float PlayerSpeed = 200.0f; // 像素/秒 public GamePlayScene(GameEngine engine) : base(engine) { _playerPosition = new Vector2(400, 225); // 屏幕中心 } public override void Update(float deltaTime) { var input = Engine.Input; Vector2 movement = Vector2.Zero; if (input.IsKeyDown(KeyboardKey.KEY_W)) movement.Y -= 1; if (input.IsKeyDown(KeyboardKey.KEY_S)) movement.Y += 1; if (input.IsKeyDown(KeyboardKey.KEY_A)) movement.X -= 1; if (input.IsKeyDown(KeyboardKey.KEY_D)) movement.X += 1; // 归一化向量,使得斜向移动速度不会更快 if (movement.LengthSquared() > 0) { movement = Vector2.Normalize(movement); } // 关键:速度 * 增量时间,实现帧率无关移动 _playerPosition += movement * PlayerSpeed * deltaTime; // 简单的边界检查,防止玩家移出屏幕 _playerPosition.X = Math.Clamp(_playerPosition.X, 20, 780); _playerPosition.Y = Math.Clamp(_playerPosition.Y, 20, 430); } public override void Render() { // 绘制一个简单的圆形代表玩家 Raylib.DrawCircleV(_playerPosition, 20, Color.Blue); // 绘制说明文字 Raylib.DrawText("Use WASD to move, ESC to pause", 10, 10, 20, Color.DarkGray); } } class Program { static void Main(string[] args) { var game = new GameEngine(); game.Run(); } } }

这段代码已经构成了一个可运行的游戏框架。运行dotnet run,你会看到一个窗口,一个蓝色圆形可以用WASD键平滑移动,按ESC可以暂停。这虽然简单,但已经包含了游戏引擎的所有核心要素:循环、输入、更新、渲染、状态管理和简单的场景系统。

4. 核心游戏机制实现:实体、碰撞与场景管理

有了基础框架,我们就可以往里面填充真正的游戏内容了。我们将实现一个简单的“收集星星”游戏:玩家控制角色移动,触碰随机生成的星星得分,同时要避开移动的敌人。

4.1 完善游戏对象(Entity)系统

首先,我们创建一个更健壮的Entity基类,它代表游戏世界中的任何动态或静态物体。

public abstract class Entity { public Vector2 Position { get; set; } public Vector2 Velocity { get; set; } public float Rotation { get; set; } public float Scale { get; set; } = 1.0f; public Color Tint { get; set; } = Color.White; public bool IsActive { get; set; } = true; // 碰撞体:这里用一个简单的圆形边界 public float CollisionRadius { get; set; } public Entity(Vector2 position) { Position = position; } // 每帧更新逻辑 public virtual void Update(float deltaTime) { // 基础更新:根据速度移动 Position += Velocity * deltaTime; } // 每帧渲染自身 public virtual void Render() { // 基类不进行具体渲染,由子类实现 } // 检查与另一个实体的碰撞(圆形碰撞) public bool CheckCollision(Entity other) { if (!IsActive || !other.IsActive) return false; float distance = Vector2.Distance(Position, other.Position); return distance < (CollisionRadius + other.CollisionRadius); } // 碰撞发生时的处理 public virtual void OnCollision(Entity other) { } }

4.2 实现具体的游戏实体:Player, Star, Enemy

接下来,我们创建玩家、星星和敌人的具体类。

public class Player : Entity { public int Score { get; private set; } private float _speed; private InputManager _input; public Player(Vector2 position, InputManager input, float speed = 250.0f) : base(position) { _input = input; _speed = speed; CollisionRadius = 15.0f; // 玩家碰撞半径 Tint = Color.Blue; } public override void Update(float deltaTime) { // 重置速度 Velocity = Vector2.Zero; // 根据输入设置速度向量 if (_input.IsKeyDown(KeyboardKey.KEY_W)) Velocity.Y -= 1; if (_input.IsKeyDown(KeyboardKey.KEY_S)) Velocity.Y += 1; if (_input.IsKeyDown(KeyboardKey.KEY_A)) Velocity.X -= 1; if (_input.IsKeyDown(KeyboardKey.KEY_D)) Velocity.X += 1; // 归一化并应用速度 if (Velocity.LengthSquared() > 0) { Velocity = Vector2.Normalize(Velocity) * _speed; } // 调用基类Update,实现移动 base.Update(deltaTime); // 简单的屏幕边界约束 Position = new Vector2( Math.Clamp(Position.X, CollisionRadius, 800 - CollisionRadius), Math.Clamp(Position.Y, CollisionRadius, 450 - CollisionRadius) ); } public override void Render() { // 绘制玩家:一个圆,内部有一个指向方向的小三角形 Raylib.DrawCircleV(Position, CollisionRadius, Tint); // 绘制一个“鼻子”指示方向(基于速度方向,如果没有速度则默认向右) Vector2 direction = Velocity.LengthSquared() > 0 ? Vector2.Normalize(Velocity) : Vector2.UnitX; Vector2 noseTip = Position + direction * CollisionRadius; Raylib.DrawTriangleLines( Position, Position + new Vector2(-direction.Y, direction.X) * 10, // 垂直方向的一个点 Position + new Vector2(direction.Y, -direction.X) * 10, // 垂直方向的另一个点 Color.DarkBlue ); Raylib.DrawLineV(Position, noseTip, Color.DarkBlue); } public override void OnCollision(Entity other) { if (other is Star) { Score++; other.IsActive = false; // “收集”星星,使其消失 Console.WriteLine($"Score: {Score}"); // 暂时输出到控制台 } else if (other is Enemy) { Tint = Color.Red; // 碰到敌人变红 // 在实际游戏中,这里可能会触发生命值减少或游戏结束 } } } public class Star : Entity { public Star(Vector2 position) : base(position) { CollisionRadius = 8.0f; Tint = Color.Gold; } public override void Render() { // 绘制一个五角星(简化版:一个圆加一个中心点) Raylib.DrawCircleV(Position, CollisionRadius, Tint); Raylib.DrawCircleV(Position, CollisionRadius * 0.4f, Color.Yellow); } } public class Enemy : Entity { private Vector2 _targetDirection; private float _directionChangeTimer; private Random _rng; public Enemy(Vector2 position, Random rng) : base(position) { _rng = rng; CollisionRadius = 12.0f; Tint = Color.Red; PickNewDirection(); } private void PickNewDirection() { // 随机选择一个方向向量 float angle = (float)(_rng.NextDouble() * Math.PI * 2); _targetDirection = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle)); Velocity = _targetDirection * 100.0f; // 敌人移动速度 _directionChangeTimer = (float)(_rng.NextDouble() * 2.0 + 1.0); // 1-3秒后改变方向 } public override void Update(float deltaTime) { base.Update(deltaTime); _directionChangeTimer -= deltaTime; if (_directionChangeTimer <= 0) { PickNewDirection(); } // 简单的屏幕反弹逻辑 if (Position.X <= CollisionRadius || Position.X >= 800 - CollisionRadius) Velocity.X *= -1; if (Position.Y <= CollisionRadius || Position.Y >= 450 - CollisionRadius) Velocity.Y *= -1; } public override void Render() { Raylib.DrawCircleV(Position, CollisionRadius, Tint); // 绘制两个白色小圆点作为“眼睛” Vector2 forward = Vector2.Normalize(Velocity); Raylib.DrawCircleV(Position + forward * CollisionRadius * 0.6f, 3, Color.White); } }

4.3 升级游戏场景:实体管理与碰撞检测

现在,我们需要升级之前的GamePlayScene,让它能够管理多个实体,并处理它们之间的碰撞。

public class GamePlayScene : Scene { private Player _player; private List<Entity> _entities; private Random _rng; private float _starSpawnTimer; private float _enemySpawnTimer; private const float StarSpawnInterval = 1.5f; // 每1.5秒尝试生成一个星星 private const float EnemySpawnInterval = 3.0f; // 每3秒尝试生成一个敌人 private int _maxStars = 10; private int _maxEnemies = 5; public GamePlayScene(GameEngine engine) : base(engine) { _rng = new Random(); _entities = new List<Entity>(); _player = new Player(new Vector2(400, 225), engine.Input); _entities.Add(_player); _starSpawnTimer = StarSpawnInterval; _enemySpawnTimer = EnemySpawnInterval; } public override void Update(float deltaTime) { // 1. 更新所有实体 foreach (var entity in _entities.ToList()) // 使用ToList()遍历副本,因为更新过程中列表可能被修改 { if (entity.IsActive) { entity.Update(deltaTime); } } // 2. 处理碰撞检测(简单的双重循环,对于小规模实体足够) for (int i = 0; i < _entities.Count; i++) { var entityA = _entities[i]; if (!entityA.IsActive) continue; for (int j = i + 1; j < _entities.Count; j++) { var entityB = _entities[j]; if (!entityB.IsActive) continue; if (entityA.CheckCollision(entityB)) { entityA.OnCollision(entityB); entityB.OnCollision(entityA); } } } // 3. 清理不活动的实体(如被收集的星星) _entities.RemoveAll(e => !e.IsActive); // 4. 生成新实体 UpdateSpawners(deltaTime); } private void UpdateSpawners(float deltaTime) { // 生成星星 _starSpawnTimer -= deltaTime; if (_starSpawnTimer <= 0 && CountEntities<Star>() < _maxStars) { TrySpawnEntity<Star>(() => new Star(GetRandomSpawnPosition())); _starSpawnTimer = StarSpawnInterval; } // 生成敌人 _enemySpawnTimer -= deltaTime; if (_enemySpawnTimer <= 0 && CountEntities<Enemy>() < _maxEnemies) { TrySpawnEntity<Enemy>(() => new Enemy(GetRandomSpawnPosition(), _rng)); _enemySpawnTimer = EnemySpawnInterval; } } private Vector2 GetRandomSpawnPosition() { // 在屏幕边缘内随机生成位置,避免在正中心生成 float margin = 50; return new Vector2( margin + (float)_rng.NextDouble() * (800 - 2 * margin), margin + (float)_rng.NextDouble() * (450 - 2 * margin) ); } private void TrySpawnEntity<T>(Func<T> creator) where T : Entity { var newEntity = creator(); // 简单检查,避免生成时立即与玩家重叠 if (!newEntity.CheckCollision(_player)) { _entities.Add(newEntity); } } private int CountEntities<T>() where T : Entity { return _entities.OfType<T>().Count(e => e.IsActive); } public override void Render() { // 渲染所有实体 foreach (var entity in _entities) { if (entity.IsActive) { entity.Render(); } } // 渲染UI:分数 Raylib.DrawText($"Score: {_player.Score}", 10, 10, 20, Color.DarkGray); Raylib.DrawText($"Stars: {CountEntities<Star>()}/{_maxStars}", 10, 35, 20, Color.DarkGray); Raylib.DrawText($"Enemies: {CountEntities<Enemy>()}/{_maxEnemies}", 10, 60, 20, Color.DarkGray); Raylib.DrawText("WASD: Move, ESC: Pause", 10, 420, 15, Color.LightGray); } public override void Unload() { _entities.Clear(); } }

现在,再次运行游戏,你将控制一个蓝色角色,在屏幕上收集金色的星星(碰到即得分),同时要避开红色的敌人。星星和敌人会定期生成,并受到数量上限的限制。一个具备基本玩法的游戏原型就完成了。

5. 性能优化、调试与内容扩展

当游戏实体数量增多,或者逻辑变复杂时,性能和维护性就成为关键。同时,我们也需要让游戏更有趣。

5.1 性能优化要点

  1. 对象池(Object Pooling):对于频繁创建和销毁的对象(如子弹、特效、星星),使用对象池是必须的。与其不断new和等待垃圾回收,不如预先创建一批对象,需要时从池中取出激活,不用时放回池中并失活。这能极大减少GC(垃圾回收)压力。

    public class ObjectPool<T> where T : Entity, new() { private Queue<T> _pool = new Queue<T>(); private Func<T> _createFunc; public ObjectPool(Func<T> createFunc, int initialSize) { _createFunc = createFunc; for (int i = 0; i < initialSize; i++) { Return(CreateNew()); } } public T Get() { if (_pool.Count > 0) { var obj = _pool.Dequeue(); obj.IsActive = true; return obj; } return CreateNew(); } public void Return(T obj) { obj.IsActive = false; _pool.Enqueue(obj); } private T CreateNew() => _createFunc(); }

    在场景中,你可以为星星和敌人分别创建对象池,在TrySpawnEntity时从池中Get,在实体失活时Return到池中。

  2. 空间分割与碰撞优化:当实体数量上百时,双重循环的碰撞检测(O(n²))会成为性能瓶颈。此时需要引入空间分割算法,如四叉树(Quadtree)网格(Grid)。它将空间划分为多个区域,只检查同一区域或相邻区域内的实体是否可能碰撞,能极大减少检测次数。

  3. 批处理渲染:Raylib本身已经对同种绘制调用做了优化,但我们自己也要注意。例如,如果有很多相同纹理的精灵,尽量在渲染循环外设置好纹理,避免在循环内频繁绑定。

5.2 调试技巧与常见问题

  1. 可视化调试信息:在开发过程中,将关键数据实时绘制在屏幕上非常有用。例如,显示帧率(FPS)、实体数量、玩家坐标、碰撞体轮廓等。Raylib提供了Raylib.DrawFPSRaylib.DrawText来轻松实现。

    // 在Render方法中 Raylib.DrawFPS(700, 10); // 在右上角显示FPS Raylib.DrawText($"Entities: {_entities.Count}", 700, 30, 10, Color.Gray); // 绘制所有实体的碰撞体轮廓(调试用) foreach(var e in _entities) { Raylib.DrawCircleLines((int)e.Position.X, (int)e.Position.Y, e.CollisionRadius, Color.Green); }
  2. “子弹时间”与暂停检查:遇到诡异的物理bug时,可以临时将deltaTime乘以一个很小的系数(如0.1),让游戏进入慢动作模式,方便观察。也可以按下一个键(如P)让deltaTime = 0,完全暂停游戏逻辑,但保持渲染,逐帧步进检查状态。

  3. 常见问题:实体抖动或穿透

    • 抖动:通常是因为渲染位置和物理更新位置不同步,或者碰撞响应逻辑有误。确保Update中计算的位置,就是Render中绘制的位置。
    • 穿透:在高速移动的物体(如子弹)中常见。因为一帧内移动的距离可能远超其自身尺寸,导致从碰撞体的一侧直接“跳”到了另一侧,错过了碰撞检测。解决方法有:
      • 连续碰撞检测(CCD):计算物体在本帧的运动轨迹(线段),检测轨迹是否与目标碰撞体相交。
      • 增加物理更新频率:将物理更新(包括碰撞检测)的频率提高到高于渲染帧率(例如,每秒120次更新)。
      • 限制最大速度:为高速物体设置一个合理的速度上限。

5.3 内容扩展:添加音效、粒子与场景切换

  1. 音效与音乐:Raylib支持加载和播放WAV、OGG、MP3等格式。使用Raylib.LoadSoundRaylib.PlaySound可以轻松为碰撞、得分等事件添加音效。记得在游戏结束时用Raylib.UnloadSound释放资源。

  2. 简单粒子系统:为碰撞、收集等事件添加视觉反馈。一个简单的粒子可以包含位置、速度、加速度、生命周期、颜色和大小。在Update中更新粒子状态,在Render中绘制(如用小方块或圆形)。

  3. 场景切换:完善SceneManager。让它持有一个场景栈(Stack),PushScene用于进入新场景(如从菜单进入游戏),PopScene用于返回上一场景(如游戏结束返回菜单)。引擎的主循环始终更新和渲染栈顶的场景。

  4. 数据持久化:使用System.Text.JsonNewtonsoft.Json将玩家的最高分、设置等保存到本地文件。这是一个让游戏更完整的点睛之笔。

通过这个从零开始的C#游戏开发实战,我们不仅实现了一个可玩的游戏,更重要的是,我们亲手搭建了一个微型游戏引擎的骨架,并深入理解了其核心运作原理。这套模式和方法论,是你在未来使用任何高级游戏引擎(Unity、Godot、Unreal的C#部分)时,理解其背后黑盒的宝贵钥匙。当你再看到Unity的Update方法或Godot的_Process函数时,你会会心一笑,因为你已经知道它们背后那个循环的故事。