noita_mode_0

本文用于记录本人的第一个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工作原理:

  1. 文件路径重定向: 当游戏请求读取某个文件时,VFS首先检查是否有模组提供了该文件的替换版本
  2. 内容合并: 对于支持追加的文件(如Lua脚本),VFS将原始内容与模组内容合并
  3. 优先级处理: 多个模组修改同一文件时,按照模组加载顺序确定优先级
  4. 透明访问: 游戏代码无需修改,通过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

把文件夹放到指定目录里,完成,比想象中简单

如何新增法术/物品,在游戏窗口绘制交互图标有待进一步学习