ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

用Trae和Flutter Web开发2048:AI IDE+跨平台游戏实战

用Trae和Flutter Web开发2048:AI IDE+跨平台游戏实战 1. 为什么用 Trae Flutter Web 做 2048 这个组合1.1 Trae 到底是什么级别的 AI IDE先说结论Trae 是一款把对话式 AI 直接塞进编辑器里的集成开发环境它的定位和 Cursor、Windsurf 这类产品基本一致但它在中文语义理解、国内网络环境下的稳定性、以及项目级代码生成能力上都做得更顺手。对我这种平时要写 Flutter、偶尔又要切 Web 工程的人来说Trae 最值钱的地方不是“能聊天”而是它能直接理解你的项目结构然后基于整个工程上下文帮你改代码而不是像普通 Copilot 那样只盯着当前文件。实际体验下来Trae 的 Agent 模式可以自己读报错、自己翻文件、自己跑命令你只需要把需求描述清楚剩下的迭代循环它自己走。这个项目里我就让它帮我搭了 2048 的骨架然后把核心算法和 UI 拆分出来分别实现整个过程基本没有离开编辑器。如果你之前只用过传统 IDE第一次用 Trae 的感觉会有点像从手动挡换自动挡尤其是“我改了一个类帮我找出所有受影响的地方”这种需求它处理得相当利落。1.2 Flutter Web 做小游戏是合适的场景吗很多人一听到 Flutter Web 就觉得性能差、首屏加载慢这个印象有对的部分但要分场景。2048 这种游戏棋盘只有 4x4 个格子界面状态有限核心逻辑是纯 Dart 运算动画也只是简单的位移动画Flutter Web 完全能轻松胜任。关键点在于Flutter 的 Canvas 渲染机制对 2048 这种“网格移动数字块”的游戏天然友好。你不需要像写传统 Web 游戏那样手动操作 DOM 去挪 div也不需要像 Canvas 2D 那样自己管理每一帧的绘制状态。用 Flutter 只需要声明“格子应该在哪数字应该是什么”剩下的重绘由框架自己搞定这对刚从移动端转过来的开发者特别友好。还有一个很实际的考虑Flutter 项目可以一套代码同时编译到 Android、iOS、Web 桌面端。用这个项目练手后面的收益不在于“做个网页”而在于你掌握了“一套 Dart 逻辑到处跑”的完整链路。2048 是我见过最适合做这个链路的练手项目它足够小不至于让你陷在业务复杂度里又足够完整该有的渲染、状态管理、手势处理全都有。1.3 2048 作为练手项目的独特优势2048 看起来简单但它的算法里藏着一个非常典型的“状态机”问题一个 4x4 的棋盘每次滑动会发生位移、合并、生成新块、胜负判定四件事。这个逻辑如果不提前拆清楚写出来的代码会越改越乱。我特别推荐把它作为第一个 Flutter Web 项目理由有三条核心算法不依赖任何第三方库数据结构就一个二维数组非常适合理解“数据驱动 UI”的思路。UI 复杂度适中一个棋盘、一张分数卡、几个按钮能练到布局、动画、手势又不会像完整应用那样有一堆页面要管理。验证成本极低逻辑对不对跑一遍就能看出来而且测试用例非常直觉化比如“向左滑动后这一行应该变成什么”。2. 环境准备与项目初始化含踩坑2.1 Flutter SDK 安装与 FVM 版本管理如果你没有装 Flutter第一步是去官网下载 SDK这里不重复。我强烈建议你装 FVM它是 Flutter 的多版本管理器类似前端那套 nvm能让你在不同项目里固定不同的 Flutter 版本而不是被全局版本牵着鼻子走。我当时遇到的一个典型问题就是手上的老项目用的是 Flutter 2.x新项目想用 3.44现在的最新稳定版已经到这个系列了如果直接用全局 Flutter来回切换版本非常痛苦。FVM 解决的就是这个问题。安装也很简单在终端里执行 dart pub global activate fvm然后在项目目录里执行 fvm use 3.44.0 就会自动下载并锁定版本。注意后续所有 flutter 命令都要用 fvm flutter 来执行而不是直接 flutter否则版本就白锁定了。还有个细节容易踩坑如果你在 Windows 上开发 Flutter Web务必确认 flutter doctor 里的 Chrome 是 recognized 状态否则后面根本无法启动 Web 调试。正常情况下只要安装了 Chromeflutter doctor 会自动识别。2.2 在 Trae 中创建 Flutter Web 工程Trae 的界面布局和 VS Code 非常接近所以如果你在 VS Code 里配过 Flutter到这里几乎零成本迁移。第一次打开 Trae 的 Builder 面板你可以直接点击“内置终端”先跑一次 fvm flutter doctor 确认环境没问题然后执行 fluttter create 创建项目。我建议这样创建fvm flutter create --platformsweb,android,ios -e game_2048这里用 -e 参数表示创建空项目模板不带 counter demo免得还要清理一堆示例代码。如果只做 Web 端--platformsweb 就够了但保留 android 和 ios 目录其实也不碍事后续想跑移动端直接就能跑。创建完之后用 Trae 打开项目文件夹然后在对话框里对 AI 说一句话比如“帮我看一下这个项目的 pubspec.yaml 配置是否正确我需要一个干净的 Flutter Web 项目”Trae 会自动读文件、分析工程配置并给出建议。这一步的好处是能让你确认 Trae 是否真的理解你的项目上下文而不是瞎猜。2.3 pubspec.yaml 依赖配置2048 这个项目其实可以不引入任何第三方依赖纯 Flutter 自带的 material 库就够了。但我在实际开发中加了两个小依赖来提升体感dependencies: flutter: sdk: flutter cupertino_icons: ^1.0.6 shared_preferences: ^2.2.2shared_preferences 不是必须的但它能让你把最高分保存到本地对浏览器来说就是存 localStorage。这个功能看起来小但做出来之后会让项目“像个完整游戏”而不是一个跑完就丢的 demo。Trae 在生成代码时也经常会建议加这个库来持久化数据所以提前配上能让后续开发更顺。3. 游戏核心机制拆解2048 的规则如何转化为代码3.1 棋盘数据模型一维还是二维2048 的棋盘是一个 4x4 的网格最自然的建模方式当然是 ListList 也就是二维数组。每个格子存一个数字空格子存 0。不过在实际编码时也可以用一维数组 List 来存 16 个元素然后通过 index ~/ 4 和 index % 4 来换算行列。我试过两种方案二维数组在写“移动”算法时更直观一维数组在写“遍历所有格子”时更省事。我最终选择了二维数组原因是 2048 的核心难点在行列变换二维数组能直接用 row 和 col 两个变量来思考减少心智负担。这段是 Trae 给我的原始结构class Game2048 { static const int size 4; late ListListint board; int score 0; int highScore 0; Random random Random(); Game2048() { board List.generate(size, (_) List.filled(size, 0)); _addNewTile(); _addNewTile(); } }重点说下 Random 的初始化不要在每次生成新块时都新 new 一个 Random 对象那样在短时间连续调用时会拿到重复的随机序列。把它提成类成员变量一次初始化后面反复用是写任何带随机逻辑的游戏都要注意的细节。3.2 移动与合并算法详解这是整个项目最核心的算法。网上有很多写法但最容易理解的是“先移动后合并”的思路具体分成三步第一把一行里的所有非零数字按顺序提取出来零全部丢到后面。这一步可以理解为“压缩”。比如一行是 [2, 0, 2, 4]压缩后得到 [2, 2, 4, 0]。第二对压缩后的数组进行合并。从最左开始如果当前位置和下一个位置数字相同就合并成一个大的数字翻倍分数加上这个翻倍后的值如果不同就直接跳过。合并时要跳过已经参与过合并的数字防止一个数字被重复合并。比如 [2, 2, 2, 2]正确合并结果应该是 [4, 4, 0, 0]而不是 [4, 0, 2, 2] 或者 [8, 0, 0, 0]。第三把合并后的数组再补零到长度 4写回棋盘。你可能会问为什么不能一步到位边移动边合并可以但那样代码会变得非常绕还要处理“这个格子是刚合并过的”这种状态标记。拆成三步之后每一步的输入输出都很清晰测试也好写。这就是典型的“用空间换清晰度”的写法。方向处理上最省事的方案是把所有滑动方向统一转成“向左滑动”。具体做法是向上滑就先把整个棋盘转置向上变成向左向右滑就把每一行反转向右变成向左向下滑就先转置再反转。处理完之后再把棋盘转回去。3.3 新方块生成与胜负判定移动和合并算完之后如果棋盘位置发生变化就需要在空白格子里生成一个新方块。正常情况下 90% 概率生成 210% 概率生成 4这是原版游戏的设置void _addNewTile() { Listint empty []; for (int i 0; i size; i) { for (int j 0; j size; j) { if (board[i][j] 0) empty.add(i * size j); } } if (empty.isEmpty) return; int index empty[random.nextInt(empty.length)]; board[index ~/ size][index % size] (random.nextInt(10) 0) ? 4 : 2; }胜负判定分两种情况出现 2048 就算赢这个可以在合并时顺便判断一旦合并出 2048 就置一个 win 标记棋盘满了且任意相邻格子都没有相同数字就算输。注意“相邻格子没有相同数字”是关键因为只要有任意两个相邻格子相等你就可以通过一次滑动把它们合并游戏就还没结束。4. 手写核心逻辑Game2048 模型类实现4.1 数据层GameModel 完整代码下面是我在实际项目中使用的核心模型代码你可以在 Trae 里让它生成也可以直接把这段粘进去然后让 AI 帮你做讲解和扩展。完整代码我放在第 8 段说明获取方式这里先把最核心的部分展示出来import dart:math; class Game2048 { static const int size 4; final ListListint board List.generate(size, (_) List.filled(size, 0)); final Random random Random(); int score 0; int highScore 0; bool win false; bool gameOver false; bool _moved false; void init() { for (var row in board) { row.fillRange(0, size, 0); } score 0; win false; gameOver false; _moved false; _addNewTile(); _addNewTile(); } bool moveLeft() { _moved false; for (int i 0; i size; i) { _moveRow(board[i]); } if (_moved) { _addNewTile(); _checkGameOver(); } return _moved; } bool moveRight() { _moved false; for (int i 0; i size; i) { var reversed board[i].reversed.toList(); _moveRow(reversed); board[i] reversed.reversed.toList(); } if (_moved) { _addNewTile(); _checkGameOver(); } return _moved; } bool moveUp() { _moved false; _transpose(); for (int i 0; i size; i) { _moveRow(board[i]); } _transpose(); if (_moved) { _addNewTile(); _checkGameOver(); } return _moved; } bool moveDown() { _moved false; _transpose(); for (int i 0; i size; i) { var reversed board[i].reversed.toList(); _moveRow(reversed); board[i] reversed.reversed.toList(); } _transpose(); if (_moved) { _addNewTile(); _checkGameOver(); } return _moved; } void _moveRow(Listint row) { // Step 1: compress Listint compressed row.where((e) e ! 0).toList(); // Step 2: merge for (int i 0; i compressed.length - 1; i) { if (compressed[i] compressed[i 1]) { compressed[i] * 2; score compressed[i]; if (compressed[i] 2048) win true; compressed.removeAt(i 1); } } // Step 3: pad zeros while (compressed.length size) { compressed.add(0); } // Step 4: write back and track movement for (int i 0; i size; i) { if (row[i] ! compressed[i]) { _moved true; row[i] compressed[i]; } } } void _transpose() { for (int i 0; i size; i) { for (int j i 1; j size; j) { int temp board[i][j]; board[i][j] board[j][i]; board[j][i] temp; } } } void _checkGameOver() { for (int i 0; i size; i) { for (int j 0; j size; j) { if (board[i][j] 0) return; if (j 1 size board[i][j] board[i][j 1]) return; if (i 1 size board[i][j] board[i 1][j]) return; } } gameOver true; } }4.2 为什么合并要“先压缩再合并再补零”很多人第一次写 2048 会卡在“合并逻辑”上原因在于没有把“移动”和“合并”这两个动作拆开。举个例子一行是 [2, 0, 0, 2]向左滑动后正确结果是 [4, 0, 0, 0]。如果你直接在原数组上做交换位置的逻辑就需要先让两个 2 靠拢再判断是否相等合并中间还要处理另一个格子的位置变化非常容易出 bug。拆成“先压缩再合并再补零”之后[2, 0, 0, 2] 先压成 [2, 2]然后合并成 [4]补零后变成 [4, 0, 0, 0]整个过程一目了然。我在写代码时还额外保留了一个 _moved 标记用途是判断这次滑动是否真的改变了棋盘只有改变了才会生成新方块、才会判定 game over。这个标记也是后面做撤销功能的基础。5. UI 与交互让页面动起来5.1 页面布局与自绘棋盘Flutter 中做一个 4x4 棋盘最直观的方式是用 GridView但我实际写下来更推荐直接用 Column Row 嵌套或者用 Stack 做底层圆角背景加上层数字块。原因很简单2048 的格子数量固定而且是正方形不需要 GridView 的懒加载和滚动能力用简单布局反而更好控制间距和圆角。一个合格的棋盘布局需要三层最外层是 Container 设置圆角和背景色中间是 Padding 让格子与棋盘边缘保持距离最内层是 4 行 Row每行放 4 个格子。每个格子用 AspectRatio 保证是正方形里面的文本用 FittedBox 自动缩放这样不管数字是 2 还是 16384都能完整显示在格子里。5.2 手势识别与键盘控制Web 端和移动端最大的区别就是输入方式。在手机上2048 靠滑动手势在电脑上靠键盘方向键。Flutter 的 GestureDetector 可以同时处理这两种输入代码如下GestureDetector( onTapUp: (details) { // 可选支持点击边界方向 }, child: Listener( onPointerMove: (event) { // 也可以在这里用 PointerEvent 来计算滑动方向 }, ), )但更规范的做法是直接用 onPanEnd通过计算滑动起止点的位移差来判定方向onPanEnd: (details) { Offset velocity details.velocity.pixelsPerSecond; if (velocity.dx.abs() velocity.dy.abs()) { if (velocity.dx 0) _controller.moveRight(); else _controller.moveLeft(); } else { if (velocity.dy 0) _controller.moveDown(); else _controller.moveUp(); } }需要注意在 Web 端如果只是用鼠标点击GestureDetector 的 pan 手势也能识别鼠标拖拽但体验不够好。所以代码里同时加了 KeyboardListener监听箭头键和 WASD这对在浏览器里试玩的用户来说几乎是刚需。Flutter Web 对键盘事件的支持已经很成熟了直接在 build 方法里包一层 Focus KeyboardListener 就能收到按键事件。5.3 数字颜色与动画细节2048 的美感很大程度上来自颜色系统。每个数字对应一个背景色比如 2 是浅黄、4 是橙色、8 是红色、16 是粉色等。这个可以用一个 switch 或 map 来实现Color _bgColor(int value) { switch (value) { case 0: return Color(0xFFCDC1B4); case 2: return Color(0xFFEEE4DA); case 4: return Color(0xFFEDE0C8); case 8: return Color(0xFFF2B179); case 16: return Color(0xFFF59563); case 32: return Color(0xFFF67C5F); case 64: return Color(0xFFF65E3B); case 128: return Color(0xFFEDCF72); case 256: return Color(0xFFEDCC61); case 512: return Color(0xFFEDC850); case 1024: return Color(0xFFEDC53F); case 2048: return Color(0xFFEDC22E); default: return Color(0xFF3C3A32); } }动画方面如果想让方块滑动时更自然可以用 AnimatedContainer 给每个格子添加 100 毫秒的动画过渡让数字位置的改变看起来是“滑过去”而不是“瞬移”。但要注意如果你把每个格子当成一个独立 Widget滑动动画会有点难处理因为你移动的是数据而 Widget 的位置是根据数据重建的。我在实际项目中做了简化只对“新生成的方块”做缩放动画对“合并后的方块”做淡入效果整体移动靠 AnimatedContainer 的 duration 来过渡这样代码量小很多视觉上也够用。6. Web 端专项性能优化与部署6.1 Flutter Web 常见坑Service Worker 与本地测试Flutter Web 编译产物默认会用 CanvasKit 渲染并且自带 Service Worker 来做静态资源缓存。这个设计在部署到服务器后体验很好但在本地调试时会有一个非常经典的报错加载 Web 视图时出错Error: Could not register service worker: InvalidStateError原因通常是浏览器在非 HTTPS 环境比如局域网 IP、某些本地调试模式下禁止注册 Service Worker或者上一次的缓存还残留在浏览器里。解决方式不复杂打开开发者工具找到 Application 面板把 Service Workers 和 Cache Storage 全部清掉然后刷新页面。如果还不行就在 pubspec.yaml 里关掉 PWA 相关的选项或者开发时用 webServer 模式启动而不是直接编译发布。顺带说一句Flutter Web 本地调试时推荐用这套组合fvm flutter run -d chrome --web-port8080固定端口的好处是调试器、后端联调、以及后续 Nginx 代理配置都要稳定得多。不要用默认随机端口否则调试时重启一次端口就变一次非常影响效率。6.2 打包体积优化与内存优化Flutter Web 的打包体积一直是被人吐槽的重灾区3.x 版本之后flutter build web默认会做 tree shaking 和 CanvasKit 的按需加载但这还不够。我在这个项目里做了三件有效的优化第一编译时用 release 模式并且声明不生成 debug 版本。flutter build web --release默认就是压缩过的不用额外配置。第二把加载策略改成从 CDN 加载 CanvasKit而不是打进本地资源包。在web/index.html里设置window.flutterConfiguration指向官方 CDN 地址这样本地包里就不用携带七八兆的 CanvasKit 文件首屏加载速度会明显提升。这种方式对已经部署在国内服务器的场景不一定最优但作为了解这个机制是很有价值的。第三做内存优化。Flutter Web 在长时间运行后CanvasKit 的 GPU 缓存有时不会自动释放表现为页面越来越卡。2048 这类游戏虽然逻辑不重但如果你给每个数字块都建了独立的动画控制器并且没有在 dispose 里释放就会造成内存泄漏。排查方法是用 Chrome 的 Performance 面板录制一段操作观察内存曲线是否只涨不降。对这个项目来说滑动操作触发 setState 的次数不要太频繁合并动画完成后主动清理 AnimationController基本就不会有问题了。6.3 用 Nginx 部署并支持多个 Web 工程开发完成后flutter build web会在 build/web 目录下生成静态文件。这个目录直接丢到 Nginx 的 root 下就能跑但有一个关键点需要处理Flutter Web 的路由是 hash 路由还是 history 路由默认是 hash 路由所以 Nginx 配置里不需要像 Vue Router 那样做 try_files 回退。如果你服务器上还有别的 Web 工程想一个域名下挂多个项目可以用子路径的方式server { listen 80; server_name yourdomain.com; location /game2048/ { alias /var/www/game2048/build/web/; index index.html; } location /blog/ { alias /var/www/blog/dist/; index index.html; } }这里要注意 Nginx 的 alias 和 root 的差异。root 会把 location 后面的路径拼接到实际的目录路径后面而 alias 是直接映射到指定目录。用 alias 配 Flutter Web 时需要确保 index.html 引用的资源路径是相对路径否则会因为前缀路径不一致导致资源 404。这个小问题我在部署 Vue 工程时也踩过属于 Web 工程的通用坑。7. 常见问题排查实录Trae Flutter 高频报错7.1 unable to find suitable visual studio toolc... 的解法这个报错主要出现在 Windows 上跑 Flutter Android 工程时而不是 Web 工程。Trae 内置终端和系统终端在某些环境下没有继承完整的 Visual Studio 环境变量导致 Flutter 找不到 C 编译工具链。解决思路有三种按优先级排列第一去 Visual Studio Installer 里确认安装了“使用 C 的桌面开发”工作负载第二重启 Trae让它能重新读取系统环境变量第三在 Trae 设置里手动指定 Visual Studio 的安装路径或者直接切换到 Android Studio 的终端来执行构建。对纯 Web 开发来说这个报错其实可以完全避开——用fvm flutter run -d chrome根本不触发原生编译。所以如果你只是做 Web 小游戏遇到这个报错基本可以无视。7.2 主 Gradle 插件命令式应用错误的处理这个报错是典型的 Flutter Android 工程版本不匹配问题错误信息长这样You are applying Flutters main Gradle plugin imperatively using the apply script method核心原因是 Flutter 3.16 之后的版本把 Gradle 插件声明方式从 apply script 改成了 declarative plugins DSL而本地 Gradle 版本或历史模板还在用老写法。Trae 在生成项目时偶尔也会把老模板混进来。解决办法是打开 android/settings.gradle 和 android/app/build.gradle把插件声明改成新版写法或者用fvm flutter create --platformsandroid --project-name game_2048 .在当前目录重新生成 android 子工程再把 pubspec.yaml 和 lib 目录保留下来。重新生成是效率最高的方式手动改 gradle 文件容易漏掉继承关系。7.3 Trae 生成代码编译不过的通用排查思路用 Trae 写代码时生成报错是最常见的情况。我踩了几次之后总结出一套“三层排查法”你可以直接套用第一层看报错位置是不是类型不匹配。AI 生成的代码经常出现 int 和 double 混用的情况。比如 Flutter 的 EdgeInsets.all 接收的是 double如果你传了一个 int 表达式编译直接报错。这类问题最简单让 Trae 自己读报错信息就能修。第二层看是不是 import 缺失。AI 在生成代码时会默认你引用了某个包但它自己不会检查 import。比如用了 Random 但没有 import dart:math用了 GestureDetector 的 onPanEnd 但没意识到手势回调里拿到的 velocity 类型。这种情况把完整报错信息发给 Trae它会自动补 import。第三层看是不是版本兼容问题。Trae 的训练数据里有些代码是基于旧版本 Flutter 写的比如旧版本的 initState 里可以直接调用 context新版本会提示 invalid_use_of_context。这种问题单看报错看不出所以然需要你自己确认 Flutter 版本。解决思路是在 Trae 对话框里明确指定“使用 Flutter 3.44 API”或者在 pubspec.yaml 里锁定版本后让它重新检查。8. 完整代码获取与后续扩展思路8.1 完整代码说明由于篇幅原因博客里没法把完整工程代码全部贴出但我在第 4 小节的模型类已经是可运行的核心再加上你自己实现的 main.dart 界面部分就可以跑通了。如果不想从零补全 UI可以把这篇博文里提到的核心类、颜色映射、滑动方向转换、键盘手势绑定这四部分在 Trae 里一句句描述给 AI让它基于这些信息生成完整工程。我给 Trae 的提示词大概是这样的你是一个 Flutter 专家。请基于以下需求生成一个完整的 Flutter Web 2048 游戏工程 1. 使用提供的 Game2048 模型类代码 2. 页面包含分数、最高分、重新开始按钮 3. 支持手机滑动手势和键盘方向键/WASD 4. 数字块颜色按 2048 官方配色实现 5. 布局要适配 Web 窗口最大宽度 500 居中 6. 在合适的时机播放简单动效把这段提示词丢给 Trae再把这个模型类的代码贴进对话里它生成的工程基本可以直接跑。我实测下来Trae 在处理这种“已有核心逻辑、补齐 UI”的任务时成功率比从零生成整个游戏高得多。如果遇到个别报错按第 7 节的方法让 AI 自行修复即可。8.2 还能继续扩展的方向做完基础版 2048 之后有四个方向值得继续深入难度从低到高排列一是加分动画与音效把游戏反馈做得更细腻。音效可以用 AudioPlayers 插件分数变化时播放短促的音效合并且数字大于 128 时播放特效用音效。这个方向能练到 Flutter 的动画控制器和音频生命周期管理。二是加入“撤销”功能记录每一步之前的棋盘状态点击撤销按钮可以回退。这项技术其实是练“深拷贝”和“状态栈”的好机会面试时经常被问到用这个小项目来说明会很有说服力。三是把“最佳移动策略”作为提示系统。2048 这个游戏看起来是纯手动但如果你接入一个简单的启发式算法比如空位数单调性平滑度评分就能在界面上提示用户“往哪个方向滑胜率更高”。这等于把一个小型 AI 决策系统做进了游戏里拿来面试讲特别加分。四是用 Dio 把成绩上报到自己的后端做成一个全球排行榜。借这个机会还能练一下 Flutter 的请求封装、抓包调试这些实战技能。很多热搜里提到 Flutter Dio 如何抓包我建议的做法是开发环境将 baseUrl 指向本地服务用 Charles 或 Fiddler 看流量上线前把 baseUrl 切换到正式域名即可。我在实际使用中的体会是2048 这个项目的最大价值不是让你复刻一个游戏而是用最小的体量走完“需求拆解、算法设计、UI 还原、Web 适配、部署上线”的完整链路。等你把这个链路跑通一遍后面再做任何 Flutter Web 项目就会发现之前的每一个坑都变成了你的经验储备。特别是把 Trae 这样的 AI 工具拉进来之后你省下的写模板代码的时间都可以拿去打磨动画和交互手感这才是做游戏最过瘾的部分。
返回列表