本文用于记录本人的第一个noita的mod开发过程
新建文件夹
第一步,在mods文件夹下新建文件夹
一个模组至少包括两个文件,mod.xml和init.lua
关于.xml文件
在pom.xml里已经被折磨过见过了,重新从头认识一下:eXtensible Markup Language,意为"可扩展标记语言"。
声明(可以没有,noita的文件都没有):
<?xml version="1.0" encoding="UTF-8"?>
交代编码方式
元素和属性
元素用成对的标签包裹内容,属性附着在起始标签上提供附加信息,必须加引号
<Mod
name="Daily Practice Run"
description="You can practice by replaying the same seed within 24 hours."
ui_newgame_name="$gamemode_daily_practice_run"
ui_newgame_description="$gamemode_daily_practice_run_desc"
ui_newgame_gfx_banner_bg="mods/daily_practice/files/menu_banner_background.png"
ui_newgame_gfx_banner_fg="mods/daily_practice/files/menu_banner_overlay.png"
request_no_api_restrictions="0"
is_game_mode="1"
>
</Mod>
空元素
自闭合
<br />
<item type="wand" />
注释
同html
<!-- 这是一个魔法物品列表 -->
实体引用
一种用特殊字符来表示另一些字符的机制。比如"<"会被解析成一个标签开头,可以用一套以&开头;结尾的字符替代
| 字符 | 实体引用 | 含义 |
|---|---|---|
| < | < | less than(小于) |
| > | > | greater than(大于) |
| & | & | ampersand(&符号) |
| " | " | quotation mark(双引号) |
| ’ | ' | apostrophe(单引号) |
CDATA 区块
当你不想让某些字符被解析成标签,可以放入 CDATA:
<script><![CDATA[
if (hp < 10) then explode()
]]></script>
添加到游戏里的mod列表
我们已经在对应目录下创建了文件夹,接下来只要正确初始化mod.xml和init.lua就可以了
对于最简的可实现在mod列表里看见的模组,xml文件里Mod元素写好name和description属性,init.lua空着即可
ESC系统——游戏如何运行
要了解Noita的游戏逻辑,就需要了解“实体组件系统”(ECS,Entity Component System)
noita以 Lua 和 XML 语言实现ESC,游戏中每个独立的 “东西”(敌人、子弹、魔杖、物品、包括玩家)都是实体(Entity),只不过构成它们的组件不同。
比如sheep_fly.xml文件:
<Entity tags="mortal,prey,sheep" name="$animal_sheep_fly">
<Base file="data/entities/base_helpless_animal.xml" >
<AnimalAIComponent
preferred_job="Escaping"
attack_melee_enabled="0"
attack_ranged_entity_file=""
attack_ranged_predict="1"
attack_ranged_enabled="0"
attack_melee_enabled="0"
attack_melee_damage_min="0"
attack_melee_damage_max="0"
can_fly="1"
food_material="grass"
needs_food="0"
>
</AnimalAIComponent >
<DamageModelComponent
ragdoll_offset_y="-11"
ragdoll_filenames_file="data/ragdolls/sheep_fly/filenames.txt"
hp="0.2"
materials_create_messages="1"
materials_that_create_messages="grass"
>
</DamageModelComponent>
<SpriteComponent
image_file="data/enemies_gfx/sheep_fly.xml"
offset_x="10"
offset_y="12"
>
</SpriteComponent>
<SpriteAnimatorComponent
rotate_to_surface_normal="0" >
</SpriteAnimatorComponent>
<PathFindingGridMarkerComponent
marker_work_flag="24"
>
</PathFindingGridMarkerComponent>
<GenomeDataComponent
herd_id="helpless"
food_chain_rank="20"
is_predator="0"
>
</GenomeDataComponent>
<CharacterPlatformingComponent
jump_velocity_y="0"
run_velocity="10"
>
</CharacterPlatformingComponent>
<HitboxComponent
_enabled="1"
aabb_max_x="6"
aabb_max_y="4"
aabb_min_x="-6"
aabb_min_y="-7"
>
</HitboxComponent>
<CharacterDataComponent
collision_aabb_min_x="-3.0"
collision_aabb_max_x="3.0"
collision_aabb_min_y="-6"
collision_aabb_max_y="2"
buoyancy_check_offset_y="-3"
mass="1.4"
>
</CharacterDataComponent>
<PathFindingComponent
can_jump="0"
jump_speed="0"
can_walk="0"
can_fly="1"
initial_jump_max_distance_x="0"
initial_jump_max_distance_y="0"
>
</PathFindingComponent>
</Base>
<AudioComponent
file="data/audio/Desktop/animals.bank"
event_root="animals/sheep" >
</AudioComponent>
<AudioLoopComponent
file="data/audio/Desktop/animals.bank"
event_name="animals/wing_flap_insect/movement_loop"
set_speed_parameter="1"
set_speed_parameter_only_based_on_y_movement="1"
auto_play="1">
</AudioLoopComponent>
</Entity>
Base file标签是一个继承,后续是添加更多新组件,比如动物AI组件等等
继承规则详解
include_children="1": 继承所有子实体- 同名组件会被完全覆盖,而不是合并
- 新组件会被添加到现有组件列表
- 支持多级继承链
- 子实体也支持继承机制
虚拟文件系统(VFS)——模组如何生效
VFS工作原理:
- 文件路径重定向: 当游戏请求读取某个文件时,VFS首先检查是否有模组提供了该文件的替换版本
- 内容合并: 对于支持追加的文件(如Lua脚本),VFS将原始内容与模组内容合并
- 优先级处理: 多个模组修改同一文件时,按照模组加载顺序确定优先级
- 透明访问: 游戏代码无需修改,通过VFS透明地访问合并后的内容
物理文件系统布局:
Noita/
├── data/ # 游戏原始数据
│ ├── scripts/
│ ├── entities/
│ └── materials.xml
├── mods/
│ ├── mod_a/
│ │ ├── mod.xml
│ │ ├── init.lua
│ │ └── files/
│ │ ├── scripts/
│ │ └── entities/
│ └── mod_b/
│ └── files/
虚拟文件系统映射:
data/scripts/gun/gun_actions.lua -> [原始文件] + [mod_a追加] + [mod_b追加]
data/entities/animals/sheep.xml -> [mod_a覆盖版本] (如果存在)
data/materials.xml -> [原始文件] + [mod_a材料] + [mod_b材料]
也就是说,VFS系统给我们提供了方便地修改/增添游戏内容的机制
一个简单的实操
目标是实现“混乱变形魔药固定变成蠕虫”这一功能
略过创建文件的过程
首先需要确定“变形”这一效果是如何实现的:
data/materials.xml 这是一个一万七千行的大文件,魔药的定义就在其中,而产生的效果定义在另外的文件中
<CellData
name="magic_liquid_random_polymorph"
ui_name="$mat_magic_liquid_random_polymorph"
tags="[liquid],[water],[magic_liquid],[magic_polymorph],[impure]"
burnable="0"
density="4.15"
cell_type="liquid"
wang_color="8091448D"
generates_smoke="0"
liquid_gravity="0.8"
liquid_sand="0"
gfx_glow="150"
on_fire="0"
requires_oxygen="0"
liquid_stains="1"
liquid_sprite_stains_status_threshold="0.25"
liquid_sprite_stain_shaken_drop_chance="5"
status_effects="POLYMORPH_RANDOM"
audio_materialaudio_type="MAGICAL"
show_in_creative_mode="1"
liquid_sprite_stains_check_offset="1"
>
<ParticleEffect
vel.y="0"
vel_random.min_x="-17.43"
vel_random.max_x="17.43"
vel_random.min_y="-45.75"
vel_random.max_y="40"
lifetime.min="5"
lifetime.max="10"
gravity.y="0"
render_on_grid="1"
draw_as_long="1"
airflow_force="1.974"
airflow_scale="0.1371"
friction="3.714"
probability="0.0857"
count.min="0"
count.max="1"
>
</ParticleEffect>
<StatusEffects>
<Ingestion>
<StatusEffect type="POLYMORPH_RANDOM" amount="0.2" />
</Ingestion>
</StatusEffects>
</CellData>
具体属性我们并不关心,只关心会产生一个叫POLYMORPH_RANDOM的效果
data/entities/misc/effect_polymorph_random.xml
<Entity>
<InheritTransformComponent>
</InheritTransformComponent>
<GameEffectComponent
effect="POLYMORPH_RANDOM"
frames="600"
disable_movement="0"
polymorph_target="[RANDOM]"
>
</GameEffectComponent >
<Entity name="explosion">
<InheritTransformComponent>
</InheritTransformComponent>
<SpriteParticleEmitterComponent
sprite_file="data/particles/puff_0$[1-3].xml"
delay="0"
lifetime="0"
color.r="1" color.g="1" color.b="1" color.a="1"
color_change.r="0" color_change.g="0" color_change.b="0" color_change.a="-2"
velocity.x="0" velocity.y="0"
gravity.x="0" gravity.y="10"
velocity_slowdown="0"
rotation="0"
angular_velocity="0"
use_velocity_as_rotation="0"
scale.x="1" scale.y="1"
scale_velocity.x="0" scale_velocity.y="0"
emission_interval_min_frames="1"
emission_interval_max_frames="1"
count_min="5" count_max="10"
randomize_rotation.min="-3.1415"
randomize_rotation.max="3.1415"
randomize_position.min_x="-10"
randomize_position.max_x="10"
randomize_position.min_y="-10"
randomize_position.max_y="10"
randomize_angular_velocity.min="-4"
randomize_angular_velocity.max="4"
randomize_velocity.min_x="-20"
randomize_velocity.max_x="20"
randomize_velocity.min_y="-20"
randomize_velocity.max_y="20"
>
</SpriteParticleEmitterComponent>
<LifetimeComponent
lifetime="10">
</LifetimeComponent>
</Entity>
<LuaComponent
execute_on_removed="1"
execute_every_n_frame="-1"
script_source_file="data/scripts/projectiles/polymorph_explosion.lua" >
</LuaComponent>
<AudioComponent
file="data/audio/Desktop/misc.bank"
event_root="game_effect/polymorph" >
</AudioComponent>
</Entity>
发现决定了变形目标的代码是: polymorph_target=“[RANDOM]”
作为对比,这是变形魔药(变羊)对应的部分:polymorph_target=“data/entities/animals/sheep.xml”
那么模组的制作思路就一目了然了——把蠕虫文件换进去。而新的问题也出现了,蠕虫有五种:大、中、小、地狱的、艺之神殿的,而xml如果写路径只能放一个
只好进一步研究[RANDOM]的实现了,有一个polymorph表,游戏启动时会把可变形的生物加进去
需要借助nolla提供的api来修改,这里要用到PolymorphTableSet和PolymorphTableGet
实现代码:
<!-->mod.xml<!-->
<Mod
name="Always Worm"
description="将混乱变形效果限制为只在基础蠕虫系列中随机变形 (worm, worm_big, worm_tiny, worm_end, worm_skull)"
author="nil"
request_no_api_restrictions="0"
is_game_mode="0"
>
</Mod>
-- init.lua
-- 设置polymorph表只包含各种蠕虫
function set_worm_only_polymorph()
local worm_entities = {
"data/entities/animals/worm.xml", -- 普通蠕虫
"data/entities/animals/worm_big.xml", -- 大蠕虫
"data/entities/animals/worm_tiny.xml", -- 小蠕虫
"data/entities/animals/worm_end.xml", -- 地狱蠕虫
"data/entities/animals/worm_skull.xml" -- 幽灵蠕虫
}
PolymorphTableSet(worm_entities, false) -- 核心API
PolymorphTableSet({}, true)
end
-- 模组初始化
function OnModPostInit()
set_worm_only_polymorph()
end
function OnWorldInitialized()
end
-- nothing
function OnWorldPreUpdate()
end
把文件夹放到指定目录里,完成,比想象中简单
如何新增法术/物品,在游戏窗口绘制交互图标有待进一步学习