帝国时代 4 地图编辑器 lua 脚本编写个人采坑记录。更新中。
从脚本文档的垃圾程度和编辑器的不便利程度来说,非常不推荐入这个坑。还有愿意入的联系我一起探索吧……
# Mod 模式
Mod 有几种模式,固定地图、生成式地图、调整包和新模式。
固定图就是正常意义的地图,所有资源不会变。
生成式就是系统中自带的地图,有一个大框架,每次都会有随机的部分,可以用种子去固定。
调整包就是不改变其他模式,主要做数值调整。
最后一个 Mod 模式其实就是脚本模式,API 撒开了用,用脚本设定你需要的一切逻辑。
我重点研究 Mod 模式
# 脚本记录
调试使用-nomovies -dev
附加命令打开游戏。进入需要选游戏模式为当前正在开发的 mod。
shift+alt+~ 打开脚本控制台
控制台要打印的变量前面不要加 local,不然 print 不出来。
使用脚本控制台可以直接输入函数执行,报错时可以两次 ESC 恢复,重新执行。
# 小概念
EGroup: Entity groups are used for buildings and objects such as rocks and trees.\n If you need to issue orders to a group vehicles or units you must use a SGroup.\n Note that you cannot create egroups with duplicate names.\n To be safe, you can check if the EGroup you want to create exists using EGroup_Exists( )。创建使用 EGroup_Create( String name )
SGroup: Squad groups are used for all units and vehicles. You can issue orders such as move and attack to an entire squad group. 创建 SGroup_CreateIfNotFound( String name )。
SBP: Squad blueprint is used to create a squad.
PBG: PropertyBagGroup
# lua 基础
字符串连接符..
if 要跟 then 结束是 end。没有括号。
for 要跟 do,结束 end。遍历尽量用 ipairs,免得顺序不符.
非声明 local 的变量默认都是全局变量-_-!
--最常用的table打印函数
function printa(_table)
for i, v in ipairs(_table) do
print(i..":"..v)
end
end
2
3
4
5
6
# 编写剧情相关
# 角色台词
在游戏中出现黑色台词框,英文台词会使用语音朗读出来,中文只有文字没有语音,不会卡住游戏执行。
Actor_PlaySpeech(ACTOR.NARR, Loc_GetString("$c314352e5a344afa972642239f981130:17"))
ACTOR 为音色,有几个值可以选。Loc 为本地化。AOE4 的本地化在编辑器的 locdb/xxx.locdb 下面。打开可以看到是一张数字索引的表。然后创建不同语言的翻译文件 csv 表格,表格按照行 id 对应。通过给不同语言相同的序号,来做到多语言的切换。使用时就用Loc_GetString("$c314352e5a344afa972642239f981130:17")
这种$modid:序号
的方式调用,适配不同的语种。
# 字幕台词
Subtitle_PlayNarrativeLine("what's up")
API 尚未测试成功
# 关卡名称开始
Util_MissionTitle(Loc_GetString("$c314352e5a344afa972642239f981130:17"))
只有文字,没有背景
# 视角移动
sq=SGroup_GetSquadAt(sg_player_horseman,1)
Camera_MoveTo( Variable var, Boolean pan, Real panRate, Boolean keepInputLocked, Boolean resetToDefault )
Move the camera to an entity/marker/pos/egroup/sgroup/squad at a given speed
Camera_PanOverTimeTo( Variable var, Float seconds, Boolean keepQueue ) --可以不为整数
Queue moving the camera to an entity/marker/pos/egroup/sgroup/squad within seconds.
The camera will move from the position that was in front of it in the queue.
2
3
4
5
6
7
8
9
10
pan 为 true,rate 才有效。为 false 则瞬移。 rate 速度为米 keepInputLocked 锁定要为 false 才不会锁死镜头
- 示例
sg=getAHorseman() --squad
Camera_MoveTo(sq,true,500,false,false)
Camera_PanOverTimeTo(Squad_GetPosition(test),0.5,false)
2
3
4
# 延迟执行
Rule_AddOneShot(callback, second)
在一个函数内,所有需要延迟几秒执行的步骤,都要通过这种方式去指定时间调用(异步)。
# 单位
这里有几个概念,实体/单位(Entiy),小队(Squad),小队组(SGroup)。
我粗浅的理解为:
单位是最底层,什么样子、数值。
小队大概是单位用于操作的部分,比如移动、进攻、技能等。
小队组是小队的集合,可以视为编队。
在很多函数中是要区分这 3 者的,所以互相之间要能精准的拿到,并按要求传参。
# 完整的角色对话方法
--利用延迟执行,进行台词和角色移动的序列执行
-- 完成的生成角色,添加台词,执行台词和视角移动
-- 初始化单位、对话内容,每句等待时长
function initSpeech(unit1,unit2)
local speeches={
createSpeech(unit1,Loc_GetString("$c314352e5a344afa972642239f981130:18"),2),
createSpeech(unit2,Loc_GetString("$c314352e5a344afa972642239f981130:19"),1),
createSpeech(unit1,Loc_GetString("$c314352e5a344afa972642239f981130:20"),3),
createSpeech(unit2,Loc_GetString("$c314352e5a344afa972642239f981130:21"),2)
}
return speeches
end
-- 生产要对话的单位
function Mod_SpwanMainCharactor()
local player = Core_GetPlayersTableEntry(Game_GetLocalPlayer())
local sbp_knight = BP_GetSquadBlueprint("unit_knight_3_eng")
local sbp_horseman = BP_GetSquadBlueprint("unit_horseman_4_eng")
local pos1 = Util_GetOffsetPosition(player.town_center.position, 40, 10)
local pos2 = Util_GetOffsetPosition(player.town_center.position, 60, 10)
local sgroup_name = "sg_player_knight_" .. tostring(localPlayer.id)
local sg_player_knight = SGroup_CreateIfNotFound(sgroup_name)
local sgroup_name2 = "sg_player_horseman_" .. tostring(localPlayer.id)
local sg_player_horseman = SGroup_CreateIfNotFound(sgroup_name2)
UnitEntry_DeploySquads(player.id, sg_player_knight, {{sbp = sbp_knight, numSquads = 1}}, pos1)
UnitEntry_DeploySquads(player.id, sg_player_horseman, {{sbp = sbp_horseman, numSquads = 2}}, pos2)
initTalking()
end
g_talkingIndex = 1
g_speeches = nil
g_lens = nil
-- 找到已生产的单位,初始化台词,依次执行台词动作
function initTalking()
local sgroup_name = "sg_player_knight_" .. tostring(localPlayer.id)
local sg_player_knight = SGroup_CreateIfNotFound(sgroup_name)
local sgroup_name2 = "sg_player_horseman_" .. tostring(localPlayer.id)
local sg_player_horseman = SGroup_CreateIfNotFound(sgroup_name2)
g_talkingIndex = 1 --重新初始化
g_speeches = initSpeech(sg_player_knight, sg_player_horseman)
g_lens = #g_speeches
setTimeout(talkingNext, 1)
end
-- 递归对话
function talkingNext()
if g_talkingIndex < g_lens then
speech = g_speeches[g_talkingIndex]
local sq = SGroup_GetSquadAt(speech.unit, 1)
Camera_PanOverTimeTo(Squad_GetPosition(sq), 0.55, true)
Actor_PlaySpeech(ACTOR.NARR, speech.speech, speech.unit)
g_talkingIndex = g_talkingIndex + 1
setTimeout(talkingNext, speech.time)
end
end
-- 对延迟执行改名
function setTimeout(cb, second)
Rule_AddOneShot(cb, second)
end
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 给定地图坐标点
地图上的 xyz 坐标决定单位的位置,平面是 xz,高度是 y。
设定 xyz 的 table 不能直接应用,需要用World_Pos
转一下 pos。
示例:
point = {x = -336.0, y = 91.0, z = -274.0}
pos = World_Pos(point.x, point.y, point.z)
pos1 = Util_GetOffsetPosition(pos, 10, 0)
-- ......下一步使用pos1生成单位
2
3
4
# 官网文档
# 生产单位
https://github.com/aoemods/wiki/wiki/Spawning-through-script
To spawn units we have to first get the squad blueprint for the unit with BP_GetSquadBlueprint. Then we can create a squad group and deploy the squad into it. Finally the entity can be extracted from our squad group. 要生成单位,我们必须首先使用 BP_GetSquadBlueprint 获取单位的小队蓝图。然后我们可以创建一个小队组,并将小队部署到其中。最后可以从我们的小队组中提取实体。
Keep in mind that the unit has to be precached by adding it to the win condition's precache list or it won't show up properly in game. 请记住,单位必须通过添加到获胜条件的预缓存列表中来预缓存,否则它将无法在游戏中正确显示。
-- Get the squad blueprint by name
local squad_blueprint = BP_GetSquadBlueprint("gaia_herdable_sheep")
-- Get or create a squad group named sg_sheep
local squad_group = SGroup_CreateIfNotFound("sg_sheep")
UnitEntry_DeploySquads(player.id, squad_group, {{sbp=squad_blueprint, num_squads=1}})
-- Select the single entity in our squad group
local entity = Squad_EntityAt(SGroup_GetSpawnedSquadAt(squad_group, 1), 0)
2
3
4
5
6
7
8
9
10
Spawning buildings is same as spawning units for most parts, but requires few additional steps. 建造建筑和建造单位的大部分是一样的,但是需要一些额外的步骤。
Following example spawns keep next to start position of player at index 1. 下面的例子产卵保持旁边的开始位置的球员在索引 1。
-- Obtaining position and adjusting it to correct grid location.
-- It must be aligned otherwise it will incorrectly place influence of buildings.
local player = World_GetPlayerAt(1)
local ps = Player_GetStartingPosition(player)
local nx = 5 * math.floor(math.floor(ps.x) / 5)
local ny = 5 * math.floor(math.floor(ps.y) / 5)
local nz = 5 * math.floor(math.floor(ps.z) / 5)
-- this is position at which building will appear
local psx = World_Pos(nx + 15, ny, nz)
-- this vector is used for rotation target, so building is facing correctly toward camera
local pst = World_Pos(nx + 15, ny, nz + 5)
-- Spawning is following 4 lines
local e = Entity_CreateFacing(BP_GetEntityBlueprint("building_defense_keep_control_abb"), player, psx, pst, true)
Entity_Spawn(e)
Entity_ForceConstruct(e)
Entity_SnapToGridAndGround(e, false)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 中立兵种
Setup_SetPlayerTeam( PlayerID p, Integer team_id )
Core_SetMutualRelationship( PlayerID/Table player/teams_table, PlayerID/Table player/teams_table, Relationship relationship )
Sets the relationship between two entries
NOTE: You should generally be setting Team to Team only. Valid Relationships: R_NEUTRAL, R_ALLY, R_ENEMY, R_UNDEFINED
2
3
4
5
6
# 其他资源
# discord
有一个非官方 discord 频道,有很多 aoe4 玩家和 mod 经验。邀请链接 https://discord.gg/gyjVtxpK6U
General
Official resources - good place to start if you're new to modding AoE4: https://support.ageofempires.com/hc/en-us/sections/360012376652
Wiki editable by anyone: https://github.com/aoemods/wiki/wiki/ (old wiki: https://wiki.aoemods.com/wiki/Main_Page)
Mod browser (requires login): https://www.ageofempires.com/mods/mods-search/?q=&game=4
Our 🐣faq
Scripting
Scripting types (WIP): https://aoemods.github.io/aoetypes-docs/
With the -dev parameter the ingame console can be opened with CTRL + Shift + Tilde (key over tab). Console commands are lua / scar (eg. restart)
Generated Maps
Debug Generated Maps in your browser: https://aoe4.app/ (source: https://github.com/Drumsin/aoe4-generated-map-debugger)
Misc
SGA archive viewer: https://github.com/Janne252/essence-archive-viewer
C# library, CLI and GUI for all AOE4/Essence files (currently .sga read-write, .rrtex read, .rrgeom read, .rgd read-write, .rrmaterial read): https://github.com/aoemods/AOEMods.Essence
attrib.sga dump (holds all the damage values etc.): https://github.com/aoemods/attrib
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
wiki https://github.com/aoemods/wiki/wiki/Command-line-parameters
# 安装 TS to LUA
社区有一个开源解决方案,编写 TS 脚本,自动转换为 LUA。这个深得我心,作为一个业余前端,写 TS/JS 要比 LUA 省心太多了。而且社区维护了一个类型库,可以推导参数类型,Very good。所以直接用起来。
git clone https://github.com/aoemods/aoe4-typescript-template
cd ./aoe4-typescript-template
npm i --force
npm run dev
2
3
4
这里用--force 是因为库两年没维护了,不强制会报错。 dev 命令运行前要先创新 mod 项目,并把 scar 的路径放入.env。参照 readme。 这样就可以热更新了。
# 以下是一些临时调试脚本
function Mod_SpwanMainCharactor()
local player = Core_GetPlayersTableEntry(Game_GetLocalPlayer())
local sbp_knight = BP_GetSquadBlueprint("unit_knight_3_eng")
local pos1 = Util_GetOffsetPosition(player.town_center.position, 40, 10)
local sgroup_name = "sg_player_knight_" .. tostring(player.id)
local sg_player_knight = SGroup_CreateIfNotFound(sgroup_name)
UnitEntry_DeploySquads(player.id, sg_player_knight, {{sbp = sbp_knight, numSquads = 1}}, pos1)
print(sg_player_knight)
local sg = SGroup_GetSpawnedSquadAt(sg_player_knight, 1)
end
player = Core_GetPlayersTableEntry(Game_GetLocalPlayer())
sgroup_name = "sg_player_knight_" .. tostring(player.id)
sg_player_knight = SGroup_CreateIfNotFound(sgroup_name)
print(sgroup_name)
print(sg_player_knight)
sg = SGroup_GetSpawnedSquadAt(sg_player_knight, 1)
sq = SGroup_GetSquadAt(sg_player_knight, 1)
et0=Squad_GetFirstEntity(sq)
Squad_SetAnimatorEvent(sq, String actionName )
Squad_SetAnimatorAction(sq,"HPAT_MovementLooping")
Squad_SetAnimatorAction(sq,SCMD_Attack)
-- local et = Squad_EntityAt(sq, 0)
local et2 = Squad_SBPEntityAt(sq, 0)
local et3= Squad_SBPEntityAt(sbp_knight, 0) //这个拿出的是PBG(ID,ModPack,Type)
-- // 疑似
-- // Squad_SBPEntityAt
-- // 原为 Squad_EntityAt ,方法已不存在
-- 测试Squad_GetFirstEntity
const sg = SGroup_GetSpawnedSquadAt(squadGroup, 1)
// const et = Squad_EntityAt(sg, 0)
const et = Squad_SBPEntityAt(sg.SquadID, 0)
-- 再测试一下 EGroup_GetEntityAt
et4 = EGroup_GetEntityAt(sg_player_knight.SGroupID, 0)
printa(sg_player_knight)
Squad_SetAnimatorAction( SquadID squadid, String actionName )
Trigger animation action for a squad. Please only use this for simple animations
Squad_SetAnimatorEvent( SquadID squadid, String actionName )
Trigger animation event for a squad. Please only use this for simple animations
Squad_SetAnimatorState( SquadID squadid, String stateMachineName, String stateName )
Set animation state of a state machine for a squad. Please only use this for simple animations
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
# 队伍相关
Core_GetActiveTeamCount()
Returns the number of non-eliminated teams remaining.
Core_AddPlayerToTeam( PlayerID player, Team teams_table )
Has a player join a specific team
Core_GetPlayerTeamsEntry( PlayerID player )
Returns the table reference of a player's TEAMS table entry
Core_GetTeamsEntryFromIndex( Integer index ) //1 2 3
Returns the entry in the Teams table that corresponds to the given teamIndex.
Core_IsPlayerOnPlayerTeam()
Returns true if the two given players are on the same team.
Core_SetTeamDefeated( Team teams_table, Table presentationTable )
Sets if a team is defeated
Core_SetTeamVictorious( Team teams_table, function presentationFunction, winReason reason )
Sets a team as the match winner.
Player_GetTeam( Player& p )
Get the team a player is on
Team_GetRelationship( Integer team1, Integer team2 )
Returns the relationship between 2 teams.
Setup_SetPlayerTeam( PlayerID p, Integer team_id )
Put a player in a team. Use TEAM_NEUTRAL as the team_id to set the player as neutral
team = Core_GetTeamsEntryFromIndex(1)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47