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游戏我通常建议分为以下几个核心模块GameEngine核心引擎类负责驱动主循环管理游戏状态开始、运行、暂停、结束并作为其他模块的协调中心。InputManager输入管理器封装所有输入设备键盘、鼠标的查询逻辑。外部模块只需询问“A键是否被按下”而不需要知道底层是如何检测的。SceneManager场景管理器游戏可能有开始菜单、游玩关卡、结束画面等不同场景。这个管理器负责加载、切换和卸载场景。Entity/GameObject游戏对象基类游戏中的所有实体玩家、敌人、子弹、道具都应继承自一个共同的基类。这个基类至少包含位置、速度、更新(Update)和渲染(Draw)方法。PhysicsSystem简易物理系统可选但推荐处理碰撞检测与响应。即使是简单的矩形AABB碰撞检测单独封装成一个系统也大有裨益。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 SdkMicrosoft.NET.Sdk PropertyGroup OutputTypeExe/OutputType TargetFrameworknet8.0/TargetFramework !-- 发布为独立应用包含运行时 -- PublishSingleFiletrue/PublishSingleFile SelfContainedtrue/SelfContained !-- 重要启用原生AOT编译预览可以显著减小最终exe体积并提升启动速度 -- PublishAottrue/PublishAot /PropertyGroup ItemGroup PackageReference IncludeRaylib-cs Version5.0.0 / /ItemGroup /Project3.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 HashSetKeyboardKey _currentKeys new HashSetKeyboardKey(); private HashSetKeyboardKey _previousKeys new HashSetKeyboardKey(); public void Update() { // 将当前帧状态存入“上一帧” _previousKeys new HashSetKeyboardKey(_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 ListEntity _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 ListEntity(); _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 CountEntitiesStar() _maxStars) { TrySpawnEntityStar(() new Star(GetRandomSpawnPosition())); _starSpawnTimer StarSpawnInterval; } // 生成敌人 _enemySpawnTimer - deltaTime; if (_enemySpawnTimer 0 CountEntitiesEnemy() _maxEnemies) { TrySpawnEntityEnemy(() 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 TrySpawnEntityT(FuncT creator) where T : Entity { var newEntity creator(); // 简单检查避免生成时立即与玩家重叠 if (!newEntity.CheckCollision(_player)) { _entities.Add(newEntity); } } private int CountEntitiesT() where T : Entity { return _entities.OfTypeT().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: {CountEntitiesStar()}/{_maxStars}, 10, 35, 20, Color.DarkGray); Raylib.DrawText($Enemies: {CountEntitiesEnemy()}/{_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 性能优化要点对象池Object Pooling对于频繁创建和销毁的对象如子弹、特效、星星使用对象池是必须的。与其不断new和等待垃圾回收不如预先创建一批对象需要时从池中取出激活不用时放回池中并失活。这能极大减少GC垃圾回收压力。public class ObjectPoolT where T : Entity, new() { private QueueT _pool new QueueT(); private FuncT _createFunc; public ObjectPool(FuncT 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到池中。空间分割与碰撞优化当实体数量上百时双重循环的碰撞检测O(n²)会成为性能瓶颈。此时需要引入空间分割算法如四叉树Quadtree或网格Grid。它将空间划分为多个区域只检查同一区域或相邻区域内的实体是否可能碰撞能极大减少检测次数。批处理渲染Raylib本身已经对同种绘制调用做了优化但我们自己也要注意。例如如果有很多相同纹理的精灵尽量在渲染循环外设置好纹理避免在循环内频繁绑定。5.2 调试技巧与常见问题可视化调试信息在开发过程中将关键数据实时绘制在屏幕上非常有用。例如显示帧率FPS、实体数量、玩家坐标、碰撞体轮廓等。Raylib提供了Raylib.DrawFPS和Raylib.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); }“子弹时间”与暂停检查遇到诡异的物理bug时可以临时将deltaTime乘以一个很小的系数如0.1让游戏进入慢动作模式方便观察。也可以按下一个键如P让deltaTime 0完全暂停游戏逻辑但保持渲染逐帧步进检查状态。常见问题实体抖动或穿透抖动通常是因为渲染位置和物理更新位置不同步或者碰撞响应逻辑有误。确保Update中计算的位置就是Render中绘制的位置。穿透在高速移动的物体如子弹中常见。因为一帧内移动的距离可能远超其自身尺寸导致从碰撞体的一侧直接“跳”到了另一侧错过了碰撞检测。解决方法有连续碰撞检测CCD计算物体在本帧的运动轨迹线段检测轨迹是否与目标碰撞体相交。增加物理更新频率将物理更新包括碰撞检测的频率提高到高于渲染帧率例如每秒120次更新。限制最大速度为高速物体设置一个合理的速度上限。5.3 内容扩展添加音效、粒子与场景切换音效与音乐Raylib支持加载和播放WAV、OGG、MP3等格式。使用Raylib.LoadSound和Raylib.PlaySound可以轻松为碰撞、得分等事件添加音效。记得在游戏结束时用Raylib.UnloadSound释放资源。简单粒子系统为碰撞、收集等事件添加视觉反馈。一个简单的粒子可以包含位置、速度、加速度、生命周期、颜色和大小。在Update中更新粒子状态在Render中绘制如用小方块或圆形。场景切换完善SceneManager。让它持有一个场景栈StackPushScene用于进入新场景如从菜单进入游戏PopScene用于返回上一场景如游戏结束返回菜单。引擎的主循环始终更新和渲染栈顶的场景。数据持久化使用System.Text.Json或Newtonsoft.Json将玩家的最高分、设置等保存到本地文件。这是一个让游戏更完整的点睛之笔。通过这个从零开始的C#游戏开发实战我们不仅实现了一个可玩的游戏更重要的是我们亲手搭建了一个微型游戏引擎的骨架并深入理解了其核心运作原理。这套模式和方法论是你在未来使用任何高级游戏引擎Unity、Godot、Unreal的C#部分时理解其背后黑盒的宝贵钥匙。当你再看到Unity的Update方法或Godot的_Process函数时你会会心一笑因为你已经知道它们背后那个循环的故事。