R2C(Requirement to Code)
领域模型(Domain Model)+ CRUD 矩阵(业务 × 实体 访问矩阵)+ ER 图(概念数据模型 CDM)
一、核心答案:需求 → 代码之间的"副产品三件套"
| 层级 | 名称 | 回答什么问题 | 形态 |
|---|---|---|---|
| 认知层 | 领域模型 | 业务世界里有哪些核心概念、它们的生命周期是什么 | 类图 / 领域对象图 |
| 行为层 | CRUD 矩阵 | 哪个角色在哪个用例下对哪个实体做了什么操作 | 二维表 |
| 数据层 | ER 图(CDM) | 实体之间到底是 1-1 / 1-N / N-N / 自关联 | E-R 图 / 概念模型 |
这三件套一旦做出来,表结构 + Java 实体 + Service 接口几乎可以机械式生成。
二、完整七层管道(需求 → 代码的完备方法论)
html
┌───────────────────────────────────────────────────────────────┐
│ L0 业务愿景 / 立项目标 (Why) │
│ ↓ 提炼 │
│ L1 业务流程 + 用例模型 (What, Who, When) ← 调研产物 │
│ ↓ 名词抽取 + 动词抽取 │
│ L2 领域模型 (Domain Model) ★ 副产品① │
│ ↓ 用例 × 实体 交叉分析 │
│ L3 CRUD 矩阵 (Entity × Use Case) ★ 副产品② │
│ ↓ 规约关系(1-1/1-N/N-N/自关联) │
│ L4 概念数据模型 CDM(ER 图) ★ 副产品③ │
│ ↓ 规范化(1NF→3NF)+ 补充属性 + 外键 │
│ L5 逻辑数据模型 LDM │
│ ↓ 适配具体 DBMS(MySQL/PG/Oracle) │
│ L6 物理数据模型 PDM + DDL │
│ ↓ 代码生成器(MBG / JPA Buddy / JHipster) │
│ L7 Entity + Mapper/Repository + Service + Controller │
└───────────────────────────────────────────────────────────────┘
关键洞察:
- L1 → L2 是"语义"关(业务语言 → 领域语言)
- L2 → L4 是"结构"关(对象语义 → 关系结构)
- L4 → L7 是"工程"关(可自动化、纯机械)
能被工具自动化的只有 L4 → L7;真正需要人思考的在 L1 → L4。所以**"能把需求直接变代码的人"其实是"能把需求直接变 ER 图的人"**。
三、以 BBS 论坛为例:一步步推演整条管道
L0 业务愿景
"给校园用户搭一个发帖、回帖、搜索、可管理的在线交流平台。"
L1 用例抽取(动词 + 角色)
| 角色 | 用例(动词短语) |
|---|---|
| 游客 | 浏览帖子、搜索帖子、注册 |
| 会员 | 登录、发帖、回帖、搜索、查看个人帖子 |
| 管理员 | 登录后台、删除帖子、封禁用户、管理版块 |
L2 领域模型(名词抽取 → 领域对象)
从你附件文档里一抽就是这些名词:用户、管理员、帖子、回复、版块(隐含)、搜索记录(可选)。
用 Mermaid 画出领域模型:

L3 CRUD 矩阵(这是你最想要的"副产品")
这一步一旦画出来,Service 接口几乎 1:1 映射出来。
| 用例 \ 实体 | User | Admin | Article | Board |
|---|---|---|---|---|
| 注册 | C | - | - | - |
| 登录 | R | R | - | - |
| 发帖 | R | - | C | R |
| 回帖 | R | - | C(自关联) | R |
| 搜索帖子 | - | - | R | R |
| 删除帖子 | - | R | D | - |
| 封禁用户 | U | R | - | - |
| 管理版块 | - | R | - | CRUD |
解读规则:
- 每一个 C 对应一个
create()方法 - 每一个 R 对应
findById / list / search方法 - 每一个 U 对应
update()方法 - 每一个 D 对应
delete()方法 - 一列里有多种操作的实体 = 需要完整 Service
- 只有 R 的实体 = 只读 Repository
L4 概念数据模型(ER 图)

L5 / L6 物理模型 DDL(直接从 ER 图落地)
sql
CREATE TABLE user (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(32) NOT NULL UNIQUE,
password VARCHAR(64) NOT NULL,
email VARCHAR(64),
sex VARCHAR(4)
);
CREATE TABLE admin_use (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(32) NOT NULL UNIQUE,
password VARCHAR(64) NOT NULL
);
CREATE TABLE board (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(64) NOT NULL,
description VARCHAR(255)
);
CREATE TABLE article (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
pid BIGINT DEFAULT 0,
root_id BIGINT,
user_id BIGINT,
board_id BIGINT,
title VARCHAR(255),
content TEXT,
pdate DATETIME,
is_leaf INT DEFAULT 1,
pno INT DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES user(id),
FOREIGN KEY (board_id) REFERENCES board(id),
FOREIGN KEY (pid) REFERENCES article(id)
);
L7 代码生成(机械化输出)
实体类(由 ER 直接映射):
java
// Article.java ------ 从 ER 图机械式生成
@Data
@TableName("article")
public class Article {
@TableId(type = IdType.AUTO)
private Long id;
private Long pid; // 父帖 ID,自关联
private Long rootId; // 根帖 ID
private Long userId; // FK → user
private Long boardId; // FK → board
private String title;
private String content;
private LocalDateTime pdate;
private Integer isLeaf;
private Integer pno;
}
Service 接口(由 CRUD 矩阵直接映射):
java
public interface ArticleService {
// ------ 来自 CRUD 矩阵"发帖/回帖" → C
Long postArticle(Long userId, Long boardId, String title, String content);
Long replyArticle(Long userId, Long parentId, String content);
// ------ 来自 CRUD 矩阵"搜索帖子" → R
Article getById(Long id);
List<Article> listByBoard(Long boardId, int page, int size);
List<Article> search(String keyword, int page, int size);
List<Article> listReplies(Long rootId);
// ------ 来自 CRUD 矩阵"删除帖子" → D(管理员)
void deleteByAdmin(Long articleId, Long adminId);
// ------ 浏览计数
void incrementViewCount(Long id);
}
public interface UserService {
Long register(User user); // 来自 "注册" → C
User login(String username, String password); // 来自 "登录" → R
User getById(Long id);
void banUser(Long userId); // 来自 "封禁" → U
}
每一个 Service 方法都能在 CRUD 矩阵里找到出处;每一个字段都能在 ER 图里找到出处;每一个实体都能在领域模型里找到出处;每个领域对象都能在业务用例名词里找到出处。
四、把这套方法论工具化(真正落地)
推荐实战配方(最小可行组合)
| 阶段 | 推荐工具 | 产出 |
|---|---|---|
| 领域建模 | Event Storming(便利贴/Miro)+ PlantUML | 领域模型 |
| ER 设计 | MySQL Workbench / dbdiagram.io / drawDB / chiner | ER 图 + DDL |
| DDL → 代码 | MyBatis-Plus CodeGenerator 或 JPA Buddy | Entity+Mapper+Service+Controller |
| 一条龙方案 | JHipster + JDL | 从 DSL 直接出完整项目 |
| AI 加持 | ChatGPT/Claude:需求 → ER → DDL → 代码 | 整条管道半自动 |
JHipster JDL 示例(这就是"需求 DSL → 代码"的极致)
java
entity User {
username String required unique maxlength(32),
password String required,
email String,
sex String
}
entity Article {
title String required,
content TextBlob,
pdate Instant,
pno Integer
}
entity Board {
name String required,
description String
}
relationship ManyToOne {
Article{author} to User,
Article{board} to Board,
Article{parent} to Article // 自关联
}
service all with serviceImpl
paginate Article with pagination
一条 jhipster jdl bbs.jdl 命令直接生成完整 Spring Boot 项目,包含 Entity、Repository、Service、Controller、前端页面。这描述的"需求 → 多表关系图 → 代码"全自动管道的工业级实现。
五、总结:回答最初的三个疑问
| 你的疑问 | 答案 |
|---|---|
| 表建立前有没有完备方法? | 有:领域建模 + CRUD 矩阵 + ER 图三件套 |
| 需求和 ER 图之间有没有桥梁? | 有:名词抽取 → 领域模型 → CRUD 矩阵 → ER 四步法 |
| 能否从需求直达多表关系? | 能半自动:JDL / Prisma / 事件风暴 + AI 辅助 |
| 能否从表关系直达 Service 代码? | 能全自动:MBG / JPA Buddy / JHipster |
一句话记忆法:
"名词变实体,动词变方法,关系变外键,角色变权限,用例变接口。"
这就是从业务调研到代码生成的完备桥梁。
游戏领域的扩展:G2C(Game-Design-to-Code) ------ 从策划案到 Unity 6 工程
完全可以扩展,而且游戏领域比业务软件更需要这种工具(因为策划迭代频繁、系统间耦合复杂、实现成本高)。但有一个关键认知必须先建立:
不能把 R2C 的模板换一套就当 G2C。游戏开发的"副产品三件套"与业务软件本质不同,需要重新设计整条管道。
下面用和回答 BBS 同样的深度,给出一份完整的 G2C 产品设计蓝图,包括范式差异、管道重构、案例演示、Unity 6 代码产出、双模态产品架构,以及边界讨论。
一、本质差异:为什么不能直接套 R2C
| 维度 | 业务软件(R2C) | 游戏(G2C) |
|---|---|---|
| 核心抽象 | 数据 + 流程 | 机制 + 反馈循环 |
| 建模重心 | 实体关系 (ER) | 组件组合 + 状态机 + 事件流 |
| 代码骨架 | CRUD + Service | MonoBehaviour + ScriptableObject + Systems |
| 生命周期 | 请求-响应式(无状态) | 每帧 Update(强状态) |
| 交互模型 | 用户点按钮 → 调接口 | 输入 → 状态变化 → 物理/渲染/音效/AI 多管线联动 |
| 正确性判据 | 数据一致性 | "手感"(game feel) ------无法纯规则验证 |
| 可自动生成度 | 高(60%~80%) | 中(30%~50%)+ 高价值脚手架 |
| 数据驱动单位 | 数据库表 | ScriptableObject / YAML 配置 |
| 系统解耦手段 | Service 分层 | 事件总线 / Signal / UnityEvent |
关键洞察 :游戏里"表 "不是真正的中间产物,ScriptableObject(数据资产)+ Component 组合图 + 状态机 + 事件图才是。
二、游戏领域的"副产品三件套"重定义
把业务版三件套升级为游戏版五件套:
| # | 名称 | 回答什么 | 形态 |
|---|---|---|---|
| ① | GMM 游戏机制模型 | 核心玩法循环是什么?胜负条件?节奏曲线? | 玩法循环图 + 核心循环表 |
| ② | System × Feature 矩阵 | 每个系统承担哪些游戏功能? | 二维矩阵 |
| ③ | ECD 组件组合图(Entity-Component-Data) | 游戏对象由哪些组件组成?数据在哪? | 组件树 / Prefab 结构 |
| ④ | State Machine Graph | 角色、AI、UI、关卡有哪些状态和转移? | 状态图 |
| ⑤ | Event Bus Graph | 系统之间通过什么信号解耦? | 事件流图 |
这五件套做出来,Unity 项目 60% 的骨架代码可机械生成。
三、L0~L7 管道(游戏版 vs 业务版对照表)
html
┌─────────────────────────────────────────────────────────────────────────┐
│ L0 游戏愿景 / Pitch ←→ 业务愿景 / 立项目标 │
│ ↓ │
│ L1 GDD 核心玩法 + 用户体验旅程 ←→ 业务流程 + 用例 │
│ ↓ │
│ L2 实体 + 系统 领域模型 ←→ 领域模型 │
│ (Entities: Player/Enemy/Item... │
│ Systems: Combat/AI/Economy...) │
│ ↓ │
│ L3 System × Feature 矩阵 + ←→ CRUD 矩阵 │
│ 状态机 + 事件流 │
│ ↓ │
│ L4 ECD 组件组合图 + SO 数据结构 ←→ ER 图 │
│ ↓ │
│ L5 Unity 项目结构 + 场景树 + ←→ 逻辑数据模型 │
│ Prefab 层级 + Assembly Defs │
│ ↓ │
│ L6 C# 脚本骨架 + SO 资产 + UXML ←→ DDL + 建表 │
│ ↓ │
│ L7 可运行的 Unity 6 工程 ←→ 可运行的 Spring Boot 工程 │
│ (.unitypackage 或 源码目录) │
└─────────────────────────────────────────────────────────────────────────┘
能被工具自动化的在 L4~L7 (脚手架、数据结构、状态机、事件总线、UI 骨架) 真正需要策划/程序思考的在 L1~L3(核心玩法、手感、平衡、数值曲线)
四、案例演示:一句话 → Unity 6 塔防游戏
输入:
"做一个 2D 塔防游戏:玩家在地图上放置不同类型的防御塔抵御一波波进攻的敌人,击败敌人获得金币升级或建造新塔,守住基地直到最终波次胜利。"
L1 抽取:GDD 骨架
| 维度 | 内容 |
|---|---|
| 类型 | 2D 塔防 Tower Defense |
| 视角 | 俯视 2D |
| 核心循环 | 观察来袭 → 放置/升级塔 → 击败敌人 → 获得金币 → 强化 |
| 胜利条件 | 击败所有波次 |
| 失败条件 | 基地 HP ≤ 0 |
| 乐趣来源 | 策略布阵、资源分配、爽感清屏 |
动词 → 用例:放置塔、升级塔、拆除塔、敌人寻路、敌人攻击基地、塔射击敌人、开始波次、暂停、切换波次。
名词 → 实体:玩家、塔(多种)、敌人(多种)、子弹、波次、路径、基地、金币、地图格子。
L2 领域模型(实体 + 系统)
L3 System × Feature 矩阵
| Feature \ System | WaveSystem | PlacementSystem | TargetingSystem | CombatSystem | EconomySystem | UISystem |
|---|---|---|---|---|---|---|
| 波次生成与推进 | 主 | - | - | - | - | 通知 |
| 放置新塔 | - | 主 | - | - | 扣款 | 显示 |
| 升级塔 | - | 主 | - | - | 扣款 | 显示 |
| 塔锁定敌人 | - | - | 主 | 输入 | - | - |
| 子弹飞行+命中 | - | - | - | 主 | - | - |
| 敌人死亡奖励 | 通知 | - | - | 主 | 增加 | 刷新 |
| 敌人触底扣血 | - | - | - | - | 通知 | 震屏 |
| HUD 金币/波次/HP | - | - | - | - | 数据 | 主 |
规则 :每格填 "主/输入/通知/数据/刷新/显示/扣款/增加" 等职责,"主"= 该 System 拥有这个 Feature 的实现权。
L3 状态机:波次 + 敌人 + 游戏整体

L3 事件总线图

