Unity中Genome生成树形Body及神经网络Brain控制的技术实现问询
我的场景
我正在Unity中开发一个涉及遗传算法和神经网络的大型项目,目前刚启动,需要初始实现方面的帮助。我暂未使用任何库,但未来计划尝试用ml-agents实现部分Brain功能。
我的初始方案
我的初步想法是使用Genome来生成Body。Body是由部件组成的树形结构,其“躯干”为Body的根节点。Body的每个部件由一个Chromosome表示,部件的属性(如半径、旋转)分别由Gene表示。部分属性(Gene)是Brain(神经网络)的可观测输入,部分属性(Gene)是Brain可控制的输出。
问题1:如何将层级树形结构最佳表示为Genome?
在我见过的大多数实现中,Genome都以类数组结构表示。
解答
采用带父节点标记的扁平数组结构是最优方案,既兼容遗传算法常用的一维基因组操作,又能保留树形层级信息:
- 给每个代表部件的Chromosome添加
ParentID字段,根节点(躯干)的ParentID设为特殊值(如-1)。 - 将所有Chromosome按固定遍历顺序(比如深度优先、广度优先)存入一维数组,每个Chromosome包含自身ID、ParentID、以及该部件对应的所有Gene。
- 解析时,通过遍历数组、匹配ParentID即可重建Body的树形结构。
这种方式既避免了复杂的嵌套序列化,又能让遗传算法的交叉、变异操作直接在一维数组上完成,效率更高。
问题2:以下这类C#类结构是否适用于我的需求?
假设我能解决问题1,我或许可以从字符串(或某种一维结构)解析Genome。
public class Gene { public int ID; public string Name; public string Description; public Chromosome ChromosomeReference { get; set; } public Genome GenomeReference { get; set; } public Gene(string name, string desc) { Name = name; Description = desc; } } public class Gene<T> : Gene { public T Value; public Gene(string name, string desc = "") : base(name, desc) => Value = default; public Gene(string name, T value, string desc = "") : this(name, desc) => Value = value; }
public class Chromosome { public int ID; public string Name; public string Description; private Genome _genomeRef; public Genome GenomeReference { get => _genomeRef; set { _genomeRef = value; foreach(Gene gen in GeneDict.Values) { gen.GenomeReference = value; } } } public Dictionary<string, Gene> GeneDict = new(); public Chromosome(string name, string desc = "") { Name = name; Description = desc; GeneDict = new(); } public Chromosome(string name, List<Gene> genes, string desc = "") : this(name, desc) { GeneDict = new(genes.Count); foreach(Gene gen in genes) { gen.ChromosomeReference = this; AddGene(gen); } } public Gene AddGene(Gene gene) { if(GeneDict.ContainsKey(gene.Name)) GeneDict[gene.Name] = gene; else GeneDict.Add(gene.Name, gene); return gene; } }
public class Genome { public string Name; public string Description; public Dictionary<string, Chromosome> ChromosomeDict; public Genome(string name, string desc = "") { Name = name; Description = desc; } public Genome(string name, List<Chromosome> chromosomes, string desc = "") : this(name, desc) { ChromosomeDict = new(chromosomes.Count); foreach(Chromosome chro in chromosomes) { chro.GenomeReference = this; ChromosomeDict.Add(chro.Name, chro); } } public Chromosome AddChromosome(Chromosome chrom) { if(ChromosomeDict.ContainsKey(chrom.Name)) ChromosomeDict[chrom.Name] = chrom; else ChromosomeDict.Add(chrom.Name, chrom); return chrom; } }
public class TestGenome : Genome { public TestGenome() : base("TestGenome", new()) { AddChromosome(new("Body", "Chromosome for the Body")); ChromosomeDict["Body"].AddGene(new Gene<float>("Radius", 3f, "Body radius.")); for(int i = 0; i < 3; i++) { ChromosomeDict["Body"].AddGene(new Gene<float>($"Anchor{i}", i / 3f, $"Location of anchor point {i}")); } } }
解答
这个结构基础可行,但有几个关键优化点需要补充,以适配遗传算法和Unity开发场景:
- 泛型约束:给
Gene<T>添加类型约束(如where T : struct, IConvertible),确保基因值是可序列化的数值类型,避免后续解析、交叉操作出现类型兼容问题。 - ID自动生成:给
Gene和Chromosome的ID字段添加自动生成逻辑(比如用Guid或全局递增整数),避免手动赋值导致的ID冲突问题。 - 顺序维护:
GeneDict用Dictionary方便按名称查找,但遗传算法操作需要固定顺序,建议新增List<Gene> Genes字段维护基因的添加顺序,确保扁平化时的一致性。 - 循环引用处理:
ChromosomeReference和GenomeReference会导致序列化时的循环引用,建议给这些字段添加[JsonIgnore]标记(如果用Newtonsoft.Json序列化),或者在序列化时只保存ID而非对象引用。 - 基因类型标记:新增枚举
GeneType(如Static/Observable/Controllable),给Gene添加该字段,方便后续快速筛选与神经网络交互的基因。
问题3:若采用上述类结构,如何表示数据结构以使其易于解析且适合交叉等操作?
解答
核心思路是将嵌套结构扁平化,分离元数据与基因值:
- 扁平化基因序列:
- 按固定顺序(比如先根部件,再子部件的深度优先顺序)遍历Genome的所有Chromosome,将每个
Gene<T>.Value转换为统一数值类型(如float,非数值类型可编码为整数映射值),存入一维float[]数组。 - 该数组就是遗传算法中用于交叉、变异的核心“基因组”,操作完全遵循一维数组的逻辑(比如单点交叉:随机选一个位置,交换两个基因组该位置后的所有元素;变异:随机选位置修改数值)。
- 按固定顺序(比如先根部件,再子部件的深度优先顺序)遍历Genome的所有Chromosome,将每个
- 元数据存储:
- 单独保存一份元数据,记录每个基因的位置索引、名称、类型、取值范围(如
0~1、-π~π)、所属Chromosome的ParentID等信息。元数据只需保存一次,所有同类型的Genome共享该元数据。
- 单独保存一份元数据,记录每个基因的位置索引、名称、类型、取值范围(如
- 解析与重构:
- 解析时,根据元数据的索引映射,将一维数组的数值还原到对应的
Gene<T>.Value中,再通过ParentID重建Body的树形结构。
- 解析时,根据元数据的索引映射,将一维数组的数值还原到对应的
- 序列化格式:
- 用二进制格式序列化扁平化数组(效率高),或JSON格式(易调试),元数据可单独存为配置文件,避免重复存储。
问题4:最后,假设我用神经网络控制生成的Body,如何将部件属性与神经网络的输入输出关联?
解答
按观测输入和控制输出两类属性分别处理,结合Unity的生命周期和后续ML-Agents的集成需求:
- 观测输入(神经网络感知):
- 筛选标记为
Observable的Gene,收集其Value转换为float数组,作为神经网络的输入层。 - 在Unity的
Update或FixedUpdate中,定期更新该数组(比如包含部件的旋转角度、位置偏移、碰撞状态等)。 - 若后续使用ML-Agents,可在
Agent.CollectObservations()方法中调用AddObservation()添加这些数值。
- 筛选标记为
- 控制输出(神经网络驱动):
- 筛选标记为
Controllable的Gene,将神经网络的输出数组元素一一映射到这些Gene的目标值。 - 在Unity中添加平滑过渡逻辑(比如用
Mathf.Lerp),将部件的当前属性值逐步过渡到目标值,避免刚体运动出现突兀变化。 - 若使用ML-Agents,可在
Agent.OnActionReceived()方法中接收输出值,直接赋值或过渡到对应的Gene属性。
- 筛选标记为
- 优化建议:
- 对观测输入做归一化处理(比如将角度转换为
-1~1的范围),提升神经网络的训练效率。 - 给可控制的Gene设置取值范围约束(比如旋转角度限制在
-90°~90°),避免神经网络输出异常值导致Body行为失控。
- 对观测输入做归一化处理(比如将角度转换为
内容的提问来源于stack exchange,提问作者Martin Bocanegra
相关产品推荐
相关产品推荐

