Jump to content

Using custom explosion particles with working scripts in A20


closer_ex

Recommended Posts

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)

Spoiler

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.

  1. 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.
  2. 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.
  3. Right click anywhere, and hit Create -> Assembly Definition.
  4. Now rename it to a desired name. Check the Inspector to make sure it is your desired name.
  5. Export your prefabs as usual.
  6.  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. 
  7. 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.
  8. 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.

1134095304_CEJP)L1DVD3MNXAX0)CZM.png.065469b14b293947597b7eef9d3b4893.png

An assembly definition covering all scripts in one package folder and its subfolders.

357546817_CWZADOK@0K7D44RAD.thumb.png.c6882833d846fd7194e28daa7dc09d8f.png

MultiPlatformExportAssetBundles.cs is placed in root Assets folder so that it's not included in any assembly definition.

 

1936075512_URU5YX@F0QS40EWJ.png.a58f7cf338839ff8148992497d77652e.png

Assembly name is set here, not the asset file name.

 

 

 

[No longer needed]Second step(if you have attached scripts): compile your assembly

Spoiler

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 :)

 

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

Spoiler

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.

 

  • 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

Spoiler

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.

 

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!

Edited by closer_ex
update (see edit history)
Link to comment
Share on other sites

  • 2 weeks later...

WIP: updating tutorial for latest features

 

May 26 update:

  • Changed the way custom property works for better performance and simpler syntax.
  • Rename CustomParticleComponents to ExplosionComponentCustomParticleLoader to CustomExplosionManager for more unified naming standard.

 

Showcase video

Showcase for spawn entity group

Showcase for spawn loot group

 

In the latest GitHub version, I introduced a few new features along with 2 expansion patch on the loader to assist you with developing various functionalities. Here's the full documentation:

 

New Features

Spoiler

This update focuses mainly on scripting expandability. It may sound irrelevant to xml modders, but it's the basement of the other 2 patches, and also makes it more feasible to write reusable scripts with powerful functions.

 

Custom Properties

Spoiler

Each ExplosionComponent now contains a custom property field which can store any object type. The workflow of adding custom xml properties is as follows:

  1. Implement IExplosionPropertyParser.
  2. In MatchScriptType(), return the type of the script that accesses your custom properties. ParseProperty() is only invoked when the component contains the returned type.
  3. In Name(), return a unique identifier. You'll need to access your custom property with this string.
  4. In ParseProperty(), parse your custom properties from the DynamicProperties param, and output the custom property. Return true if successful, and false when failed. The output can be any type, it can holds more fields that stores other properties.
  5. Access your property with ExplosionComponent.TryGetCustomProperty() in your script. Again, make sure you only access it in Awake().

This is exactly how the following patches are done.

 

Network Sync

Spoiler

By default, particles with random properties won't be synced on clients as they are mostly visual effects. But what if those effects actually mean something? This is the basis of net sync, and you'll see a working example in Sub Explosion section.

 

All explosion params are sent to clients now.

 

To help with finding correct particle, a unique explosion id is attached to each explosion, stored in ExplosionParams._explId .

 

Now a NetSyncHelper script is added to the particle before any other custom scripts are added. It has multiple Actions reacting to different events to help sync stats on various occasions, working in pair. The Action list is as follows:

 

  • ClientConnected and ConnectedToServer is invoked when new clients connected to server.
  • ExplosionServerInit and ExplosionClientInit is invoked when server sends explosion info to clients.

 

Moreover, CustomExplosionManager also has an static Action named HandleClientInfo, which is invoked only on server when clients connected to server.

 

It may be tedious to add and remove those actions repeatedly in your own scripts. No worries: I've also included 3 MonoBehaviour subclasses to assist you with easy event responding.

 

TrackedBehaviour

Spoiler

