转载、引用请告知
背景
先说背景,《玩具帝国》是一个玩法决策复杂、画风独特性强的策略游戏。
游戏基本玩法:玩法。
Demo地址:Demo。
ml-agent
《玩具帝国》的人机AI采用的是Unity的ml-agent,通过强化学习训练能够进行长周期复杂决策的人机AI。原来写决策树很痛苦,现在可以直接挂机炼丹。
为什么选择自己开发ai 没有选用调动ChatGPT之类线上接口的原因有很多,首先这是人机AI是在离线游戏模式使用的,而且对决策的实时性有要求,因此不可能接受连入一个线上的接口。而且《玩具帝国》需要兼容低配置甚至移动平台,所以对这个本地AI运行的性能也有很高的要求。
ml-agent里给的实例都算是比较简单的决策问题,一次任务的周期也很短,《玩具帝国》的就比较长,决策也很复杂,所以我们使用了“即时奖励”和“预测奖励”进行长周期决策AI的训练。因为数学模型是可调的,所以AI依然可控,只需要根据通过简单的参数调整,就可以改变AI的决策倾向。
推公式还是要一步步推的,希望以后有个AI帮我把这部分工作也做了。
为了让每次输入的向量等长,在观察时,场上的三条路被分成了许多块,在每一块上,统计在其上单位的平均数值或求和,最后加上单独的数值,组合得到完整的向量。
因为不同的文明玩法不一样,但是基本的规则又是一致的,所以先训练一个能掌握基本规则的底模。从教会AI基本的分配工人开始,每次增加训练一项新科目,不断迭代完善,就能得到一个掌握大致规则的底模。在这个底模的基础上做分支训练,就可以得到适用于不同文明策略的模型。
为了避免过拟合,每个Episode前,都对初始条件进行一次随机,譬如不同的资源水平、不同的敌人强度,每次决策时也会对AI的可选项进行随机Dropout,总之尽可能地让AI在训练时接触到所有的可选行为。
ml-agent里默认的决策间隔非常短,但毕竟《玩具帝国》里不需要每时每刻都进行决策,所以我人为地把间隔时间拉长了,这样也能更好地让AI把自己的决策和结果关联起来。
最后基本上只要放着AI在场景里挂机就好了。
进阶一些的话,可以检测一些AI的精彩操作给奖励。毕竟我主要是想训练AI对战略上、宏观的把握,一些小的战术配合可以简单地引导一下,比如在前排有肉盾的时候派出远程。
经过这些调教之后,AI在战术上的表现强了不少。比如先派骑兵突击,再跟上工人采矿:
比如用近战单位保护远程单位:
比如偷袭(虽然只偷掉了我两个工人):
在训练效果上,修改之后的明显比原生ml-agent强不少:
从全局战略上看,也能看出AI具有很强的倾向性:
由于《玩具帝国》需要在移动平台实时运行,起初我还很担心ml-agent在移动平台上跑不动,后来证明我的担心是多余的。在小米10上以最高画质运行基于ml-agent的极难AI,也能维持在45FPS左右。
AI绘画
游戏里的科技树图标实在太多了,根本画不过来……在没有AI画画之前,我都不敢想啥时候能把这些玩意填完。
AI画画出来之后,我抱着满腔热情去试,结果发现三个严重的问题:
- 全TM在画二次元,没有适合的风格,想用到游戏里必须自己炼丹。
- 画出来的画好多都是美少女看镜头,没有叙事性,没法当icon用。
- 已有素材几乎全是中世纪大胡子男人呆呆站着,图生图不可行,训练出来的泛化性也很差。
一开始的规划是:
- 画出卡通简笔画风格。
- 资产条件:有98张人像和8张UI,且人像全是男人。
- 需要能产出带有该画风的具有一定叙事内容的图像,内容形式一定要多元。
我尝试了最开始的Embedding:
后来换成CKPT(画和训练集里接近的小人已经不错了,但泛化性还是不理想):
然后是Lora(好!很接近了!):
最后是Locon:
现在这个版本画人画物画事都很完美了,甚至能从全是大胡子男人的训练集里学会画女人:
从中世纪里摘出摩托车:
为了引导AI画出前景后景区分明显的画,还专门画了一组引导图。分别是只保留前景、只保留背景、全图共三张图,在Caption里打组:
由于训练集中人物朝向太固定,此处额外做了镜像处理。
由于训练集缺乏建筑、风景、完整图像,为了丰富训练集,我先进行一次时间较短的训练,并用得到的模型生成与目标画风类似的建筑、风景图像,再将这些图片放回训练集。
还做了正则化:
模型出来之后,也不是说生了图就能直接用,像一些比较复杂的图,我的方案是先去掉我的微调模型用底模生一张图,然后用ControlNet加上我的微调模型出新图。下面从左到右就是:底模 微调,底模,底模 微调 ControlNet。
最后效果不错:
而且这个模型在美术做设计参考的时候也能用:
AI代码
单独给AI从零开始写一些小功能没有任何问题,相信这类案例已经不少了,但是《玩具帝国》的情况不足以让AI从零开始,而AI还没到可以完全写出程序架构的时候。 如果让AI给我写小功能的话,我写的程序框架又比较复杂,而现在的AI还不足以把我所有的程序框架学会,所以我的解决方案是,把不方便配表而又需要撰写的代码交给AI。这种代码的特点是简单、模板化、多是调用API,只牵涉小部分的特殊逻辑。 以Buff系统为例,游戏里的Buff特别多,有些Buff带有不同的执行逻辑,不方便统一配表,撰写这些Buff的代码又非常耗时。 试了下用Cursor让它仿照我的代码写一些Buff,发现生成结果可以直接用。
不过目前Cursor生成复杂代码还需要复杂的前期调教,多开几次之后觉得很麻烦,用多了之后发现还是ChatGPT比较方便。
以下面这段代码为例,我给出的模板代码是对ProducebleCombatUnitBase单位的Attack、Defence、Speed属性进行调整:
代码语言:javascript复制{
// This is a buff
240000, () => new UnitBuffBase(new List<UnityAction<BuffBase, BuffContainerBase>>
{
// When the Buff is enabled, it affects
(b, c) =>
{
// Buffs that only work on ProducebleCombatUnitBase units
var u = ((UnitBuffContainer)c).unit as ProducebleCombatUnitBase;
if (u is {}) {
// Scales units to 50% attack
u.SetNumericalValueBuff(BuffNumericalValueType.Attack, false, .5f);
// Scales units to 50% defence
u.SetNumericalValueBuff(BuffNumericalValueType.Defence, false, .5f);
// Scales units to 80% speed
u.SetNumericalValueBuff(BuffNumericalValueType.Speed, false, .8f);
} else throw new BuffUnitTypeError();
}
}, new List<UnityAction<BuffBase, BuffContainerBase>>
{
// When the buff is disabled, restore the effect
(b, c) =>
{
var u = ((UnitBuffContainer)c).unit as ProducebleCombatUnitBase;
if (u is {}) {
// Restores unit's attack
u.SetNumericalValueBuff(BuffNumericalValueType.Attack, false, 2f);
// Restores unit's defence
u.SetNumericalValueBuff(BuffNumericalValueType.Defence, false, 2f);
// Restores unit's speed
u.SetNumericalValueBuff(BuffNumericalValueType.Speed, false, 1.25f);
}
}
}, false)
{
buffID = 240000,
isSuperimposable = false
}
},
我将多段类似的代码喂给AI后,询问它是否学会了,并让他总结规律:
然后,尝试让它撰写一个类似的:
代码语言:javascript复制Then please write a Buff, which acts on a Musketeer, and Musketeer is a kind of unit.What it does is make its CostFood 1.5 times what it was.
{
103, () => new UnitBuffBase(new List<UnityAction<BuffBase, BuffContainerBase>>
{
(bb, cc) =>
{
var ucc = cc as UnitBuffContainer;
var uccu = ucc != null ? ucc.unit as Musketeer : null;
if (uccu is {}) uccu.SetNumericalValueBuff(BuffNumericalValueType.CostFood, false, 1.5f);
else throw new BuffUnitTypeError();
}
}, new List<UnityAction<BuffBase, BuffContainerBase>>
{
(bb, cc) =>
{
var ucc = cc as UnitBuffContainer;
var uccu = ucc != null ? ucc.unit as Musketeer: null;
if (uccu is {}) uccu.SetNumericalValueBuff(BuffNumericalValueType.CostFood, false, 1.0f / 1.5f);
}
}, false)
{
buffID = 103,
isSuperimposable = false
}
},
教AI就像哄小孩,有正确的需要及时肯定,不过似乎AI对否定非常敏感,指出错误的时候要克制,不断完善它的经验。 现在尝试写一个复杂的Buff,作用于一个Worker,Buff生效后,Worker的生命值和速度将翻倍。 Buff失败时,如果Worker的HP不大于50,Buff的效果会恢复,否则不恢复。 这里用到了取Worker的HP的操作,所以我需要告知AI怎么取一个Unit的某个属性:
代码语言:javascript复制if you want to get a certain attribute of a unit, such as Worker's HP, you can use "Worker.HP" to get it.Let's write a slightly more complicated Buff, which acts on a Worker, and Worker is a kind of Unit. When the Buff takes effect, the Worker's HP and Speed will be doubled. When the Buff fails, if the Worker's HP is not greater than 50, the effect of the Buff will be restored, otherwise it will not be restored.
{
105, () => new UnitBuffBase(new List<UnityAction<BuffBase, BuffContainerBase>>
{
(bb, cc) =>
{
var ucc = cc as UnitBuffContainer;
var uccu = ucc != null ? ucc.unit as Worker : null;
if (uccu is {})
{
// Double Worker's HP and Speed
uccu.SetNumericalValueBuff(BuffNumericalValueType.HP, false, 2);
uccu.SetNumericalValueBuff(BuffNumericalValueType.MoveSpeed, false, 2);
}
else throw new BuffUnitTypeError();
}
}, new List<UnityAction<BuffBase, BuffContainerBase>>
{
(bb, cc) =>
{
var ucc = cc as UnitBuffContainer;
var uccu = ucc != null ? ucc.unit as Worker : null;
if (uccu is {})
{
// If Worker's HP is not greater than 50, restore the Buff's effect
if (uccu.HP <= 50)
{
uccu.SetNumericalValueBuff(BuffNumericalValueType.HP, false, 0.5f);
uccu.SetNumericalValueBuff(BuffNumericalValueType.MoveSpeed, false, 0.5f);
}
}
}
}, false)
{
buffID = 105,
isSuperimposable = false
}
},
完全能用…… 再进一步,让它撰写能作用于多个Unit对象的Buff:
代码语言:javascript复制Write a slightly more complicated Buff, which acts on the Unit. If the Unit is a Spearman, then his Attack will be increased by two. If the Unit is an Archer, then his Defense will be increased by two.
{
105, () => new UnitBuffBase(new List<UnityAction<BuffBase, BuffContainerBase>>()
{
(b, c) =>
{
if (c is UnitBuffContainer)
{
UnitBase unit = (c as UnitBuffContainer).unit;
if (unit is Spearman) {
unit.SetNumericalValueBuff(BuffNumericalValueType.Attack, true, 2f);
}
else if (unit is Archer) {
unit.SetNumericalValueBuff(BuffNumericalValueType.Defense, true, 2f);
}
}
}
}, new List<UnityAction<BuffBase, BuffContainerBase>>()
{
(b, c) =>
{
if (c is UnitBuffContainer)
{
UnitBase unit = (c as UnitBuffContainer).unit;
if (unit is Spearman) {
unit.SetNumericalValueBuff(BuffNumericalValueType.Attack, true, -2f);
}
else if (unit is Archer) {
unit.SetNumericalValueBuff(BuffNumericalValueType.Defense, true, -2f);
}
}
}
}, false)
{
buffID = 105,
isSuperimposable = false
}
},
总之,ChatGPT在应付简单的、模板化的代码时还是比较轻松的。核心的步骤是:
- 给它看你的代码模板,确认其中的注释清晰。
- 如果有新的API需要调用,需要及时告诉它。
- 说出你的需求。
工作流
《玩具帝国》的AI工作流如下:
总之,到目前为止,AI作为开发辅助的工具表现还不赖。能确定的是,它目前还不足以完全替代开发者进行开发,对复杂的项目也远远没到“言出法随”的境界,但作为生产力工具加速玩法验证和Demo制作已经完全没有任何问题了。 对独立开发者是一个绝对的福音。