C#与.NET游戏开发入门:从零构建游戏循环与资源管理

C#与.NET游戏开发入门:从零构建游戏循环与资源管理 1. 项目概述为什么是C#与.NET如果你对游戏开发感兴趣尤其是想进入PC、主机或者移动端游戏领域那么C#和.NET框架绝对是你绕不开的技术栈。很多人一提到游戏编程第一反应可能是C或者Unity的C#但往往忽略了C#背后那个强大、稳定且不断进化的运行时环境——.NET。这个项目就是带你从零开始亲手用C#和.NET框架搭建一个可运行的游戏原型让你不仅学会写游戏逻辑更能理解现代游戏开发中一个成熟的运行时环境是如何为你“兜底”和“赋能”的。我见过太多新手跟着教程用Unity拖拖拽拽也能做出个小游戏但一旦遇到性能卡顿、内存泄漏或者想实现一些引擎没有封装的高级功能时就完全束手无策了。究其原因是对底层运行机制和所用语言生态缺乏系统性的理解。C#作为一门优雅且功能强大的语言配合上经过二十年锤炼的.NET框架为游戏开发提供了从内存管理、多线程处理到网络通信、文件IO等一整套工业级解决方案。通过这个实践项目你将掌握如何不依赖重型游戏引擎仅用.NET的基础类库和少量图形库构建一个结构清晰、可维护的游戏循环并在此过程中深刻理解帧率控制、输入处理、资源加载与释放等核心概念。这不仅是“入门”更是为你未来深入任何游戏引擎如Unity、Godot的C#版本或自研引擎打下坚实的地基。2. 核心思路与架构设计2.1 技术选型为什么是.NET 6/8而非Mono或.NET Framework在开始敲代码之前我们必须明确一个关键选择使用哪个版本的.NET。目前主流选择有三个传统的.NET Framework、跨平台的Mono以及现代的.NET 6/8即.NET Core的后续版本。对于游戏编程入门与实践我强烈推荐直接使用最新的**.NET 8**。理由很充分.NET Framework是Windows的“原住民”但已停止重大更新且跨平台能力弱。Mono虽然为跨平台而生也是Unity早期使用的运行时但其发展已逐步融入.NET统一生态。.NET 8是真正的跨平台、开源、高性能的现代化运行时。它拥有更高的性能特别是AOT原生编译特性对游戏启动速度提升巨大更小的部署体积以及官方的长期支持。对于游戏开发性能就是生命线.NET 8在数值计算、内存访问和垃圾回收GC方面都做了大量优化这对游戏这种实时性要求高的应用至关重要。注意虽然Unity目前仍使用基于Mono或IL2CPP的定制化运行时但了解标准的.NET生态能让你更好地理解C#语言本身和基础库的工作原理这种知识是通用的不会过时。2.2 最小化游戏循环架构设计一个游戏最核心的部分就是“游戏循环”Game Loop。我们的目标是构建一个最小化、可理解的结构。这里我们不直接使用像Unity那样的全功能引擎而是从底层概念开始搭建。核心架构包含以下几个模块初始化模块负责创建游戏窗口、初始化图形设备、加载基础资源。输入处理模块轮询键盘、鼠标等输入设备的状态并将其转化为游戏内可理解的事件或命令。更新模块这是游戏逻辑的心脏。以固定的时间间隔如每秒60次运行处理游戏状态、物理模拟、AI决策等。渲染模块负责将当前的游戏状态绘制到屏幕上。与更新模块解耦可能以不同的频率运行如依赖显示器刷新率。资源管理模块统一管理纹理、音效、字体等资源的加载、缓存和释放防止内存泄漏。我们将采用一个简单的“固定时间步长”循环。它的核心思想是更新逻辑的时间间隔是固定的而渲染则是尽可能快地执行。这能保证游戏逻辑在不同性能的电脑上运行结果一致避免出现“快机器上角色飞起慢机器上角色慢动作”的问题。3. 环境搭建与第一个窗口3.1 开发环境配置工欲善其事必先利其器。你需要安装以下工具.NET 8 SDK从微软官网下载并安装。安装后在命令行输入dotnet --version确认安装成功。代码编辑器强烈推荐Visual Studio 2022社区版免费或JetBrains Rider。它们对C#和.NET的游戏开发支持非常完善包括调试、性能分析等。如果你喜欢轻量级Visual Studio Code配合C#扩展也是不错的选择。图形库选择为了绘制窗口和图形我们需要一个跨平台的库。这里我推荐Raylib-cs或MonoGame。Raylib-cs一个极其简单易用的C#绑定库API设计直观非常适合入门和原型开发。它的理念是“让学习游戏编程变得有趣”。MonoGame一个更强大、更接近XNA框架的开源框架被许多独立游戏如《星露谷物语》原型使用功能更全面但学习曲线稍陡。为了快速上手本项目将使用Raylib-cs。它让你能专注于游戏逻辑本身而不是复杂的图形API。3.2 创建项目并显示游戏窗口打开命令行创建一个新的控制台应用并添加Raylib-cs的引用# 创建一个新的控制台项目 dotnet new console -n CSharpGameIntro cd CSharpGameIntro # 添加Raylib-cs的NuGet包引用 dotnet add package Raylib-cs接下来打开生成的Program.cs文件将其内容替换为我们第一个游戏窗口的代码using Raylib_cs; using System.Numerics; namespace CSharpGameIntro; class Program { // 定义屏幕宽高和标题 const int ScreenWidth 800; const int ScreenHeight 450; const string WindowTitle 我的第一个C#游戏窗口; static void Main() { // 1. 初始化窗口 Raylib.InitWindow(ScreenWidth, ScreenHeight, WindowTitle); // 设置目标帧率例如60FPS Raylib.SetTargetFPS(60); // 2. 游戏主循环窗口关闭前持续运行 while (!Raylib.WindowShouldClose()) { // ---------- 更新逻辑 ---------- // 此处暂时为空后续填充游戏状态更新代码 // ---------- 绘制渲染 ---------- Raylib.BeginDrawing(); // 开始绘制 Raylib.ClearBackground(Color.RayWhite); // 用米白色清空背景 // 在屏幕中央绘制一行文字 string welcomeText Hello, .NET Game World!; int textWidth Raylib.MeasureText(welcomeText, 20); Raylib.DrawText( welcomeText, ScreenWidth / 2 - textWidth / 2, ScreenHeight / 2 - 10, 20, Color.DarkGray ); Raylib.EndDrawing(); // 结束绘制 } // 3. 关闭窗口释放资源 Raylib.CloseWindow(); } }运行dotnet run你应该能看到一个800x450的窗口中央显示着“Hello, .NET Game World!”。实操心得Raylib.InitWindow和Raylib.CloseWindow必须成对出现。游戏循环的核心是while (!Raylib.WindowShouldClose())这个条件会在用户点击窗口关闭按钮时变为false从而退出循环结束游戏。SetTargetFPS并不是强制锁帧而是一个“建议”值Raylib会尝试通过微小延迟来稳定帧率这对于控制功耗和发热很有用。4. 实现核心游戏循环与一个简单互动现在我们已经有了窗口接下来要实现一个包含“固定时间步长”的游戏循环并添加一个可以控制的角色。4.1 实现固定时间步长游戏循环我们将修改主循环将“更新”和“渲染”逻辑分离。更新逻辑以固定的时间间隔如每秒60次即16.67毫秒一次执行而渲染则尽可能快地执行。// ... 前面的using和常量定义保持不变 ... class Program { // 游戏状态一个可以移动的小球 static Vector2 ballPosition new Vector2(ScreenWidth / 2, ScreenHeight / 2); static Vector2 ballSpeed new Vector2(3.0f, 3.0f); static int ballRadius 20; static void Main() { Raylib.InitWindow(ScreenWidth, ScreenHeight, WindowTitle); Raylib.SetTargetFPS(60); // 渲染帧率目标 // 固定时间步长参数 float fixedTimeStep 1.0f / 60.0f; // 每秒更新60次 float accumulatedTime 0.0f; float lastTime (float)Raylib.GetTime(); // 获取当前时间秒 while (!Raylib.WindowShouldClose()) { float currentTime (float)Raylib.GetTime(); float deltaTime currentTime - lastTime; // 计算上一帧到这一帧的真实时间差 lastTime currentTime; accumulatedTime deltaTime; // **固定更新**只要累积时间超过固定步长就执行一次更新 while (accumulatedTime fixedTimeStep) { UpdateGame(fixedTimeStep); // 传入固定的时间步长 accumulatedTime - fixedTimeStep; } // **渲染**不受固定步长限制尽可能快地绘制当前状态 RenderGame(); } Raylib.CloseWindow(); } static void UpdateGame(float deltaTime) { // 处理输入用键盘WASD移动小球 float speed 200.0f; // 每秒移动200像素 Vector2 movement Vector2.Zero; if (Raylib.IsKeyDown(KeyboardKey.KEY_W)) movement.Y - 1; if (Raylib.IsKeyDown(KeyboardKey.KEY_S)) movement.Y 1; if (Raylib.IsKeyDown(KeyboardKey.KEY_A)) movement.X - 1; if (Raylib.IsKeyDown(KeyboardKey.KEY_D)) movement.X 1; // 归一化向量防止斜向移动更快 if (movement.Length() 0) { movement Vector2.Normalize(movement); } // 根据时间和速度更新位置 ballPosition movement * speed * deltaTime; // 简单的边界碰撞检测 if (ballPosition.X - ballRadius 0) ballPosition.X ballRadius; if (ballPosition.X ballRadius ScreenWidth) ballPosition.X ScreenWidth - ballRadius; if (ballPosition.Y - ballRadius 0) ballPosition.Y ballRadius; if (ballPosition.Y ballRadius ScreenHeight) ballPosition.Y ScreenHeight - ballRadius; } static void RenderGame() { Raylib.BeginDrawing(); Raylib.ClearBackground(Color.RayWhite); // 绘制小球 Raylib.DrawCircleV(ballPosition, ballRadius, Color.Maroon); // 显示帧率等信息 Raylib.DrawText($FPS: {Raylib.GetFPS()}, 10, 10, 20, Color.Black); Raylib.DrawText($Position: ({ballPosition.X:F0}, {ballPosition.Y:F0}), 10, 40, 20, Color.Black); Raylib.EndDrawing(); } }这段代码实现了一个经典的游戏循环模式。UpdateGame函数接收一个固定的deltaTime永远是1/60秒这意味着无论你的电脑快慢小球每秒移动的距离speed * deltaTime在逻辑上是恒定的。而RenderGame函数则负责将当前ballPosition即时地画出来所以渲染是平滑的。accumulatedTime这个变量是关键它像一个蓄水池积累真实流逝的时间然后每次取出固定的一桶水fixedTimeStep来更新游戏逻辑直到蓄水池的水不够一桶为止。这确保了更新逻辑的频率稳定。4.2 引入简单的面向对象设计目前所有状态和逻辑都写在静态类里随着游戏变复杂这会变得难以维护。我们来引入一个简单的Player类。// 新建一个 Player.cs 文件 using System.Numerics; using Raylib_cs; namespace CSharpGameIntro; public class Player { public Vector2 Position { get; private set; } public Vector2 Velocity { get; private set; } public int Radius { get; set; } 20; public Color Color { get; set; } Color.DarkBlue; // 移动速度像素/秒 private float _moveSpeed 200.0f; public Player(Vector2 startPosition) { Position startPosition; Velocity Vector2.Zero; } public void Update(float deltaTime) { HandleInput(); // 根据速度更新位置 Position Velocity * deltaTime; ApplyBoundaryConstraints(); } private void HandleInput() { Velocity Vector2.Zero; if (Raylib.IsKeyDown(KeyboardKey.KEY_W)) Velocity.Y - 1; if (Raylib.IsKeyDown(KeyboardKey.KEY_S)) Velocity.Y 1; if (Raylib.IsKeyDown(KeyboardKey.KEY_A)) Velocity.X - 1; if (Raylib.IsKeyDown(KeyboardKey.KEY_D)) Velocity.X 1; if (Velocity.Length() 0) { Velocity Vector2.Normalize(Velocity) * _moveSpeed; } } private void ApplyBoundaryConstraints() { int screenWidth 800; // 应通过依赖注入获取这里简化 int screenHeight 450; float x Math.Clamp(Position.X, Radius, screenWidth - Radius); float y Math.Clamp(Position.Y, Radius, screenHeight - Radius); Position new Vector2(x, y); } public void Draw() { Raylib.DrawCircleV(Position, Radius, Color); } }然后修改主Program.cs// ... 在Program类中 ... static Player player; static void Main() { // ... 初始化代码 ... player new Player(new Vector2(ScreenWidth / 2, ScreenHeight / 2)); while (!Raylib.WindowShouldClose()) { // ... 时间计算循环 ... while (accumulatedTime fixedTimeStep) { UpdateGame(fixedTimeStep); accumulatedTime - fixedTimeStep; } RenderGame(); } // ... } static void UpdateGame(float deltaTime) { // 现在只需要更新玩家对象 player.Update(deltaTime); } static void RenderGame() { Raylib.BeginDrawing(); Raylib.ClearBackground(Color.RayWhite); player.Draw(); // 玩家自己负责绘制自己 Raylib.DrawText($FPS: {Raylib.GetFPS()}, 10, 10, 20, Color.Black); Raylib.DrawText($Player Pos: ({player.Position.X:F0}, {player.Position.Y:F0}), 10, 40, 20, Color.Black); Raylib.EndDrawing(); }注意事项这里将屏幕尺寸硬编码在Player类中是不好的设计更好的做法是通过构造函数传入或者从某个全局配置服务中获取。这里为了示例清晰做了简化。面向对象的设计让代码更清晰Player封装了自身的状态和行为主循环变得非常简洁。这是构建更复杂游戏实体如敌人、道具的基础模式。5. 资源管理与游戏状态一个完整的游戏需要加载图片、声音等资源并且管理不同的游戏状态如开始菜单、游戏中、暂停、结束。5.1 使用.NET的IDisposable管理资源Raylib的资源如Texture2D,Sound,Music通常需要手动卸载。在C#中我们可以利用using语句和IDisposable接口来确保资源被正确释放即使发生异常也不例外。我们可以创建一个简单的资源管理器。// ResourceManager.cs using Raylib_cs; using System.Collections.Concurrent; namespace CSharpGameIntro; public class ResourceManager : IDisposable { private ConcurrentDictionarystring, Texture2D _textures new(); private ConcurrentDictionarystring, Sound _sounds new(); private bool _disposed false; public Texture2D LoadTexture(string key, string filePath) { if (_textures.TryGetValue(key, out var existingTexture)) { return existingTexture; } var texture Raylib.LoadTexture(filePath); _textures[key] texture; return texture; } public Sound LoadSound(string key, string filePath) { if (_sounds.TryGetValue(key, out var existingSound)) { return existingSound; } var sound Raylib.LoadSound(filePath); _sounds[key] sound; return sound; } public void UnloadTexture(string key) { if (_textures.TryRemove(key, out var texture)) { Raylib.UnloadTexture(texture); } } public void UnloadSound(string key) { if (_sounds.TryRemove(key, out var sound)) { Raylib.UnloadSound(sound); } } // 实现IDisposable释放所有资源 public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (!_disposed) { if (disposing) { foreach (var texture in _textures.Values) { Raylib.UnloadTexture(texture); } _textures.Clear(); foreach (var sound in _sounds.Values) { Raylib.UnloadSound(sound); } _sounds.Clear(); } _disposed true; } } ~ResourceManager() { Dispose(false); } }在主程序中我们可以这样使用static ResourceManager resourceManager; static void Main() { resourceManager new ResourceManager(); // 加载资源 var playerTexture resourceManager.LoadTexture(player, assets/player.png); try { // ... 游戏主循环 ... } finally { // 游戏结束时释放所有资源 resourceManager.Dispose(); } }5.2 实现简单的游戏状态机游戏状态机是管理游戏流程的经典模式。我们用一个枚举定义状态并用一个简单的方式来切换和更新。public enum GameState { MainMenu, Playing, Paused, GameOver } // 在Program类中 static GameState currentState GameState.MainMenu; static Player player; static void UpdateGame(float deltaTime) { switch (currentState) { case GameState.MainMenu: UpdateMainMenu(deltaTime); break; case GameState.Playing: UpdatePlaying(deltaTime); break; case GameState.Paused: UpdatePaused(deltaTime); break; case GameState.GameOver: UpdateGameOver(deltaTime); break; } } static void RenderGame() { Raylib.BeginDrawing(); Raylib.ClearBackground(Color.RayWhite); switch (currentState) { case GameState.MainMenu: RenderMainMenu(); break; case GameState.Playing: RenderPlaying(); break; case GameState.Paused: RenderPaused(); break; case GameState.GameOver: RenderGameOver(); break; } // 在任何状态下都显示FPS Raylib.DrawText($FPS: {Raylib.GetFPS()}, 10, 10, 20, Color.Black); Raylib.EndDrawing(); } static void UpdatePlaying(float deltaTime) { player.Update(deltaTime); // 检查游戏结束条件等... if (Raylib.IsKeyPressed(KeyboardKey.KEY_ESCAPE)) { currentState GameState.Paused; } } static void RenderPlaying() { player.Draw(); // 渲染游戏中的其他元素... } static void UpdateMainMenu(float deltaTime) { if (Raylib.IsKeyPressed(KeyboardKey.KEY_ENTER)) { currentState GameState.Playing; // 重置游戏状态 player new Player(new Vector2(ScreenWidth / 2, ScreenHeight / 2)); } } static void RenderMainMenu() { string title 我的C#游戏; Raylib.DrawText(title, ScreenWidth / 2 - Raylib.MeasureText(title, 40) / 2, 100, 40, Color.Black); Raylib.DrawText(按 ENTER 开始游戏, ScreenWidth / 2 - Raylib.MeasureText(按 ENTER 开始游戏, 20) / 2, 200, 20, Color.DarkGray); }这种简单的switch状态机对于小型游戏足够了。当状态和状态间的转换更复杂时可以考虑使用状态模式为每个状态定义一个类。6. 性能考量与常见问题排查用C#和.NET做游戏性能是必须关注的话题。以下是几个关键点和常见陷阱。6.1 垃圾回收GC与内存分配游戏是实时应用垃圾回收器GC的暂停可能会导致帧率骤降卡顿。.NET的GC非常高效但频繁产生短期存活的小对象在Update循环中仍会触发GC。避坑技巧对象池对于频繁创建和销毁的对象如子弹、粒子使用对象池复用它们而不是每次都new。避免在热路径上分配在Update或Render循环中尽量避免使用new关键字创建新的引用类型对象如ListT,string拼接。对于向量计算考虑使用System.Numerics中的Vector2、Vector3它们是结构体在栈上分配。对于字符串可以使用StringBuilder或预定义字符串。使用结构体struct对于小的、不可变的数据集合如位置、颜色使用struct而非class它们分配在栈上没有GC压力。// 不好的做法每帧都new一个List void Update() { var newList new ListEnemy(); // 每帧分配GC压力大 // ... 处理逻辑 ... } // 较好的做法复用对象池或集合 private ListEnemy _enemyList new ListEnemy(100); // 预分配容量 void Update() { _enemyList.Clear(); // 清空复用而不是new一个新的 // ... 重新填充列表 ... }6.2 多线程与任务游戏循环通常是单线程的但一些耗时操作如资源加载、网络请求、复杂AI计算可以放在后台线程避免阻塞主循环。.NET提供了强大的Task和async/await支持static async TaskTexture2D LoadTextureAsync(string path) { // 模拟一个耗时的IO操作 await Task.Delay(100); // 假设是磁盘读取 // 注意Raylib的加载函数可能不是线程安全的通常需要在主线程调用 // 这里只是演示异步模式实际加载可能需要特殊处理或使用支持异步的库。 return Raylib.LoadTexture(path); } // 在初始化时使用 async Task InitializeGameAsync() { // 同时加载多个资源 var loadPlayerTask LoadTextureAsync(assets/player.png); var loadBackgroundTask LoadTextureAsync(assets/bg.jpg); await Task.WhenAll(loadPlayerTask, loadBackgroundTask); playerTexture loadPlayerTask.Result; backgroundTexture loadBackgroundTask.Result; }重要提示大多数图形API包括Raylib底层的OpenGL要求资源创建和渲染操作在同一个线程通常是主线程进行。因此异步加载完成后将资源赋值给主线程管理的变量要小心线程同步问题。简单的UI反馈如加载进度条非常适合用异步。6.3 常见问题排查表问题现象可能原因排查与解决思路游戏运行卡顿帧率不稳定1.GC频繁触发在Update中大量分配临时对象。2.单帧计算量过大复杂物理或AI计算。3.渲染调用过多每帧绘制大量未合批的精灵。1. 使用性能分析器如.NET的dotnet-counters或VS的性能探查器监控GC触发频率和内存分配。2. 对算法进行优化考虑空间换时间或使用更高效的数据结构如SpanT。3. 使用精灵图集Texture Atlas减少纹理切换对于静态背景只渲染一次或使用渲染目标缓存。输入响应延迟1.游戏循环设计问题更新逻辑放在渲染之后或循环内有阻塞操作。2.固定时间步长累积误差处理不当导致更新“卡住”。1. 确保输入检测在Update阶段最早执行。检查循环中是否有同步的File.ReadAllText或网络请求。2. 检查accumulatedTime逻辑确保它不会无限增长可以设置一个上限。使用float时注意精度问题。游戏在不同电脑上速度不同未使用与时间相关的增量deltaTime而是直接使用固定值更新位置。确保所有运动、动画、计时器都乘以deltaTime或固定的时间步长使速度单位是“单位/秒”而非“单位/帧”。内存使用量持续增长内存泄漏1.资源未正确释放加载了Texture、Sound但未Unload。2.事件未取消订阅注册了事件但对象销毁时未取消导致对象无法被GC回收。3.静态集合无限增长用静态List或Dictionary缓存数据但只增不减。1. 使用ResourceManager并确保Dispose被调用。2. 遵循“谁注册谁注销”的原则在对象的生命周期结束时如Dispose或析构函数中取消事件订阅。3. 为缓存设置大小上限或过期策略。发布后游戏exe很大使用了框架依赖发布但目标机器可能没有安装对应的.NET运行时。对于游戏分发建议使用独立部署或Native AOT发布。dotnet publish -c Release -r win-x64 --self-contained true会打包运行时体积较大但兼容性好。.NET 8的Native AOT可以生成真正的本地代码体积和启动速度有极大优化但兼容性有一定限制需测试。6.4 调试与性能分析工具Visual Studio调试器设置条件断点、监视变量、检查调用堆栈是基础但最强大的工具。Visual Studio性能探查器可以分析CPU使用率、内存分配、GC活动等直观找到热点函数。dotnet-counters/dotnet-trace命令行工具可以在游戏运行时实时监控GC、CPU、异常等指标对诊断线上问题特别有用。Raylib内置工具Raylib.GetFPS()可以监控帧率。你也可以通过Raylib.SetTraceLogLevel(TraceLogLevel.LOG_WARNING)来控制日志输出避免调试信息影响性能。7. 项目扩展与下一步方向完成这个基础框架后你已经拥有了一个用纯C#和.NET构建的可运行游戏骨架。接下来你可以选择多个方向进行深化丰富游戏内容图形使用Raylib加载精灵动画、实现粒子系统、添加光照和阴影如果库支持。音频集成背景音乐和音效Raylib的音频模块使用起来非常简单。物理集成一个轻量级的2D物理库如Box2D的C#端口Box2D.NetStandard来处理碰撞、刚体运动。UI实现一个简单的即时模式GUIImmediate Mode GUI来制作游戏内的按钮、血条、菜单。架构升级实体组件系统ECS当游戏实体类型非常多时传统的面向对象继承会变得臃肿。可以探索像DefaultEcs这样的ECS库它能极大提升数据局部性和缓存友好性对性能有显著好处尤其适合大量相似实体如成千上万的粒子、子弹的场景。依赖注入使用像Microsoft.Extensions.DependencyInjection这样的DI容器来管理ResourceManager、AudioService、SceneManager等全局服务使代码更解耦、更易测试。向成熟引擎过渡你现在的经验将让你能更深刻地理解Unity或Godot使用C#引擎底层在做什么。你可以尝试用Unity重写这个小游戏你会发现MonoBehaviour的Update对应你的UpdateGameStart对应初始化资源管理由引擎负责但原理是相通的。你也会更清楚何时该使用引擎提供的功能何时需要自己写底层优化。这个项目的价值不在于复现一个多么炫酷的游戏而在于亲手触摸了游戏编程的脉搏——那个驱动一切的游戏循环并学会了如何用现代C#和.NET的强大功能去构建和维护它。当你下次在Unity中编写脚本时你会对每一帧的执行、资源的生命周期有更底层、更清晰的认识这将使你从一个简单的脚本使用者成长为真正的问题解决者和系统设计者。