These scripts templates are available:

  • TrackedBehaviourBase deals with all the Action delegate addition and removal for you. If you don't need to keep track of your particle belongings, inherit this class will simplify your work significantly.
  • TrackedBehaviour subclasses TrackedBehaviourBase, automatically keep your script tracked with a nested hashmap. Useful when you need to track multiple script on the same particle.
  • ReverseTrackedBehaviour subclasses TrackedBehaviourBase, also track with a nested hashmap. Useful when you need to track scripts on different particles with the same owner, which is, for example, an entity.

 

When subclassing these types, you need to assign value to following fields in Awake() when necessary:

  • syncOnConnect = true if you need to react to client connection events. Overriding OnClientConnected and OnConnectedToServer is also required in this case.
  • syncOnInit = true if you need to react to explosion initialization events. Overriding OnExplosionInitServer and OnExplosionInitClient is also required in this case.
  • handleClientInfo =true if you need to react to client connection event on server with the connecting client info. Overriding OnHandleClientInfo is also required in this case.
  • A valid key when you are subclassing the two subclass.

After initializing above fields, call base.Awake() to finish the setup. Also call base.OnDestroy() if you need to override OnDestroy, otherwise don't add your own OnDestroy.

 

When subclassing TrackedBehaviour or ReverseTrackedBehaviour, it is recommended to define the generic type as follows:

  • Myscript ReverseTrackedBehaviour<Myscript>

as the script references are stored in a static hashmap, and only generic with different templates creates unique static fields in subclasses.

 

Call TryGetValue(uint id, object key, out MonoBehaviour script) to retrieve your script and cast it to correct type.

 

 

Bound item class and explosion data

Spoiler

Now each ExplosionComponenthas a BoundItemClass and BoundExplosionData. They are bound with the first item using the fullpath of the component as ParticleIndex, or the last item with Explosion.Overwrite = true

 

For example, in the following xml codes, component of particle is bound with item1:

<item name="item1">
	<!-- ...... /-->
	<property name="Explosion.ParticleIndex" value="particle"/>
	<!-- ...... /-->
</item>
<item name="item2">
  	<!-- ...... /-->
	<property name="Explosion.ParticleIndex" value="particle"/>
	<!-- ...... /-->
</item>

 

In the next example, component of particle is bound with item2:

<item name="item1">
	<!-- ...... /-->
	<property name="Explosion.ParticleIndex" value="particle"/>
	<!-- ...... /-->
</item>
<item name="item2">
  	<!-- ...... /-->
	<property name="Explosion.ParticleIndex" value="particle"/>
  	<property name="Explosion.Overwrite" value="true"/>
	<!-- ...... /-->
</item>

 

And in this example, we generate 2 components with one particle, each with its own bound item and explosion data.

<item name="item1">
	<!-- ...... /-->
	<property name="Explosion.ParticleIndex" value="particle$postfix1"/>
	<!-- ...... /-->
</item>
<item name="item2">
  	<!-- ...... /-->
	<property name="Explosion.ParticleIndex" value="particle$postfix2"/>
	<!-- ...... /-->
</item>

 This will be used in the sub explosion patch.

 

 

Sub Explosion Expansion

Spoiler

The concept of this patch is triggering explosions when particles collide with something, in order to create cluster bombs and delayed explosions easily. I've got most work done in the scripts, so in this part we'll cover the rest xml and unity part.

 

Prepare your particle in unity

Spoiler

Enable collision

Since we are going to trigger explosion by collision, the first thing we need to do is enabling collision module on the particle systems you wish to trigger explosion with, and enable Send Collision Messages. Moreover, you need to set lifetime loss to 1 so that it dies after the collision.

 

Rename the transform

In order to find the correct transform, give your transforms with sub explosion particle system a unique name.

 

Handling clipping

Not sure if clipping is the word, but here is the demonstration of this situation:

44KKNBDSVY3JW4GF0.png.a1437f6f2c23580fccda0e95774a1004.png

When explosion happens on the wall, the center of your particle system will be located on the surface of the wall. If the spawn shape radius is greater than wall width, some particles might spawn on the other side of the wall.

To solve this problem, simply attach a collider to the particle system transform. Fit the collider shape and radius to the spawn shape module. Example: 

WQ5UVZQBG4QN6DH.thumb.png.7c0195ca37f8ca1338dd8393508780e4.png

Sphere spawn shape with sphere collider.

Valid collider types are: Sphere, Box and Capsule.

You can check IsTrigger, but I'll force it to be trigger in script.

 

Disable Auto Random Seed when needed

Uncheck auto random seed in main module when necessary. When is it necessary? I need you to be aware of one issue: random particle spawn behaves differently on each client. Consider following situation (drawn with windows paint XD):

1546705318_N90YXQ9DW_(FQB3()995.png.eb5e41d4e80b00109f54ac4e177085de.png

You can see the client particle position does not match sub explosion position. This can be solved by sending a random seed to all clients on explosion initialization. I already have the sync part covered in the script, so you simply need some xml prefix to tell it to sync. And disable this auto random seed. Though unity document says setting a seed in script will disable it by default, but somehow that's not the case here; so uncheck it.

 

Explode on particle death instead of collision

Spoiler

If you need the sub explosion to trigger when the lifetime of a particle depletes, careless of the collision states: this is also feasible. In this case you don't need to configure the collision module anymore; you can uncheck the send collision message and set lifetime loss to 0 and set bounciness etc. The xml part will explain how to tell the script to explode on death.

 

One more thing you need to do is set Max particle count in the main module as precise as possible, for example you have 40 particles at most at the same time then set it to 40. This is all you need to optimize the performance.

 

 

Xml editting

Spoiler

There are 2 custom properties in this patch:

<!--SubExplosion and SubExplosionTransform takes multiple particles and transforms, separating by comma(,). Particles and transforms are paired in sequence.
SubExplosionTransform accepts one prefix and one postfix.
-For prefix:
	$ indicates this transform contains a sub explosion particle system and should be synced on all clients.
	# indicates this transform contains a non sub explosion particle system, but still should be synced on all clients. When # is used, the corresponding sub explosion particle will pair with next transform.
	no prefix indicates this transform contains a sub explosion particle system but does not need to be synced.
-For postfix:
	$ indicates this transform explode on particle death instead of collision.
	# indicates this transform explode on both particle death and collision.
	no postfix indicates this transform explode on particle collision./-->
<property name="Explosion.SubExplosionTransform" value="($/#/ )transform1($/#/ ),($/#/ )transform2($/#/ )"/>
<property name="Explosion.SubExplosion" value="particle1,particle2"/>

 

Source of the sub explosion will be the bound item class of each particle, and the initiator will be the same as the first explosion.

Following triggers will also be fired from the bound item class and the initiator:

  • onProjectileImpact
  • onSelfExplosionDamagedOther
  • onSelfExplosionAttackedOther

Passive effects in the bound item class will also work.

 

Finally, add SubExplosionController,CustomParticleLoaderMultiExplosion to the value of Explosion.CustomScriptTypes.

 

 

Spawn Entity Expansion

Spoiler

The concept of this patch is triggering spawning on explosion, in order to simulate death loot drop or division spawn. Most of the work can be done in xml.

 

Xml editting

Spoiler

The properties are quite straightforward:

<!-- if lifetime is not specified, the entity will stay alive until someone kills it /-->
<property name="Explosion.SpawnEntityLifetime" value="time before kill entity"/>
<!-- spawn chance is defaulted to 1 /-->
<property name="Explosion.SpawnChance" value="the overall chance to trigger the spawn, range: 0 - 1"/>
<!-- only one of following spawn options is accepted /-->
<property name="Explosion.SpawnEntityClass" value="entity class name"/>
<property name="Explosion.SpawnEntityGroup" value="entity group name"/>
<property name="Explosion.SpawnEntityItem" value="itemname($mincount(,maxcount))"/>
<property name="Explosion.SpawnLootGroup" value="loot group name"/>

Add ExplosionSpawnEntity,CustomParticleLoaderSpawnEntity to the value of Explosion.CustomScriptTypes.

 

 

Edited by closer_ex (see edit history)
Link to comment
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×
×
  • Create New...