This tutorial describes how to add your own explosion particles to the game with my Custom Particle Loader mod. The mod is available on GitHub Closer_ex 7D2D mods and NexusMods: A20_Custom_Explosion_Particle_Loader
Before jumping into the water, I assume you have prepare yourself with the knowledge of unity particle system and a Visual Studio installed following SphereII's video. You do not need to know anything about programming if you only need the visual and audio effect working however. I'm no way an expert on unity or C#, so feel free to point out what I'm doing wrong.
First step: export your asset bundle (and attached scripts, if exist)
[No longer needed]Second step(if you have attached scripts): compile your assembly
Special note for those running a dedicated server: some scripts trying to access camera won't throw compile errors, but will floor your server log with NREs. Better test all your particles on the server and delete those scripts and script components (or transforms containing the components) accordingly, or comment out usage of methods accessing camera.
Third step(optional, coding experience required): write your gameplay scripts
Final step: XML editing
You can also use following format:
<property class="Explosion">
<property name="ParticleIndex" value="4"/> <!-- which prefab/particle is used -->
<property name="RadiusBlocks" value="3.5"/> <!-- damage radius for blocks -->
<property name="BlockDamage" value="500"/> <!-- damage for blocks in the center of the explosion -->
<property name="RadiusEntities" value="5"/> <!-- damage radius for entities -->
<property name="EntityDamage" value="250"/> <!-- damage for entities in the center of the explosion -->
</property>
The names are assembled during xml parsing.
I hope this tutorial is detailed enough to get you started. If you have any questions, comment freely or find me on discord: A tiny channel in Guppy's discord server
Credit to Guppycur and Zilox for testing out my messy early version of this mod. Really helped a lot with debugging and expanding functionality!
Before jumping into the water, I assume you have prepare yourself with the knowledge of unity particle system and a Visual Studio installed following SphereII's video. You do not need to know anything about programming if you only need the visual and audio effect working however. I'm no way an expert on unity or C#, so feel free to point out what I'm doing wrong.
First step: export your asset bundle (and attached scripts, if exist)
I presume you already know how to do basic exporting, so this part is pretty simple:
For particles without scripts, you can just export them the same way as other prefabs.
For those with attached scripts however, you need to make some changes.
For particles without scripts, you can just export them the same way as other prefabs.
For those with attached scripts however, you need to make some changes.
- Create a new project that will contain all your further particles. Click Edit -> Project Settings -> Player, locate the combobox named Api Compatibility Level, set the value to .NET 4.x. You can also check Allow 'unsafe' Code, but I'm not sure if it's necessary.
- Navigate to the folder containing ALL your scripts. It can contain subfolders with scripts, that doesn't matter. Important note: We all know we need that MultiPlatformExportAssetBundles.cs to export our bundle. What you probably don't know is that it also causes your custom assembly compiling to fail, so let's keep it away from other scripts. For example, you can put the export script in the root Assets folder and keep other particle packages in separate folders, then create one assembly definition per package; or you can keep all the particle packages in one folder and create one assembly definition in the folder to cover all the packages. Your call. See the picture below for more descriptive details.
- Right click anywhere, and hit Create -> Assembly Definition.
- Now rename it to a desired name. Check the Inspector to make sure it is your desired name.
- Export your prefabs as usual.
- I made some mistakes before, let's follow the right track now. Search for Editor in your Assets and delete all folders with that name, along with all scripts in the result containing using UnityEditor. This is easier than trying to exclude them in the build.
Now navigate to your unity project folder, go to Library/ScriptAssemblies, and you will find your compiled assembly there. Drag it into your mod folder( where your Modinfo.xml is located ).In theory this should work; but if it does not, refer to old step 2, or help me figure out what's wrong. Not working for me currently and I don't have time to figure out why.
- Now click File -> Build Settings, in the popup window click Build, then in the popup file browser continue with the default selected folder. This will fail and throw errors for sure, but we don't need the whole project.
- Navigate to your unity project folder, go to Library/PlayerScriptAssemblies and you'll find the valid assemblies with the above names there. Copy it/them to your mod folder (put it/them where your ModInfo.xml is located) and you are done with these scripts.
An assembly definition covering all scripts in one package folder and its subfolders.
MultiPlatformExportAssetBundles.cs is placed in root Assets folder so that it's not included in any assembly definition.
Assembly name is set here, not the asset file name.
[No longer needed]
Thanks Sphereii and Laydor for correcting me; now that you can have multiple assembly in one mod folder, you can have unity compile attached scripts for you, and write your harmony patches in VS. Just keep in mind that only one IModApi subclass is allowed per mod, usually the init.cs. Thus this step is now obsolete.
However, if unity exported assemblies don't work, you can still fall back to this method.
If you have no experience with coding at all, I suggest you to follow sphereii's harmony tutorial video till your project is created and you are editing the assembly name.
First of all, change the assembly name to the mentioned Assembly Definition name in section 1, then you can follow the rest of the property editing. Delete the default Class1.cs, and that init.cs in video is also unnecessary in our case.
Now that you've got the project set up, you can drag the folder containing all your scripts into the project, and hit that build button. Check for errors if it failed.
Compilation failures are often caused by some scripts that only work in Unity Editor, if some scripts contains
using UnityEditor.Build
and is causing error, you can safely delete those scripts.
Another case is scripts are checking for unity version and performing differently according to the version. The problem is VS doesn't have a unity version defined, so it will always be the last "else" being true.
For example, the following code will cause an error, because Camera.main.hdr doesn't exist in newer unity anymore.
bool IsSupportedHdr()
{
#if UNITY_5_6_OR_NEWER
return Camera.main.allowHDR;
#else
return Camera.main.hdr;
#endif
}
In this case, you should delete all the # statements, and keep only codes under the right version. For the code above, the result should be:
bool IsSupportedHdr()
{
return Camera.main.allowHDR;
}
I'm only aware of these two culprits for compilation error, if you encounter something else, feel free to ask in the discord linked below
However, if unity exported assemblies don't work, you can still fall back to this method.
If you have no experience with coding at all, I suggest you to follow sphereii's harmony tutorial video till your project is created and you are editing the assembly name.
First of all, change the assembly name to the mentioned Assembly Definition name in section 1, then you can follow the rest of the property editing. Delete the default Class1.cs, and that init.cs in video is also unnecessary in our case.
Now that you've got the project set up, you can drag the folder containing all your scripts into the project, and hit that build button. Check for errors if it failed.
Compilation failures are often caused by some scripts that only work in Unity Editor, if some scripts contains
using UnityEditor.Build
and is causing error, you can safely delete those scripts.
Another case is scripts are checking for unity version and performing differently according to the version. The problem is VS doesn't have a unity version defined, so it will always be the last "else" being true.
For example, the following code will cause an error, because Camera.main.hdr doesn't exist in newer unity anymore.
bool IsSupportedHdr()
{
#if UNITY_5_6_OR_NEWER
return Camera.main.allowHDR;
#else
return Camera.main.hdr;
#endif
}
In this case, you should delete all the # statements, and keep only codes under the right version. For the code above, the result should be:
bool IsSupportedHdr()
{
return Camera.main.allowHDR;
}
I'm only aware of these two culprits for compilation error, if you encounter something else, feel free to ask in the discord linked below

