关于 unity-procedural-gen
面向 Unity 游戏开发者的程序化生成设计模式技能,涵盖噪声、网格、地牢、随机种子、内容约束及编辑器与运行时生成。重点是让生成参数可由设计师调整,并支持可复现结果和预览。
主要功能
- 01噪声与地形生成配置
- 02网格与放置规则
- 03BSP 地牢生成与连通性验证
- 04基于种子的可复现随机
- 05内容预算与约束校验
- 06编辑器烘焙及运行时生成
适用场景
- 01为不同生物群系调整地形噪声
- 02生成具有有效路径的地牢布局
- 03复现测试或玩家分享的关卡种子
- 04按难度配置敌人与道具数量
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ nice-wolf-studio/ unity-claude-skills --skill unity-procedural-gen安装后在 Claude Code 中输入/unity-procedural-gen确认已加载。
附带文件references/procgen-scaffolds.md
详细介绍
程序化生成——设计转换模式
前置技能:
unity-3d-math(噪声数学、空间运算)、unity-data-driven(用于生成参数的 ScriptableObject 配置)、unity-editor-tools(预览工具、自定义检查器)
这些模式针对程序化生成中最常见的问题:Claude 生成的程序化内容使用硬编码算法,且没有设计师控制。结果虽然能运行一次,却无法调整、预览或约束。设计师需要可调参数、约束和预览工具,而不只是算法代码。每个生成系统都应将参数作为 ScriptableObject 配置公开,支持基于种子的可复现生成,并提供编辑器预览。
模式 1:基于噪声的生成
设计意图:设计师希望地形(“起伏的丘陵,偶尔出现山峰”)或摆放效果(“茂密的森林,间有空地”)呈现自然感。他们需要在不修改代码的情况下调整随机性的特征。
错误示例:
// Hardcoded single octave, no designer control, no reproducibility
float height = Mathf.PerlinNoise(x * 0.1f, y * 0.1f) * 10f;
terrain.SetHeight(x, y, height);
正确做法:使用带有频率、振幅、倍频程、持久度、间隙和偏移量的 NoiseConfig ScriptableObject。NoiseGenerator 静态工具类实现分形布朗运动(fBm)采样。支持域扭曲以获得自然的效果。所有参数都可由设计师编辑。
脚手架:
/// <summary>
/// Configuration for noise-based generation. Each biome or terrain type gets its own asset.
/// </summary>
[CreateAssetMenu(fileName = "New Noise Config", menuName = "ProcGen/Noise Config")]
public class NoiseConfig : ScriptableObject
{
[Header("Base Noise")]
[Range(0.001f, 1f)] public float frequency = 0.05f;
[Range(0f, 100f)] public float amplitude = 10f;
[Header("Fractal")]
[Range(1, 8)] public int octaves = 4;
[Range(0f, 1f)] public float persistence = 0.5f;
[Range(1f, 4f)] public float lacunarity = 2f;
[Header("Transform")]
public Vector2 offset;
[Range(0f, 1f)] public float domainWarpStrength = 0f;
}
/// <summary>
/// Stateless noise sampling utilities. Uses fBm for natural-looking results.
/// </summary>
public static class NoiseGenerator
{
/// <summary>
/// Samples 2D fractal Brownian motion noise at the given coordinates.
/// </summary>
/// <returns>Noise value scaled by config amplitude.</returns>
public static float Sample2D(float x, float y, NoiseConfig config)
{
float sum = 0f;
float freq = config.frequency;
float amp = 1f;
float maxAmp = 0f;
for (int i = 0; i < config.octaves; i++)
{
float sampleX = (x + config.offset.x) * freq;
float sampleY = (y + config.offset.y) * freq;
sum += Mathf.PerlinNoise(sampleX, sampleY) * amp;
maxAmp += amp;
amp *= config.persistence;
freq *= config.lacunarity;
}
return (sum / maxAmp) * config.amplitude;
}
}
设计扩展点:设计师可在检查器中通过 [Range] 属性调整噪声。不同生物群系使用不同的 NoiseConfig ScriptableObject。通过 [ExecuteAlways] 和 OnValidate 重新生成预览。有关构建噪声预览 EditorWindow 的说明,请参阅 unity-editor-tools。
注意事项:Mathf.PerlinNoise 返回 0-1 范围内的值,而不是大多数噪声库(libnoise、FastNoiseLite)使用的 -1 到 1 范围。为确保行为一致,请在 NoiseConfig 中归一化或注明预期范围。如果需要 -1 到 1 范围,请使用 Mathf.PerlinNoise(x, y) * 2f - 1f。
模式 2:瓦片与网格系统
设计意图:基于网格进行摆放(城市建造游戏、农场模拟游戏、战术 RPG),并遵循相邻关系和有效摆放规则。设计师通过数据而非代码定义瓦片类型和摆放规则。
错误示例:
// Raw 2D array, adjacency rules buried in placement code, no validation
int[,] grid = new int[width, height];
grid[x, y] = tileId;
// Scattered checks like: if (grid[x-1, y] == WATER) canPlace = false;
正确做法:使用带有坐标辅助方法(邻居、边界、迭代)的 Grid<T> 泛型容器。每种瓦片类型对应一个 TileDefinition ScriptableObject。使用 IPlacementRule 接口验证相邻关系和摆放条件。GridPlacer 在摆放前根据所有规则进行验证。
脚手架:
/// <summary>
/// Generic 2D grid container with coordinate utilities.
/// </summary>
[System.Serializable]
public class Grid<T>
{
[SerializeField] private int width;
[SerializeField] private int height;
[SerializeField] private float cellSize = 1f;
private T[] cells;
public int Width => width;
public int Height => height;
public float CellSize => cellSize;
public Grid(int width, int height, float cellSize = 1f)
{
this.width = width;
this.height = height;
this.cellSize = cellSize;
cells = new T[width * height];
}
/// <summary>Returns true if coordinates are within grid bounds.</summary>
public bool InBounds(Vector2Int coord)
=> coord.x >= 0 && coord.x < width && coord.y >= 0 && coord.y < height;
public T this[Vector2Int coord]
{
get => cells[coord.y * width + coord.x];
set => cells[coord.y * width + coord.x] = value;
}
/// <summary>Converts grid coordinates to world position (center of cell).</summary>
public Vector3 GridToWorld(Vector2Int coord)
=> new Vector3((coord.x + 0.5f) * cellSize, 0f, (coord.y + 0.5f) * cellSize);
/// <summary>Converts world position to grid coordinates.</summary>
public Vector2Int WorldToGrid(Vector3 worldPos)
=> new Vector2Int(Mathf.FloorToInt(worldPos.x / cellSize),
Mathf.FloorToInt(worldPos.z / cellSize));
}
/// <summary>Defines a tile type and its placement constraints.</summary>
[CreateAssetMenu(fileName = "New Tile", menuName = "ProcGen/Tile Definition")]
public class TileDefinition : ScriptableObject
{
public string tileName;
public GameObject prefab;
public TileDefinition[] allowedNeighbors;
}
/// <summary>Validates whether a tile can be placed at a grid position.</summary>
public interface IPlacementRule
{
bool CanPlace(Vector2Int coord, TileDefinition tile, Grid<TileDefinition> grid);
}
设计扩展点:通过创建带有允许邻居列表的 TileDefinition ScriptableObject 来添加新瓦片。使用 Gizmos 可视化网格状态,以便调试。规则可组合——添加新的 IPlacementRule 实现即可,无需修改摆放逻辑。
注意事项:网格坐标与世界坐标之间的转换务必显式处理。提供带可配置单元格大小的 GridToWorld(Vector2Int) 和 WorldToGrid(Vector3) 辅助方法。网格边缘的差一错误是最常见的 bug;每次访问前都要使用 InBounds()。
模式 3:地牢/房间生成
设计意图:Roguelike 游戏需要随机但有效的地牢布局,其中包含房间、走廊,以及一条从起点到出口的必经路径。设计师可以控制房间大小、密度和特殊房间的位置。
错误做法:
// Random placement with no overlap check, no connectivity guarantee
for (int i = 0; i < roomCount; i++)
{
var room = new Rect(Random.Range(0, mapWidth), Random.Range(0, mapHeight),
Random.Range(5, 15), Random.Range(5, 15));
rooms.Add(room); // May overlap, may be disconnected
}
正确做法:使用 BSP(二叉空间分割)来放置房间——递归地将空间划分为多个区域,在叶节点内放置房间,并用走廊连接兄弟叶节点。生成后进行验证:通过泛洪填充检查连通性、确保房间数量符合最小值和最大值要求,并确定从起点到出口的关键路径。
脚手架:
/// <summary>
/// Configuration for dungeon generation. Designers tune all parameters in Inspector.
/// </summary>
[CreateAssetMenu(fileName = "New Dungeon Config", menuName = "ProcGen/Dungeon Config")]
public class DungeonConfig : ScriptableObject
{
[Header("Dimensions")]
[Min(20)] public int mapWidth = 64;
[Min(20)] public int mapHeight = 64;
[Header("Rooms")]
[Range(4, 20)] public int minRoomSize = 6;
[Range(6, 30)] public int maxRoomSize = 14;
[Range(3, 20)] public int roomCountMin = 5;
[Range(5, 30)] public int roomCountMax = 12;
[Header("BSP")]
[Range(2, 8)] public int splitDepth = 4;
[Range(0.3f, 0.7f)] public float minSplitRatio = 0.4f;
[Header("Corridors")]
[Range(1, 5)] public int corridorWidth = 2;
[Header("Special Rooms")]
public GameObject bossRoomPrefab;
public GameObject treasureRoomPrefab;
}
/// <summary>Represents a room in the dungeon layout.</summary>
public class Room
{
public RectInt Bounds { get; }
public Vector2Int Center => new(Bounds.x + Bounds.width / 2,
Bounds.y + Bounds.height / 2);
public bool IsSpecial { get; set; }
public Room(RectInt bounds) => Bounds = bounds;
}
/// <summary>Represents a corridor connecting two rooms.</summary>
public class Corridor
{
public Vector2Int Start { get; }
public Vector2Int End { get; }
public int Width { get; }
public Corridor(Vector2Int start, Vector2Int end, int width)
{
Start = start;
End = end;
Width = width;
}
}
/// <summary>
/// Validates dungeon connectivity using flood fill.
/// </summary>
public static class ConnectivityValidator
{
/// <summary>
/// Returns true if all rooms are reachable from the first room.
/// </summary>
public static bool IsFullyConnected(Grid<bool> walkableGrid, List<Room> rooms)
{
if (rooms.Count == 0) return true;
var visited = new HashSet<Vector2Int>();
var queue = new Queue<Vector2Int>();
queue.Enqueue(rooms[0].Center);
visited.Add(rooms[0].Center);
while (queue.Count > 0)
{
var current = queue.Dequeue();
foreach (var neighbor in GetCardinalNeighbors(current))
{
if (walkableGrid.InBounds(neighbor)
&& walkableGrid[neighbor]
&& visited.Add(neighbor))
{
queue.Enqueue(neighbor);
}
}
}
return rooms.All(r => visited.Contains(r.Center));
}
private static IEnumerable<Vector2Int> GetCardinalNeighbors(Vector2Int pos)
{
yield return pos + Vector2Int.up;
yield return pos + Vector2Int.down;
yield return pos + Vector2Int.left;
yield return pos + Vector2Int.right;
}
}
设计扩展点:DungeonConfig ScriptableObject 让设计师可以调整所有生成参数。特殊房间(Boss 房间、宝藏房间)可以使用预先制作的房间模板 Prefab,并在配置中指定。设计师无需修改代码即可添加新的特殊房间类型。
注意事项:如果不限制分割比例,BSP 可能会生成非常狭窄的房间——应在分割步骤中强制要求房间的最小尺寸,而不只是分割后再检查。如果只在分割后检查,就会浪费 BSP 迭代次数,最终生成的房间可能少于预期。
模式 4:种子与可复现性
设计意图:玩家可以分享种子(“试试种子 42!”),速通玩家需要可复现的布局,QA 可以通过重新输入种子来复现 Bug。给定同一个种子时,每次运行都必须保持确定性。
错误做法:
// Global mutable state, non-reproducible, state leaks between systems
int count = UnityEngine.Random.Range(3, 8);
var pos = new Vector3(UnityEngine.Random.Range(-10f, 10f), 0,
UnityEngine.Random.Range(-10f, 10f));
正确做法:使用 SeededRandom 封装 System.Random。主种子通过哈希派生出各子系统的种子(地牢布局、战利品、敌人放置分别使用独立的随机数流)。生成代码中完全不使用 UnityEngine.Random。种子会保存在存档数据中,并显示给玩家。
脚手架:
/// <summary>
/// Deterministic random number generator wrapping System.Random.
/// Each subsystem gets its own instance derived from a master seed.
/// </summary>
public class SeededRandom
{
private readonly System.Random rng;
/// <summary>The seed used to initialize this instance.</summary>
public int Seed { get; }
public SeededRandom(int seed)
{
Seed = seed;
rng = new System.Random(seed);
}
/// <summary>Returns a random int in [min, max).</summary>
public int Range(int min, int max) => rng.Next(min, max);
/// <summary>Returns a random float in [0, 1).</summary>
public float Float01() => (float)rng.NextDouble();
/// <summary>Returns a random float in [min, max).</summary>
public float Range(float min, float max) => min + Float01() * (max - min);
/// <summary>
/// Selects an item from weighted entries. Higher weight = more likely.
/// </summary>
public T WeightedChoice<T>(IReadOnlyList<(T item, float weight)> entries)
{
float totalWeight = 0f;
foreach (var e in entries) totalWeight += e.weight;
float roll = Float01() * totalWeight;
float cumulative = 0f;
foreach (var e in entries)
{
cumulative += e.weight;
if (roll < cumulative) return e.item;
}
return entries[^1].item;
}
/// <summary>
/// Derives a child seed for a named subsystem. Deterministic for the
/// same master seed + subsystem name combination.
/// </summary>
public static int DeriveSeed(int masterSeed, string subsystem)
=> HashCode.Combine(masterSeed, subsystem.GetHashCode(StringComparison.Ordinal));
}
设计扩展点:种子可以由玩家输入(UI 中的文本字段),也可以自动生成(System.Environment.TickCount)。种子会显示在暂停菜单中,并与本次运行的数据一同保存。设计师可以在试玩测试期间锁定种子,以便进行一致的迭代。
注意事项:System.Random 不是线程安全的。如果使用 Jobs 系统或 Burst,请改用 Unity.Mathematics.Random(它是值类型,可安全地按线程使用)。切勿在线程之间共享同一个 System.Random 实例。此外,string.GetHashCode() 在不同 .NET 版本间并不确定——请使用 GetHashCode(StringComparison.Ordinal) 来稳定地派生种子。
模式 5:内容预算与约束系统
设计意图:生成的内容必须满足设计师设定的约束——每个房间至少有 3 个敌人,每层最多有 2 个宝箱,难度曲线从简单逐渐升高到困难。纯随机放置会导致游戏体验缺乏连贯性。
错误做法:
// No awareness of balance, guarantees, or budgets
for (int i = 0; i < Random.Range(1, 10); i++)
{
var prefab = enemyPrefabs[Random.Range(0, enemyPrefabs.Length)];
Instantiate(prefab, randomPosition, Quaternion.identity);
}
正确做法:ContentBudget ScriptableObject 定义每个类别的最小/最大数量、稀有度权重和必放内容。ContentPlacer 消耗预算并跟踪累计数量,优先确保达到最小数量。放置后进行验证,确保满足所有约束。
脚手架:
/// <summary>
/// A single entry in a content budget -- one prefab with placement rules.
/// </summary>
[System.Serializable]
public struct ContentEntry
{
public GameObject prefab;
public string category;
[Range(0.01f, 100f)] public float weight;
[Min(0)] public int minimum;
[Min(0)] public int maximum;
}
/// <summary>
/// Defines what content can be placed and in what quantities.
/// One per floor, biome, or difficulty tier.
/// </summary>
[CreateAssetMenu(fileName = "New Content Budget", menuName = "ProcGen/Content Budget")]
public class ContentBudget : ScriptableObject
{
[Tooltip("Total placement slots available for this budget.")]
[Min(1)] public int totalSlots = 20;
public ContentEntry[] entries;
}
/// <summary>
/// Places content according to budget constraints.
/// Guarantees minimums first, then fills with weighted random.
/// </summary>
public class ContentPlacer
{
private readonly Dictionary<string, int> placedCounts = new();
/// <summary>
/// Generates a placement list respecting budget constraints.
/// </summary>
public List<GameObject> GeneratePlacements(ContentBudget budget, SeededRandom rng)
{
var result = new List<GameObject>();
placedCounts.Clear();
// Phase 1: Place all mandatory minimums
foreach (var entry in budget.entries)
{
for (int i = 0; i < entry.minimum; i++)
{
result.Add(entry.prefab);
IncrementCount(entry.category);
}
}
// Phase 2: Fill remaining slots with weighted random
int remaining = budget.totalSlots - result.Count;
var eligible = BuildWeightedList(budget);
for (int i = 0; i < remaining && eligible.Count > 0; i++)
{
var chosen = rng.WeightedChoice(eligible);
result.Add(chosen.prefab);
IncrementCount(chosen.category);
// Rebuild eligible list (removes entries at max)
eligible = BuildWeightedList(budget);
}
return result;
}
private List<(ContentEntry item, float weight)> BuildWeightedList(ContentBudget budget)
{
var list = new List<(ContentEntry, float)>();
foreach (var entry in budget.entries)
{
int placed = placedCounts.GetValueOrDefault(entry.category);
if (entry.maximum <= 0 || placed < entry.maximum)
list.Add((entry, entry.weight));
}
return list;
}
private void IncrementCount(string category)
{
placedCounts.TryGetValue(category, out int count);
placedCounts[category] = count + 1;
}
}
设计扩展点:设计师可以为每一层或每个生物群系创建 ContentBudget ScriptableObject。使用 [Range] 和 [Min] 属性,在 Inspector 中调整权重和约束。不同难度等级使用不同预算(简单层 = 更多生命值补给,困难层 = 更多精英敌人)。
注意事项:加权随机必须先确保达到最小数量,再通过加权随机选择填满剩余预算。如果将必放内容和随机放置交错进行,预算耗尽时必放内容可能无法放入。务必验证 sum of all minimums <= totalSlots。
模式 6:运行时生成与烘焙生成
设计意图:某些内容在构建时或加载时生成一次(世界地图地形网格、静态环境);某些内容则按会话生成(地牢布局、战利品随机结果);还有一些内容需要在编辑器中预览,以便设计师迭代调整。生成算法应能在这三种场景中复用。
错误示例:
// Everything at runtime -- huge load spike, no editor preview
void Start()
{
for (int x = 0; x < 1000; x++)
for (int y = 0; y < 1000; y++)
GenerateChunk(x, y); // Freezes for seconds
}
// Or everything baked -- no dynamic content
// Designer manually places 500 trees in the editor
正确做法:使用 IGenerator<TOutput> 接口及其 Generate(GenerationContext) 方法。GenerationContext 用于指示当前是运行时还是编辑器环境。编辑器生成器通过 AssetDatabase 生成资源;运行时生成器则生成轻量数据,供异步实例化使用。算法相同,输出目标不同。
脚手架:
/// <summary>Indicates whether generation runs in Editor (bake) or Runtime (dynamic).</summary>
public enum GenerationMode { Editor, Runtime }
/// <summary>Context passed to generators with mode and seed information.</summary>
public struct GenerationContext
{
public GenerationMode Mode;
public SeededRandom Random;
public CancellationToken CancellationToken;
}
/// <summary>
/// Interface for all procedural generators. Implement once, run in Editor or Runtime.
/// </summary>
public interface IGenerator<TOutput>
{
/// <summary>Generates output data from the given context.</summary>
TOutput Generate(GenerationContext context);
}
// --- Runtime runner: async, non-blocking ---
public class RuntimeGeneratorRunner : MonoBehaviour
{
/// <summary>
/// Runs a generator asynchronously, yielding periodically to avoid frame spikes.
/// </summary>
public async Awaitable<TOutput> RunAsync<TOutput>(
IGenerator<TOutput> generator, int seed)
{
var context = new GenerationContext
{
Mode = GenerationMode.Runtime,
Random = new SeededRandom(seed),
CancellationToken = destroyCancellationToken
};
// Run generation off the main thread if it's pure data
return await Awaitable.FromAsyncOperation(
System.Threading.Tasks.Task.Run(
() => generator.Generate(context),
context.CancellationToken));
}
}
#if UNITY_EDITOR
// --- Editor runner: bakes output to asset ---
public static class EditorGeneratorRunner
{
[UnityEditor.MenuItem("ProcGen/Bake Selected Generator")]
private static void BakeFromMenu()
{
// Editor-only bake workflow
// Produces assets saved to disk via AssetDatabase
}
}
#endif
设计要点:同一生成器实现既可在编辑器中运行(通过 MenuItem 烘焙为资源),也可在运行时运行(使用异步 Awaitable)。设计师可以在自定义 EditorWindow 中或通过 [ExecuteAlways] 组件预览结果。有关构建生成预览窗口的信息,请参阅 unity-editor-tools。
注意事项:编辑器专用代码(AssetDatabase、EditorWindow、MenuItem)必须置于 #if UNITY_EDITOR 守卫内,否则运行时构建会因编译错误而失败。生成器逻辑应放在运行时可访问的代码中;只有烘焙工具代码需要置于守卫内。
生成策略对比
| 策略 | 最适合的场景 | 优势 | 劣势 |
|---|---|---|---|
| Perlin/Simplex 噪声(fBm) | 地形、高度图、生物群系分布、有机式布局 | 连续、可平铺、参数直观 | 不适用于离散结构(房间、走廊) |
| BSP(二叉空间分割) | 地牢房间、建筑平面图、空间划分 | 保证不会重叠,能自然地连接走廊 | 房间容易呈矩形,有机感较弱 |
| 波函数坍缩 | 基于瓦片的关卡、街区、模式驱动生成 | 自动遵循相邻约束,设计师可方便地提供示例 | 大型网格处理速度慢,可能无法求解(需要回溯) |
| L 系统 | 植物、分支结构、道路网络、河流 | 规则表示精简,分支自然 | 难以限制在特定边界内,规则编写不直观 |
| 泊松圆盘采样 | 对象布局(树木、岩石、NPC) | 均匀分布且保持最小间距,外观自然 | 密度固定,不适用于聚集式布局 |
| Voronoi / Delaunay | 区域划分、生物群系边界、领地地图 | 区域形状自然,双图适用于寻路 | 需要外部库或自定义实现 |
反模式
| 反模式 | 问题 | 修复方法 |
|---|---|---|
| 硬编码噪声参数 | 无法在不修改代码的情况下调整 | 使用带有 [Range] 属性的 NoiseConfig ScriptableObject |
在生成过程中使用 UnityEngine.Random | 全局状态、无法复现、状态泄漏 | 使用封装 System.Random 的 SeededRandom |
| 不验证连通性 | 地牢中存在无法到达的房间 | 生成后进行泛洪填充验证 |
在 Start() 中生成所有内容 | 加载时出现帧卡顿或冻结 | 使用异步 Awaitable 生成,并分散到多个帧中 |
| 忽略布局约束 | 随机内容缺乏平衡 | 使用施加最小值/最大值/权重约束的 ContentBudget SO |
| 编辑器烘焙与运行时烘焙使用相同代码 | 核心逻辑中充斥 #if UNITY_EDITOR | 使用 IGenerator<T> 接口,并采用独立的运行器 |
| 混淆网格坐标与世界坐标 | 对象放置在错误位置 | 使用明确的 GridToWorld / WorldToGrid 转换辅助方法 |
在线程间共享 System.Random | 竞争条件、输出不确定 | 在 Jobs/Burst 中使用 Unity.Mathematics.Random(值类型) |
相关技能
unity-3d-math-- 噪声数学基础、空间运算、坐标变换unity-data-driven-- 用于生成参数的 ScriptableObject 配置层级unity-editor-tools-- 自定义检查器、用于预览生成器输出的 EditorWindowunity-level-design-- 将创作内容整合到生成内容中unity-performance-- 异步生成、分块加载、生成网格的 LODunity-game-loop-- 相对于游戏状态转换的生成时机unity-physics-- 对生成的放置结果进行碰撞验证(重叠检查)
其他资源
- Unity.Mathematics.noise -- 适用于 GPU 的噪声函数,用于 Burst/Jobs(
noise.cnoise、noise.snoise) - 泊松圆盘采样 -- Bridson 算法,用于均匀分布点
- 波函数坍缩 -- 基于约束的图块放置(参见 OpenUPM 上的社区软件包)
- Red Blob Games -- Amit Patel 关于网格、寻路和程序化生成的指南(redblobgames.com)
- 游戏设计中的程序化生成(Tanya X. Short、Tarn Adams)-- 侧重设计的生成系统参考资料