上周有位读者私信我,说照着第一篇的教程搓了个新单位,改完攻击力扔进游戏,新鲜劲儿一过就不知道干嘛了。想让这个单位在特定地形加战斗力、想让某栋建筑在城市有学院时多给两点科研、想让领袖有自己的议程……结果一翻文件全是白纸。这个感觉我太熟了。第一次用XML写出能跑的mod,只能算摸到了门把手;文明6真正的modding大门,是从你开始面对它的数据库和Modifier系统才打开的。这篇第二篇不讲废话,直接从数据层、逻辑层、调试工具三块带你把整个链条过一遍,目标是让你能独立做出一个带自定义能力、自定义议程和出生地倾向的完整文明mod。这篇的东西偏硬,但都是我做mod时天天在用的东西。
1. 从XML到SQL:为什么第二个mod应该开始动数据库
1.1 XML的三个短板,越写越难受
第一篇教的是在ModBuddy里建XML类型的Gameplay Data,往<Buildings>里塞两行<Row>,游戏就读进去了。这套流程对单个物件非常直白,但只要你开始接触真正的mod,很快会撞上三个墙:
第一,XML在游戏加载时只能做插入操作,不能更新、不能删除。你想把原版某个建筑的成本从100改成60,用XML写一个同名的<Row>进去,结果往往是主键冲突,加载时直接红字。就算把原有Tag覆盖对,也要踩一堆坑。
第二,改动量一大,XML的冗余会把人逼疯。每加一条数据都要写完整的父标签和属性名,一个mod几十个物件,文件翻起来比翻通讯录还累。
第三,XML没法写注释和分段执行。你想临时禁用某条数据,只能手动删一大段;想在调试时快速试错,也没有"只执行某一段"这种操作。
Civ6的底层其实是一份SQLite数据库,游戏本身所有数据都存在里面。你用XML改数据,等于拿翻译软件跟本地人对话,能通,但绕。直接用SQL,就是对原始数据库说话。
1.2 同一个改动,SQL比XML短一半还多
拿一个最简单的建筑举例。XML写法是这样的:
xml复制<GameData>
<Buildings>
<Row>
<BuildingType>BUILDING_MY_HOUSE</BuildingType>
<Name>LOC_BUILDING_MY_HOUSE_NAME</Name>
<PrereqCivic>CIVIC_CODE_OF_LAWS</PrereqCivic>
<Cost>90</Cost>
<AdvisorType>ADVISOR_CULTURE</AdvisorType>
</Row>
</Buildings>
</GameData>
换成SQL,一行搞定:
sql复制INSERT INTO Buildings (BuildingType, Name, PrereqCivic, Cost, AdvisorType)
VALUES ('BUILDING_MY_HOUSE', 'LOC_BUILDING_MY_HOUSE_NAME', 'CIVIC_CODE_OF_LAWS', 90, 'ADVISOR_CULTURE');
这还不算完。SQL能直接改写原版数据:
sql复制UPDATE Buildings SET Cost = 60 WHERE BuildingType = 'BUILDING_MY_HOUSE';
这一句在XML里怎么都写不出来。SQL还能用INSERT OR REPLACE、INSERT OR IGNORE处理重复数据,用注释临时屏蔽一块,用分号把逻辑拆得清清楚楚。做mod超过两个版本之后,回看XML,你大概率会感谢自己早换成了SQL。
1.3 直接打开游戏数据库,比啥教程都实在
在Steam库里找到文明6安装目录,下面有一份关键文件:
code复制SteamLibrary\steamapps\common\Sid Meier's Civilization VI\Base\Assets\Gameplay\Data\Gameplay.sqlite
下载一个DB Browser for SQLite,打开这个文件,Civ6的所有数据表结构、字段名、原版所有ModifierType、所有RequirementType全都摆在眼前。我到现在做mod的第一步永远是查这个数据库,而不是翻教程。网上任何攻略写错一个表名,你对着数据库一查就能识破。这个习惯比记住任何ModifierType的名字都管用,因为你不用记,查得到就行。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Modifier链:文明6里"动态加成"到底是怎么算出来的
2.1 文明6所有能力的底层是五张表
很多新手卡在一点:为什么我往Buildings表里写了Yield列,游戏根本不认?因为文明6的建筑、特性、政策产生的"动态效果",并不直接在建筑表里写加成值,而是走一套统一的Modifier系统。这套系统由五类对象串起来:
Types:所有东西的"户口本"。建筑、特性、Modifier、Requirement,凡是游戏里的对象都要先在Types表里登记一个Type和Kind。Modifiers:行为的定义。一行Modifier记录着一个行为的模板和它生效的条件。ModifierArguments:给行为模板填充的具体参数。比如给多少产出、作用于什么类型。Requirements和RequirementSets:触发条件。满足条件时Modifier才生效,RequirementSet是把多个Requirement用AND或者OR组合起来的容器。BuildingModifiers、TraitModifiers、LeaderTraits等挂载表:负责把Modifier挂到建筑、特性或者领袖身上。
拿生活里的事打比方:一条Modifier是一张"补贴政策",参数是"每月发多少钱、发给谁",requirement是"申领人必须满足什么条件",挂载表是"这份政策贴在哪个部门的墙上"。游戏启动时逐条读取,条件满足就发钱,不满足就晾着。
2.2 一个完整的条件加成示例:有学院才加科研
我写一个很典型的例子:造一栋新建筑,如果城市里有了学院,这栋建筑就给城市额外加2点科研。SQL完整写法如下:
sql复制-- 1. 给建筑、modifier、requirement、requirement set登记户口
INSERT INTO Types (Type, Kind) VALUES
('BUILDING_TUTORIAL_HOUSE', 'KIND_BUILDING'),
('MODIFIER_TUTORIAL_HOUSE_SCIENCE', 'KIND_MODIFIER'),
('REQUIREMENT_TUTORIAL_HOUSE_HAS_CAMPUS', 'KIND_REQUIREMENT'),
('REQUIREMENTSET_TUTORIAL_HOUSE_HAS_CAMPUS', 'KIND_REQUIREMENT_SET');
-- 2. 建筑主数据
INSERT INTO Buildings (BuildingType, Name, PrereqCivic, Cost, AdvisorType)
VALUES ('BUILDING_TUTORIAL_HOUSE', 'LOC_BUILDING_TUTORIAL_HOUSE_NAME', 'CIVIC_CODE_OF_LAWS', 90, 'ADVISOR_CULTURE');
-- 3. 条件:城市拥有学院(DISTRICT_CAMPUS)
INSERT INTO Requirements (RequirementId, RequirementType)
VALUES ('REQUIREMENT_TUTORIAL_HOUSE_HAS_CAMPUS', 'REQUIREMENT_CITY_HAS_DISTRICT');
INSERT INTO RequirementArguments (RequirementId, Name, Value)
VALUES ('REQUIREMENT_TUTORIAL_HOUSE_HAS_CAMPUS', 'DistrictType', 'DISTRICT_CAMPUS');
INSERT INTO RequirementSets (RequirementSetId, RequirementSetType)
VALUES ('REQUIREMENTSET_TUTORIAL_HOUSE_HAS_CAMPUS', 'REQUIREMENTSET_TEST_ALL');
INSERT INTO RequirementSetRequirements (RequirementSetId, RequirementId)
VALUES ('REQUIREMENTSET_TUTORIAL_HOUSE_HAS_CAMPUS', 'REQUIREMENT_TUTORIAL_HOUSE_HAS_CAMPUS');
-- 4. 定义Modifier,挂上条件
INSERT INTO Modifiers (ModifierId, ModifierType, SubjectRequirementSetId)
VALUES ('MODIFIER_TUTORIAL_HOUSE_SCIENCE', 'MODIFIER_SINGLE_CITY_ADJUST_CITY_YIELD_CHANGE', 'REQUIREMENTSET_TUTORIAL_HOUSE_HAS_CAMPUS');
INSERT INTO ModifierArguments (ModifierId, Name, Value)
VALUES ('MODIFIER_TUTORIAL_HOUSE_SCIENCE', 'YieldType', 'YIELD_SCIENCE'),
('MODIFIER_TUTORIAL_HOUSE_SCIENCE', 'Amount', '2');
-- 5. 挂到建筑上
INSERT INTO BuildingModifiers (BuildingType, ModifierId)
VALUES ('BUILDING_TUTORIAL_HOUSE', 'MODIFIER_TUTORIAL_HOUSE_SCIENCE');
这套五段式几乎是Civ6所有自定义能力的地基。你写任何mod,最后都会回到这个套路:注册Type,写主数据,写Requirement和RequirementSet,写Modifier和Arguments,最后用挂载表绑到目标上。顺序不能乱,尤其不能漏掉第1步的Types注册,漏了必报外键错误。
2.3 常用ModifierType速查,但一定要以数据库为准
我整理几个高频ModifierType,方便你先有个概念:
| 目的 | ModifierType | 主要参数 |
|---|---|---|
| 城市场地产出变化 | MODIFIER_SINGLE_CITY_ADJUST_CITY_YIELD_CHANGE | YieldType, Amount |
| 玩家所有城市产出变化 | MODIFIER_PLAYER_CITIES_ADJUST_CITY_YIELD_CHANGE | YieldType, Amount |
| 建筑产物调整 | MODIFIER_SINGLE_CITY_ADJUST_BUILDING_YIELD_CHANGE | BuildingType, YieldType, Amount |
| 单位战斗力调整 | MODIFIER_PLAYER_UNITS_ADJUST_STRENGTH | Amount |
| 单位移动力调整 | MODIFIER_PLAYER_UNITS_ADJUST_MOVEMENT | Amount |
| 地形相邻产出 | MODIFIER_PLAYER_CITIES_TERRAIN_ADJACENCY | TerrainType, YieldType, Amount |
我必须强调一句:你游戏版本里的实际ModifierType名字,务必以Gameplay.sqlite里查到的为准。不同版本、不同DLC会影响命名。我怀疑一个名字的时候,就打开数据库执行:
sql复制SELECT ModifierType FROM Modifiers WHERE ModifierType LIKE '%YIELD_CHANGE%';
列出来一核对,基本不会错。这个习惯能帮你省掉大量"明明照着教程写却不生效"的冤枉时间。
3. 完整实例:手搓一个带专属议程和出生地倾向的自定义文明
3.1 定设计:一个靠山脉和学院吃饭的科技文明
实例讲太多概念容易飘,我直接带你看我最近做的"旭光文明"是怎么落地的。设计意图很简单:一个科技导向的文明,领袖叫"曦和",文明特性是山脉地块额外产出1点科研,领袖议程是喜欢科研强的文明、讨厌落后的文明,出生地倾向靠近山脉。
按这个设计,我需要的数据包括:文明本体、文明特性、领袖、领袖议程、出生地倾向、以及本地化文本。逐个拆开写SQL。
3.2 文明本体与文明特性
sql复制-- 文明本体
INSERT INTO Types (Type, Kind)
VALUES ('CIVILIZATION_MORNING_LIGHT', 'KIND_CIVILIZATION');
INSERT INTO Civilizations
(CivilizationType, Name, Description, Adjective, StartingCivilizationLevelType, RandomCityNameDepth, Ethnicity)
VALUES
('CIVILIZATION_MORNING_LIGHT',
'LOC_CIVILIZATION_MORNING_LIGHT_NAME',
'LOC_CIVILIZATION_MORNING_LIGHT_DESCRIPTION',
'LOC_CIVILIZATION_MORNING_LIGHT_ADJECTIVE',
'CIVILIZATION_LEVEL_FULL_CIV',
1,
'ETHNICITY_ASIAN');
-- 文明特性
INSERT INTO Types (Type, Kind)
VALUES ('TRAIT_CIVILIZATION_MORNING_LIGHT_SCIENCE', 'KIND_TRAIT');
INSERT INTO CivilizationTraits (CivilizationType, TraitType)
VALUES ('CIVILIZATION_MORNING_LIGHT', 'TRAIT_CIVILIZATION_MORNING_LIGHT_SCIENCE');
-- 特性效果:山脉地块产出+1科研
INSERT INTO Modifiers (ModifierId, ModifierType, SubjectRequirementSetId)
VALUES ('MODIFIER_MORNING_LIGHT_SCIENCE', 'MODIFIER_PLAYER_CITIES_TERRAIN_ADJACENCY', NULL);
INSERT INTO ModifierArguments (ModifierId, Name, Value)
VALUES ('MODIFIER_MORNING_LIGHT_SCIENCE', 'TerrainType', 'TERRAIN_MOUNTAIN'),
('MODIFIER_MORNING_LIGHT_SCIENCE', 'YieldType', 'YIELD_SCIENCE'),
('MODIFIER_MORNING_LIGHT_SCIENCE', 'Amount', '1');
INSERT INTO TraitModifiers (TraitType, ModifierId)
VALUES ('TRAIT_CIVILIZATION_MORNING_LIGHT_SCIENCE', 'MODIFIER_MORNING_LIGHT_SCIENCE');
这里有个新手容易混的概念:CivilizationTraits管的是"这个文明有什么特性",TraitModifiers管的是"这个特性具体有什么用"。两层是分开的。你只挂特性不挂效果,游戏里能看到特性名字,但啥也不加。
3.3 领袖、议程与关联
领袖和议程这块,光写一个名字是不够的,要把Leaders、LeaderTraits、Agendas、AgendaTraits四张表串起来:
sql复制-- 领袖
INSERT INTO Types (Type, Kind)
VALUES ('LEADER_MORNING_LIGHT_LEADER', 'KIND_LEADER');
INSERT INTO Leaders (LeaderType, Name, InheritFrom)
VALUES ('LEADER_MORNING_LIGHT_LEADER', 'LOC_LEADER_MORNING_LIGHT_LEADER_NAME', 'LEADER_DEFAULT');
-- 领袖特性(也是议程用的trait)
INSERT INTO Types (Type, Kind)
VALUES ('TRAIT_LEADER_MORNING_LIGHT_LEADER', 'KIND_TRAIT');
INSERT INTO LeaderTraits (LeaderType, TraitType)
VALUES ('LEADER_MORNING_LIGHT_LEADER', 'TRAIT_LEADER_MORNING_LIGHT_LEADER');
-- 议程
INSERT INTO Types (Type, Kind)
VALUES ('AGENDA_MORNING_LIGHT_PROGRESS', 'KIND_AGENDA');
INSERT INTO Agendas (AgendaType, Name, Description)
VALUES ('AGENDA_MORNING_LIGHT_PROGRESS',
'LOC_AGENDA_MORNING_LIGHT_PROGRESS_NAME',
'LOC_AGENDA_MORNING_LIGHT_PROGRESS_DESCRIPTION');
INSERT INTO AgendaTraits (AgendaType, TraitType)
VALUES ('AGENDA_MORNING_LIGHT_PROGRESS', 'TRAIT_LEADER_MORNING_LIGHT_LEADER');
-- 文明和领袖的关联
INSERT INTO CivilizationLeaders (CivilizationType, LeaderType)
VALUES ('CIVILIZATION_MORNING_LIGHT', 'LEADER_MORNING_LIGHT_LEADER');
插一句经验之谈:领导原版就有大量AI逻辑挂在与LeaderType同名的AiListTypes和AiFavoredItems上。如果你只写到上面这一步,曦和就是一个"性格空白"的领袖,不会主动追求科技,也不会因为别人科技低而翻脸。真正想调AI行为,还得去查AiListTypes、AiLists、AiFavoredItems这几张表,这个坑比较大,这篇先不展开,写清楚结构和注册流程就够你跑起来了。
3.4 出生地倾向和玩家配色
sql复制-- 出生地倾向:靠近山脉
INSERT INTO StartBiasTerrains (CivilizationType, TerrainType, Tier)
VALUES ('CIVILIZATION_MORNING_LIGHT', 'TERRAIN_MOUNTAIN', 4);
-- 玩家配色
INSERT INTO PlayerColors
(Type, Usage, PrimaryColor, SecondaryColor, TextColor)
VALUES
('CIVILIZATION_MORNING_LIGHT', 'Minor', 'COLOR_PLAYER_PURPLE', 'COLOR_PLAYER_BLUE', 'COLOR_PLAYER_WHITE');
Tier这个字段代表权重,数字越大越优先。文明6的地图脚本在生成出生地时会参考这张表,把旭光文明尽量放在山脉旁边。但注意这只是倾向,不是保证,地图类型和AI竞争都可能把它挤走。
3.5 本地化文本:没有中文你只会看到一串LOC_**
游戏里显示的所有文字都来自本地化表,直接写在SQL里的中文是不会显示的。在Data文件夹下新建一个Text.xml,内容大致是:
xml复制<LocalizedText>
<Row Language="zh_Hans_CN">
<Tag>LOC_CIVILIZATION_MORNING_LIGHT_NAME</Tag>
<Text>旭光</Text>
</Row>
<Row Language="zh_Hans_CN">
<Tag>LOC_CIVILIZATION_MORNING_LIGHT_DESCRIPTION</Tag>
<Text>旭光文明</Text>
</Row>
<Row Language="zh_Hans_CN">
<Tag>LOC_CIVILIZATION_MORNING_LIGHT_ADJECTIVE</Tag>
<Text>旭光的</Text>
</Row>
<Row Language="zh_Hans_CN">
<Tag>LOC_LEADER_MORNING_LIGHT_LEADER_NAME</Tag>
<Text>曦和</Text>
</Row>
<Row Language="zh_Hans_CN">
<Tag>LOC_AGENDA_MORNING_LIGHT_PROGRESS_NAME</Tag>
<Text>追寻黎明</Text>
</Row>
<Row Language="zh_Hans_CN">
<Tag>LOC_AGENDA_MORNING_LIGHT_PROGRESS_DESCRIPTION</Tag>
<Text>曦和尊重那些注重科技发展的文明,厌恶科技落后的文明。</Text>
</Row>
</LocalizedText>
Language字段里的zh_Hans_CN是简体中文的语言代码,别写错。如果你想让国外玩家也能正常玩,最好把英文的en_US也补上,否则老外那边全是一串LOC_。
在ModBuddy里的操作很简单:右键项目 → Add → New Item,选择Gameplay Data(SQL)、Text(XML),ModBuddy会自动把文件注册进.modinfo的UpdateDatabase和UpdateText节点。改完SQL重新加载mod,文明就能在选文明界面看到了。一个比较重要的坑:加了Lua脚本或者改了游戏性数据的mod,要在modinfo的Properties里把AffectsSavedGames设为1,否则旧存档加载时可能不认这个mod。
4. Lua事件脚本:让mod从"改数值"走向"写行为"
4.1 事件驱动,而不是每回合扫描
SQL能表达"什么条件下加多少",但表达不了"当某个行为发生时做什么"。城市被占领、单位升级、玩家触发某个奇观,这种动态逻辑必须靠Lua。
新手最常犯的错是写一个全局循环,每回合遍历所有城市、所有单位,判断有没有要处理的事。Civ6的Lua是事件驱动的,游戏引擎每时每刻都在广播各种事件,你只需要"报名"倾听你想要的那个,然后在回调函数里做事就行。事件驱动比轮询省大量性能,也好维护得多。常用事件我列几个:
| 事件 | 触发时机 | 典型用途 |
|---|---|---|
| Events.PlayerTurnStarted(iPlayer) | 玩家回合开始 | 回合奖励、阈值判断 |
| Events.CityFounded(iPlayer, iCity) | 城市建立 | 送建筑、送单位 |
| Events.CityCaptureComplete(...) | 城市易手 | 征服奖励 |
| Events.CityDistrictCompleted(iPlayer, iCity, iDistrict) | 区域建成 | 区域联动奖励 |
| Events.UnitAddedToMap(...) | 单位进入地图 | 出生加成 |
事件的具体参数列表在不同版本里有差异,我写脚本第一步永远是先往回调里塞一句print,把参数全打出来,确认好再写逻辑。
4.2 实战:旭光文明占领城市时立刻获得20科技
延续上一个例子,我想让旭光文明在攻占城市后立刻获得20点科技。在ModBuddy里添加一个Gameplay Script文件,代码可以这样写:
lua复制local CIVILIZATION_MORNING_LIGHT = GameInfo.Civilizations["CIVILIZATION_MORNING_LIGHT"].Index
function OnCityCaptured(ownerPlayerID, ownerCityID, newPlayerID, bConquest)
local player = Players[newPlayerID]
if player == nil then return end
if not player:IsAlive() then return end
if player:GetCivilizationType() ~= CIVILIZATION_MORNING_LIGHT then return end
player:GetScience():ChangeScience(20)
print("Morning Light captured a city, +20 science")
end
Events.CityCaptureComplete.Add(OnCityCaptured)
注意三点:
第一,回调里我用的是newPlayerID而不是ownerPlayerID,因为我想给"占领后真正拿到城市的玩家"发奖励。事件参数里两个玩家ID含义不同,写错就变成被占领方得奖励了。
第二,每个Player对象都可能因为各种原因为nil,或者已经灭国,所以拿到对象先判空、再判IsAlive()。多人游戏里还有可能是断线玩家,这一层防御能省下好多"为什么有时生效有时不生效"的玄学问题。
第三,GameInfo.Civilizations["CIVILIZATION_MORNING_LIGHT"].Index这种写法是读取静态表,拿到文明ID。Players[iPlayer]是运行时对象,两者一个静态一个动态,别搞混。
4.3 Lua里常见的坑
.lua文件放在ModBuddy的GameplayScripts文件夹里,modinfo会多出<GameplayScript>节点。如果你手动改modinfo,记得同时维护<Files>里的文件列表,漏了任何一个,游戏加载时直接报"文件缺失"。print()的输出只在Lua.log里,游戏界面里看不到。没有装调试UI的话,别指望屏幕上弹消息。- Gameplay Script在主机端运行,多人游戏中也是主机在模拟。别在GameplayScript里碰UI控件,那是UIScript的活,两边上下文是隔离的。
- Lua是弱类型语言,Civ6的API又经常返回
nil而不是空对象,所以if object == nil then return end这种防御性代码,写多少都不嫌多。
5. 日志、FireTuner与二分法:调试文明6mod的完整链路
5.1 先开日志和调试模式
打开文明6的配置文件,位置在:
code复制我的文档\My Games\Sid Meier's Civilization VI\AppOptions.txt
找到两行,改成1:
code复制EnableTuner 0
EnableLogging 0
改成:
code复制EnableTuner 1
EnableLogging 1
保存后,用游戏目录里的Sid Meier's Civilization VI_DBG.exe启动游戏。这个调试版exe会把数据库和Lua的运行日志写到:
code复制我的文档\My Games\Sid Meier's Civilization VI\Logs\
同时打开SDK自带的FireTuner,填好端口后连上有_DBG.exe启动的客户端,就能看到实时log和控制台。
5.2 三份日志分别看什么
Logs文件夹里文件不少,关键看这三个:
Database.log:每次加载mod时执行的SQL、XML,以及所有数据库报错。mod起不来的第一现场,这里的报错最完整。Lua.log:Lua运行时报错和print()输出。Lua脚本没反应、报红字,全在这里。GameCore.log和Gameplay.log:游戏模拟层面的warning和error,像AI崩了、资源缺失之类的问题会出现在这。
我调试的顺序永远一样:先开Database.log看有没有SQL错误,没有再看Lua.log,最后才开一把游戏跑行为测试。直接把游戏跑起来等报错,是最浪费时间的方式。
5.3 常见报错速查表
| 报错/现象 | 原因 | 处理 |
|---|---|---|
Database.log里FOREIGN KEY constraint failed |
引用了Types表里不存在的Type | 检查拼写,先补Types行 |
游戏里看到LOC_XXX原文 |
缺少对应语言的LocalizedText | 检查Text.xml和Language代码 |
| Lua没有任何反应 | 脚本没注册、事件名错、或函数没被Add | 确认GameplayScript节点,先用print定位 |
| 建筑没有图标、显示问号 | 缺少Icon/IconDefinitions | 参照原版建筑补Icon数据 |
| 加载mod直接红字 | SQL有语法错误或者文件路径错 | 在DB Browser里先执行一遍SQL,再查modinfo文件列表 |
5.4 二分法定位:注释掉一半,再运行
遇到"加载就崩溃"这种最头疼的问题,不要一行行盯代码。把SQL文件里的内容先注释一半,运行看还崩不崩。不崩,说明问题在后半段;还崩,说明问题在前半段。然后继续二分,很快就能把问题锁定到几条语句里。这个方法听上去笨,但在几十行甚至上百行数据里找错,它比我见过的任何"聪明办法"都稳定。
6. 兼容性、性能与发布:在创意工坊翻车前想清楚的事
6.1 别让你的mod拖垮玩家的回合
你写的mod不只是给你一个人玩的,别人会跑完整局。性能上最常见的坑就是Lua里做全图遍历:
lua复制-- 反面教材:每回合扫描全图所有单位
function OnTurnStart(playerID)
local player = Players[playerID]
for _, unit in player:GetUnits():Members() do
-- 做一些并不必要的处理
end
end
单位列表在后期轻松破百,还只是玩家自己的单位。如果做的是地图遍历,比如Game.GetUnits(),性能更夸张。能用事件解决的,绝不用轮询。比如"单位进入山脉时加buff"就监听单位移动事件,而不是每回合检查所有单位的位置。
还有一点,SQL里堆大量Modifier本身也会拖慢城市面板的刷新速度。你给一个建筑挂了20个Modifier,游戏里每点开一次城市界面就多算20次。能用一张表直接写产出就用表,别啥都上Modifier,这是很多"性能爆炸mod"的病根。
6.2 和其它mod打架的解法
文明6创意工坊里mod数量多,冲突不可避免。我的经验是:
- 改原版数据时优先用
UPDATE,别去动DELETE,因为你不知道别的mod是不是也依赖那条数据。 - 自己新增的数据用
INSERT OR REPLACE比裸INSERT安全,重复加载不报错。 - 如果确实需要调整加载顺序,在modinfo的
<Action Properties>里设置LoadOrder,数字小的先加载。 - 别在自己的mod里引用你没装DLC的类型。比如你写了
RequirementSetId指向资料片才有的Requirement,玩家没装那个DLC,加载直接红屏。做mod前想清楚面向的是原版还是全DLC玩家。
6.3 发布前自检六连
我每次上传创意工坊前会过一遍自检清单,贴在下面给你参照:
- 从远古时代开始,用标准速度玩到至少工业时代,中间没有任何红字和弹窗。
- 用
_DBG.exe跑一遍,日志里没有FOREIGN KEY和nil报错。 - 把SQL和Lua文件里调试用的临时
print全部删干净。 - 检查modinfo里
AffectsSavedGames设置是否正确,有没有漏掉新增文件。 - 缩略图、简介、版本号都更新好,创意工坊描述里写明改动内容。
- 用干净的游戏目录,不带别的mod测试一遍,排除冲突干扰。
这套流程看着繁琐,但能帮你的mod避免一大半差评。创意工坊玩家可不会看你代码逻辑多优雅,他们只关心加载有没有红字、游戏后期卡不卡、别的地方会不会冲突。
每次做完一个mod,我都会再问自己一遍:别人第一次打开这个mod时,会不会一头雾水?文明6的modding资料其实很碎,很多玩法藏在原版和DLC的数据里,多花点时间翻Gameplay.sqlite,比在任何一篇教程里抄代码都管用。我自己的习惯是,每做一个新机制,先找原版里最接近的功能,把涉及的所有关联表抄一遍,然后一点点改成自己的。这比从零写省太多事,也几乎不会出错。