L4 ECD 组件组合图(Prefab 结构)
html
TowerPrefab (GameObject)
├── Transform
├── SpriteRenderer
├── Tower.cs // 逻辑
├── Targeting.cs // 索敌
├── Shooter.cs // 开火
├── TowerAnimator (Animator)
└── RangeIndicator (Child)
└── CircleRenderer
EnemyPrefab
├── Transform
├── SpriteRenderer
├── Rigidbody2D
├── Collider2D
├── Enemy.cs
├── PathFollower.cs
├── Health.cs
└── HealthBar (Child, UGUI)
ProjectilePrefab
├── Transform
├── SpriteRenderer
├── Rigidbody2D
├── Projectile.cs
└── Homing.cs
五、Unity 6 代码产出:具体样例(机械生成)
① ScriptableObject 数据资产(来自 L4 数据结构)
cs
// TowerData.cs ------ 由实体属性机械生成
using UnityEngine;
[CreateAssetMenu(fileName = "TowerData", menuName = "TD/Tower Data")]
public class TowerData : ScriptableObject
{
[Header("基础")]
public string towerName;
public Sprite icon;
public GameObject prefab;
[Header("数值")]
public int cost = 50;
public float damage = 10f;
public float attackRange = 3f;
public float attackInterval = 1f;
[Header("升级")]
public TowerData upgradeTo;
public int upgradeCost = 100;
[Header("弹药")]
public GameObject projectilePrefab;
}
[CreateAssetMenu(fileName = "EnemyData", menuName = "TD/Enemy Data")]
public class EnemyData : ScriptableObject
{
public string enemyName;
public GameObject prefab;
public float maxHp = 50f;
public float moveSpeed = 2f;
public int reward = 10;
public int damageToBase = 1;
}
[CreateAssetMenu(fileName = "WaveData", menuName = "TD/Wave Data")]
public class WaveData : ScriptableObject
{
[System.Serializable]
public class SpawnEntry { public EnemyData enemy; public int count; public float interval; }
public SpawnEntry[] entries;
public float delayBeforeWave = 3f;
}
② 事件总线(来自 L3 事件图)
cs
// GameEvents.cs ------ 统一事件中枢,由事件图机械生成
using System;
public static class GameEvents
{
public static event Action<Tower> OnTowerPlaced;
public static event Action<Enemy> OnEnemySpawned;
public static event Action<Enemy, int> OnEnemyKilled; // enemy + reward
public static event Action<int> OnBaseHpChanged;
public static event Action<int> OnGoldChanged;
public static event Action<int> OnWaveStarted;
public static event Action OnAllWavesCleared;
public static event Action OnGameOver;
public static void RaiseTowerPlaced(Tower t) => OnTowerPlaced?.Invoke(t);
public static void RaiseEnemySpawned(Enemy e) => OnEnemySpawned?.Invoke(e);
public static void RaiseEnemyKilled(Enemy e, int r) => OnEnemyKilled?.Invoke(e, r);
public static void RaiseBaseHpChanged(int hp) => OnBaseHpChanged?.Invoke(hp);
public static void RaiseGoldChanged(int g) => OnGoldChanged?.Invoke(g);
public static void RaiseWaveStarted(int i) => OnWaveStarted?.Invoke(i);
public static void RaiseAllWavesCleared() => OnAllWavesCleared?.Invoke();
public static void RaiseGameOver() => OnGameOver?.Invoke();
}
③ System 骨架(来自 System × Feature 矩阵)
cs
// WaveSystem.cs ------ "主"职责:波次生成
using System.Collections;
using UnityEngine;
public class WaveSystem : MonoBehaviour
{
[SerializeField] WaveData[] waves;
[SerializeField] Transform spawnPoint;
int currentWave = 0, aliveEnemies = 0;
void OnEnable() => GameEvents.OnEnemyKilled += OnEnemyKilled;
void OnDisable() => GameEvents.OnEnemyKilled -= OnEnemyKilled;
public void StartNextWave()
{
if (currentWave >= waves.Length) { GameEvents.RaiseAllWavesCleared(); return; }
StartCoroutine(SpawnWave(waves[currentWave]));
GameEvents.RaiseWaveStarted(currentWave);
currentWave++;
}
IEnumerator SpawnWave(WaveData wave)
{
yield return new WaitForSeconds(wave.delayBeforeWave);
foreach (var entry in wave.entries)
for (int i = 0; i < entry.count; i++)
{
var go = Instantiate(entry.enemy.prefab, spawnPoint.position, Quaternion.identity);
go.GetComponent<Enemy>().Init(entry.enemy);
aliveEnemies++;
GameEvents.RaiseEnemySpawned(go.GetComponent<Enemy>());
yield return new WaitForSeconds(entry.interval);
}
}
void OnEnemyKilled(Enemy _, int __)
{
aliveEnemies--;
if (aliveEnemies == 0) Invoke(nameof(StartNextWave), 2f);
}
}
cs
// EconomySystem.cs ------ "主"职责:金币
public class EconomySystem : MonoBehaviour
{
[SerializeField] int startingGold = 200;
int gold;
void Awake() { gold = startingGold; GameEvents.RaiseGoldChanged(gold); }
void OnEnable() { GameEvents.OnEnemyKilled += AddReward; }
void OnDisable() { GameEvents.OnEnemyKilled -= AddReward; }
void AddReward(Enemy _, int reward) { gold += reward; GameEvents.RaiseGoldChanged(gold); }
public bool TrySpend(int cost)
{
if (gold < cost) return false;
gold -= cost; GameEvents.RaiseGoldChanged(gold); return true;
}
}
④ 状态机(来自 L3 状态图)
cs
// GameStateMachine.cs ------ 由状态图机械生成
public enum GameState { MainMenu, Playing, Paused, Victory, GameOver }
public class GameStateMachine : MonoBehaviour
{
public GameState Current { get; private set; } = GameState.MainMenu;
void OnEnable()
{
GameEvents.OnAllWavesCleared += () => TransitionTo(GameState.Victory);
GameEvents.OnGameOver += () => TransitionTo(GameState.GameOver);
}
public void TransitionTo(GameState next)
{
// 合法性检查表(由状态图生成)
if (!IsValidTransition(Current, next)) return;
OnExit(Current); Current = next; OnEnter(next);
}
// ... OnEnter/OnExit/IsValidTransition 从状态图自动展开
}
⑤ UI Toolkit UXML(来自原型稿)
XML
<!-- HUD.uxml -->
<ui:UXML xmlns:ui="UnityEngine.UIElements">
<ui:VisualElement name="hud" class="hud-root">
<ui:Label name="gold-label" text="💰 0" class="hud-stat"/>
<ui:Label name="wave-label" text="Wave 0/10" class="hud-stat"/>
<ui:Label name="basehp-label" text="❤ 20" class="hud-stat"/>
<ui:VisualElement name="tower-shop" class="tower-shop">
<ui:Button name="btn-arrow" text="Arrow 50g"/>
<ui:Button name="btn-cannon" text="Cannon 100g"/>
<ui:Button name="btn-magic" text="Magic 150g"/>
</ui:VisualElement>
</ui:VisualElement>
</ui:UXML>
⑥ Unity 项目目录结构(机械生成)
cs
Assets/
├── _Project/
│ ├── Art/ (占位 placeholder 贴图 + 引导)
│ ├── Audio/ (占位 SFX/BGM 标签)
│ ├── Data/ ★ ScriptableObject 实例
│ │ ├── Towers/ ArrowTower.asset, CannonTower.asset...
│ │ ├── Enemies/ Grunt.asset, Runner.asset, Tank.asset
│ │ └── Waves/ Wave_01.asset ... Wave_10.asset
│ ├── Prefabs/
│ ├── Scenes/ MainMenu.unity, Level01.unity
│ ├── Scripts/
│ │ ├── Core/ GameEvents, GameStateMachine
│ │ ├── Entities/ Tower, Enemy, Projectile, Base
│ │ ├── Systems/ Wave, Placement, Targeting, Combat, Economy
│ │ ├── UI/ HUDController
│ │ └── Editor/ TowerDataEditor, WaveDataEditor (编辑器扩展)
│ ├── UI/ HUD.uxml, HUD.uss
│ └── Settings/ InputActions, URP Asset
├── TD.asmdef (Assembly Definition)
└── TD.Editor.asmdef
六、产品架构:双模态设计 ------ R2C + G2C 共生
在已规划的 PyQt6 应用里增加模式切换,共用同一套管道引擎:
html
┌─────────────────────────────────────────────────────────────┐
│ R2C/G2C Unified Shell (PyQt6) │
├─────────────────────────────────────────────────────────────┤
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Mode: WebApp │ │ Mode: Game │ │ Mode: ... │ │
│ │ (R2C) │ │ (G2C) │ │ Mobile? IoT? │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ ┌──────▼───────────────────▼───────────────────▼───────┐ │
│ │ Common Pipeline Engine │ │
│ │ L1 NLU → L2 Model → L3 Matrix → L4 Struct → L7 Gen │ │
│ └──────┬───────────────────┬───────────────────┬───────┘ │
│ │ │ │ │
│ ┌──────▼───────┐ ┌──────▼───────┐ ┌──────▼───────┐ │
│ │ KB: Business │ │ KB: Game │ │ KB: ... │ │
│ │ • CRUD pat │ │ • GMM pat │ │ │ │
│ │ • Jinja2 │ │ • Unity tpl │ │ │ │
│ │ • DDL gen │ │ • SO/ECS │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Export: Spring Boot zip │ Unity folder / .unitypackage│ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
差异化部分仅在三层:
- 知识库 KB:业务 → CRUD 模式库;游戏 → 玩法模式库(塔防/Rogue/RPG/解谜...)
- 中间产物:业务 → ER/CRUD 矩阵;游戏 → GMM/SF 矩阵/状态机/事件图
- 模板库:业务 → Jinja2 Java 模板;游戏 → Unity C# + SO + UXML 模板
共用 80%:UI 框架、项目管理、NLU、LLM 插槽、文档导出、流程图引擎、可视化编辑器。
七、边界 ------ 能自动生成什么 / 不能生成什么
| 能力 | 自动化程度 | 备注 |
|---|---|---|
| 项目脚手架(目录/asmdef/场景/Prefab) | 🟢 90% | 纯机械 |
| ScriptableObject 数据模型 | 🟢 95% | 从实体定义直出 |
| MonoBehaviour 骨架 + System 类 | 🟢 80% | 业务逻辑 TODO 留给策划/程序 |
| 事件总线与订阅管线 | 🟢 95% | 从事件图展开 |
| 状态机代码 | 🟢 85% | 从状态图展开(可选 FSM 库) |
| UI Toolkit UXML 骨架 | 🟢 75% | 布局/样式需美术细化 |
| 数值/平衡/曲线 | 🔴 10% | 必须策划手调 |
| 手感(feel):打击/节奏/反馈 | 🔴 0% | 只有人能调 |
| 美术/音效/动画资源 | 🔴 0% | 用占位资源,后期替换 |
| 关卡设计 | 🟡 30% | 能生成程序化关卡模板,具体由关卡设计师 |
| AI 行为(BT/GOAP) | 🟡 50% | 节点骨架可生成,决策逻辑需策划 |
| Shader / VFX | 🔴 0% | 超出 scope |
结论 :G2C 不是"替代策划/程序",而是吃掉 60% 的脚手架时间,让团队把精力留给手感、平衡、美术、玩法创新。
八、Unity 6 专属集成点(要在模板里用上)
| Unity 6 特性 | G2C 中如何利用 |
|---|---|
| UI Toolkit (UXML+USS) | UI 原型稿 → UXML 机械生成 |
| Input System | 从用例中"玩家输入"动词生成 InputActions.asset |
| Addressables 2.0 | 大资源自动归入 Addressable group |
| Assembly Definitions | 按 System 分 asmdef,编译提速 |
| ScriptableObject Architecture | 数据/事件/变量全 SO 化,编辑器友好 |
| Cinemachine 3 | 相机方案模板(跟随/震屏/过场) |
| DOTS/ECS(可选) | 大规模单位游戏(RTS/弹幕)切 ECS 模板 |
| Timeline | 过场/教程用 Timeline 骨架 |
| Unity Test Framework | 生成 PlayMode/EditMode 测试骨架 |
九、落地路线图(建议 4 迭代)
| 迭代 | 目标 | 产物 |
|---|---|---|
| M1 基础管道 | 一句话 → GMM + SF 矩阵 + Unity 目录 | 能跑空项目 |
| M2 玩法模式库 | 塔防、Roguelite、RPG、解谜、平台跳跃 5 套模板 | 各出 1 个可玩 demo |
| M3 可视化编辑 | PyQt6 里做节点式编辑器调整 ECD / 状态机 / 事件图 | 二次编辑回流 |
| M4 Unity 插件侧 | Unity Editor 里装 G2C 导入插件,直接同步更新 | 策划改一下,代码秒更 |
十、一句话总结
G2C = R2C 的"范式变体":共用前端壳和管道引擎,替换知识库 + 中间产物 + 模板库,即可从"给校园做论坛"无缝扩展到"做一个塔防游戏"。
核心变化 :ER 图 → ECD 组件图 + 状态机 + 事件图 ;CRUD 矩阵 → System × Feature 矩阵 ;DDL → ScriptableObject + Prefab ;Service 接口 → System MonoBehaviour + 事件总线。
搭建完整的 G2C 工具,从零开始实现一个能运行的 PyQt6 桌面应用,以塔防为 Demo 跑通完整的策划→Unity 6 导出管道。
先看下工作区环境:
bash
pwd && ls -la && python3 --version && pip3 list 2>/dev/null | head -20
创建项目结构:
bash
mkdir -p /home/user/g2c/g2c && cd /home/user/g2c && find . -type f
先建依赖文件:
bash
cat > /home/user/g2c/requirements.txt << 'EOF'
PyQt6>=6.5.0
Jinja2>=3.1.0
EOF
安装依赖:
bash
cd /home/user/g2c && pip install PyQt6 Jinja2 2>&1 | tail -5
第一步:数据模型层
python
cat > /home/user/g2c/g2c/__init__.py << 'EOF'
"""G2C - Game Design to Code: 从游戏策划到 Unity 6 工程的自动化管道"""
__version__ = "0.1.0"
EOF
python
cat > /home/user/g2c/g2c/models.py << 'PYEOF'
"""
G2C 核心数据模型
================
整条管道的数据流定义:从策划语义到 Unity 工件,每一层都有对应的数据结构。
设计哲学:所有中间产物都是可序列化、可编辑、可回流的 dataclass。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum, auto
from typing import Optional
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# L0 愿景层
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@dataclass
class GameVision:
"""一句话需求 + 解析出的元信息"""
raw_input: str = ""
genre: str = "" # 识别出的游戏类型
perspective: str = "" # 2D/3D/2.5D
theme: str = "" # 主题/美术风格
target_platform: str = "PC"
summary: str = "" # 生成的一句话摘要
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# L1 用例层
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class ActorType(Enum):
PLAYER = auto()
ENEMY = auto()
SYSTEM = auto()
ENVIRONMENT = auto()
@dataclass
class UseCase:
"""一个用例 = 谁 + 做什么 + 触发什么"""
actor: str = ""
actor_type: ActorType = ActorType.PLAYER
action: str = "" # 动词短语
target: str = "" # 作用对象
trigger: str = "" # 触发条件
outcome: str = "" # 结果/效果
priority: int = 1 # 1=核心 2=重要 3=次要
@dataclass
class CoreLoop:
"""核心玩法循环"""
steps: list[str] = field(default_factory=list)
win_condition: str = ""
lose_condition: str = ""
fun_source: str = "" # 乐趣来源
@dataclass
class GDDSkeleton:
"""L1 产物:GDD 骨架"""
vision: GameVision = field(default_factory=GameVision)
core_loop: CoreLoop = field(default_factory=CoreLoop)
use_cases: list[UseCase] = field(default_factory=list)
actors: list[str] = field(default_factory=list)
nouns: list[str] = field(default_factory=list) # 名词(候选实体)
verbs: list[str] = field(default_factory=list) # 动词(候选行为)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# L2 领域模型层
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class FieldType(Enum):
INT = "int"
FLOAT = "float"
STRING = "string"
BOOL = "bool"
VECTOR2 = "Vector2"
VECTOR3 = "Vector3"
SPRITE = "Sprite"
GAMEOBJECT = "GameObject"
PREFAB = "GameObject"
LIST = "List"
AUDIO = "AudioClip"
ANIM = "RuntimeAnimatorController"
@dataclass
class FieldDef:
"""一个字段定义"""
name: str = ""
field_type: FieldType = FieldType.INT
default_value: str = ""
tooltip: str = ""
is_header: bool = False # Unity [Header] 标记
header_text: str = ""
generic_type: str = "" # List<T> 中的 T
is_serialized: bool = True
is_readonly: bool = False
@dataclass
class MethodDef:
"""一个方法定义"""
name: str = ""
return_type: str = "void"
params: list[tuple[str, str]] = field(default_factory=list)
body_hint: str = "" # 逻辑提示(注释)
is_public: bool = True
class EntityKind(Enum):
SCRIPTABLE_OBJECT = "ScriptableObject"
MONO_BEHAVIOUR = "MonoBehaviour"
STATIC_CLASS = "StaticClass"
PLAIN_CLASS = "PlainClass"
ENUM = "Enum"
STRUCT = "Struct"
INTERFACE = "Interface"
@dataclass
class EntityDef:
"""一个游戏实体定义"""
name: str = ""
kind: EntityKind = EntityKind.MONO_BEHAVIOUR
fields: list[FieldDef] = field(default_factory=list)
methods: list[MethodDef] = field(default_factory=list)
parent_class: str = ""
interfaces: list[str] = field(default_factory=list)
description: str = ""
# ScriptableObject 专用
so_menu_name: str = ""
so_file_name: str = ""
# 组件关联
required_components: list[str] = field(default_factory=list)
tags: list[str] = field(default_factory=list)
class RelationType(Enum):
HAS_ONE = "1:1"
HAS_MANY = "1:N"
MANY_TO_MANY = "N:N"
SELF_REF = "self"
SPAWNS = "spawns"
CONTAINS = "contains"
USES_DATA = "uses_data"
LISTENS_TO = "listens"
INHERITS = "inherits"
@dataclass
class Relation:
"""实体间关系"""
source: str = ""
target: str = ""
relation_type: RelationType = RelationType.HAS_ONE
label: str = ""
@dataclass
class DomainModel:
"""L2 产物:领域模型"""
entities: list[EntityDef] = field(default_factory=list)
relations: list[Relation] = field(default_factory=list)
systems: list[EntityDef] = field(default_factory=list)
def find_entity(self, name: str) -> Optional[EntityDef]:
for e in self.entities + self.systems:
if e.name == name:
return e
return None
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# L3 行为模型层
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class CRUDOp(Enum):
CREATE = "C"
READ = "R"
UPDATE = "U"
DELETE = "D"
NOTIFY = "N"
TRIGGER = "T"
@dataclass
class MatrixCell:
"""System×Feature 矩阵单元格"""
ops: list[CRUDOp] = field(default_factory=list)
is_primary: bool = False # 是否"主"职责
note: str = ""
@dataclass
class SystemFeatureMatrix:
"""L3 产物之一:System×Feature 矩阵"""
systems: list[str] = field(default_factory=list)
features: list[str] = field(default_factory=list)
cells: dict[tuple[str, str], MatrixCell] = field(default_factory=dict)
def set_cell(self, system: str, feature: str, cell: MatrixCell):
self.cells[(system, feature)] = cell
def get_cell(self, system: str, feature: str) -> MatrixCell:
return self.cells.get((system, feature), MatrixCell())
@dataclass
class StateNode:
"""状态机节点"""
name: str = ""
is_initial: bool = False
is_final: bool = False
on_enter: str = ""
on_exit: str = ""
@dataclass
class StateTransition:
"""状态转移"""
source: str = ""
target: str = ""
trigger: str = ""
guard: str = ""
action: str = ""
@dataclass
class StateMachine:
"""一个状态机"""
name: str = ""
owner: str = "" # 属于哪个实体/系统
states: list[StateNode] = field(default_factory=list)
transitions: list[StateTransition] = field(default_factory=list)
@dataclass
class GameEvent:
"""一个游戏事件"""
name: str = ""
params: list[tuple[str, str]] = field(default_factory=list) # [(type, name)]
description: str = ""
publisher: str = "" # 谁发出
subscribers: list[str] = field(default_factory=list) # 谁监听
@dataclass
class BehaviorModel:
"""L3 产物:行为模型"""
matrix: SystemFeatureMatrix = field(default_factory=SystemFeatureMatrix)
state_machines: list[StateMachine] = field(default_factory=list)
events: list[GameEvent] = field(default_factory=list)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# L4 组件组合层
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@dataclass
class ComponentSlot:
"""Prefab 上的一个组件槽"""
component_type: str = "" # 组件类型名
is_custom: bool = True # 是否自定义脚本
config_hint: str = "" # 配置提示
@dataclass
class PrefabChild:
"""Prefab 子物体"""
name: str = ""
components: list[ComponentSlot] = field(default_factory=list)
children: list[PrefabChild] = field(default_factory=list)
@dataclass
class PrefabDef:
"""一个 Prefab 定义"""
name: str = ""
tag: str = "Untagged"
layer: str = "Default"
components: list[ComponentSlot] = field(default_factory=list)
children: list[PrefabChild] = field(default_factory=list)
@dataclass
class ECDModel:
"""L4 产物:ECD 组件组合模型"""
prefabs: list[PrefabDef] = field(default_factory=list)
data_assets: list[EntityDef] = field(default_factory=list) # SO 列表
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# L5~L7 工程层
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@dataclass
class FileNode:
"""文件树节点"""
name: str = ""
is_dir: bool = False
content: str = ""
children: list[FileNode] = field(default_factory=list)
file_type: str = "" # cs / uxml / uss / asset / meta / unity
@dataclass
class UnityProject:
"""L5~L7 产物:完整 Unity 项目"""
project_name: str = ""
root: FileNode = field(default_factory=lambda: FileNode(name="Assets", is_dir=True))
scripts: list[tuple[str, str]] = field(default_factory=list) # [(path, content)]
scenes: list[str] = field(default_factory=list)
assembly_defs: list[tuple[str, str]] = field(default_factory=list)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 管道整体产物
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@dataclass
class PipelineResult:
"""整条管道的最终产物"""
gdd: GDDSkeleton = field(default_factory=GDDSkeleton)
domain: DomainModel = field(default_factory=DomainModel)
behavior: BehaviorModel = field(default_factory=BehaviorModel)
ecd: ECDModel = field(default_factory=ECDModel)
project: UnityProject = field(default_factory=UnityProject)
# 文档产物
gdd_markdown: str = ""
dev_plan_markdown: str = ""
er_mermaid: str = ""
state_mermaid: str = ""
event_mermaid: str = ""
# 统计
total_scripts: int = 0
total_so: int = 0
total_events: int = 0
total_states: int = 0
PYEOF
第二步:游戏知识库 --- 塔防模式
python
cat > /home/user/g2c/g2c/knowledge.py << 'PYEOF'
"""
G2C 游戏知识库
==============
规则引擎的核心:游戏类型模式库 + NLU 关键词库。
每种游戏类型定义:标准实体、标准系统、标准事件、标准状态机、核心循环模板。
当前实现:塔防 (Tower Defense),架构预留其他类型扩展。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from .models import (
FieldDef, FieldType, MethodDef, EntityDef, EntityKind,
Relation, RelationType, GameEvent, StateNode, StateTransition,
StateMachine, UseCase, ActorType, CoreLoop, MatrixCell, CRUDOp,
ComponentSlot, PrefabDef, PrefabChild,
)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# NLU 关键词库
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
GENRE_KEYWORDS: dict[str, list[str]] = {
"tower_defense": [
"塔防", "tower defense", "td", "防御塔", "防守",
"波次", "wave", "敌人进攻", "守住", "放置塔",
"建造", "抵御", "入侵", "路径", "基地防御",
],
"roguelite": [
"roguelite", "rogue", "肉鸽", "随机地图", "永久升级",
"随机生成", "死亡重开", "技能组合",
],
"rpg": [
"rpg", "角色扮演", "升级", "技能树", "装备",
"任务", "npc", "对话", "经验值",
],
"platformer": [
"平台跳跃", "platformer", "横版", "跳跃", "关卡",
],
}
ENTITY_KEYWORDS: dict[str, list[str]] = {
"tower": ["塔", "防御塔", "炮塔", "tower", "turret", "建筑"],
"enemy": ["敌人", "怪物", "enemy", "monster", "入侵者", "进攻者", "怪"],
"player": ["玩家", "player", "用户", "操作者"],
"projectile": ["子弹", "弹丸", "投射物", "projectile", "bullet", "射击"],
"wave": ["波次", "wave", "回合", "一波", "wave"],
"base": ["基地", "base", "总部", "家", "核心"],
"map": ["地图", "map", "关卡", "场景", "terrain"],
"grid": ["格子", "grid", "tile", "网格", "位置"],
"gold": ["金币", "gold", "资源", "货币", "钱", "经济"],
"buff": ["buff", "增益", "减益", "效果", "状态效果"],
"skill": ["技能", "skill", "能力", "spell"],
"item": ["道具", "item", "物品", "装备"],
}
VERB_KEYWORDS: dict[str, list[str]] = {
"place": ["放置", "建造", "安放", "place", "build", "deploy"],
"upgrade": ["升级", "强化", "upgrade", "enhance", "improve"],
"attack": ["攻击", "射击", "attack", "shoot", "fire", "打击"],
"move": ["移动", "寻路", "行走", "move", "walk", "patrol"],
"spawn": ["生成", "刷新", "spawn", "summon", "产生"],
"destroy": ["摧毁", "消灭", "destroy", "kill", "击败", "击杀"],
"collect": ["收集", "获取", "collect", "gain", "earn", "获得"],
"defend": ["防御", "防守", "defend", "protect", "守住"],
"search": ["搜索", "查找", "search"],
"manage": ["管理", "manage", "设置"],
"sell": ["出售", "卖", "sell", "拆除", "拆"],
"damage": ["伤害", "damage", "hurt"],
}
PERSPECTIVE_KEYWORDS: dict[str, list[str]] = {
"2D": ["2d", "2D", "俯视", "横版", "像素"],
"3D": ["3d", "3D", "三维"],
"2.5D": ["2.5d", "等距", "isometric"],
}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 游戏类型模式(Pattern)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@dataclass
class GamePattern:
"""一种游戏类型的完整模式定义"""
genre: str = ""
display_name: str = ""
description: str = ""
default_perspective: str = "2D"
core_loop: CoreLoop = field(default_factory=CoreLoop)
standard_entities: list[EntityDef] = field(default_factory=list)
standard_systems: list[EntityDef] = field(default_factory=list)
standard_events: list[GameEvent] = field(default_factory=list)
standard_state_machines: list[StateMachine] = field(default_factory=list)
standard_prefabs: list[PrefabDef] = field(default_factory=list)
standard_relations: list[Relation] = field(default_factory=list)
standard_use_cases: list[UseCase] = field(default_factory=list)
standard_features: list[str] = field(default_factory=list)
standard_matrix: dict = field(default_factory=dict)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 塔防模式定义
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
def _build_tower_defense_pattern() -> GamePattern:
"""构建塔防的完整模式定义"""
p = GamePattern(
genre="tower_defense",
display_name="塔防 Tower Defense",
description="玩家布置防御塔抵御一波波进攻的敌人,核心是策略布阵与资源分配",
default_perspective="2D",
)
# ═══════ 核心循环 ═══════
p.core_loop = CoreLoop(
steps=[
"观察即将来袭的敌人",
"用金币放置或升级防御塔",
"塔自动索敌射击",
"击败敌人获得金币",
"进入下一波次并强化阵型",
],
win_condition="击败所有波次敌人而基地未陷落",
lose_condition="基地 HP 降至 0",
fun_source="策略布阵、资源分配、清屏爽感、波次递进压力",
)
# ═══════ 标准用例 ═══════
p.standard_use_cases = [
UseCase("玩家", ActorType.PLAYER, "放置", "防御塔", "点击空地", "扣除金币并生成塔", 1),
UseCase("玩家", ActorType.PLAYER, "升级", "防御塔", "点击已有塔", "扣除金币并替换为强化版", 2),
UseCase("玩家", ActorType.PLAYER, "拆除", "防御塔", "拆除按钮", "返还部分金币", 2),
UseCase("玩家", ActorType.PLAYER, "开始", "波次", "点击开始按钮", "启动下一波敌人", 1),
UseCase("玩家", ActorType.PLAYER, "暂停", "游戏", "暂停键", "时间停止", 3),
UseCase("系统", ActorType.SYSTEM, "生成", "敌人", "波次开始", "按配置刷出敌人", 1),
UseCase("敌人", ActorType.ENEMY, "寻路", "路径", "被生成后", "沿路径前进", 1),
UseCase("敌人", ActorType.ENEMY, "攻击", "基地", "抵达终点", "扣除基地 HP", 1),
UseCase("防御塔", ActorType.SYSTEM, "射击", "敌人", "敌人进入范围", "发射子弹", 1),
UseCase("子弹", ActorType.SYSTEM, "伤害", "敌人", "命中", "扣除敌人 HP", 1),
UseCase("系统", ActorType.SYSTEM, "奖励", "玩家", "敌人被击杀", "增加金币", 1),
UseCase("系统", ActorType.SYSTEM, "结算", "游戏", "所有波次完成", "进入胜利界面", 1),
]
# ═══════ 标准数据 SO ═══════
tower_data = EntityDef(
name="TowerData", kind=EntityKind.SCRIPTABLE_OBJECT,
so_menu_name="G2C/Tower Data", so_file_name="TowerData",
description="防御塔的数值配置(可在 Project 面板创建多个实例)",
fields=[
FieldDef("towerName", FieldType.STRING, "\"Arrow Tower\"", "塔名称", True, "基础"),
FieldDef("icon", FieldType.SPRITE, "null", "UI 图标"),
FieldDef("prefab", FieldType.PREFAB, "null", "场上实例的 Prefab"),
FieldDef("cost", FieldType.INT, "50", "建造费用", True, "数值"),
FieldDef("damage", FieldType.FLOAT, "10f", "每发伤害"),
FieldDef("attackRange", FieldType.FLOAT, "3f", "攻击范围(格)"),
FieldDef("attackInterval", FieldType.FLOAT, "1f", "攻击间隔(秒)"),
FieldDef("projectilePrefab", FieldType.PREFAB, "null", "子弹 Prefab", True, "弹药"),
FieldDef("upgradeTo", FieldType.GAMEOBJECT, "null", "升级后版本", True, "升级"),
FieldDef("upgradeCost", FieldType.INT, "100", "升级费用"),
FieldDef("sellReturnRatio", FieldType.FLOAT, "0.5f", "拆除返还比例"),
],
)
# TowerData 的 upgradeTo 类型改为自引用
tower_data.fields[8].field_type = FieldType.GAMEOBJECT
tower_data.fields[8].tooltip = "升级后的 TowerData(可为空)"
enemy_data = EntityDef(
name="EnemyData", kind=EntityKind.SCRIPTABLE_OBJECT,
so_menu_name="G2C/Enemy Data", so_file_name="EnemyData",
description="敌人的数值配置",
fields=[
FieldDef("enemyName", FieldType.STRING, "\"Grunt\"", "敌人名称", True, "基础"),
FieldDef("prefab", FieldType.PREFAB, "null", "实例 Prefab"),
FieldDef("maxHp", FieldType.FLOAT, "50f", "最大 HP", True, "数值"),
FieldDef("moveSpeed", FieldType.FLOAT, "2f", "移动速度(格/秒)"),
FieldDef("reward", FieldType.INT, "10", "击杀奖励金币"),
FieldDef("damageToBase", FieldType.INT, "1", "抵达基地时的扣血量"),
],
)
wave_data = EntityDef(
name="WaveData", kind=EntityKind.SCRIPTABLE_OBJECT,
so_menu_name="G2C/Wave Data", so_file_name="WaveData",
description="一波敌人的刷怪配置",
fields=[
FieldDef("waveNumber", FieldType.INT, "1", "波次编号", True, "基础"),
FieldDef("delayBeforeWave", FieldType.FLOAT, "3f", "开始前延迟(秒)"),
FieldDef("entries", FieldType.LIST, "", "刷怪序列",
is_header=True, header_text="刷怪列表", generic_type="SpawnEntry"),
],
)
level_data = EntityDef(
name="LevelData", kind=EntityKind.SCRIPTABLE_OBJECT,
so_menu_name="G2C/Level Data", so_file_name="LevelData",
description="关卡总配置",
fields=[
FieldDef("levelName", FieldType.STRING, "\"Level 1\"", "关卡名", True, "基础"),
FieldDef("startingGold", FieldType.INT, "200", "初始金币"),
FieldDef("baseMaxHp", FieldType.INT, "20", "基地最大 HP"),
FieldDef("waves", FieldType.LIST, "", "波次列表",
is_header=True, header_text="内容", generic_type="WaveData"),
],
)
p.standard_entities = [tower_data, enemy_data, wave_data, level_data]
# ═══════ 标准运行时组件(MonoBehaviour) ═══════
tower_comp = EntityDef(
name="Tower", kind=EntityKind.MONO_BEHAVIOUR,
description="场上防御塔实例",
fields=[
FieldDef("data", FieldType.GAMEOBJECT, "null", "TowerData 引用", True, "配置", is_serialized=True),
FieldDef("currentLevel", FieldType.INT, "1", "当前等级"),
FieldDef("currentTarget", FieldType.GAMEOBJECT, "null", "当前目标", is_serialized=False),
FieldDef("cooldown", FieldType.FLOAT, "0f", "攻击冷却", is_serialized=False),
],
methods=[
MethodDef("Initialize", "void", [("TowerData", "towerData")],
"使用 SO 数据初始化塔的视觉与数值"),
MethodDef("UpdateCooldown", "void", [],
"每帧递减 cooldown,到 0 时尝试射击"),
MethodDef("TryFire", "void", [],
"若有目标则调用 Shooter 发射子弹"),
MethodDef("Upgrade", "bool", [],
"若 data.upgradeTo 存在且金币够,替换为升级版"),
MethodDef("Sell", "void", [],
"拆除并通过 EconomySystem 返还金币"),
],
required_components=["Targeting", "Shooter", "SpriteRenderer"],
tags=["Tower"],
)
# TowerData 字段类型修正(用 TowerData 而非 GameObject)
tower_comp.fields[0].generic_type = "TowerData"
enemy_comp = EntityDef(
name="Enemy", kind=EntityKind.MONO_BEHAVIOUR,
description="场上敌人实例",
fields=[
FieldDef("data", FieldType.GAMEOBJECT, "null", "EnemyData 引用", True, "配置"),
FieldDef("currentHp", FieldType.FLOAT, "0f", "当前 HP", is_serialized=False),
FieldDef("pathIndex", FieldType.INT, "0", "当前路径点索引", is_serialized=False),
],
methods=[
MethodDef("Initialize", "void", [("EnemyData", "enemyData"), ("Transform[]", "path")],
"初始化敌人数值与路径"),
MethodDef("TakeDamage", "void", [("float", "amount")],
"扣血,HP<=0 时触发 EnemyKilled 事件并销毁"),
MethodDef("FollowPath", "void", [],
"沿路径点移动,抵达终点触发 ReachedBase 事件"),
],
required_components=["Rigidbody2D", "Collider2D", "SpriteRenderer"],
tags=["Enemy"],
)
enemy_comp.fields[0].generic_type = "EnemyData"
projectile_comp = EntityDef(
name="Projectile", kind=EntityKind.MONO_BEHAVIOUR,
description="塔发射的子弹",
fields=[
FieldDef("speed", FieldType.FLOAT, "8f", "飞行速度"),
FieldDef("damage", FieldType.FLOAT, "10f", "伤害值", is_serialized=False),
FieldDef("target", FieldType.GAMEOBJECT, "null", "追踪目标", is_serialized=False),
],
methods=[
MethodDef("Configure", "void", [("Transform", "targetTransform"), ("float", "damageAmount")],
"设置目标与伤害(由 Shooter 调用)"),
MethodDef("UpdateMovement", "void", [], "追踪目标;命中时对 Enemy 造成伤害并自销毁"),
],
required_components=["Rigidbody2D", "Collider2D"],
)
targeting_comp = EntityDef(
name="Targeting", kind=EntityKind.MONO_BEHAVIOUR,
description="塔的索敌组件",
fields=[
FieldDef("range", FieldType.FLOAT, "3f", "索敌范围", is_serialized=False),
],
methods=[
MethodDef("FindClosestEnemy", "Transform", [],
"遍历所有带 Enemy 标签的物体,返回最近的一个"),
MethodDef("SetRange", "void", [("float", "r")], "设置索敌范围(由 Tower 初始化时调用)"),
],
)
shooter_comp = EntityDef(
name="Shooter", kind=EntityKind.MONO_BEHAVIOUR,
description="塔的开火组件",
fields=[
FieldDef("projectilePrefab", FieldType.PREFAB, "null", "子弹 Prefab", is_serialized=False),
FieldDef("damage", FieldType.FLOAT, "10f", "伤害", is_serialized=False),
FieldDef("muzzle", FieldType.GAMEOBJECT, "null", "炮口位置"),
],
methods=[
MethodDef("Fire", "void", [("Transform", "target")],
"实例化子弹并传入目标与伤害"),
MethodDef("Configure", "void", [("GameObject", "prefab"), ("float", "dmg")],
"设置子弹 Prefab 与伤害(由 Tower 初始化时调用)"),
],
)
shooter_comp.fields[2].generic_type = "Transform"
base_comp = EntityDef(
name="Base", kind=EntityKind.MONO_BEHAVIOUR,
description="玩家基地",
fields=[
FieldDef("currentHp", FieldType.INT, "20", "当前 HP", is_serialized=False),
FieldDef("maxHp", FieldType.INT, "20", "最大 HP", is_serialized=False),
],
methods=[
MethodDef("Initialize", "void", [("int", "hp")], "设置最大与当前 HP"),
MethodDef("TakeDamage", "void", [("int", "amount")],
"扣血并触发 BaseHpChanged;HP=0 时触发 GameOver"),
],
tags=["Base"],
)
path_comp = EntityDef(
name="PathFollower", kind=EntityKind.MONO_BEHAVIOUR,
description="让 Enemy 沿路径点移动",
fields=[
FieldDef("waypoints", FieldType.LIST, "", "路径点列表", generic_type="Transform"),
FieldDef("moveSpeed", FieldType.FLOAT, "2f", "速度", is_serialized=False),
FieldDef("waypointIdx", FieldType.INT, "0", "当前目标点索引", is_serialized=False),
],
methods=[
MethodDef("Configure", "void", [("Transform[]", "path"), ("float", "speed")],
"设置路径点与速度"),
MethodDef("Tick", "bool", [],
"朝当前路径点移动;返回 true 表示已到达终点"),
],
)
p.standard_entities.extend([
tower_comp, enemy_comp, projectile_comp,
targeting_comp, shooter_comp, base_comp, path_comp,
])
# ═══════ 标准系统(全局管理器) ═══════
wave_system = EntityDef(
name="WaveSystem", kind=EntityKind.MONO_BEHAVIOUR,
description="[主系统] 波次生成与推进",
fields=[
FieldDef("levelData", FieldType.GAMEOBJECT, "null", "关卡配置", True, "配置"),
FieldDef("spawnPoint", FieldType.GAMEOBJECT, "null", "敌人生成点"),
FieldDef("pathParent", FieldType.GAMEOBJECT, "null", "路径点的父物体(子物体按顺序组成路径)"),
FieldDef("currentWaveIndex", FieldType.INT, "0", "当前波次索引", is_serialized=False),
FieldDef("aliveEnemies", FieldType.INT, "0", "存活敌人数", is_serialized=False),
],
methods=[
MethodDef("StartNextWave", "void", [], "启动下一波;全部完成后触发 AllWavesCleared"),
MethodDef("SpawnWave", "IEnumerator", [("WaveData", "wave")], "协程:按配置依次生成敌人"),
MethodDef("OnEnemyKilled", "void", [("Enemy", "e"), ("int", "reward")], "敌人减少,等待下一波"),
],
)
wave_system.fields[0].generic_type = "LevelData"
wave_system.fields[1].generic_type = "Transform"
wave_system.fields[2].generic_type = "Transform"
placement_system = EntityDef(
name="PlacementSystem", kind=EntityKind.MONO_BEHAVIOUR,
description="[主系统] 塔的放置、升级、拆除",
fields=[
FieldDef("currentSelection", FieldType.GAMEOBJECT, "null", "玩家选中的 TowerData", is_serialized=False),
FieldDef("groundMask", FieldType.INT, "0", "可放置地面的 LayerMask", True, "配置"),
],
methods=[
MethodDef("SelectTower", "void", [("TowerData", "data")], "从 UI 点击塔种类按钮时调用"),
MethodDef("TryPlaceAt", "bool", [("Vector3", "worldPos")],
"检查金币与地块合法性,成功则生成 Tower 并触发事件"),
MethodDef("UpgradeTower","bool", [("Tower", "tower")], "升级指定塔"),
MethodDef("SellTower", "void", [("Tower", "tower")], "拆除指定塔并返还金币"),
],
)
placement_system.fields[0].generic_type = "TowerData"
economy_system = EntityDef(
name="EconomySystem", kind=EntityKind.MONO_BEHAVIOUR,
description="[主系统] 金币管理",
fields=[
FieldDef("currentGold", FieldType.INT, "0", "当前金币", is_serialized=False),
],
methods=[
MethodDef("Initialize", "void", [("int", "startingGold")], "设置初始金币并广播"),
MethodDef("Add", "void", [("int", "amount")], "增加金币并广播"),
MethodDef("TrySpend", "bool", [("int", "cost")], "尝试扣款;不足返回 false"),
],
)
input_system = EntityDef(
name="InputSystem", kind=EntityKind.MONO_BEHAVIOUR,
description="[主系统] 鼠标点击路由到 PlacementSystem",
fields=[
FieldDef("mainCamera", FieldType.GAMEOBJECT, "null", "主相机"),
FieldDef("placement", FieldType.GAMEOBJECT, "null", "PlacementSystem 引用"),
],
methods=[
MethodDef("HandleClick", "void", [], "每帧检查鼠标左键,转 worldPos 投给 Placement"),
],
)
input_system.fields[0].generic_type = "Camera"
input_system.fields[1].generic_type = "PlacementSystem"
game_state_machine = EntityDef(
name="GameStateMachine", kind=EntityKind.MONO_BEHAVIOUR,
description="[主系统] 游戏整体状态机",
fields=[
FieldDef("current", FieldType.GAMEOBJECT, "GameState.MainMenu", "当前状态", is_serialized=False),
],
methods=[
MethodDef("TransitionTo", "void", [("GameState", "next")], "合法性校验并切换状态"),
MethodDef("IsValidTransition", "bool", [("GameState", "from"), ("GameState", "to")], "查转移表"),
],
)
game_state_machine.fields[0].generic_type = "GameState"
hud_controller = EntityDef(
name="HUDController", kind=EntityKind.MONO_BEHAVIOUR,
description="[主系统] HUD 显示(UI Toolkit)",
fields=[
FieldDef("uiDocument", FieldType.GAMEOBJECT, "null", "UIDocument 引用"),
FieldDef("towerDataList", FieldType.LIST, "", "可购买塔列表", generic_type="TowerData"),
],
methods=[
MethodDef("OnEnable", "void", [], "订阅事件并绑定 UXML 元素"),
MethodDef("OnDisable", "void", [], "取消订阅"),
MethodDef("RefreshGold", "void", [("int", "gold")], "更新金币显示"),
MethodDef("RefreshBaseHp", "void", [("int", "hp")], "更新基地 HP 显示"),
MethodDef("RefreshWave", "void", [("int", "idx")], "更新波次显示"),
MethodDef("BuildShopButtons", "void", [], "根据 towerDataList 动态生成商店按钮"),
],
)
hud_controller.fields[0].generic_type = "UIDocument"
p.standard_systems = [
wave_system, placement_system, economy_system,
input_system, game_state_machine, hud_controller,
]
# ═══════ 标准事件 ═══════
p.standard_events = [
GameEvent("OnTowerPlaced", [("Tower", "tower")],
"塔成功放置", "PlacementSystem", ["EconomySystem", "HUDController"]),
GameEvent("OnTowerSold", [("Tower", "tower"), ("int", "refund")],
"塔被拆除", "PlacementSystem", ["EconomySystem"]),
GameEvent("OnTowerUpgraded", [("Tower", "tower")],
"塔升级", "PlacementSystem", ["HUDController"]),
GameEvent("OnEnemySpawned", [("Enemy", "enemy")],
"敌人生成", "WaveSystem", []),
GameEvent("OnEnemyKilled", [("Enemy", "enemy"), ("int", "reward")],
"敌人被击杀", "Projectile", ["EconomySystem", "WaveSystem", "HUDController"]),
GameEvent("OnEnemyReachedBase", [("Enemy", "enemy"), ("int", "damage")],
"敌人抵达基地", "Enemy", ["Base"]),
GameEvent("OnBaseHpChanged", [("int", "newHp")],
"基地 HP 变化", "Base", ["HUDController"]),
GameEvent("OnBaseDestroyed", [],
"基地被摧毁", "Base", ["GameStateMachine"]),
GameEvent("OnGoldChanged", [("int", "newGold")],
"金币变化", "EconomySystem", ["HUDController"]),
GameEvent("OnWaveStarted", [("int", "waveIndex")],
"波次开始", "WaveSystem", ["HUDController"]),
GameEvent("OnAllWavesCleared", [],
"所有波次完成", "WaveSystem", ["GameStateMachine"]),
GameEvent("OnGameStateChanged", [("GameState", "newState")],
"游戏状态变化", "GameStateMachine", ["HUDController"]),
]
# ═══════ 状态机 ═══════
game_fsm = StateMachine(
name="GameStateMachine", owner="GameStateMachine",
states=[
StateNode("MainMenu", is_initial=True),
StateNode("Playing"),
StateNode("Paused"),
StateNode("Victory", is_final=True),
StateNode("GameOver", is_final=True),
],
transitions=[
StateTransition("MainMenu", "Playing", "StartGame"),
StateTransition("Playing", "Paused", "Pause"),
StateTransition("Paused", "Playing", "Resume"),
StateTransition("Playing", "Victory", "AllWavesCleared"),
StateTransition("Playing", "GameOver", "BaseDestroyed"),
StateTransition("Victory", "MainMenu", "BackToMenu"),
StateTransition("GameOver", "MainMenu", "BackToMenu"),
],
)
wave_fsm = StateMachine(
name="WavePhaseMachine", owner="WaveSystem",
states=[
StateNode("WaveWaiting", is_initial=True),
StateNode("WaveSpawning"),
StateNode("WaveInProgress"),
StateNode("AllCleared", is_final=True),
],
transitions=[
StateTransition("WaveWaiting", "WaveSpawning", "TimerElapsed"),
StateTransition("WaveSpawning", "WaveInProgress", "AllEnemiesSpawned"),
StateTransition("WaveInProgress", "WaveWaiting", "AllEnemiesGone"),
StateTransition("WaveInProgress", "AllCleared", "LastWaveCleared"),
],
)
p.standard_state_machines = [game_fsm, wave_fsm]
# ═══════ 标准关系 ═══════
p.standard_relations = [
Relation("Tower", "TowerData", RelationType.USES_DATA, "配置来源"),
Relation("Enemy", "EnemyData", RelationType.USES_DATA, "配置来源"),
Relation("WaveSystem", "WaveData", RelationType.CONTAINS, "波次列表"),
Relation("WaveSystem", "Enemy", RelationType.SPAWNS, "生成敌人"),
Relation("LevelData", "WaveData", RelationType.HAS_MANY, "包含多波"),
Relation("PlacementSystem", "Tower", RelationType.SPAWNS, "生成塔"),
Relation("Tower", "Projectile", RelationType.SPAWNS, "发射子弹"),
Relation("Tower", "Targeting", RelationType.HAS_ONE, "索敌组件"),
Relation("Tower", "Shooter", RelationType.HAS_ONE, "开火组件"),
Relation("Enemy", "PathFollower",RelationType.HAS_ONE, "寻路组件"),
Relation("Enemy", "Base", RelationType.LISTENS_TO, "攻击基地"),
Relation("EconomySystem", "Tower", RelationType.LISTENS_TO, "扣款/返还"),
Relation("HUDController", "EconomySystem", RelationType.LISTENS_TO, "金币刷新"),
Relation("HUDController", "WaveSystem", RelationType.LISTENS_TO, "波次刷新"),
Relation("HUDController", "Base", RelationType.LISTENS_TO, "HP 刷新"),
]
# ═══════ Prefab 结构 ═══════
p.standard_prefabs = [
PrefabDef(
name="TowerPrefab", tag="Tower",
components=[
ComponentSlot("Transform", is_custom=False),
ComponentSlot("SpriteRenderer", is_custom=False, config_hint="占位贴图"),
ComponentSlot("Tower", is_custom=True),
ComponentSlot("Targeting", is_custom=True),
ComponentSlot("Shooter", is_custom=True),
],
children=[
PrefabChild(name="RangeIndicator",
components=[ComponentSlot("SpriteRenderer", is_custom=False, config_hint="范围圈")]),
PrefabChild(name="Muzzle",
components=[ComponentSlot("Transform", is_custom=False, config_hint="炮口点")]),
],
),
PrefabDef(
name="EnemyPrefab", tag="Enemy",
components=[
ComponentSlot("Transform", is_custom=False),
ComponentSlot("SpriteRenderer", is_custom=False),
ComponentSlot("Rigidbody2D", is_custom=False, config_hint="Body Type: Kinematic"),
ComponentSlot("CircleCollider2D", is_custom=False, config_hint="Is Trigger: true"),
ComponentSlot("Enemy", is_custom=True),
ComponentSlot("PathFollower", is_custom=True),
],
children=[
PrefabChild(name="HealthBar",
components=[ComponentSlot("Canvas", is_custom=False, config_hint="World Space"),
ComponentSlot("HealthBarUI", is_custom=True)]),
],
),
PrefabDef(
name="ProjectilePrefab", tag="Projectile",
components=[
ComponentSlot("Transform", is_custom=False),
ComponentSlot("SpriteRenderer", is_custom=False),
ComponentSlot("Rigidbody2D", is_custom=False, config_hint="Kinematic"),
ComponentSlot("CircleCollider2D", is_custom=False, config_hint="Trigger"),
ComponentSlot("Projectile", is_custom=True),
],
),
PrefabDef(
name="BasePrefab", tag="Base",
components=[
ComponentSlot("Transform", is_custom=False),
ComponentSlot("SpriteRenderer", is_custom=False),
ComponentSlot("BoxCollider2D", is_custom=False, config_hint="Trigger"),
ComponentSlot("Base", is_custom=True),
],
),
]
# ═══════ Features(用于 L3 矩阵) ═══════
p.standard_features = [
"波次生成与推进",
"放置新塔",
"升级塔",
"拆除塔",
"塔索敌",
"塔发射子弹",
"子弹命中伤害",
"敌人寻路",
"敌人死亡奖励",
"敌人触底扣基地血",
"金币变化广播",
"HUD 显示刷新",
"游戏状态切换",
]
# (system, feature) -> (primary, ops, note)
p.standard_matrix = {
("WaveSystem", "波次生成与推进"): (True, [CRUDOp.CREATE, CRUDOp.TRIGGER], "协程刷怪"),
("WaveSystem", "敌人死亡奖励"): (False, [CRUDOp.NOTIFY], "减少计数"),
("WaveSystem", "HUD 显示刷新"): (False, [CRUDOp.NOTIFY], "广播 WaveStarted"),
("PlacementSystem", "放置新塔"): (True, [CRUDOp.CREATE], "扣款+生成"),
("PlacementSystem", "升级塔"): (True, [CRUDOp.UPDATE], "替换为升级版"),
("PlacementSystem", "拆除塔"): (True, [CRUDOp.DELETE], "返还金币"),
("EconomySystem", "金币变化广播"): (True, [CRUDOp.UPDATE, CRUDOp.NOTIFY], "中心化"),
("EconomySystem", "敌人死亡奖励"): (False, [CRUDOp.UPDATE], "收到事件+金币"),
("EconomySystem", "放置新塔"): (False, [CRUDOp.UPDATE], "扣款"),
("EconomySystem", "升级塔"): (False, [CRUDOp.UPDATE], "扣款"),
("EconomySystem", "拆除塔"): (False, [CRUDOp.UPDATE], "返还"),
("InputSystem", "放置新塔"): (False, [CRUDOp.TRIGGER], "转发点击"),
("GameStateMachine", "游戏状态切换"): (True, [CRUDOp.UPDATE, CRUDOp.NOTIFY], "状态机中枢"),
("HUDController", "HUD 显示刷新"): (True, [CRUDOp.READ, CRUDOp.UPDATE], "UXML 绑定"),
("HUDController", "金币变化广播"): (False, [CRUDOp.READ], "订阅"),
("HUDController", "波次生成与推进"): (False, [CRUDOp.READ], "订阅"),
# 组件级(由 Tower/Enemy 自身驱动但归到系统语义)
("PlacementSystem", "塔索敌"): (False, [CRUDOp.READ], "组件自治"),
("PlacementSystem", "塔发射子弹"): (False, [CRUDOp.CREATE], "组件自治"),
("WaveSystem", "敌人寻路"): (False, [CRUDOp.UPDATE], "组件自治"),
("WaveSystem", "敌人触底扣基地血"): (False, [CRUDOp.TRIGGER], "Enemy 通知"),
("WaveSystem", "子弹命中伤害"): (False, [CRUDOp.UPDATE], "组件自治"),
}
return p
# ═══════ 模式库注册表 ═══════
PATTERNS: dict[str, GamePattern] = {
"tower_defense": _build_tower_defense_pattern(),
}
def detect_genre(text: str) -> str:
"""从自由文本检测游戏类型"""
t = text.lower()
scores: dict[str, int] = {}
for genre, kws in GENRE_KEYWORDS.items():
scores[genre] = sum(1 for k in kws if k.lower() in t)
best = max(scores, key=scores.get)
return best if scores[best] > 0 else "tower_defense"
def get_pattern(genre: str) -> GamePattern:
return PATTERNS.get(genre, PATTERNS["tower_defense"])
PYEOF
bash
<tool_call: run_command>
{"command": "python3 -c 'from g2c.knowledge import get_pattern, detect_genre; p = get_pattern("tower_defense"); print(f"genre={p.genre} entities={len(p.standard_entities)} systems={len(p.standard_systems)} events={len(p.standard_events)} fsm={len(p.standard_state_machines)} prefabs={len(p.standard_prefabs)}"); print("detected:", detect_genre("做一个2D塔防游戏"))'", "cwd": "/home/user/g2c"}
</tool_call>
第三步:管道引擎 --- L1~L4 处理器
python
cat > /home/user/g2c/g2c/pipeline.py << 'PYEOF'
"""
G2C 管道引擎
============
实现 L1 → L4 的语义理解与结构化建模。
每一层是一个 Processor:
L1Processor: 一句话 → GDDSkeleton
L2Processor: GDDSkeleton → DomainModel
L3Processor: DomainModel → BehaviorModel (矩阵+状态机+事件)
L4Processor: DomainModel+BehaviorModel → ECDModel (Prefab+SO)
设计哲学:每层产物都保留"pattern 默认值 + 从输入提取的语义"的 merge 结果,
这样用户即使只输入一句话,也能得到完整可用的模型。
"""
from __future__ import annotations
import re
import copy
from .models import (
GameVision, UseCase, ActorType, GDDSkeleton, CoreLoop,
DomainModel, EntityDef, Relation,
BehaviorModel, SystemFeatureMatrix, MatrixCell,
ECDModel, PrefabDef,
)
from .knowledge import (
GENRE_KEYWORDS, ENTITY_KEYWORDS, VERB_KEYWORDS, PERSPECTIVE_KEYWORDS,
detect_genre, get_pattern, GamePattern,
)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 分词与实体抽取工具
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
def _extract_by_keywords(text: str, kw_map: dict[str, list[str]]) -> list[str]:
"""在 text 中匹配 kw_map 的关键词,返回命中的 key 列表(去重保序)"""
t = text.lower()
hits: list[str] = []
for key, kws in kw_map.items():
for k in kws:
if k.lower() in t and key not in hits:
hits.append(key)
break
return hits
def _split_sentences(text: str) -> list[str]:
return [s for s in re.split(r"[。..!!??;;\n]+", text) if s.strip()]
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# L1 处理器:自然语言 → GDD 骨架
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class L1Processor:
"""从一句话需求提炼出 GDD 骨架"""
def process(self, raw_input: str) -> GDDSkeleton:
genre = detect_genre(raw_input)
pattern = get_pattern(genre)
perspective = self._detect_perspective(raw_input) or pattern.default_perspective
vision = GameVision(
raw_input=raw_input.strip(),
genre=genre,
perspective=perspective,
theme=self._detect_theme(raw_input),
summary=self._summarize(raw_input, pattern),
)
# 抽取名词(游戏实体候选)和动词(行为候选)
noun_hits = _extract_by_keywords(raw_input, ENTITY_KEYWORDS)
verb_hits = _extract_by_keywords(raw_input, VERB_KEYWORDS)
# 合并 pattern 的标准用例
use_cases = [copy.deepcopy(uc) for uc in pattern.standard_use_cases]
# 若用户描述里有额外动词-名词组合,追加为扩展用例
extra = self._extract_custom_use_cases(raw_input, noun_hits, verb_hits)
use_cases.extend(extra)
actors = self._collect_actors(use_cases)
return GDDSkeleton(
vision=vision,
core_loop=copy.deepcopy(pattern.core_loop),
use_cases=use_cases,
actors=actors,
nouns=noun_hits,
verbs=verb_hits,
)
def _detect_perspective(self, text: str) -> str:
for p, kws in PERSPECTIVE_KEYWORDS.items():
if any(k in text for k in kws):
return p
return ""
def _detect_theme(self, text: str) -> str:
theme_map = {
"奇幻": ["魔法", "奇幻", "fantasy", "精灵", "龙"],
"科幻": ["科幻", "sci-fi", "未来", "机甲", "太空"],
"卡通": ["卡通", "可爱", "q版", "萌"],
"写实": ["写实", "realistic"],
"像素": ["像素", "pixel", "8bit"],
}
for theme, kws in theme_map.items():
if any(k.lower() in text.lower() for k in kws):
return theme
return "通用"
def _summarize(self, text: str, pattern: GamePattern) -> str:
return f"{pattern.display_name} · {pattern.description}"
def _extract_custom_use_cases(
self, text: str, nouns: list[str], verbs: list[str]
) -> list[UseCase]:
"""从用户自定义词组合提取额外用例(简单启发式)"""
extras: list[UseCase] = []
# 如果用户明确提到某些实体但不在标准实体里,也提取出来作为扩展
# 此处保守:只处理提到的稀有组合
return extras
def _collect_actors(self, use_cases: list[UseCase]) -> list[str]:
seen = []
for uc in use_cases:
if uc.actor and uc.actor not in seen:
seen.append(uc.actor)
return seen
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# L2 处理器:GDD → 领域模型
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class L2Processor:
"""从 GDD 骨架构建领域模型(实体 + 系统 + 关系)"""
def process(self, gdd: GDDSkeleton) -> DomainModel:
pattern = get_pattern(gdd.vision.genre)
entities = [copy.deepcopy(e) for e in pattern.standard_entities]
systems = [copy.deepcopy(s) for s in pattern.standard_systems]
relations = [copy.deepcopy(r) for r in pattern.standard_relations]
# 语义注入:如果用户原文显式提到了某些额外概念,标注说明
mentioned_nouns = set(gdd.nouns)
for e in entities:
base_name = e.name.replace("Data", "").lower()
if base_name in mentioned_nouns:
e.description += "(用户明确提及)"
return DomainModel(entities=entities, relations=relations, systems=systems)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# L3 处理器:领域模型 → 行为模型
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class L3Processor:
"""生成 System×Feature 矩阵 + 状态机集合 + 事件集合"""
def process(self, gdd: GDDSkeleton, domain: DomainModel) -> BehaviorModel:
pattern = get_pattern(gdd.vision.genre)
# 矩阵
matrix = SystemFeatureMatrix(
systems=[s.name for s in domain.systems],
features=list(pattern.standard_features),
)
for (sys_name, feat), (primary, ops, note) in pattern.standard_matrix.items():
if sys_name in matrix.systems and feat in matrix.features:
matrix.set_cell(sys_name, feat,
MatrixCell(ops=list(ops), is_primary=primary, note=note))
# 状态机
state_machines = [copy.deepcopy(fsm) for fsm in pattern.standard_state_machines]
# 事件
events = [copy.deepcopy(ev) for ev in pattern.standard_events]
return BehaviorModel(matrix=matrix, state_machines=state_machines, events=events)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# L4 处理器:领域+行为 → ECD 组件组合
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class L4Processor:
"""生成 Prefab 组件组合图 + 数据资产清单"""
def process(self, gdd: GDDSkeleton, domain: DomainModel) -> ECDModel:
pattern = get_pattern(gdd.vision.genre)
prefabs = [copy.deepcopy(pf) for pf in pattern.standard_prefabs]
# 数据资产 = 所有 ScriptableObject 类型的实体
from .models import EntityKind
data_assets = [e for e in domain.entities if e.kind == EntityKind.SCRIPTABLE_OBJECT]
return ECDModel(prefabs=prefabs, data_assets=data_assets)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Mermaid 图生成器
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class MermaidRenderer:
"""把领域模型/状态机/事件图渲染为 Mermaid DSL"""
@staticmethod
def render_domain(domain: DomainModel) -> str:
lines = ["classDiagram"]
for e in domain.entities + domain.systems:
lines.append(f" class {e.name} {{")
for f in e.fields[:6]: # 只展示前 6 个字段,避免过长
lines.append(f" +{f.field_type.value} {f.name}")
if len(e.fields) > 6:
lines.append(f" ... +{len(e.fields)-6} more")
for m in e.methods[:4]:
params = ",".join(p[1] for p in m.params)
lines.append(f" +{m.name}({params}) {m.return_type}")
if len(e.methods) > 4:
lines.append(f" ... +{len(e.methods)-4} more")
lines.append(" }")
for r in domain.relations:
arrow = {
"1:1": "-->",
"1:N": "-->",
"N:N": "<-->",
"spawns": "..>",
"contains": "*--",
"uses_data": "..>",
"listens": "..>",
"inherits": "--|>",
"self": "-->",
}.get(r.relation_type.value, "-->")
lines.append(f" {r.source} {arrow} {r.target} : {r.label}")
return "\n".join(lines)
@staticmethod
def render_state_machine(fsm) -> str:
lines = ["stateDiagram-v2"]
initial = next((s for s in fsm.states if s.is_initial), None)
if initial:
lines.append(f" [*] --> {initial.name}")
for t in fsm.transitions:
lines.append(f" {t.source} --> {t.target} : {t.trigger}")
for s in fsm.states:
if s.is_final:
lines.append(f" {s.name} --> [*]")
return "\n".join(lines)
@staticmethod
def render_events(events) -> str:
lines = ["flowchart LR"]
seen_nodes = set()
for ev in events:
pub = ev.publisher or "System"
if pub not in seen_nodes:
lines.append(f" {pub}[{pub}]")
seen_nodes.add(pub)
for sub in ev.subscribers:
if sub not in seen_nodes:
lines.append(f" {sub}[{sub}]")
seen_nodes.add(sub)
lines.append(f" {pub} -->|{ev.name}| {sub}")
return "\n".join(lines)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 完整管道 orchestrator
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class Pipeline:
"""把 L1~L4 串起来,返回 (gdd, domain, behavior, ecd)"""
def __init__(self):
self.l1 = L1Processor()
self.l2 = L2Processor()
self.l3 = L3Processor()
self.l4 = L4Processor()
def run(self, raw_input: str):
gdd = self.l1.process(raw_input)
domain = self.l2.process(gdd)
behavior = self.l3.process(gdd, domain)
ecd = self.l4.process(gdd, domain)
return gdd, domain, behavior, ecd
PYEOF
bash
python3 -c "
from g2c.pipeline import Pipeline, MermaidRenderer
p = Pipeline()
gdd, domain, behavior, ecd = p.run('做一个2D塔防游戏:玩家放置防御塔抵御一波波敌人')
print(f'genre={gdd.vision.genre} perspective={gdd.vision.perspective}')
print(f'use_cases={len(gdd.use_cases)} actors={gdd.actors}')
print(f'entities={len(domain.entities)} systems={len(domain.systems)} relations={len(domain.relations)}')
print(f'matrix cells={len(behavior.matrix.cells)} fsm={len(behavior.state_machines)} events={len(behavior.events)}')
print(f'prefabs={len(ecd.prefabs)} data_assets={len(ecd.data_assets)}')
print('---mermaid domain---')
print(MermaidRenderer.render_domain(domain)[:400])
"
bash
<tool_call: run_command>
{"command": "python3 -c "\nfrom g2c.pipeline import Pipeline, MermaidRenderer\np = Pipeline()\ngdd, domain, behavior, ecd = p.run('做一个2D塔防游戏:玩家放置防御塔抵御一波波敌人')\nprint(f'genre={gdd.vision.genre} perspective={gdd.vision.perspective}')\nprint(f'use_cases={len(gdd.use_cases)} actors={gdd.actors}')\nprint(f'entities={len(domain.entities)} systems={len(domain.systems)} relations={len(domain.relations)}')\nprint(f'matrix cells={len(behavior.matrix.cells)} fsm={len(behavior.state_machines)} events={len(behavior.events)}')\nprint(f'prefabs={len(ecd.prefabs)} data_assets={len(ecd.data_assets)}')\nprint('---mermaid---')\nprint(MermaidRenderer.render_domain(domain)[:300])\n"", "cwd": "/home/user/g2c"}
</tool_call>
第四步:Unity 代码生成器(L5~L7)
因为代码较多,我建立模板目录:
bash
mkdir -p /home/user/g2c/g2c/templates/csharp /home/user/g2c/g2c/templates/unity
bash
<tool_call: run_command>
{"command": "mkdir -p /home/user/g2c/g2c/templates/csharp /home/user/g2c/g2c/templates/unity /home/user/g2c/g2c/templates/docs /home/user/g2c/g2c/ui", "cwd": "/home/user/g2c"}
</tool_call>
现在写 Jinja2 模板。ScriptableObject 模板:
python
cat > /home/user/g2c/g2c/templates/csharp/scriptable_object.cs.j2 << 'J2EOF'
// AUTO-GENERATED by G2C. Safe to extend --- regenerate will preserve region blocks.
using System;
using System.Collections.Generic;
using UnityEngine;
namespace {{ namespace }}
{
{% if nested_types %}
{% for nt in nested_types %}
[Serializable]
public class {{ nt.name }}
{
{% for f in nt.fields -%}
public {{ f.type }} {{ f.name }}{% if f.default %} = {{ f.default }}{% endif %};
{% endfor %}
}
{% endfor %}
{% endif %}
/// <summary>
/// {{ entity.description }}
/// </summary>
[CreateAssetMenu(fileName = "{{ entity.so_file_name }}", menuName = "{{ entity.so_menu_name }}")]
public class {{ entity.name }} : ScriptableObject
{
{%- for f in entity.fields %}
{%- if f.is_header %}
[Header("{{ f.header_text }}")]
{%- endif %}
{%- if f.tooltip %}
[Tooltip("{{ f.tooltip }}")]
{%- endif %}
public {{ f.cs_type }} {{ f.name }}{% if f.default_value %} = {{ f.default_value }}{% endif %};
{%- endfor %}
// #region CustomLogic --- add your methods below; kept across regenerations.
// #endregion
}
}
J2EOF
python
cat > /home/user/g2c/g2c/templates/csharp/mono_behaviour.cs.j2 << 'J2EOF'
// AUTO-GENERATED by G2C. MonoBehaviour skeleton --- extend in #region UserLogic.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
{% if uses_ui_toolkit %}using UnityEngine.UIElements;{% endif %}
namespace {{ namespace }}
{
/// <summary>
/// {{ entity.description }}
/// </summary>
{%- for rc in entity.required_components %}
[RequireComponent(typeof({{ rc }}))]
{%- endfor %}
public class {{ entity.name }} : MonoBehaviour
{
{%- for f in entity.fields %}
{%- if f.is_header %}
[Header("{{ f.header_text }}")]
{%- endif %}
{%- if f.tooltip %}
[Tooltip("{{ f.tooltip }}")]
{%- endif %}
{%- if f.is_serialized %}
[SerializeField] private {{ f.cs_type }} {{ f.name }}{% if f.default_value %} = {{ f.default_value }}{% endif %};
{%- else %}
private {{ f.cs_type }} {{ f.name }}{% if f.default_value %} = {{ f.default_value }}{% endif %};
{%- endif %}
{%- endfor %}
{%- if lifecycle.awake %}
private void Awake()
{
{{ lifecycle.awake | indent(12) }}
}
{%- endif %}
{%- if lifecycle.on_enable %}
private void OnEnable()
{
{{ lifecycle.on_enable | indent(12) }}
}
{%- endif %}
{%- if lifecycle.on_disable %}
private void OnDisable()
{
{{ lifecycle.on_disable | indent(12) }}
}
{%- endif %}
{%- if lifecycle.start %}
private void Start()
{
{{ lifecycle.start | indent(12) }}
}
{%- endif %}
{%- if lifecycle.update %}
private void Update()
{
{{ lifecycle.update | indent(12) }}
}
{%- endif %}
{%- for m in entity.methods %}
/// <summary>{{ m.body_hint }}</summary>
public {{ m.return_type }} {{ m.name }}({% for p in m.params %}{{ p[0] }} {{ p[1] }}{% if not loop.last %}, {% endif %}{% endfor %})
{
{%- if method_bodies.get(m.name) %}
{{ method_bodies[m.name] | indent(12) }}
{%- else %}
// TODO: {{ m.body_hint }}
{%- if m.return_type == 'bool' %}
return false;
{%- elif m.return_type == 'int' %}
return 0;
{%- elif m.return_type == 'float' %}
return 0f;
{%- elif m.return_type == 'IEnumerator' %}
yield break;
{%- elif m.return_type not in ('void',) %}
return default;
{%- endif %}
{%- endif %}
}
{%- endfor %}
// #region UserLogic
// Add your custom methods here. This region is preserved during regeneration.
// #endregion
}
}
J2EOF
核心代码生成器:
python
cat > /home/user/g2c/g2c/generator.py << 'PYEOF'
"""
G2C 代码生成器(L5~L7)
=======================
- L5: 规划 Unity 项目目录结构
- L6: 生成 .cs 脚本、ScriptableObject、UXML、状态机、事件总线
- L7: 打包为可导入 Unity 6 的目录/.unitypackage
核心产物清单:
Scripts/Core/ GameEvents.cs, GameState.cs, GameStateMachine.cs
Scripts/Data/ *Data.cs (ScriptableObject 定义)
Scripts/Entities/ Tower.cs, Enemy.cs, Projectile.cs, ...
Scripts/Systems/ WaveSystem.cs, PlacementSystem.cs, ...
Scripts/UI/ HUDController.cs
UI/ HUD.uxml, HUD.uss
*.asmdef Assembly Definitions
_Project.md 导入 Unity 后的快速开始
"""
from __future__ import annotations
import os
import json
import re
import uuid
from pathlib import Path
from jinja2 import Environment, FileSystemLoader, StrictUndefined
from .models import (
EntityDef, EntityKind, FieldDef, FieldType, MethodDef,
DomainModel, BehaviorModel, ECDModel, GDDSkeleton,
StateMachine, GameEvent, FileNode, UnityProject,
)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 类型映射
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
def cs_type(f: FieldDef) -> str:
"""把 FieldType 映射为 C# 类型字符串"""
if f.field_type == FieldType.LIST:
inner = f.generic_type or "object"
return f"List<{inner}>"
if f.field_type in (FieldType.GAMEOBJECT, FieldType.PREFAB):
# 若定义了 generic_type 表示实际想要的类型(如 TowerData / Transform)
return f.generic_type or "GameObject"
mapping = {
FieldType.INT: "int",
FieldType.FLOAT: "float",
FieldType.STRING: "string",
FieldType.BOOL: "bool",
FieldType.VECTOR2: "Vector2",
FieldType.VECTOR3: "Vector3",
FieldType.SPRITE: "Sprite",
FieldType.AUDIO: "AudioClip",
FieldType.ANIM: "RuntimeAnimatorController",
}
return mapping.get(f.field_type, "object")
def _field_ctx(f: FieldDef) -> dict:
"""把 FieldDef 转为模板上下文"""
return {
"name": f.name,
"cs_type": cs_type(f),
"default_value": f.default_value,
"tooltip": f.tooltip,
"is_header": f.is_header,
"header_text": f.header_text,
"is_serialized": f.is_serialized,
}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 方法体生成(给关键方法注入有意义的骨架代码)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
METHOD_BODY_RECIPES: dict[str, dict[str, str]] = {
# ------ Tower ------
"Tower": {
"Initialize":
"this.data = towerData;\n"
"GetComponent<Targeting>()?.SetRange(towerData.attackRange);\n"
"GetComponent<Shooter>()?.Configure(towerData.projectilePrefab, towerData.damage);\n"
"var sr = GetComponent<SpriteRenderer>();\n"
"if (sr != null && towerData.icon != null) sr.sprite = towerData.icon;\n"
"cooldown = 0f;",
"UpdateCooldown":
"cooldown -= Time.deltaTime;\n"
"if (cooldown <= 0f) TryFire();",
"TryFire":
"currentTarget = GetComponent<Targeting>()?.FindClosestEnemy()?.gameObject;\n"
"if (currentTarget == null) return;\n"
"GetComponent<Shooter>()?.Fire(currentTarget.transform);\n"
"cooldown = data.attackInterval;",
"Upgrade":
"if (data.upgradeTo == null) return false;\n"
"var eco = FindFirstObjectByType<EconomySystem>();\n"
"if (eco == null || !eco.TrySpend(data.upgradeCost)) return false;\n"
"var newGo = Instantiate(data.upgradeTo.prefab, transform.position, transform.rotation);\n"
"newGo.GetComponent<Tower>().Initialize(data.upgradeTo);\n"
"GameEvents.RaiseTowerUpgraded(newGo.GetComponent<Tower>());\n"
"Destroy(gameObject);\n"
"return true;",
"Sell":
"int refund = Mathf.RoundToInt(data.cost * data.sellReturnRatio);\n"
"FindFirstObjectByType<EconomySystem>()?.Add(refund);\n"
"GameEvents.RaiseTowerSold(this, refund);\n"
"Destroy(gameObject);",
},
# ------ Enemy ------
"Enemy": {
"Initialize":
"this.data = enemyData;\n"
"this.currentHp = enemyData.maxHp;\n"
"var pf = GetComponent<PathFollower>();\n"
"if (pf != null) pf.Configure(path, enemyData.moveSpeed);\n"
"var sr = GetComponent<SpriteRenderer>();",
"TakeDamage":
"currentHp -= amount;\n"
"if (currentHp <= 0f)\n"
"{\n"
" GameEvents.RaiseEnemyKilled(this, data.reward);\n"
" Destroy(gameObject);\n"
"}",
"FollowPath":
"var pf = GetComponent<PathFollower>();\n"
"if (pf == null) return;\n"
"bool reached = pf.Tick();\n"
"if (reached)\n"
"{\n"
" GameEvents.RaiseEnemyReachedBase(this, data.damageToBase);\n"
" Destroy(gameObject);\n"
"}",
},
# ------ Projectile ------
"Projectile": {
"Configure":
"this.target = targetTransform != null ? targetTransform.gameObject : null;\n"
"this.damage = damageAmount;",
"UpdateMovement":
"if (target == null) { Destroy(gameObject); return; }\n"
"var dir = (target.transform.position - transform.position).normalized;\n"
"transform.position += dir * speed * Time.deltaTime;\n"
"if (Vector3.Distance(transform.position, target.transform.position) < 0.2f)\n"
"{\n"
" target.GetComponent<Enemy>()?.TakeDamage(damage);\n"
" Destroy(gameObject);\n"
"}",
},
# ------ Targeting ------
"Targeting": {
"FindClosestEnemy":
"var enemies = GameObject.FindGameObjectsWithTag(\"Enemy\");\n"
"Transform best = null; float bestDist = range;\n"
"foreach (var e in enemies)\n"
"{\n"
" float d = Vector3.Distance(transform.position, e.transform.position);\n"
" if (d <= bestDist) { bestDist = d; best = e.transform; }\n"
"}\n"
"return best;",
"SetRange": "this.range = r;",
},
# ------ Shooter ------
"Shooter": {
"Fire":
"if (projectilePrefab == null || target == null) return;\n"
"var spawnPos = muzzle != null ? muzzle.position : transform.position;\n"
"var go = Instantiate(projectilePrefab, spawnPos, Quaternion.identity);\n"
"go.GetComponent<Projectile>()?.Configure(target, damage);",
"Configure":
"this.projectilePrefab = prefab;\n"
"this.damage = dmg;",
},
# ------ Base ------
"Base": {
"Initialize":
"this.maxHp = hp;\n"
"this.currentHp = hp;\n"
"GameEvents.RaiseBaseHpChanged(currentHp);",
"TakeDamage":
"currentHp = Mathf.Max(0, currentHp - amount);\n"
"GameEvents.RaiseBaseHpChanged(currentHp);\n"
"if (currentHp == 0) GameEvents.RaiseBaseDestroyed();",
},
# ------ PathFollower ------
"PathFollower": {
"Configure":
"this.waypoints = new List<Transform>(path);\n"
"this.moveSpeed = speed;\n"
"this.waypointIdx = 0;",
"Tick":
"if (waypoints == null || waypointIdx >= waypoints.Count) return true;\n"
"var target = waypoints[waypointIdx].position;\n"
"transform.position = Vector3.MoveTowards(transform.position, target, moveSpeed * Time.deltaTime);\n"
"if (Vector3.Distance(transform.position, target) < 0.05f) waypointIdx++;\n"
"return waypointIdx >= waypoints.Count;",
},
# ------ WaveSystem ------
"WaveSystem": {
"StartNextWave":
"if (levelData == null || currentWaveIndex >= levelData.waves.Count)\n"
"{\n"
" GameEvents.RaiseAllWavesCleared();\n"
" return;\n"
"}\n"
"var wave = levelData.waves[currentWaveIndex];\n"
"GameEvents.RaiseWaveStarted(currentWaveIndex);\n"
"StartCoroutine(SpawnWave(wave));\n"
"currentWaveIndex++;",
"SpawnWave":
"yield return new WaitForSeconds(wave.delayBeforeWave);\n"
"Transform[] path = BuildPathArray();\n"
"foreach (var entry in wave.entries)\n"
"{\n"
" for (int i = 0; i < entry.count; i++)\n"
" {\n"
" var go = Instantiate(entry.enemy.prefab, spawnPoint.position, Quaternion.identity);\n"
" var e = go.GetComponent<Enemy>();\n"
" e.Initialize(entry.enemy, path);\n"
" aliveEnemies++;\n"
" GameEvents.RaiseEnemySpawned(e);\n"
" yield return new WaitForSeconds(entry.interval);\n"
" }\n"
"}",
"OnEnemyKilled":
"aliveEnemies = Mathf.Max(0, aliveEnemies - 1);\n"
"if (aliveEnemies == 0) Invoke(nameof(StartNextWave), 2f);",
},
# ------ PlacementSystem ------
"PlacementSystem": {
"SelectTower": "currentSelection = data;",
"TryPlaceAt":
"if (currentSelection == null) return false;\n"
"var eco = FindFirstObjectByType<EconomySystem>();\n"
"if (eco == null || !eco.TrySpend(currentSelection.cost)) return false;\n"
"var go = Instantiate(currentSelection.prefab, worldPos, Quaternion.identity);\n"
"var t = go.GetComponent<Tower>();\n"
"t.Initialize(currentSelection);\n"
"GameEvents.RaiseTowerPlaced(t);\n"
"return true;",
"UpgradeTower": "return tower != null && tower.Upgrade();",
"SellTower": "if (tower != null) tower.Sell();",
},
# ------ EconomySystem ------
"EconomySystem": {
"Initialize":
"currentGold = startingGold;\n"
"GameEvents.RaiseGoldChanged(currentGold);",
"Add":
"currentGold += amount;\n"
"GameEvents.RaiseGoldChanged(currentGold);",
"TrySpend":
"if (currentGold < cost) return false;\n"
"currentGold -= cost;\n"
"GameEvents.RaiseGoldChanged(currentGold);\n"
"return true;",
},
# ------ InputSystem ------
"InputSystem": {
"HandleClick":
"if (!Input.GetMouseButtonDown(0)) return;\n"
"if (mainCamera == null) return;\n"
"var pos = mainCamera.ScreenToWorldPoint(Input.mousePosition);\n"
"pos.z = 0;\n"
"placement?.TryPlaceAt(pos);",
},
# ------ GameStateMachine ------
"GameStateMachine": {
"TransitionTo":
"if (!IsValidTransition(current, next)) return;\n"
"current = next;\n"
"GameEvents.RaiseGameStateChanged(current);",
"IsValidTransition":
"return GameStateTransitions.IsValid(from, to);",
},
# ------ HUDController ------
"HUDController": {
"OnEnable":
"GameEvents.OnGoldChanged += RefreshGold;\n"
"GameEvents.OnBaseHpChanged += RefreshBaseHp;\n"
"GameEvents.OnWaveStarted += RefreshWave;\n"
"BuildShopButtons();",
"OnDisable":
"GameEvents.OnGoldChanged -= RefreshGold;\n"
"GameEvents.OnBaseHpChanged -= RefreshBaseHp;\n"
"GameEvents.OnWaveStarted -= RefreshWave;",
"RefreshGold":
"var root = uiDocument?.rootVisualElement;\n"
"var label = root?.Q<Label>(\"gold-label\");\n"
"if (label != null) label.text = $\"💰 {gold}\";",
"RefreshBaseHp":
"var label = uiDocument?.rootVisualElement?.Q<Label>(\"basehp-label\");\n"
"if (label != null) label.text = $\"❤ {hp}\";",
"RefreshWave":
"var label = uiDocument?.rootVisualElement?.Q<Label>(\"wave-label\");\n"
"if (label != null) label.text = $\"Wave {idx + 1}\";",
"BuildShopButtons":
"var root = uiDocument?.rootVisualElement;\n"
"var shop = root?.Q<VisualElement>(\"tower-shop\");\n"
"if (shop == null) return;\n"
"shop.Clear();\n"
"var placement = FindFirstObjectByType<PlacementSystem>();\n"
"foreach (var td in towerDataList)\n"
"{\n"
" var btn = new Button(() => placement?.SelectTower(td)) { text = $\"{td.towerName} {td.cost}g\" };\n"
" btn.AddToClassList(\"shop-button\");\n"
" shop.Add(btn);\n"
"}",
},
}
LIFECYCLE_RECIPES: dict[str, dict[str, str]] = {
"Tower": {"update": "UpdateCooldown();"},
"Enemy": {"update": "FollowPath();"},
"Projectile": {"update": "UpdateMovement();"},
"WaveSystem": {
"on_enable": "GameEvents.OnEnemyKilled += OnEnemyKilled;",
"on_disable": "GameEvents.OnEnemyKilled -= OnEnemyKilled;",
"start": "Invoke(nameof(StartNextWave), 1f);",
},
"EconomySystem": {
"awake": "if (levelData != null) Initialize(levelData.startingGold);",
},
"InputSystem": {"update": "HandleClick();"},
"Base": {"awake": "Initialize(maxHp > 0 ? maxHp : 20);"},
"HUDController": {}, # OnEnable/OnDisable 通过 methods 生成
}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 代码生成器主体
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class UnityCodeGenerator:
def __init__(self, namespace: str = "G2C.Game"):
self.namespace = namespace
template_dir = Path(__file__).parent / "templates"
self.env = Environment(
loader=FileSystemLoader(str(template_dir)),
trim_blocks=False, lstrip_blocks=False,
keep_trailing_newline=True,
)
# ─────── ScriptableObject ───────
def gen_scriptable_object(self, entity: EntityDef) -> str:
nested_types = []
if entity.name == "WaveData":
nested_types.append({
"name": "SpawnEntry",
"fields": [
{"type": "EnemyData", "name": "enemy", "default": "null"},
{"type": "int", "name": "count", "default": "5"},
{"type": "float", "name": "interval", "default": "1f"},
],
})
if entity.name == "LevelData":
# 无嵌套类型
pass
ctx = {
"namespace": self.namespace,
"entity": {
"name": entity.name,
"description": entity.description,
"so_menu_name": entity.so_menu_name,
"so_file_name": entity.so_file_name,
"fields": [_field_ctx(f) for f in entity.fields],
},
"nested_types": nested_types,
}
tpl = self.env.get_template("csharp/scriptable_object.cs.j2")
return tpl.render(**ctx)
# ─────── MonoBehaviour ───────
def gen_mono_behaviour(self, entity: EntityDef) -> str:
bodies = METHOD_BODY_RECIPES.get(entity.name, {})
lifecycle = LIFECYCLE_RECIPES.get(entity.name, {})
uses_ui_toolkit = entity.name == "HUDController"
ctx = {
"namespace": self.namespace,
"entity": {
"name": entity.name,
"description": entity.description,
"fields": [_field_ctx(f) for f in entity.fields],
"methods": entity.methods,
"required_components": entity.required_components,
},
"method_bodies": bodies,
"lifecycle": {
"awake": lifecycle.get("awake", ""),
"on_enable": lifecycle.get("on_enable", ""),
"on_disable": lifecycle.get("on_disable", ""),
"start": lifecycle.get("start", ""),
"update": lifecycle.get("update", ""),
},
"uses_ui_toolkit": uses_ui_toolkit,
}
tpl = self.env.get_template("csharp/mono_behaviour.cs.j2")
return tpl.render(**ctx)
# ─────── GameEvents(全局事件总线) ───────
def gen_events_bus(self, events: list[GameEvent]) -> str:
lines = [
"// AUTO-GENERATED by G2C. Central event hub --- edit with care.",
"using System;",
"using UnityEngine;",
"",
f"namespace {self.namespace}",
"{",
" /// <summary>全局事件中心:所有系统通过此类解耦</summary>",
" public static class GameEvents",
" {",
]
for ev in events:
sig = ", ".join(p[0] for p in ev.params)
action = f"Action<{sig}>" if sig else "Action"
lines.append(f" /// <summary>{ev.description}(由 {ev.publisher} 触发)</summary>")
lines.append(f" public static event {action} {ev.name};")
lines.append("")
for ev in events:
raise_name = "Raise" + ev.name.removeprefix("On")
params_sig = ", ".join(f"{p[0]} {p[1]}" for p in ev.params)
params_call = ", ".join(p[1] for p in ev.params)
lines.append(f" public static void {raise_name}({params_sig})")
lines.append(" {")
lines.append(f" {ev.name}?.Invoke({params_call});")
lines.append(" }")
lines.append("")
lines.append(" }")
lines.append("}")
return "\n".join(lines)
# ─────── GameState 枚举 + 合法转移表 ───────
def gen_state_enum(self, fsms: list[StateMachine]) -> str:
# 以 GameStateMachine 为主枚举
main = next((f for f in fsms if f.name == "GameStateMachine"), fsms[0] if fsms else None)
if main is None:
return ""
state_names = [s.name for s in main.states]
lines = [
"// AUTO-GENERATED by G2C.",
f"namespace {self.namespace}",
"{",
" public enum GameState",
" {",
]
for s in state_names:
lines.append(f" {s},")
lines.append(" }")
lines.append("")
lines.append(" public static class GameStateTransitions")
lines.append(" {")
lines.append(" private static readonly (GameState from, GameState to)[] _valid =")
lines.append(" {")
for t in main.transitions:
lines.append(f" (GameState.{t.source}, GameState.{t.target}),")
lines.append(" };")
lines.append("")
lines.append(" public static bool IsValid(GameState from, GameState to)")
lines.append(" {")
lines.append(" foreach (var t in _valid)")
lines.append(" if (t.from == from && t.to == to) return true;")
lines.append(" return false;")
lines.append(" }")
lines.append(" }")
lines.append("}")
return "\n".join(lines)
# ─────── UXML / USS ───────
def gen_hud_uxml(self) -> str:
return """<?xml version="1.0" encoding="utf-8"?>
<ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements"
xsi="http://www.w3.org/2001/XMLSchema-instance"
engine="UnityEngine.UIElements" editor="UnityEditor.UIElements"
noNamespaceSchemaLocation="../../UIElementsSchema/UIElements.xsd">
<ui:VisualElement name="hud-root" class="hud-root">
<ui:VisualElement name="top-bar" class="top-bar">
<ui:Label name="gold-label" text="💰 0" class="hud-stat"/>
<ui:Label name="wave-label" text="Wave 0" class="hud-stat"/>
<ui:Label name="basehp-label" text="❤ 20" class="hud-stat"/>
</ui:VisualElement>
<ui:VisualElement name="tower-shop" class="tower-shop"/>
</ui:VisualElement>
</ui:UXML>
"""
def gen_hud_uss(self) -> str:
return """/* AUTO-GENERATED by G2C */
.hud-root {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
}
.top-bar {
flex-direction: row;
justify-content: space-between;
padding: 12px 20px;
background-color: rgba(0,0,0,0.5);
}
.hud-stat {
color: white;
font-size: 20px;
-unity-font-style: bold;
}
.tower-shop {
position: absolute;
bottom: 20px; left: 20px; right: 20px;
flex-direction: row;
justify-content: center;
}
.shop-button {
margin: 0 8px;
padding: 10px 20px;
background-color: rgba(50,60,80,0.85);
color: white;
border-radius: 6px;
border-width: 0;
font-size: 16px;
}
.shop-button:hover { background-color: rgba(70,90,120,1); }
"""
# ─────── Assembly Definitions ───────
def gen_asmdef_runtime(self) -> str:
return json.dumps({
"name": f"{self.namespace}.Runtime",
"rootNamespace": self.namespace,
"references": [],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": False,
"overrideReferences": False,
"precompiledReferences": [],
"autoReferenced": True,
"defineConstraints": [],
"versionDefines": [],
"noEngineReferences": False,
}, indent=4)
# ─────── Meta 文件(简化版,让 Unity 自动重新生成) ───────
@staticmethod
def gen_simple_meta(folder=False) -> str:
guid = uuid.uuid4().hex
if folder:
return f"fileFormatVersion: 2\nguid: {guid}\nfolderAsset: yes\nDefaultImporter:\n externalObjects: {{}}\n userData: \n assetBundleName: \n assetBundleVariant: \n"
return f"fileFormatVersion: 2\nguid: {guid}\n"
# ─────── ReadMe ───────
def gen_readme(self, gdd: GDDSkeleton, domain, behavior, ecd) -> str:
return f"""# {gdd.vision.summary}
> Auto-generated by **G2C** (Game-Design-to-Code)
## 如何在 Unity 6 中打开
1. 打开 Unity Hub,新建 Unity 6 项目(2D URP 模板)。
2. 关闭 Unity,把这个导出目录 **整体复制** 到项目根(与 `Assets` 平级)或直接替换 `Assets` 下的对应子目录。
3. 重新打开 Unity,让它自动生成 `.meta` 文件。
4. 在 Project 视图 → 右键 → `Create/G2C/...` 创建数据资产:
- `Tower Data` 若干(例如 Arrow/Cannon/Magic)
- `Enemy Data` 若干(例如 Grunt/Runner/Tank)
- `Wave Data` 若干
- `Level Data` 一个(把上面这些拖进来)
5. 新建场景 `Level01.unity`,在场景中添加:
- 主相机(Orthographic)
- 空物体 `SpawnPoint` 与 `Path`(Path 下放若干空子物体作为路径点)
- 基地物体(挂 `BasePrefab`)
- 一个空物体挂所有系统:`WaveSystem` `PlacementSystem` `EconomySystem` `InputSystem` `GameStateMachine` `HUDController`
- UIDocument(拖入 `UI/HUD.uxml`,拖入 `UI/HUD.uss`)
6. 把 `LevelData` 拖到 `WaveSystem.levelData`;`SpawnPoint` 与 `Path` 分别拖入;`HUDController` 的 `towerDataList` 填入你创建的塔数据。
7. 按播放 ▶ 即可。
## 项目架构
- **实体数**: {len(domain.entities)}
- **系统数**: {len(domain.systems)}
- **事件数**: {len(behavior.events)}
- **状态机数**: {len(behavior.state_machines)}
- **Prefab 类型数**: {len(ecd.prefabs)}
## 核心循环
{chr(10).join(f"{i+1}. {s}" for i, s in enumerate(gdd.core_loop.steps))}
- 胜利:{gdd.core_loop.win_condition}
- 失败:{gdd.core_loop.lose_condition}
## 解耦架构
所有系统通过 `GameEvents` 静态事件中心通信,不直接引用彼此。这样:
- 删除/替换某系统不会破坏编译
- 新增系统只需订阅事件
- 单元测试可独立 raise 事件验证
## 扩展建议
- 美术:当前使用占位 Sprite,替换为你的美术资源即可
- 平衡:所有数值都在 ScriptableObject 里,策划可独立调整
- 新塔种:复制一份 TowerData + 新建 Shooter 子类
- 新敌人:复制一份 EnemyData + 新建 Prefab
"""
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 项目打包器(L7)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class UnityProjectBuilder:
"""把生成的各类文件组织成完整的 Unity 目录结构"""
def __init__(self, namespace: str = "G2C.Game"):
self.gen = UnityCodeGenerator(namespace)
self.namespace = namespace
def build(self, gdd: GDDSkeleton, domain: DomainModel,
behavior: BehaviorModel, ecd: ECDModel,
project_name: str = "G2CGame") -> UnityProject:
proj = UnityProject(project_name=project_name)
files: list[tuple[str, str]] = []
# ── Scripts/Core ──
files.append(("Assets/_Project/Scripts/Core/GameEvents.cs",
self.gen.gen_events_bus(behavior.events)))
files.append(("Assets/_Project/Scripts/Core/GameState.cs",
self.gen.gen_state_enum(behavior.state_machines)))
# ── Scripts/Data (ScriptableObject) ──
for e in domain.entities:
if e.kind == EntityKind.SCRIPTABLE_OBJECT:
files.append((f"Assets/_Project/Scripts/Data/{e.name}.cs",
self.gen.gen_scriptable_object(e)))
# ── Scripts/Entities (MonoBehaviour) ──
entity_names = {"Tower", "Enemy", "Projectile", "Targeting",
"Shooter", "Base", "PathFollower"}
for e in domain.entities:
if e.kind == EntityKind.MONO_BEHAVIOUR and e.name in entity_names:
files.append((f"Assets/_Project/Scripts/Entities/{e.name}.cs",
self.gen.gen_mono_behaviour(e)))
# ── Scripts/Systems ──
for s in domain.systems:
subdir = "UI" if s.name == "HUDController" else "Systems"
files.append((f"Assets/_Project/Scripts/{subdir}/{s.name}.cs",
self.gen.gen_mono_behaviour(s)))
# ── UI (UXML + USS) ──
files.append(("Assets/_Project/UI/HUD.uxml", self.gen.gen_hud_uxml()))
files.append(("Assets/_Project/UI/HUD.uss", self.gen.gen_hud_uss()))
# ── Assembly Definition ──
files.append((f"Assets/_Project/Scripts/{self.namespace}.Runtime.asmdef",
self.gen.gen_asmdef_runtime()))
# ── README / docs ──
files.append(("README.md", self.gen.gen_readme(gdd, domain, behavior, ecd)))
proj.scripts = files
proj.total_files = len(files)
return proj
def write_to_disk(self, project: UnityProject, out_dir: Path) -> None:
out_dir = Path(out_dir)
out_dir.mkdir(parents=True, exist_ok=True)
for rel_path, content in project.scripts:
full = out_dir / rel_path
full.parent.mkdir(parents=True, exist_ok=True)
full.write_text(content, encoding="utf-8")
PYEOF
bash
<tool_call: run_command>
{"command": "python3 -c "\nfrom pathlib import Path\nfrom g2c.pipeline import Pipeline\nfrom g2c.generator import UnityProjectBuilder\np = Pipeline()\ngdd, domain, behavior, ecd = p.run('做一个2D塔防游戏')\nbuilder = UnityProjectBuilder()\nproj = builder.build(gdd, domain, behavior, ecd, 'TowerDefenseDemo')\nprint(f'Generated {len(proj.scripts)} files:')\nfor path, content in proj.scripts[:8]:\n print(f' {path} ({len(content)} bytes)')\nprint('...')\nfor path, content in proj.scripts[-3:]:\n print(f' {path} ({len(content)} bytes)')\n"", "cwd": "/home/user/g2c"}
</tool_call>
第五步:文档与开发计划生成
python
cat > /home/user/g2c/g2c/docgen.py << 'PYEOF'
"""
G2C 文档生成器
==============
产出:
- GDD.md 完整游戏设计文档
- DevPlan.md 迭代开发计划
- ERDiagram.md Mermaid 类图
- StateDiagrams.md 状态机图
- EventGraph.md 事件总线图
"""
from __future__ import annotations
from .models import GDDSkeleton, DomainModel, BehaviorModel, ECDModel
from .pipeline import MermaidRenderer
def render_gdd(gdd: GDDSkeleton, domain: DomainModel,
behavior: BehaviorModel, ecd: ECDModel) -> str:
v = gdd.vision
out: list[str] = []
out.append(f"# 游戏设计文档 · {v.summary}\n")
out.append(f"> **原始需求**:{v.raw_input}\n")
out.append("## 1. 愿景\n")
out.append(f"- **类型**:{v.genre}")
out.append(f"- **视角**:{v.perspective}")
out.append(f"- **主题**:{v.theme}")
out.append(f"- **平台**:{v.target_platform}\n")
out.append("## 2. 核心循环\n")
for i, s in enumerate(gdd.core_loop.steps, 1):
out.append(f"{i}. {s}")
out.append(f"\n- **胜利条件**:{gdd.core_loop.win_condition}")
out.append(f"- **失败条件**:{gdd.core_loop.lose_condition}")
out.append(f"- **乐趣来源**:{gdd.core_loop.fun_source}\n")
out.append("## 3. 用例清单(L1)\n")
out.append("| 角色 | 动作 | 对象 | 触发 | 结果 | 优先级 |")
out.append("|------|------|------|------|------|:------:|")
for uc in gdd.use_cases:
out.append(f"| {uc.actor} | {uc.action} | {uc.target} | {uc.trigger} | {uc.outcome} | P{uc.priority} |")
out.append("\n## 4. 领域模型(L2)\n")
out.append(f"- **实体数**:{len(domain.entities)}")
out.append(f"- **系统数**:{len(domain.systems)}")
out.append(f"- **关系数**:{len(domain.relations)}\n")
out.append("### 4.1 实体列表\n")
for e in domain.entities:
out.append(f"#### `{e.name}` ({e.kind.value})\n")
out.append(f"{e.description}\n")
if e.fields:
out.append("| 字段 | 类型 | 默认值 | 说明 |")
out.append("|------|------|--------|------|")
for f in e.fields:
out.append(f"| {f.name} | {f.field_type.value} | `{f.default_value}` | {f.tooltip} |")
out.append("")
out.append("### 4.2 系统列表\n")
for s in domain.systems:
out.append(f"- **{s.name}** --- {s.description}")
out.append("")
out.append("### 4.3 实体关系图\n")
out.append("```mermaid")
out.append(MermaidRenderer.render_domain(domain))
out.append("```\n")
out.append("## 5. 行为模型(L3)\n")
out.append("### 5.1 System × Feature 矩阵\n")
m = behavior.matrix
header = "| System \\ Feature |" + "|".join(f" {f} " for f in m.features) + "|"
sep = "|:---:|" + "|".join(":---:" for _ in m.features) + "|"
out.append(header); out.append(sep)
for sys in m.systems:
row = [f"| **{sys}** "]
for feat in m.features:
cell = m.get_cell(sys, feat)
if cell.is_primary:
mark = "🟢**主**"
elif cell.ops:
mark = "🔵" + "".join(op.value for op in cell.ops)
else:
mark = "·"
row.append(f" {mark} ")
out.append("|".join(row) + "|")
out.append("")
out.append("### 5.2 状态机\n")
for fsm in behavior.state_machines:
out.append(f"#### {fsm.name}(属主:{fsm.owner})\n")
out.append("```mermaid")
out.append(MermaidRenderer.render_state_machine(fsm))
out.append("```\n")
out.append("### 5.3 事件总线图\n")
out.append("```mermaid")
out.append(MermaidRenderer.render_events(behavior.events))
out.append("```\n")
out.append("### 5.4 事件清单\n")
out.append("| 事件 | 参数 | 发布者 | 订阅者 | 说明 |")
out.append("|------|------|--------|--------|------|")
for ev in behavior.events:
params = ", ".join(f"{p[0]} {p[1]}" for p in ev.params) or "---"
subs = ", ".join(ev.subscribers) or "---"
out.append(f"| `{ev.name}` | {params} | {ev.publisher} | {subs} | {ev.description} |")
out.append("")
out.append("## 6. 组件组合模型(L4)\n")
for pf in ecd.prefabs:
out.append(f"### {pf.name}(Tag: {pf.tag})\n")
out.append("```")
out.append(f"{pf.name}")
for c in pf.components:
mark = "★" if c.is_custom else " "
hint = f" // {c.config_hint}" if c.config_hint else ""
out.append(f"├── {mark} {c.component_type}{hint}")
for i, child in enumerate(pf.children):
is_last = i == len(pf.children) - 1
connector = "└──" if is_last else "├──"
out.append(f"{connector} {child.name}/")
for c in child.components:
mark = "★" if c.is_custom else " "
prefix = " " if is_last else "│ "
out.append(f"{prefix}└── {mark} {c.component_type}")
out.append("```\n")
out.append("> ★ = G2C 生成的自定义脚本\n")
return "\n".join(out)
def render_dev_plan(gdd: GDDSkeleton, domain: DomainModel,
behavior: BehaviorModel, ecd: ECDModel) -> str:
return f"""# 开发计划 · {gdd.vision.summary}
## 迭代规划(推荐 4 周 MVP)
### Sprint 1(W1):核心战斗骨架
- [ ] 在 Unity 6 中打开导入后的工程
- [ ] 创建 3 种 TowerData 资产:Arrow / Cannon / Magic
- [ ] 创建 3 种 EnemyData 资产:Grunt / Runner / Tank
- [ ] 制作占位 Sprite(圆形/方块即可)
- [ ] 搭建一个最小关卡 Level01,铺一条折线路径
- [ ] 创建 1 个 WaveData 和 1 个 LevelData
- [ ] 跑通"放塔 → 敌人生成 → 命中 → 死亡 → 扣血"完整链路
### Sprint 2(W2):数值与 UI
- [ ] 填充 HUDController 商店按钮
- [ ] HUD 金币 / 波次 / HP 实时刷新
- [ ] 增加塔升级 / 拆除交互
- [ ] 平衡初版数值(伤害 / HP / 奖励)
- [ ] 加音效钩子(可占位)
### Sprint 3(W3):内容扩充
- [ ] 增加 2 种塔种(减速塔 / 范围塔)
- [ ] 增加 2 种敌人(护盾怪 / Boss)
- [ ] 扩充到 10 波
- [ ] 加胜利 / 失败界面(复用 GameStateMachine)
### Sprint 4(W4):打磨与发布
- [ ] 美术资源替换
- [ ] 特效:爆炸 / 命中闪白 / 金币掉落
- [ ] 相机震屏、打击反馈
- [ ] Build PC / WebGL 版本
## 技术债登记
| 项目 | 说明 | 优先级 |
|------|------|:------:|
| 寻路 | 当前使用线性 waypoint,后期可切换 A\* | 中 |
| 对象池 | Enemy/Projectile 高频 Instantiate,建议引入 Pool | 高 |
| 存档 | 暂无,视需要接 `PlayerPrefs` 或 JSON | 低 |
| 本地化 | HUD 写死中文/emoji,可切换 `LocalizationPackage` | 低 |
## 风险清单
- **美术**:占位资源不可发布,需预算
- **平衡性**:每次改数值需测 10~20 局样本
- **性能**:>200 敌人时需 ECS / Jobs 重构
- **扩展**:若未来加多人联机,需把 `GameEvents` 改为可网络化事件总线
## 里程碑
- **Milestone 1**:可玩 MVP(End of W1)
- **Milestone 2**:Feature Complete(End of W3)
- **Milestone 3**:Release Candidate(End of W4)
---
## 自动化生成统计
- 生成实体类: {len(domain.entities)}
- 生成系统类: {len(domain.systems)}
- 生成事件数: {len(behavior.events)}
- 生成状态机: {len(behavior.state_machines)}
- 生成 Prefab 模板: {len(ecd.prefabs)}
"""
def render_all(gdd: GDDSkeleton, domain: DomainModel,
behavior: BehaviorModel, ecd: ECDModel) -> dict[str, str]:
"""一次性产出所有文档"""
return {
"GDD.md": render_gdd(gdd, domain, behavior, ecd),
"DevPlan.md": render_dev_plan(gdd, domain, behavior, ecd),
}
PYEOF
```验证完整管道:
```bash
cd /home/user/g2c && python3 -c "
from pathlib import Path
from g2c.pipeline import Pipeline
from g2c.generator import UnityProjectBuilder
from g2c.docgen import render_all
p = Pipeline()
gdd, domain, behavior, ecd = p.run('做一个2D塔防游戏:玩家放置防御塔抵御一波波敌人')
# 生成 Unity 工程
builder = UnityProjectBuilder('TowerDefense')
proj = builder.build(gdd, domain, behavior, ecd, 'TowerDefenseDemo')
out_dir = Path('/tmp/td_test')
import shutil
if out_dir.exists(): shutil.rmtree(out_dir)
builder.write_to_disk(proj, out_dir)
# 生成文档
docs = render_all(gdd, domain, behavior, ecd)
for name, content in docs.items():
(out_dir / name).write_text(content, encoding='utf-8')
# 验证结果
print('=== File tree ===')
import os
for root, dirs, files in os.walk(out_dir):
rel = os.path.relpath(root, out_dir)
indent = ' ' * (rel.count('/') if rel != '.' else 0)
print(f'{indent}{os.path.basename(root) if rel != \".\" else out_dir.name}/')
for f in files:
print(f'{indent} {f}')
print('\n=== Total size ===')
total = 0
for root, _, files in os.walk(out_dir):
for f in files: total += (Path(root)/f).stat().st_size
print(f'{total/1024:.1f} KB across {sum(len(f) for _,_,f in os.walk(out_dir))} files')
"
```先修复 `knowledge.py` 里 TowerData.upgradeTo 的泛型问题:
```bash
python3 -c "
import re
path = '/home/user/g2c/g2c/knowledge.py'
s = open(path).read()
# 把 fields[8] 的修正行替换为设置 generic_type
old = ''' tower_data.fields[8].field_type = FieldType.GAMEOBJECT
tower_data.fields[8].tooltip = \"升级后的 TowerData(可为空)\"'''
new = ''' tower_data.fields[8].generic_type = \"TowerData\"
tower_data.fields[8].tooltip = \"升级后的 TowerData(可为空)\"'''
s = s.replace(old, new)
open(path, 'w').write(s)
print('patched')
"
运行完整管道验证:
bash
cd /home/user/g2c && python3 << 'PYEOF'
import shutil, os
from pathlib import Path
from g2c.pipeline import Pipeline
from g2c.generator import UnityProjectBuilder
from g2c.docgen import render_all
p = Pipeline()
gdd, domain, behavior, ecd = p.run('做一个2D塔防游戏:玩家放置防御塔抵御一波波敌人')
builder = UnityProjectBuilder('TowerDefense')
proj = builder.build(gdd, domain, behavior, ecd, 'TowerDefenseDemo')
out = Path('/tmp/td_test')
if out.exists(): shutil.rmtree(out)
builder.write_to_disk(proj, out)
for name, content in render_all(gdd, domain, behavior, ecd).items():
(out / name).write_text(content, encoding='utf-8')
print('=== Generated files ===')
for root, dirs, files in sorted(os.walk(out)):
dirs.sort()
rel = os.path.relpath(root, out)
depth = 0 if rel == '.' else rel.count(os.sep) + 1
print(f"{' ' * depth}{os.path.basename(root) if rel != '.' else out.name}/")
for f in sorted(files):
sz = (Path(root)/f).stat().st_size
print(f"{' ' * (depth+1)}{f} ({sz} bytes)")
PYEOF
```让我继续构建 PyQt6 UI 层。先写主题与组件:
<tool_call>
<tool_name>run_command</tool_name>
<parameters>
<command>mkdir -p /home/user/g2c/g2c/ui && ls /home/user/g2c/g2c</command>
<cwd>/home/user/g2c</cwd>
</parameters>
</tool_call>
```bash
cat > /home/user/g2c/g2c/ui/__init__.py << 'EOF'
"""G2C PyQt6 UI 层"""
EOF
python
cat > /home/user/g2c/g2c/ui/theme.py << 'PYEOF'
"""
G2C 主题:深色、游戏风、带霓虹强调色。
"""
APP_STYLESHEET = """
/* ═══════ 全局 ═══════ */
QWidget {
background-color: #1a1d26;
color: #e4e7ef;
font-family: "SF Pro Display", "Segoe UI", "PingFang SC", sans-serif;
font-size: 13px;
}
QMainWindow { background-color: #13151c; }
/* ═══════ 顶栏 ═══════ */
#TopBar {
background-color: #0f1117;
border-bottom: 1px solid #2a2f3d;
padding: 0;
}
#BrandLabel {
color: #6ee7ff;
font-size: 22px;
font-weight: 700;
letter-spacing: 2px;
padding: 12px 20px;
}
#BrandSub {
color: #7a8294;
font-size: 12px;
padding: 0 20px;
}
/* ═══════ 输入区 ═══════ */
#InputCard {
background-color: #1e2230;
border: 1px solid #2a2f3d;
border-radius: 10px;
padding: 16px;
}
QTextEdit, QLineEdit {
background-color: #0f1117;
color: #e4e7ef;
border: 1px solid #2a2f3d;
border-radius: 6px;
padding: 10px;
font-size: 14px;
selection-background-color: #6ee7ff;
selection-color: #0f1117;
}
QTextEdit:focus, QLineEdit:focus { border: 1px solid #6ee7ff; }
/* ═══════ 按钮 ═══════ */
QPushButton {
background-color: #2a2f3d;
color: #e4e7ef;
border: 1px solid #3a4050;
border-radius: 6px;
padding: 8px 18px;
font-weight: 600;
}
QPushButton:hover { background-color: #353c4e; border-color: #6ee7ff; }
QPushButton:pressed { background-color: #222735; }
QPushButton:disabled { color: #4a5060; background-color: #1a1d26; border-color: #2a2f3d; }
QPushButton#PrimaryBtn {
background-color: #6ee7ff;
color: #0f1117;
border: none;
font-weight: 700;
padding: 10px 22px;
}
QPushButton#PrimaryBtn:hover { background-color: #4fd4f0; }
QPushButton#PrimaryBtn:disabled { background-color: #2a2f3d; color: #4a5060; }
QPushButton#DangerBtn { background-color: #ff6b6b; color: white; border: none; }
QPushButton#DangerBtn:hover { background-color: #e55a5a; }
QPushButton#SuccessBtn { background-color: #51cf66; color: #0f1117; border: none; font-weight: 700; }
QPushButton#SuccessBtn:hover { background-color: #40b852; }
/* ═══════ 标签页 ═══════ */
QTabWidget::pane {
border: 1px solid #2a2f3d;
border-radius: 8px;
background-color: #1a1d26;
top: -1px;
}
QTabBar::tab {
background-color: #0f1117;
color: #7a8294;
padding: 10px 20px;
margin-right: 2px;
border: 1px solid #2a2f3d;
border-bottom: none;
border-top-left-radius: 8px;
border-top-right-radius: 8px;
font-weight: 600;
}
QTabBar::tab:selected {
background-color: #1a1d26;
color: #6ee7ff;
border-bottom: 2px solid #6ee7ff;
}
QTabBar::tab:hover:!selected { background-color: #1e2230; color: #e4e7ef; }
/* ═══════ 列表 / 树 / 表 ═══════ */
QListWidget, QTreeWidget, QTableWidget {
background-color: #0f1117;
border: 1px solid #2a2f3d;
border-radius: 6px;
alternate-background-color: #141720;
gridline-color: #2a2f3d;
}
QListWidget::item, QTreeWidget::item { padding: 6px; border: none; }
QListWidget::item:selected, QTreeWidget::item:selected {
background-color: #6ee7ff;
color: #0f1117;
}
QHeaderView::section {
background-color: #1e2230;
color: #6ee7ff;
border: none;
border-right: 1px solid #2a2f3d;
border-bottom: 1px solid #2a2f3d;
padding: 8px;
font-weight: 700;
}
/* ═══════ 标签 ═══════ */
QLabel#SectionTitle {
color: #6ee7ff;
font-size: 15px;
font-weight: 700;
padding: 8px 0 4px 0;
letter-spacing: 1px;
}
QLabel#Hint { color: #7a8294; font-size: 12px; }
QLabel#Badge {
background-color: #2a2f3d;
color: #6ee7ff;
padding: 4px 10px;
border-radius: 10px;
font-size: 11px;
font-weight: 700;
}
/* ═══════ 滚动条 ═══════ */
QScrollBar:vertical {
background: #0f1117;
width: 10px;
margin: 0;
border: none;
}
QScrollBar::handle:vertical {
background: #3a4050;
border-radius: 5px;
min-height: 30px;
}
QScrollBar::handle:vertical:hover { background: #6ee7ff; }
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { height: 0; }
QScrollBar:horizontal { background: #0f1117; height: 10px; margin: 0; border: none; }
QScrollBar::handle:horizontal { background: #3a4050; border-radius: 5px; min-width: 30px; }
QScrollBar::handle:horizontal:hover { background: #6ee7ff; }
/* ═══════ 状态栏 ═══════ */
QStatusBar {
background-color: #0f1117;
color: #7a8294;
border-top: 1px solid #2a2f3d;
}
/* ═══════ 进度条 ═══════ */
QProgressBar {
background-color: #0f1117;
border: 1px solid #2a2f3d;
border-radius: 4px;
text-align: center;
color: #e4e7ef;
height: 18px;
}
QProgressBar::chunk {
background-color: #6ee7ff;
border-radius: 3px;
}
/* ═══════ 代码预览 ═══════ */
QPlainTextEdit#CodeView {
background-color: #0a0c12;
color: #c9d1d9;
font-family: "JetBrains Mono", "Fira Code", Consolas, monospace;
font-size: 12px;
border: 1px solid #2a2f3d;
border-radius: 6px;
padding: 10px;
}
/* ═══════ 分隔线 ═══════ */
QFrame#Divider {
background-color: #2a2f3d;
max-height: 1px;
}
/* ═══════ 组框 ═══════ */
QGroupBox {
border: 1px solid #2a2f3d;
border-radius: 6px;
margin-top: 12px;
padding-top: 10px;
font-weight: 700;
color: #6ee7ff;
}
QGroupBox::title {
subcontrol-origin: margin;
left: 12px;
padding: 0 8px;
background-color: #1a1d26;
}
/* ═══════ ComboBox ═══════ */
QComboBox {
background-color: #0f1117;
border: 1px solid #2a2f3d;
border-radius: 6px;
padding: 6px 12px;
color: #e4e7ef;
}
QComboBox:hover { border-color: #6ee7ff; }
QComboBox::drop-down { border: none; width: 24px; }
QComboBox QAbstractItemView {
background-color: #1a1d26;
border: 1px solid #2a2f3d;
selection-background-color: #6ee7ff;
selection-color: #0f1117;
}
/* ═══════ CheckBox ═══════ */
QCheckBox { spacing: 8px; }
QCheckBox::indicator {
width: 16px; height: 16px;
border: 1px solid #3a4050;
border-radius: 3px;
background-color: #0f1117;
}
QCheckBox::indicator:checked {
background-color: #6ee7ff;
border-color: #6ee7ff;
}
"""
# ═══════ 颜色常量(给绘图用) ═══════
class Colors:
BG = "#1a1d26"
BG_DARKER = "#0f1117"
BG_LIGHTER = "#1e2230"
BORDER = "#2a2f3d"
TEXT = "#e4e7ef"
TEXT_DIM = "#7a8294"
ACCENT = "#6ee7ff"
ACCENT_DARK = "#4fd4f0"
SUCCESS = "#51cf66"
WARNING = "#ffa94d"
DANGER = "#ff6b6b"
ENTITY = "#a78bfa"
SYSTEM = "#fbbf24"
EVENT = "#f472b6"
STATE = "#34d399"
PYEOF
```现在写共享小部件和视图组件:
```python
cat > /home/user/g2c/g2c/ui/widgets.py << 'PYEOF'
"""
G2C 共享小部件集合
==================
- CodeView: 只读代码浏览器
- MarkdownView: 简单的 Markdown 渲染(用 QTextBrowser)
- DiagramCanvas: 自动布局的 QGraphicsView(给状态机/事件图用)
- FileTreeView: 生成文件列表预览
"""
from __future__ import annotations
import math
from PyQt6.QtCore import Qt, QRectF, QPointF, QSize, pyqtSignal
from PyQt6.QtGui import (
QPainter, QPen, QBrush, QColor, QFont, QPolygonF,
QFontMetrics, QSyntaxHighlighter, QTextCharFormat,
)
from PyQt6.QtWidgets import (
QWidget, QPlainTextEdit, QTextBrowser, QGraphicsView, QGraphicsScene,
QGraphicsItem, QLabel, QVBoxLayout, QHBoxLayout, QFrame,
QListWidget, QListWidgetItem, QSplitter, QTableWidget, QTableWidgetItem,
QHeaderView, QTreeWidget, QTreeWidgetItem,
)
from .theme import Colors
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# C# 语法高亮(极简版)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class CSharpHighlighter(QSyntaxHighlighter):
KEYWORDS = [
"using", "namespace", "public", "private", "protected", "internal",
"static", "void", "class", "struct", "enum", "interface",
"new", "return", "if", "else", "for", "foreach", "while", "do",
"switch", "case", "break", "continue", "default", "yield",
"this", "base", "null", "true", "false", "var",
"int", "float", "double", "bool", "string", "object",
"readonly", "const", "override", "virtual", "abstract",
"ScriptableObject", "MonoBehaviour", "SerializeField",
"Header", "Tooltip", "RequireComponent", "CreateAssetMenu",
"IEnumerator", "List", "Action", "Transform", "GameObject", "Vector3",
]
TYPES_COLOR = "#ffd479"
KEYWORD_COLOR = "#ff79c6"
STRING_COLOR = "#a6e22e"
COMMENT_COLOR = "#75715e"
NUMBER_COLOR = "#ae81ff"
ATTR_COLOR = "#66d9ef"
def __init__(self, parent):
super().__init__(parent)
self.rules = []
kw_fmt = QTextCharFormat()
kw_fmt.setForeground(QColor(self.KEYWORD_COLOR))
kw_fmt.setFontWeight(QFont.Weight.Bold)
for kw in self.KEYWORDS:
self.rules.append((f"\\b{kw}\\b", kw_fmt))
str_fmt = QTextCharFormat(); str_fmt.setForeground(QColor(self.STRING_COLOR))
self.rules.append(("\".*?\"", str_fmt))
num_fmt = QTextCharFormat(); num_fmt.setForeground(QColor(self.NUMBER_COLOR))
self.rules.append(("\\b[0-9]+\\.?[0-9]*f?\\b", num_fmt))
attr_fmt = QTextCharFormat(); attr_fmt.setForeground(QColor(self.ATTR_COLOR))
self.rules.append(("\\[[^\\]]+\\]", attr_fmt))
cmt_fmt = QTextCharFormat(); cmt_fmt.setForeground(QColor(self.COMMENT_COLOR))
cmt_fmt.setFontItalic(True)
self.rules.append(("//[^\n]*", cmt_fmt))
def highlightBlock(self, text: str):
import re
for pattern, fmt in self.rules:
for m in re.finditer(pattern, text):
self.setFormat(m.start(), m.end() - m.start(), fmt)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 代码浏览器
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class CodeView(QPlainTextEdit):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("CodeView")
self.setReadOnly(True)
font = QFont("JetBrains Mono", 10)
font.setStyleHint(QFont.StyleHint.Monospace)
self.setFont(font)
self.setLineWrapMode(QPlainTextEdit.LineWrapMode.NoWrap)
self.highlighter = CSharpHighlighter(self.document())
def load(self, code: str):
self.setPlainText(code)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# Markdown 预览(TextBrowser 自带简化解析)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class MarkdownView(QTextBrowser):
def __init__(self, parent=None):
super().__init__(parent)
self.setOpenExternalLinks(True)
self.setStyleSheet("""
QTextBrowser {
background-color: #0f1117;
color: #e4e7ef;
border: 1px solid #2a2f3d;
border-radius: 6px;
padding: 16px;
font-size: 13px;
}
""")
def load_markdown(self, md_text: str):
# Qt6 的 QTextBrowser 支持 setMarkdown
self.setMarkdown(md_text)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 图形节点(通用)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class NodeItem(QGraphicsItem):
"""图中的一个节点(矩形 + 文字)"""
def __init__(self, text: str, x: float, y: float,
width: float = 140, height: float = 54,
color: str = Colors.ENTITY, subtitle: str = ""):
super().__init__()
self.text = text
self.subtitle = subtitle
self.w = width
self.h = height
self.color = color
self.setPos(x, y)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsMovable, True)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemIsSelectable, True)
self.setFlag(QGraphicsItem.GraphicsItemFlag.ItemSendsGeometryChanges, True)
def boundingRect(self) -> QRectF:
return QRectF(0, 0, self.w, self.h)
def center(self) -> QPointF:
return self.pos() + QPointF(self.w / 2, self.h / 2)
def paint(self, painter: QPainter, option, widget=None):
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
# 背景阴影
shadow = QColor(0, 0, 0, 100)
painter.setBrush(QBrush(shadow))
painter.setPen(Qt.PenStyle.NoPen)
painter.drawRoundedRect(QRectF(2, 3, self.w, self.h), 8, 8)
# 主体
rect = QRectF(0, 0, self.w, self.h)
bg = QColor(Colors.BG_LIGHTER)
painter.setBrush(QBrush(bg))
border = QColor(self.color)
pen = QPen(border); pen.setWidthF(2)
if self.isSelected():
pen.setColor(QColor(Colors.ACCENT)); pen.setWidthF(3)
painter.setPen(pen)
painter.drawRoundedRect(rect, 8, 8)
# 顶部色条
top = QRectF(0, 0, self.w, 6)
painter.setBrush(QBrush(border)); painter.setPen(Qt.PenStyle.NoPen)
painter.drawRoundedRect(top, 8, 8)
painter.drawRect(QRectF(0, 3, self.w, 3))
# 文字
title_font = QFont("SF Pro Display", 10, QFont.Weight.Bold)
painter.setFont(title_font)
painter.setPen(QColor(Colors.TEXT))
painter.drawText(QRectF(8, 10, self.w - 16, 20),
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
self.text)
if self.subtitle:
sub_font = QFont("SF Pro Display", 8)
painter.setFont(sub_font)
painter.setPen(QColor(Colors.TEXT_DIM))
painter.drawText(QRectF(8, 30, self.w - 16, 16),
Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter,
self.subtitle)
class EdgeItem(QGraphicsItem):
"""带箭头和标签的边"""
def __init__(self, src: NodeItem, dst: NodeItem, label: str = "", color: str = "#6ee7ff"):
super().__init__()
self.src = src
self.dst = dst
self.label = label
self.color = color
self.setZValue(-1)
def boundingRect(self) -> QRectF:
p1 = self.src.center()
p2 = self.dst.center()
return QRectF(p1, p2).normalized().adjusted(-40, -40, 40, 40)
def paint(self, painter: QPainter, option, widget=None):
painter.setRenderHint(QPainter.RenderHint.Antialiasing)
p1 = self.src.center()
p2 = self.dst.center()
# 收缩到节点边界
p1, p2 = self._clip_to_rect(p1, p2, self.src), self._clip_to_rect(p2, p1, self.dst)
pen = QPen(QColor(self.color)); pen.setWidthF(1.5)
painter.setPen(pen)
painter.drawLine(p1, p2)
# 箭头
self._draw_arrow(painter, p1, p2)
# 标签
if self.label:
mid = (p1 + p2) / 2
painter.setPen(QColor(Colors.TEXT_DIM))
painter.setFont(QFont("SF Pro Display", 8))
fm = QFontMetrics(painter.font())
w = fm.horizontalAdvance(self.label) + 8
rect = QRectF(mid.x() - w / 2, mid.y() - 10, w, 16)
painter.fillRect(rect, QColor(Colors.BG_DARKER))
painter.drawText(rect, Qt.AlignmentFlag.AlignCenter, self.label)
def _clip_to_rect(self, p_from: QPointF, p_to: QPointF, node: NodeItem) -> QPointF:
"""从节点中心出发,算出到节点矩形边界的交点"""
cx, cy = node.center().x(), node.center().y()
w, h = node.w / 2, node.h / 2
dx, dy = p_to.x() - cx, p_to.y() - cy
if dx == 0 and dy == 0:
return p_from
scale = min(
w / abs(dx) if dx else 1e9,
h / abs(dy) if dy else 1e9,
)
return QPointF(cx + dx * scale, cy + dy * scale)
def _draw_arrow(self, painter: QPainter, p1: QPointF, p2: QPointF):
dx, dy = p2.x() - p1.x(), p2.y() - p1.y()
ln = math.hypot(dx, dy)
if ln < 1: return
ux, uy = dx / ln, dy / ln
size = 8
a1 = QPointF(p2.x() - ux * size + uy * size * 0.5,
p2.y() - uy * size - ux * size * 0.5)
a2 = QPointF(p2.x() - ux * size - uy * size * 0.5,
p2.y() - uy * size + ux * size * 0.5)
painter.setBrush(QBrush(QColor(self.color)))
painter.drawPolygon(QPolygonF([p2, a1, a2]))
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 图表画布(自动布局)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class DiagramCanvas(QGraphicsView):
"""通用图表画布:圆形/分层布局 + 拖拽节点"""
def __init__(self, parent=None):
super().__init__(parent)
self.scene_ = QGraphicsScene(self)
self.setScene(self.scene_)
self.setRenderHint(QPainter.RenderHint.Antialiasing)
self.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform)
self.setDragMode(QGraphicsView.DragMode.ScrollHandDrag)
self.setStyleSheet("""
QGraphicsView { background-color: #0a0c12; border: 1px solid #2a2f3d; border-radius: 6px; }
""")
self.nodes: dict[str, NodeItem] = {}
self.edges: list[EdgeItem] = []
def clear_diagram(self):
self.scene_.clear()
self.nodes.clear()
self.edges.clear()
def add_node(self, key: str, title: str, subtitle: str = "",
color: str = Colors.ENTITY) -> NodeItem:
node = NodeItem(title, 0, 0, subtitle=subtitle, color=color)
self.scene_.addItem(node)
self.nodes[key] = node
return node
def add_edge(self, src_key: str, dst_key: str, label: str = "",
color: str = "#6ee7ff"):
if src_key not in self.nodes or dst_key not in self.nodes:
return
edge = EdgeItem(self.nodes[src_key], self.nodes[dst_key], label, color)
self.scene_.addItem(edge)
self.edges.append(edge)
def auto_layout_circular(self, radius: float = 260):
"""圆形布局"""
n = len(self.nodes)
if n == 0: return
cx, cy = 400, 300
for i, node in enumerate(self.nodes.values()):
angle = 2 * math.pi * i / n - math.pi / 2
x = cx + radius * math.cos(angle) - node.w / 2
y = cy + radius * math.sin(angle) - node.h / 2
node.setPos(x, y)
self.scene_.setSceneRect(self.scene_.itemsBoundingRect().adjusted(-50, -50, 50, 50))
def auto_layout_layered(self, layers: list[list[str]],
dx: float = 200, dy: float = 140):
"""分层布局:layers 是一个 list of list,每层从左到右排列"""
for layer_idx, keys in enumerate(layers):
n = len(keys)
for i, key in enumerate(keys):
if key not in self.nodes: continue
node = self.nodes[key]
x = (i - (n - 1) / 2) * dx + 400
y = layer_idx * dy + 80
node.setPos(x - node.w / 2, y)
self.scene_.setSceneRect(self.scene_.itemsBoundingRect().adjusted(-50, -50, 50, 50))
def wheelEvent(self, event):
"""滚轮缩放"""
factor = 1.15 if event.angleDelta().y() > 0 else 1 / 1.15
self.scale(factor, factor)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 简单标题分隔条
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class SectionTitle(QLabel):
def __init__(self, text: str, parent=None):
super().__init__(text, parent)
self.setObjectName("SectionTitle")
class Badge(QLabel):
def __init__(self, text: str, parent=None):
super().__init__(text, parent)
self.setObjectName("Badge")
PYEOF
```现在写主窗口:
```python
cat > /home/user/g2c/g2c/ui/main_window.py << 'PYEOF'
"""
G2C 主窗口
==========
左侧:自然语言输入 + 预设样例
右侧:Tab 视图展示管道每层产出
底部:状态栏
"""
from __future__ import annotations
import shutil, zipfile, io
from pathlib import Path
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer, QSize
from PyQt6.QtGui import QFont, QAction, QIcon, QPixmap, QPainter, QColor
from PyQt6.QtWidgets import (
QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTextEdit, QLabel,
QPushButton, QTabWidget, QTableWidget, QTableWidgetItem, QTreeWidget,
QTreeWidgetItem, QListWidget, QListWidgetItem, QSplitter, QFrame,
QStatusBar, QFileDialog, QMessageBox, QProgressBar, QGroupBox,
QComboBox, QCheckBox, QPlainTextEdit, QScrollArea, QHeaderView,
QGridLayout, QSizePolicy, QStackedWidget,
)
from ..models import (
GDDSkeleton, DomainModel, BehaviorModel, ECDModel, PipelineResult,
EntityKind, CRUDOp,
)
from ..pipeline import Pipeline, MermaidRenderer
from ..generator import UnityProjectBuilder
from ..docgen import render_gdd, render_dev_plan
from .theme import APP_STYLESHEET, Colors
from .widgets import (
CodeView, MarkdownView, DiagramCanvas, SectionTitle, Badge,
)
SAMPLE_PROMPTS = {
"🗼 经典塔防":
"做一个2D塔防游戏:玩家在地图上放置不同类型的防御塔,抵御一波波进攻的敌人。"
"击败敌人获得金币,可用于升级或建造新塔。守住基地直到最后一波胜利。",
"🏰 奇幻塔防":
"做一个奇幻主题的塔防游戏,有魔法塔、弓箭塔和投石塔三种防御塔,"
"敌人包括哥布林、兽人和龙,每波越来越难。玩家通过策略布阵抵御入侵。",
"🚀 科幻塔防":
"科幻塔防:在太空基地周围放置激光炮塔、导弹发射器和护盾发生器,"
"抵御外星入侵者。可以升级塔和回收金币。",
"⚡ 极速塔防":
"2D塔防,节奏快,10波敌人,3种塔,关卡10分钟内可通关。",
}
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 后台工作线程
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class PipelineWorker(QThread):
progress = pyqtSignal(int, str) # (percent, message)
finished_ok = pyqtSignal(object) # PipelineResult
failed = pyqtSignal(str)
def __init__(self, raw_input: str, project_name: str = "TowerDefenseDemo"):
super().__init__()
self.raw_input = raw_input
self.project_name = project_name
def run(self):
try:
self.progress.emit(5, "L1 · 解析自然语言...")
pipeline = Pipeline()
gdd = pipeline.l1.process(self.raw_input)
self.progress.emit(25, "L2 · 构建领域模型...")
domain = pipeline.l2.process(gdd)
self.progress.emit(45, "L3 · 生成矩阵/状态机/事件...")
behavior = pipeline.l3.process(gdd, domain)
self.progress.emit(60, "L4 · 组合 Prefab / 数据资产...")
ecd = pipeline.l4.process(gdd, domain)
self.progress.emit(75, "L6 · 生成 Unity C# 代码...")
builder = UnityProjectBuilder(f"G2C.{self.project_name}")
project = builder.build(gdd, domain, behavior, ecd, self.project_name)
self.progress.emit(90, "生成文档...")
gdd_md = render_gdd(gdd, domain, behavior, ecd)
plan_md = render_dev_plan(gdd, domain, behavior, ecd)
result = PipelineResult(
gdd=gdd, domain=domain, behavior=behavior, ecd=ecd, project=project,
gdd_markdown=gdd_md, dev_plan_markdown=plan_md,
er_mermaid=MermaidRenderer.render_domain(domain),
state_mermaid="\n\n".join(
MermaidRenderer.render_state_machine(fsm) for fsm in behavior.state_machines
),
event_mermaid=MermaidRenderer.render_events(behavior.events),
total_scripts=sum(1 for p, _ in project.scripts if p.endswith(".cs")),
total_so=sum(1 for e in domain.entities if e.kind == EntityKind.SCRIPTABLE_OBJECT),
total_events=len(behavior.events),
total_states=sum(len(fsm.states) for fsm in behavior.state_machines),
)
self.progress.emit(100, "✓ 完成")
self.finished_ok.emit(result)
except Exception as e:
import traceback
self.failed.emit(f"{e}\n\n{traceback.format_exc()}")
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 顶栏品牌
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class TopBar(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self.setObjectName("TopBar")
self.setFixedHeight(64)
h = QHBoxLayout(self); h.setContentsMargins(0, 0, 20, 0); h.setSpacing(0)
brand = QLabel("G2C")
brand.setObjectName("BrandLabel")
h.addWidget(brand)
sub = QLabel("Game Design → Unity 6 Code")
sub.setObjectName("BrandSub")
h.addWidget(sub)
h.addStretch()
self.badge_genre = Badge("Tower Defense")
h.addWidget(self.badge_genre)
h.addSpacing(8)
self.badge_unity = Badge("Unity 6 · URP 2D")
h.addWidget(self.badge_unity)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 左侧:输入面板
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class InputPanel(QWidget):
run_requested = pyqtSignal(str, str) # (prompt, project_name)
def __init__(self, parent=None):
super().__init__(parent)
self._build_ui()
def _build_ui(self):
v = QVBoxLayout(self); v.setContentsMargins(16, 16, 16, 16); v.setSpacing(12)
v.addWidget(SectionTitle("① 一句话描述游戏"))
self.prompt_edit = QTextEdit()
self.prompt_edit.setPlaceholderText(
"例如:做一个2D塔防游戏,玩家放置防御塔抵御一波波敌人,击败敌人获得金币升级塔..."
)
self.prompt_edit.setMinimumHeight(140)
v.addWidget(self.prompt_edit)
v.addWidget(SectionTitle("② 快速样例"))
preset_grid = QGridLayout(); preset_grid.setSpacing(6)
for i, (name, text) in enumerate(SAMPLE_PROMPTS.items()):
btn = QPushButton(name)
btn.setCursor(Qt.CursorShape.PointingHandCursor)
btn.clicked.connect(lambda _, t=text: self.prompt_edit.setPlainText(t))
preset_grid.addWidget(btn, i // 2, i % 2)
v.addLayout(preset_grid)
v.addWidget(SectionTitle("③ 项目配置"))
cfg_box = QGroupBox("导出选项")
cfg_l = QVBoxLayout(cfg_box)
self.project_name_edit = QTextEdit(); self.project_name_edit.setFixedHeight(28)
self.project_name_edit.setPlainText("TowerDefenseDemo")
cfg_l.addWidget(QLabel("Unity 项目名"))
cfg_l.addWidget(self.project_name_edit)
self.chk_include_docs = QCheckBox("包含 GDD.md / DevPlan.md 文档"); self.chk_include_docs.setChecked(True)
self.chk_gen_meta = QCheckBox("为脚本预生成 .meta(让 Unity 保留 GUID)"); self.chk_gen_meta.setChecked(False)
cfg_l.addWidget(self.chk_include_docs)
cfg_l.addWidget(self.chk_gen_meta)
v.addWidget(cfg_box)
v.addStretch()
self.run_btn = QPushButton("▶ 运行管道 L1 → L7")
self.run_btn.setObjectName("PrimaryBtn")
self.run_btn.setMinimumHeight(44)
self.run_btn.clicked.connect(self._on_run)
v.addWidget(self.run_btn)
self.progress_bar = QProgressBar()
self.progress_bar.setValue(0)
self.progress_bar.setVisible(False)
v.addWidget(self.progress_bar)
self.progress_label = QLabel("")
self.progress_label.setObjectName("Hint")
self.progress_label.setVisible(False)
v.addWidget(self.progress_label)
def _on_run(self):
text = self.prompt_edit.toPlainText().strip()
if not text:
QMessageBox.warning(self, "G2C", "请输入游戏需求描述")
return
name = self.project_name_edit.toPlainText().strip() or "G2CGame"
self.run_requested.emit(text, name)
def set_progress(self, percent: int, message: str):
self.progress_bar.setVisible(True)
self.progress_label.setVisible(True)
self.progress_bar.setValue(percent)
self.progress_label.setText(message)
def set_running(self, running: bool):
self.run_btn.setEnabled(not running)
if not running:
QTimer.singleShot(1200, lambda: (self.progress_bar.setVisible(False),
self.progress_label.setVisible(False)))
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 视图:Overview 仪表盘
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class StatCard(QFrame):
def __init__(self, value: str, label: str, color: str = Colors.ACCENT):
super().__init__()
self.setStyleSheet(f"""
QFrame {{
background-color: {Colors.BG_LIGHTER};
border: 1px solid {Colors.BORDER};
border-left: 4px solid {color};
border-radius: 8px;
}}
""")
self.setMinimumHeight(90)
v = QVBoxLayout(self); v.setContentsMargins(16, 12, 16, 12); v.setSpacing(4)
self.val = QLabel(value); self.val.setStyleSheet(f"color:{color};font-size:28px;font-weight:800;")
self.lbl = QLabel(label); self.lbl.setStyleSheet(f"color:{Colors.TEXT_DIM};font-size:12px;")
v.addWidget(self.val); v.addWidget(self.lbl)
def update_value(self, v: str):
self.val.setText(v)
class OverviewView(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._build_ui()
def _build_ui(self):
v = QVBoxLayout(self); v.setContentsMargins(16, 16, 16, 16); v.setSpacing(12)
# 统计卡片区
grid = QGridLayout(); grid.setSpacing(12)
self.card_entities = StatCard("---", "实体 Entities", Colors.ENTITY)
self.card_systems = StatCard("---", "系统 Systems", Colors.SYSTEM)
self.card_events = StatCard("---", "事件 Events", Colors.EVENT)
self.card_states = StatCard("---", "状态 States", Colors.STATE)
self.card_scripts = StatCard("---", ".cs 脚本", Colors.ACCENT)
self.card_so = StatCard("---", "ScriptableObject", Colors.SUCCESS)
grid.addWidget(self.card_entities, 0, 0)
grid.addWidget(self.card_systems, 0, 1)
grid.addWidget(self.card_events, 0, 2)
grid.addWidget(self.card_states, 1, 0)
grid.addWidget(self.card_scripts, 1, 1)
grid.addWidget(self.card_so, 1, 2)
v.addLayout(grid)
# GDD 摘要预览
v.addWidget(SectionTitle("GDD 摘要"))
self.summary_view = MarkdownView()
v.addWidget(self.summary_view, 1)
def update_result(self, r: PipelineResult):
self.card_entities.update_value(str(len(r.domain.entities)))
self.card_systems.update_value(str(len(r.domain.systems)))
self.card_events.update_value(str(r.total_events))
self.card_states.update_value(str(r.total_states))
self.card_scripts.update_value(str(r.total_scripts))
self.card_so.update_value(str(r.total_so))
# 摘要 = GDD 的前 3 节
parts = r.gdd_markdown.split("\n## ")
summary = parts[0]
if len(parts) > 1: summary += "\n## " + parts[1]
if len(parts) > 2: summary += "\n## " + parts[2]
self.summary_view.load_markdown(summary)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 视图:实体列表 + 详情
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class EntityView(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._build_ui()
self._domain = None
def _build_ui(self):
h = QHBoxLayout(self); h.setContentsMargins(0, 0, 0, 0); h.setSpacing(0)
splitter = QSplitter(Qt.Orientation.Horizontal)
# 左侧:实体列表
left = QWidget(); lv = QVBoxLayout(left); lv.setContentsMargins(12, 12, 12, 12)
lv.addWidget(SectionTitle("实体 & 系统"))
self.list_widget = QListWidget()
self.list_widget.currentItemChanged.connect(self._on_select)
lv.addWidget(self.list_widget)
# 右侧:详情
right = QWidget(); rv = QVBoxLayout(right); rv.setContentsMargins(12, 12, 12, 12)
self.detail_title = SectionTitle("选择一个实体查看详情")
rv.addWidget(self.detail_title)
self.desc_label = QLabel(""); self.desc_label.setWordWrap(True)
self.desc_label.setStyleSheet("color:#7a8294;padding:4px 0;")
rv.addWidget(self.desc_label)
# 字段表
rv.addWidget(SectionTitle("字段"))
self.fields_table = QTableWidget(0, 4)
self.fields_table.setHorizontalHeaderLabels(["名称", "类型", "默认值", "说明"])
self.fields_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
self.fields_table.horizontalHeader().setDefaultSectionSize(140)
self.fields_table.verticalHeader().setVisible(False)
rv.addWidget(self.fields_table)
# 方法表
rv.addWidget(SectionTitle("方法"))
self.methods_table = QTableWidget(0, 3)
self.methods_table.setHorizontalHeaderLabels(["名称", "返回", "说明"])
self.methods_table.horizontalHeader().setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch)
self.methods_table.verticalHeader().setVisible(False)
rv.addWidget(self.methods_table)
splitter.addWidget(left)
splitter.addWidget(right)
splitter.setStretchFactor(0, 1)
splitter.setStretchFactor(1, 2)
h.addWidget(splitter)
def update_result(self, r: PipelineResult):
self._domain = r.domain
self.list_widget.clear()
for e in r.domain.entities:
it = QListWidgetItem(f"◆ {e.name} ({e.kind.value})")
it.setData(Qt.ItemDataRole.UserRole, e.name)
self.list_widget.addItem(it)
for s in r.domain.systems:
it = QListWidgetItem(f"★ {s.name} (System)")
it.setData(Qt.ItemDataRole.UserRole, s.name)
self.list_widget.addItem(it)
if self.list_widget.count() > 0:
self.list_widget.setCurrentRow(0)
def _on_select(self, current, _):
if current is None or self._domain is None: return
name = current.data(Qt.ItemDataRole.UserRole)
e = self._domain.find_entity(name)
if e is None: return
self.detail_title.setText(f"{e.name} · {e.kind.value}")
self.desc_label.setText(e.description or "(无描述)")
self.fields_table.setRowCount(len(e.fields))
for i, f in enumerate(e.fields):
typestr = f.field_type.value
if f.field_type.value == "List" and f.generic_type:
typestr = f"List<{f.generic_type}>"
elif f.field_type.value in ("GameObject",) and f.generic_type:
typestr = f.generic_type
self.fields_table.setItem(i, 0, QTableWidgetItem(f.name))
self.fields_table.setItem(i, 1, QTableWidgetItem(typestr))
self.fields_table.setItem(i, 2, QTableWidgetItem(f.default_value))
self.fields_table.setItem(i, 3, QTableWidgetItem(f.tooltip))
self.methods_table.setRowCount(len(e.methods))
for i, m in enumerate(e.methods):
sig = f"{m.name}(" + ", ".join(f"{p[0]} {p[1]}" for p in m.params) + ")"
self.methods_table.setItem(i, 0, QTableWidgetItem(sig))
self.methods_table.setItem(i, 1, QTableWidgetItem(m.return_type))
self.methods_table.setItem(i, 2, QTableWidgetItem(m.body_hint))
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 视图:System × Feature 矩阵
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class MatrixView(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
v = QVBoxLayout(self); v.setContentsMargins(12, 12, 12, 12); v.setSpacing(8)
v.addWidget(SectionTitle("System × Feature 矩阵(🟢主 · 🔵辅助 · ·无)"))
self.table = QTableWidget()
self.table.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeMode.Interactive)
self.table.verticalHeader().setDefaultSectionSize(44)
v.addWidget(self.table, 1)
self.hint = QLabel("")
self.hint.setObjectName("Hint")
self.hint.setWordWrap(True)
v.addWidget(self.hint)
self.table.itemClicked.connect(self._on_cell_clicked)
self._matrix = None
def update_result(self, r: PipelineResult):
m = r.behavior.matrix
self._matrix = m
self.table.clear()
self.table.setColumnCount(len(m.features))
self.table.setRowCount(len(m.systems))
self.table.setHorizontalHeaderLabels(m.features)
self.table.setVerticalHeaderLabels(m.systems)
for i, sys_name in enumerate(m.systems):
for j, feat in enumerate(m.features):
cell = m.get_cell(sys_name, feat)
if cell.is_primary:
text = "🟢 主"
elif cell.ops:
text = "🔵 " + "".join(op.value for op in cell.ops)
else:
text = "·"
it = QTableWidgetItem(text)
it.setTextAlignment(Qt.AlignmentFlag.AlignCenter)
it.setData(Qt.ItemDataRole.UserRole, (sys_name, feat, cell.note))
if cell.is_primary:
it.setForeground(QColor(Colors.SUCCESS))
elif cell.ops:
it.setForeground(QColor(Colors.ACCENT))
self.table.setItem(i, j, it)
for j in range(len(m.features)):
self.table.setColumnWidth(j, 100)
self.hint.setText("点击任意单元格查看该 System 对该 Feature 的职责说明")
def _on_cell_clicked(self, item):
data = item.data(Qt.ItemDataRole.UserRole)
if data is None: return
sys_name, feat, note = data
cell = self._matrix.get_cell(sys_name, feat) if self._matrix else None
ops = ", ".join(op.value for op in cell.ops) if cell and cell.ops else "无"
self.hint.setText(
f"<b>{sys_name}</b> × <b>{feat}</b>:ops=[{ops}] { '🟢主职责' if cell and cell.is_primary else '辅助' }"
f" · {note or '(无备注)'}"
)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 视图:状态机图
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class StateMachineView(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
v = QVBoxLayout(self); v.setContentsMargins(12, 12, 12, 12); v.setSpacing(8)
header = QHBoxLayout()
header.addWidget(SectionTitle("状态机"))
self.selector = QComboBox(); self.selector.setMinimumWidth(220)
self.selector.currentIndexChanged.connect(self._on_select)
header.addWidget(self.selector); header.addStretch()
v.addLayout(header)
self.canvas = DiagramCanvas()
v.addWidget(self.canvas, 1)
self.mermaid_view = CodeView()
self.mermaid_view.setMaximumHeight(140)
v.addWidget(self.mermaid_view)
self._fsms = []
def update_result(self, r: PipelineResult):
self._fsms = r.behavior.state_machines
self.selector.blockSignals(True)
self.selector.clear()
for fsm in self._fsms:
self.selector.addItem(f"{fsm.name} (owner: {fsm.owner})")
self.selector.blockSignals(False)
if self._fsms:
self.selector.setCurrentIndex(0)
self._on_select(0)
def _on_select(self, idx: int):
if idx < 0 or idx >= len(self._fsms): return
fsm = self._fsms[idx]
self.canvas.clear_diagram()
for s in fsm.states:
color = Colors.SUCCESS if s.is_initial else (Colors.DANGER if s.is_final else Colors.STATE)
subtitle = "(initial)" if s.is_initial else ("(final)" if s.is_final else "")
self.canvas.add_node(s.name, s.name, subtitle=subtitle, color=color)
for t in fsm.transitions:
self.canvas.add_edge(t.source, t.target, t.trigger, color=Colors.ACCENT)
self.canvas.auto_layout_circular(radius=max(180, 40 * len(fsm.states)))
self.mermaid_view.load(MermaidRenderer.render_state_machine(fsm))
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 视图:事件总线图
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class EventBusView(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
v = QVBoxLayout(self); v.setContentsMargins(12, 12, 12, 12); v.setSpacing(8)
v.addWidget(SectionTitle("事件总线图(publisher → subscribers)"))
splitter = QSplitter(Qt.Orientation.Horizontal)
self.canvas = DiagramCanvas()
splitter.addWidget(self.canvas)
right = QWidget(); rv = QVBoxLayout(right); rv.setContentsMargins(12, 0, 0, 0)
rv.addWidget(SectionTitle("事件清单"))
self.event_table = QTableWidget(0, 4)
self.event_table.setHorizontalHeaderLabels(["事件名", "参数", "发布者", "订阅者"])
self.event_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
self.event_table.verticalHeader().setVisible(False)
rv.addWidget(self.event_table)
splitter.addWidget(right)
splitter.setStretchFactor(0, 3); splitter.setStretchFactor(1, 2)
v.addWidget(splitter, 1)
def update_result(self, r: PipelineResult):
evs = r.behavior.events
self.canvas.clear_diagram()
# 收集所有角色
actors = {}
for ev in evs:
if ev.publisher: actors.setdefault(ev.publisher, "pub")
for sub in ev.subscribers: actors.setdefault(sub, "sub")
# 分层布局:发布者在上,订阅者在下
pubs = [a for a, role in actors.items() if role == "pub"]
subs = [a for a, role in actors.items() if a not in pubs]
for name in pubs:
self.canvas.add_node(name, name, subtitle="publisher", color=Colors.SYSTEM)
for name in subs:
self.canvas.add_node(name, name, subtitle="subscriber", color=Colors.EVENT)
for ev in evs:
if not ev.publisher: continue
for sub in ev.subscribers:
self.canvas.add_edge(ev.publisher, sub,
ev.name.removeprefix("On"), color=Colors.ACCENT)
self.canvas.auto_layout_layered([pubs, subs], dx=180, dy=200)
self.event_table.setRowCount(len(evs))
for i, ev in enumerate(evs):
params = ", ".join(f"{p[0]} {p[1]}" for p in ev.params) or "---"
subs_str = ", ".join(ev.subscribers) or "---"
self.event_table.setItem(i, 0, QTableWidgetItem(ev.name))
self.event_table.setItem(i, 1, QTableWidgetItem(params))
self.event_table.setItem(i, 2, QTableWidgetItem(ev.publisher))
self.event_table.setItem(i, 3, QTableWidgetItem(subs_str))
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 视图:Prefab 树
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class PrefabView(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
v = QVBoxLayout(self); v.setContentsMargins(12, 12, 12, 12); v.setSpacing(8)
v.addWidget(SectionTitle("Prefab 组件组合(★ 为 G2C 自动生成的脚本)"))
self.tree = QTreeWidget()
self.tree.setHeaderLabels(["结构", "类型", "配置提示"])
self.tree.setColumnWidth(0, 280)
self.tree.setColumnWidth(1, 180)
v.addWidget(self.tree)
def update_result(self, r: PipelineResult):
self.tree.clear()
for pf in r.ecd.prefabs:
root = QTreeWidgetItem([f"📦 {pf.name}", f"Tag: {pf.tag}", ""])
root.setExpanded(True)
for c in pf.components:
mark = "★ " if c.is_custom else " "
child = QTreeWidgetItem([f"{mark}{c.component_type}", "Component", c.config_hint])
root.addChild(child)
for ch in pf.children:
sub = QTreeWidgetItem([f"📁 {ch.name}", "Child GameObject", ""])
for c in ch.components:
mark = "★ " if c.is_custom else " "
sub.addChild(QTreeWidgetItem([f"{mark}{c.component_type}", "Component", c.config_hint]))
root.addChild(sub)
self.tree.addTopLevelItem(root)
self.tree.expandAll()
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 视图:生成代码浏览
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class CodeBrowserView(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
h = QHBoxLayout(self); h.setContentsMargins(0, 0, 0, 0); h.setSpacing(0)
splitter = QSplitter(Qt.Orientation.Horizontal)
left = QWidget(); lv = QVBoxLayout(left); lv.setContentsMargins(12, 12, 12, 12)
lv.addWidget(SectionTitle("生成文件"))
self.file_tree = QTreeWidget(); self.file_tree.setHeaderLabels(["路径", "类型", "大小"])
self.file_tree.setColumnWidth(0, 320)
self.file_tree.itemClicked.connect(self._on_file_clicked)
lv.addWidget(self.file_tree)
splitter.addWidget(left)
right = QWidget(); rv = QVBoxLayout(right); rv.setContentsMargins(12, 12, 12, 12)
self.file_label = SectionTitle("选择文件预览")
rv.addWidget(self.file_label)
self.code_view = CodeView()
rv.addWidget(self.code_view)
splitter.addWidget(right)
splitter.setStretchFactor(0, 1); splitter.setStretchFactor(1, 3)
h.addWidget(splitter)
self._files: dict[str, str] = {}
def update_result(self, r: PipelineResult):
self._files = dict(r.project.scripts)
self.file_tree.clear()
# 按目录分组
tree: dict[str, list[tuple[str, str]]] = {}
for path, content in r.project.scripts:
parts = path.split("/")
dirname = "/".join(parts[:-1]) or "(root)"
tree.setdefault(dirname, []).append((path, content))
for dirname in sorted(tree.keys()):
dir_item = QTreeWidgetItem([f"📁 {dirname}", "", ""])
for path, content in sorted(tree[dirname]):
fname = path.split("/")[-1]
ext = fname.split(".")[-1].lower()
icon = {"cs": "🔷", "uxml": "🎨", "uss": "🎨",
"asmdef": "📦", "md": "📄"}.get(ext, "📄")
child = QTreeWidgetItem([f"{icon} {fname}", ext.upper(),
f"{len(content)} B"])
child.setData(0, Qt.ItemDataRole.UserRole, path)
dir_item.addChild(child)
dir_item.setExpanded(True)
self.file_tree.addTopLevelItem(dir_item)
# 默认选中第一个 .cs
first = next((p for p in self._files if p.endswith(".cs")), None)
if first:
self._show_file(first)
def _on_file_clicked(self, item, col):
path = item.data(0, Qt.ItemDataRole.UserRole)
if path: self._show_file(path)
def _show_file(self, path: str):
self.file_label.setText(path)
self.code_view.load(self._files.get(path, ""))
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 视图:GDD 文档
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class DocView(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
v = QVBoxLayout(self); v.setContentsMargins(12, 12, 12, 12); v.setSpacing(8)
header = QHBoxLayout()
header.addWidget(SectionTitle("文档预览"))
self.doc_selector = QComboBox()
self.doc_selector.addItems(["GDD.md --- 游戏设计文档", "DevPlan.md --- 开发计划"])
self.doc_selector.currentIndexChanged.connect(self._on_select)
header.addWidget(self.doc_selector); header.addStretch()
v.addLayout(header)
self.view = MarkdownView()
v.addWidget(self.view, 1)
self._result = None
def update_result(self, r: PipelineResult):
self._result = r
self._on_select(self.doc_selector.currentIndex())
def _on_select(self, idx: int):
if self._result is None: return
text = self._result.gdd_markdown if idx == 0 else self._result.dev_plan_markdown
self.view.load_markdown(text)
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 视图:导出
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class ExportView(QWidget):
def __init__(self, parent=None):
super().__init__(parent)
self._result = None
v = QVBoxLayout(self); v.setContentsMargins(20, 20, 20, 20); v.setSpacing(16)
v.addWidget(SectionTitle("导出 Unity 6 工程"))
hint = QLabel(
"G2C 生成的是 <b>Unity Assets 目录片段</b>。推荐流程:<br>"
"1. 在 Unity Hub 新建一个空的 Unity 6 项目(2D URP 模板)<br>"
"2. 点击下方 <b>「导出到文件夹」</b>,选择该项目的根目录<br>"
"3. 回到 Unity,让它自动重新导入<br>"
"4. 按 README.md 中的步骤创建数据资产与场景"
)
hint.setWordWrap(True); hint.setStyleSheet(f"color:{Colors.TEXT};padding:8px;")
v.addWidget(hint)
btn_row = QHBoxLayout(); btn_row.setSpacing(12)
self.btn_export_folder = QPushButton("📁 导出到文件夹")
self.btn_export_folder.setObjectName("PrimaryBtn")
self.btn_export_folder.setMinimumHeight(40)
self.btn_export_folder.clicked.connect(self._export_folder)
self.btn_export_zip = QPushButton("🗜 导出为 .zip")
self.btn_export_zip.setMinimumHeight(40)
self.btn_export_zip.clicked.connect(self._export_zip)
self.btn_export_docs = QPushButton("📝 只导出文档")
self.btn_export_docs.setMinimumHeight(40)
self.btn_export_docs.clicked.connect(self._export_docs)
btn_row.addWidget(self.btn_export_folder)
btn_row.addWidget(self.btn_export_zip)
btn_row.addWidget(self.btn_export_docs)
btn_row.addStretch()
v.addLayout(btn_row)
v.addWidget(SectionTitle("导出清单预览"))
self.file_list = QListWidget()
v.addWidget(self.file_list, 1)
self.status_label = QLabel(""); self.status_label.setWordWrap(True)
self.status_label.setStyleSheet(f"color:{Colors.SUCCESS};padding:8px;")
v.addWidget(self.status_label)
def update_result(self, r: PipelineResult):
self._result = r
self.file_list.clear()
for path, content in r.project.scripts:
self.file_list.addItem(f"{path} ({len(content)} B)")
def _export_folder(self):
if self._result is None: return
dir_ = QFileDialog.getExistingDirectory(self, "选择 Unity 项目根目录(包含 Assets/ 的目录)")
if not dir_: return
try:
builder = UnityProjectBuilder()
builder.write_to_disk(self._result.project, Path(dir_))
# 写文档
(Path(dir_) / "GDD.md").write_text(self._result.gdd_markdown, encoding="utf-8")
(Path(dir_) / "DevPlan.md").write_text(self._result.dev_plan_markdown, encoding="utf-8")
self.status_label.setText(
f"✓ 已导出 {len(self._result.project.scripts)} 个文件到 {dir_}"
)
QMessageBox.information(self, "G2C", f"导出成功!\n路径:{dir_}")
except Exception as e:
QMessageBox.critical(self, "G2C", f"导出失败:{e}")
def _export_zip(self):
if self._result is None: return
path, _ = QFileDialog.getSaveFileName(self, "保存 .zip", f"{self._result.project.project_name}.zip", "Zip (*.zip)")
if not path: return
try:
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as zf:
for rel_path, content in self._result.project.scripts:
zf.writestr(rel_path, content)
zf.writestr("GDD.md", self._result.gdd_markdown)
zf.writestr("DevPlan.md", self._result.dev_plan_markdown)
self.status_label.setText(f"✓ 已打包到 {path}")
QMessageBox.information(self, "G2C", f"已生成 zip:\n{path}")
except Exception as e:
QMessageBox.critical(self, "G2C", f"打包失败:{e}")
def _export_docs(self):
if self._result is None: return
dir_ = QFileDialog.getExistingDirectory(self, "选择保存文档的目录")
if not dir_: return
try:
(Path(dir_) / "GDD.md").write_text(self._result.gdd_markdown, encoding="utf-8")
(Path(dir_) / "DevPlan.md").write_text(self._result.dev_plan_markdown, encoding="utf-8")
self.status_label.setText(f"✓ 文档已导出到 {dir_}")
except Exception as e:
QMessageBox.critical(self, "G2C", f"导出失败:{e}")
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
# 主窗口
# ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("G2C · Game Design to Code · Unity 6")
self.resize(1600, 960)
self.setStyleSheet(APP_STYLESHEET)
self._build_ui()
self._worker: PipelineWorker | None = None
self._result: PipelineResult | None = None
def _build_ui(self):
central = QWidget(); self.setCentralWidget(central)
root = QVBoxLayout(central); root.setContentsMargins(0, 0, 0, 0); root.setSpacing(0)
self.top_bar = TopBar()
root.addWidget(self.top_bar)
splitter = QSplitter(Qt.Orientation.Horizontal)
splitter.setHandleWidth(1)
self.input_panel = InputPanel()
self.input_panel.run_requested.connect(self._on_run)
self.input_panel.setMinimumWidth(340)
self.input_panel.setMaximumWidth(460)
splitter.addWidget(self.input_panel)
# 右侧 Tab 容器
self.tabs = QTabWidget()
self.overview_view = OverviewView()
self.entity_view = EntityView()
self.matrix_view = MatrixView()
self.fsm_view = StateMachineView()
self.event_view = EventBusView()
self.prefab_view = PrefabView()
self.code_view = CodeBrowserView()
self.doc_view = DocView()
self.export_view = ExportView()
self.tabs.addTab(self.overview_view, "📊 Overview")
self.tabs.addTab(self.entity_view, "◆ Entities")
self.tabs.addTab(self.matrix_view, "▦ Matrix")
self.tabs.addTab(self.fsm_view, "⚙ State Machines")
self.tabs.addTab(self.event_view, "⚡ Event Bus")
self.tabs.addTab(self.prefab_view, "📦 Prefabs")
self.tabs.addTab(self.code_view, "📝 Code")
self.tabs.addTab(self.doc_view, "📖 Docs")
self.tabs.addTab(self.export_view, "🚀 Export")
self.tabs.setEnabled(False)
splitter.addWidget(self.tabs)
splitter.setStretchFactor(0, 1)
splitter.setStretchFactor(1, 3)
splitter.setSizes([380, 1220])
root.addWidget(splitter, 1)
# 状态栏
self.status = QStatusBar()
self.status.showMessage("就绪 · 输入游戏需求,点击「运行管道」开始")
self.setStatusBar(self.status)
def _on_run(self, prompt: str, project_name: str):
if self._worker and self._worker.isRunning():
return
self.tabs.setEnabled(False)
self.input_panel.set_running(True)
self.status.showMessage("运行中...")
self._worker = PipelineWorker(prompt, project_name)
self._worker.progress.connect(self._on_progress)
self._worker.finished_ok.connect(self._on_done)
self._worker.failed.connect(self._on_failed)
self._worker.start()
def _on_progress(self, pct: int, msg: str):
self.input_panel.set_progress(pct, msg)
self.status.showMessage(msg)
def _on_done(self, result: PipelineResult):
self._result = result
self.overview_view.update_result(result)
self.entity_view.update_result(result)
self.matrix_view.update_result(result)
self.fsm_view.update_result(result)
self.event_view.update_result(result)
self.prefab_view.update_result(result)
self.code_view.update_result(result)
self.doc_view.update_result(result)
self.export_view.update_result(result)
self.top_bar.badge_genre.setText(result.gdd.vision.genre.replace("_", " ").title())
self.top_bar.badge_unity.setText(f"Unity 6 · {result.gdd.vision.perspective}")
self.tabs.setEnabled(True)
self.tabs.setCurrentIndex(0)
self.input_panel.set_running(False)
self.status.showMessage(
f"✓ 完成 · {result.total_scripts} 脚本 · {result.total_so} SO · "
f"{result.total_events} 事件 · {result.total_states} 状态"
)
def _on_failed(self, err: str):
self.input_panel.set_running(False)
self.tabs.setEnabled(True)
self.status.showMessage("✗ 失败")
QMessageBox.critical(self, "G2C · 错误", err)
PYEOF
主入口:
python
cat > /home/user/g2c/main.py << 'PYEOF'
"""
G2C 主入口
==========
运行:
python main.py
"""
import sys
from PyQt6.QtWidgets import QApplication
from g2c.ui.main_window import MainWindow
def main():
app = QApplication(sys.argv)
app.setApplicationName("G2C")
app.setOrganizationName("G2C")
win = MainWindow()
win.show()
sys.exit(app.exec())
if __name__ == "__main__":
main()
PYEOF
bash
跑一次完整验证,先离线测试管道(不起 UI):
<tool_call>
<tool_name>run_command</tool_name>
python3 -c "
import sys
sys.path.insert(0, '/home/user/g2c')
from g2c.pipeline import Pipeline
from g2c.generator import UnityProjectBuilder
from g2c.docgen import render_gdd, render_dev_plan
p = Pipeline()
gdd, domain, behavior, ecd = p.run('做一个2D塔防游戏:玩家放置防御塔抵御一波波敌人')
print(f'vision.genre={gdd.vision.genre} perspective={gdd.vision.perspective}')
print(f'L1 use_cases={len(gdd.use_cases)}')
print(f'L2 entities={len(domain.entities)} systems={len(domain.systems)} relations={len(domain.relations)}')
print(f'L3 matrix cells={len(behavior.matrix.cells)} fsm={len(behavior.state_machines)} events={len(behavior.events)}')
print(f'L4 prefabs={len(ecd.prefabs)} data_assets={len(ecd.data_assets)}')
b = UnityProjectBuilder('TowerDefense')
proj = b.build(gdd, domain, behavior, ecd, 'TowerDefenseDemo')
print(f'L6 files={len(proj.scripts)}')
for path, content in proj.scripts[:5]:
print(f' {path} [{len(content)} B]')
"
/home/user/g2c
</tool_call>
<tool_call>
<tool_name>run_command</tool_name>
pwd && ls -la
/home/user/g2c
</tool_call>
<tool_call>
<tool_name>run_command</tool_name>
ls /home/user/
/home/user
</tool_call>让我实际执行所有命令来验证。先检查工作区:
<tool_call>
<tool_name>run_command</tool_name>
ls -la
/home/user
</tool_call>