Special note for those running a dedicated server: some scripts trying to access camera won't throw compile errors, but will floor your server log with NREs. Better test all your particles on the server and delete those scripts and script components (or transforms containing the components) accordingly, or comment out usage of methods accessing camera.
Third step(optional, coding experience required): write your gameplay scripts
With the latest update, you are now able to patch the loader to parse custom properties and create universal scripts. It's recommended to begin with source code of the other 2 patches in my repo.
Ever wonder why molotov fire can lit people up without any xml definition? That is done in the particle scripts.
There are 3 special script classes working with particles in this game: TemporaryObject, ExplosionDamageArea and AudioPlayer. GameManager set value for some of their fields right after particle initialization.
Note that when you subclass above scripts, you should always implement all unity message functions the base class have, unless you do need that base class implementation to be called by unity.
You can also specify your own monoscripts. I have all the params from GameManager.explode stored in
CustomExplosionManager.LastInitializedComponent.CurrentExplosionParams
and
CustomExplosionManager.LastInitializedComponent.CurrentItemValue
to help you setup your scripts. You should access these data ONLY in Awake(), and make sure you call Clone() when copying ItemValue, and keep it SEPARATELY instead of inside a MinEventParams, if you are going to FireEvent from that initiator and need it fires exactly with the item that causes the explosion. Check my MedicGrenadeParticleData.cs included with the mod for more details.
ATTENTION: You should disable your logical scripts on client side by setting enabled to false in Awake(), and add an early return in every unity message methods( except Start() , FixedUpdate() and Update() though, they won't be called if a script is disabled). This will assure that only server is processing data, and thus keep all states synced. Also check my MedicGrenadeParticleData.cs for reference.
Ever wonder why molotov fire can lit people up without any xml definition? That is done in the particle scripts.
There are 3 special script classes working with particles in this game: TemporaryObject, ExplosionDamageArea and AudioPlayer. GameManager set value for some of their fields right after particle initialization.
- TemporaryObject keeps the Explosion.Duration property in xml, set that duration to all particle components of your prefab, and destroy the particle object after that duration. This may result in particle animation speed change if you have something like velocity over lifetime enabled on the particle, so subclassing is not recommended. If you only need your particle destroyed after that duration, I have an AutoRemover script automatically added to the root object of your particle if TemporaryObject is not presented, simply having Explosion.Duration property in xml will get it working.
- ExplosionDamageArea keeps the Explosion.Buff property in xml and the entity id of the initiator. The vanilla implementation have a private function that retrieves EntityAlive from Collider, you can copy the code to your script to get all entities in your root object's collider. Note that you need to check "is Trigger" on that collider to receive OnTriggerEnter, OnTriggerStay, and OnTriggerExit unity messages.
- AudioPlayer is an audio player. It originally uses no properties in xml, but I have 2 custom properties defined to get it working with your sound. The difference of this class and your own audio player scripts is that server will play the SoundNode in sound.xml at explosion location, thus producing noise.
Note that when you subclass above scripts, you should always implement all unity message functions the base class have, unless you do need that base class implementation to be called by unity.
You can also specify your own monoscripts. I have all the params from GameManager.explode stored in
CustomExplosionManager.LastInitializedComponent.CurrentExplosionParams
and
CustomExplosionManager.LastInitializedComponent.CurrentItemValue
to help you setup your scripts. You should access these data ONLY in Awake(), and make sure you call Clone() when copying ItemValue, and keep it SEPARATELY instead of inside a MinEventParams, if you are going to FireEvent from that initiator and need it fires exactly with the item that causes the explosion. Check my MedicGrenadeParticleData.cs included with the mod for more details.
ATTENTION: You should disable your logical scripts on client side by setting enabled to false in Awake(), and add an early return in every unity message methods( except Start() , FixedUpdate() and Update() though, they won't be called if a script is disabled). This will assure that only server is processing data, and thus keep all states synced. Also check my MedicGrenadeParticleData.cs for reference.
Final step: XML editing
Xml editing is pretty much the same as what you deal with ordinary mods, except that I have some custom properties defined. All the properties are listed below.
<!-- all the scripts you add to a item/block/itemaction work in pair with the fullname of Explosion.ParticleIndex /-->
<!-- which means all particles with the same fullname will have exactly the same scripts added to them, according to the order they are loaded /-->
<!-- path begins with #@modfolder(modname) and ends with .unity3d, modname is the name specified in Modinfo.xml /-->
<!-- assetname is the prefab in your resource file, WITHOUT filename extension(.prefab) /-->
<!-- postfix is used to change the fullname without loading redundant assets, used when you need different scripts on the same particle /-->
<property name="Explosion.ParticleIndex" value="path?assetname$postfix"/>
<!-- $ is name splitter /-->
<property name="Explosion.CustomScriptTypes" value="namespace.classname,assemblyname$namespace.classname,assemblyname"/>
<!-- Overwrite means you are applying your current script setting to all the particle with the same fullname, and overrides the former one /-->
<property name="Explosion.Overwrite" value="true/false"/>
<!-- the SoundNode name in sound.xml /-->
<property name="Explosion.AudioName" value="soundname"/>
<!-- if the audio file duration is shorter than this duration it will stop halfway, you can set Loop="true" in AudioClip node to loop /-->
<property name="Explosion.AudioDuration" value="duration"/>
<!-- whether this particle should be sent to newly connected clients. by default new clients won't be able to see particles spawned before they connect, adding this property will tell server to send essential data of this particle to those clients to help sync particle state. /-->
<!-- by default, position, rotation and remaining lifetime is sent. more properties can be added by script./-->
<property name="Explosion.SyncOnConnect" value="true/false"/>
About the path: you are probably familiar with #@modfolder:blahblah, but what's the difference and relation with #@modfolder(modname):blahblah? Well, all #@modfolder: are replaced by #@modfolder(modname): during parsing xml files in mod folder, where the modname is the name in Modinfo.xml of current mod. So if you are using assets in current mod folder, then (modname) can be omitted; when you are using assets from other mods, you need to add that name.
If you have Explosion.AudioName set and specify no AudioPlayer subclass in CustomScriptTypes, I'll add a default AudioPlayer script for you and set the sound name. AudioDuration is -1 by default which means looping forever (if clip is set to loop) until particle gets destroyed.
If you don't work with scripts or do not know how to destroy the particle object, make sure you have Explosion.Duration set to a proper value so that it gets actually removed from memory. This is because I keep a reference to each initialized particle in order to destroy unfinished ones on exiting game, and if you don't call destroy() on the root object it won't be unreferenced, thus GC won't recycle memory from it. Setting duration will tell my AutoRemover script to destroy them after that interval, and unref it when the script component is destroyed.
If you are interested in how ParticleIndex with such string works, you can refer to my source code on GitHub. I'll give you a brief intro here.
Spoiler
Basically every thing can be hashed into a unique hash number. That's the principle, but actually more is done to keep it collision-safe.
The game defines ParticleIndex as int32, but does a Greater Than 0 check on projectiles, as arrows and bolts are also projectiles and they shouldn't trigger an explosion at most times. And then it's cut into int16 on NetPackage setup, thus leaving only 32746 indexes at our disposal, under which circumstances the risk of hash collision is not something that can be safely neglected.
So actually 2 hashmaps is used to retain the safety. First one is a dictionary pairing fullpath with CustomParticleComponents which stores your custom script types, and the second one pairing the hashed index with fullpath. The index is generated checked at loading time to resolve collision, ensuring everyone gets a unique index. It's then written into the DynamicProperty and been parsed by ExplosionData.
<!-- all the scripts you add to a item/block/itemaction work in pair with the fullname of Explosion.ParticleIndex /-->
<!-- which means all particles with the same fullname will have exactly the same scripts added to them, according to the order they are loaded /-->
<!-- path begins with #@modfolder(modname) and ends with .unity3d, modname is the name specified in Modinfo.xml /-->
<!-- assetname is the prefab in your resource file, WITHOUT filename extension(.prefab) /-->
<!-- postfix is used to change the fullname without loading redundant assets, used when you need different scripts on the same particle /-->
<property name="Explosion.ParticleIndex" value="path?assetname$postfix"/>
<!-- $ is name splitter /-->
<property name="Explosion.CustomScriptTypes" value="namespace.classname,assemblyname$namespace.classname,assemblyname"/>
<!-- Overwrite means you are applying your current script setting to all the particle with the same fullname, and overrides the former one /-->
<property name="Explosion.Overwrite" value="true/false"/>
<!-- the SoundNode name in sound.xml /-->
<property name="Explosion.AudioName" value="soundname"/>
<!-- if the audio file duration is shorter than this duration it will stop halfway, you can set Loop="true" in AudioClip node to loop /-->
<property name="Explosion.AudioDuration" value="duration"/>
<!-- whether this particle should be sent to newly connected clients. by default new clients won't be able to see particles spawned before they connect, adding this property will tell server to send essential data of this particle to those clients to help sync particle state. /-->
<!-- by default, position, rotation and remaining lifetime is sent. more properties can be added by script./-->
<property name="Explosion.SyncOnConnect" value="true/false"/>
About the path: you are probably familiar with #@modfolder:blahblah, but what's the difference and relation with #@modfolder(modname):blahblah? Well, all #@modfolder: are replaced by #@modfolder(modname): during parsing xml files in mod folder, where the modname is the name in Modinfo.xml of current mod. So if you are using assets in current mod folder, then (modname) can be omitted; when you are using assets from other mods, you need to add that name.
If you have Explosion.AudioName set and specify no AudioPlayer subclass in CustomScriptTypes, I'll add a default AudioPlayer script for you and set the sound name. AudioDuration is -1 by default which means looping forever (if clip is set to loop) until particle gets destroyed.
If you don't work with scripts or do not know how to destroy the particle object, make sure you have Explosion.Duration set to a proper value so that it gets actually removed from memory. This is because I keep a reference to each initialized particle in order to destroy unfinished ones on exiting game, and if you don't call destroy() on the root object it won't be unreferenced, thus GC won't recycle memory from it. Setting duration will tell my AutoRemover script to destroy them after that interval, and unref it when the script component is destroyed.
If you are interested in how ParticleIndex with such string works, you can refer to my source code on GitHub. I'll give you a brief intro here.
Spoiler
Basically every thing can be hashed into a unique hash number. That's the principle, but actually more is done to keep it collision-safe.
The game defines ParticleIndex as int32, but does a Greater Than 0 check on projectiles, as arrows and bolts are also projectiles and they shouldn't trigger an explosion at most times. And then it's cut into int16 on NetPackage setup, thus leaving only 32746 indexes at our disposal, under which circumstances the risk of hash collision is not something that can be safely neglected.
So actually 2 hashmaps is used to retain the safety. First one is a dictionary pairing fullpath with CustomParticleComponents which stores your custom script types, and the second one pairing the hashed index with fullpath. The index is generated checked at loading time to resolve collision, ensuring everyone gets a unique index. It's then written into the DynamicProperty and been parsed by ExplosionData.
You can also use following format:
<property class="Explosion">
<property name="ParticleIndex" value="4"/> <!-- which prefab/particle is used -->
<property name="RadiusBlocks" value="3.5"/> <!-- damage radius for blocks -->
<property name="BlockDamage" value="500"/> <!-- damage for blocks in the center of the explosion -->
<property name="RadiusEntities" value="5"/> <!-- damage radius for entities -->
<property name="EntityDamage" value="250"/> <!-- damage for entities in the center of the explosion -->
</property>
The names are assembled during xml parsing.
I hope this tutorial is detailed enough to get you started. If you have any questions, comment freely or find me on discord: A tiny channel in Guppy's discord server
Credit to Guppycur and Zilox for testing out my messy early version of this mod. Really helped a lot with debugging and expanding functionality!
Last edited by a moderator: