From bcb5c475717ddfa2b30cf4585ea0e31282e4cf78 Mon Sep 17 00:00:00 2001 From: SmArtKar <44720187+SmArtKar@users.noreply.github.com> Date: Wed, 20 Nov 2024 15:46:00 +0300 Subject: [PATCH] Implements shared particle holders, re-adds slimed status particles using the new system (#87892) ## About The Pull Request Partial revert of #86701 Implements a shared holder particle system, somewhat inspired by https://github.com/Baystation12/Baystation12/pull/34014 (thanks Kapu). Atoms can be assigned "shared" particles via add_shared_particles, with an optional "alternate" key passed as a second arg if you're planning to edit the returned particle holder (for example, color it like slimed status does). Removing is done via remove_shared_particles with an option to delete the shared holder if nothing is using it anymore (on by default). This system should be prioritized over normal particle holders when a lot of entities would be using a certain particle effect (like fires) as it conserves a lot of clientside performance. Burning, acid, decaying, firestacks and slimed status now use this system which should help with clientside performance and amount of atoms created/destroyed. ## Why It's Good For The Game Less clientside lag. ## Changelog :cl: refactor: Firestacks, burning/acid/decaying effects and (brought back after being temporarily removed) slimed status effects now use a new "shared" particles system, which should considerably improve client performance when encountering a lot of burning/slimed entities. /:cl: --------- Co-authored-by: Ghom <42542238+Ghommie@users.noreply.github.com> --- code/datums/components/acid.dm | 16 +++- code/datums/components/burning.dm | 18 +++- code/datums/components/food/decomposition.dm | 25 +++--- code/datums/status_effects/debuffs/cursed.dm | 14 +-- .../status_effects/debuffs/fire_stacks.dm | 39 +++++---- .../debuffs/slime/slime_food.dm | 7 +- .../status_effects/debuffs/slime/slimed.dm | 8 ++ code/game/objects/effects/particles/fire.dm | 3 + .../objects/effects/shared_particle_holder.dm | 86 +++++++++++++++++++ tgstation.dme | 1 + 10 files changed, 170 insertions(+), 47 deletions(-) create mode 100644 code/game/objects/effects/shared_particle_holder.dm diff --git a/code/datums/components/acid.dm b/code/datums/components/acid.dm index 74fa1b1ae7f..3bb06ab639d 100644 --- a/code/datums/components/acid.dm +++ b/code/datums/components/acid.dm @@ -25,8 +25,10 @@ GLOBAL_DATUM_INIT(acid_overlay, /mutable_appearance, mutable_appearance('icons/e var/turf_acid_ignores_mobs = FALSE /// The ambient sound of acid eating away at the parent [/atom]. var/datum/looping_sound/acid/sizzle - /// Particle holder for acid particles (sick) + /// Particle holder for acid particles (sick). Still utilized over shared holders because they're movable-only var/obj/effect/abstract/particle_holder/particle_effect + /// Particle type we're using for cleaning up our shared holder + var/particle_type /// The proc used to handle the parent [/atom] when processing. TODO: Unify damage and resistance flags so that this doesn't need to exist! var/datum/callback/process_effect @@ -68,8 +70,13 @@ GLOBAL_DATUM_INIT(acid_overlay, /mutable_appearance, mutable_appearance('icons/e sizzle = new(atom_parent, TRUE) if(acid_particles) - // acid particles look pretty bad when they stack on mobs, so that behavior is not wanted for items - particle_effect = new(atom_parent, acid_particles, isitem(atom_parent) ? NONE : PARTICLE_ATTACH_MOB) + if (ismovable(parent)) + var/atom/movable/movable_parent = parent + movable_parent.add_shared_particles(acid_particles, "[acid_particles]_[isitem(parent)]", isitem(parent) ? NONE : PARTICLE_ATTACH_MOB) + particle_type = acid_particles + else + // acid particles look pretty bad when they stack on mobs, so that behavior is not wanted for items + particle_effect = new(atom_parent, acid_particles, isitem(atom_parent) ? NONE : PARTICLE_ATTACH_MOB) START_PROCESSING(SSacid, src) /datum/component/acid/Destroy(force) @@ -78,6 +85,9 @@ GLOBAL_DATUM_INIT(acid_overlay, /mutable_appearance, mutable_appearance('icons/e QDEL_NULL(sizzle) if(particle_effect) QDEL_NULL(particle_effect) + if (ismovable(parent) && particle_type) + var/atom/movable/movable_parent = parent + movable_parent.remove_shared_particles("[particle_type]_[isitem(parent)]") process_effect = null return ..() diff --git a/code/datums/components/burning.dm b/code/datums/components/burning.dm index 2535a5b6f90..fa60b3ecbce 100644 --- a/code/datums/components/burning.dm +++ b/code/datums/components/burning.dm @@ -8,8 +8,10 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e /datum/component/burning /// Fire overlay appearance we apply var/fire_overlay - /// Particle holder for fire particles, if any + /// Particle holder for fire particles, if any. Still utilized over shared holders because they're movable-only var/obj/effect/abstract/particle_holder/particle_effect + /// Particle type we're using for cleaning up our shared holder + var/particle_type /datum/component/burning/Initialize(fire_overlay = GLOB.fire_overlay, fire_particles = /particles/smoke/burning) if(!isatom(parent)) @@ -25,9 +27,14 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e return src.fire_overlay = fire_overlay - if(fire_particles) - // burning particles look pretty bad when they stack on mobs, so that behavior is not wanted for items - particle_effect = new(atom_parent, fire_particles, isitem(atom_parent) ? NONE : PARTICLE_ATTACH_MOB) + if (fire_particles) + if(ismovable(parent)) + var/atom/movable/movable_parent = parent + // burning particles look pretty bad when they stack on mobs, so that behavior is not wanted for items + movable_parent.add_shared_particles(fire_particles, "[fire_particles]_[isitem(parent)]", isitem(parent) ? NONE : PARTICLE_ATTACH_MOB) + particle_type = fire_particles + else + particle_effect = new(atom_parent, fire_particles) START_PROCESSING(SSburning, src) /datum/component/burning/Destroy(force) @@ -35,6 +42,9 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e fire_overlay = null if(particle_effect) QDEL_NULL(particle_effect) + if (ismovable(parent) && particle_type) + var/atom/movable/movable_parent = parent + movable_parent.remove_shared_particles("[particle_type]_[isitem(parent)]") return ..() /datum/component/burning/RegisterWithParent() diff --git a/code/datums/components/food/decomposition.dm b/code/datums/components/food/decomposition.dm index a3850ab39a6..c883f82f658 100644 --- a/code/datums/components/food/decomposition.dm +++ b/code/datums/components/food/decomposition.dm @@ -27,8 +27,6 @@ var/produce_ants = FALSE /// Stink particle type, if we are supposed to create stink particles var/stink_particles - /// Stink particle holder - var/obj/effect/abstract/particle_holder/particle_effect /datum/component/decomposition/Initialize(mapload, decomp_req_handle, decomp_flags = NONE, decomp_result, ant_attracting = FALSE, custom_time = 0, stink_particles = /particles/stink) if(!ismovable(parent) || !HAS_TRAIT(parent, TRAIT_GERM_SENSITIVE)) @@ -52,9 +50,11 @@ src.stink_particles = stink_particles /datum/component/decomposition/Destroy() - . = ..() - if(particle_effect) - QDEL_NULL(particle_effect) + remove_timer() + if (stink_particles) + var/atom/movable/movable_parent = parent + movable_parent.remove_shared_particles("[stink_particles]_[isitem(parent)]") + return ..() /datum/component/decomposition/RegisterWithParent() RegisterSignal(parent, COMSIG_ATOM_GERM_EXPOSED, PROC_REF(start_timer)) @@ -78,17 +78,13 @@ // If all other checks fail, then begin decomposition. decomp_timerid = addtimer(CALLBACK(src, PROC_REF(decompose)), time_remaining, TIMER_STOPPABLE | TIMER_UNIQUE) - // Also start the stinking timer, if have stink particles and aren't stinking yet - if(!stink_particles || particle_effect) + // Also start the stinking timer, if have stink particles + if(!stink_particles) return var/stink_time = max(0, time_remaining - (original_time * 0.5)) stink_timerid = addtimer(CALLBACK(src, PROC_REF(stink_up)), stink_time, TIMER_STOPPABLE | TIMER_UNIQUE) -/datum/component/decomposition/Destroy() - remove_timer() - return ..() - /// Returns the time remaining in decomp, either from our potential timer or our own value, whichever is more useful /datum/component/decomposition/proc/get_time() if(!decomp_timerid) @@ -108,11 +104,12 @@ /datum/component/decomposition/proc/stink_up() stink_timerid = null - // Neither should happen, but to be sure - if(particle_effect || !stink_particles) + // Shouldn't happen, but to be sure + if(!stink_particles) return // we don't want stink lines on mobs (even though it'd be quite funny) - particle_effect = new(parent, stink_particles, isitem(parent) ? NONE : PARTICLE_ATTACH_MOB) + var/atom/movable/movable_parent = parent + movable_parent.add_shared_particles(stink_particles, "[stink_particles]_[isitem(parent)]", isitem(parent) ? NONE : PARTICLE_ATTACH_MOB) /datum/component/decomposition/proc/decompose() decomp_timerid = null diff --git a/code/datums/status_effects/debuffs/cursed.dm b/code/datums/status_effects/debuffs/cursed.dm index 8d331bbe90a..083bd678635 100644 --- a/code/datums/status_effects/debuffs/cursed.dm +++ b/code/datums/status_effects/debuffs/cursed.dm @@ -31,6 +31,8 @@ /datum/status_effect/grouped/cursed/Destroy() UnregisterSignal(SSdcs, COMSIG_GLOB_CURSED_SLOT_MACHINE_WON) branded_hand = null + if (smoke_path) + owner.remove_shared_particles(smoke_path) return ..() /// Checks the number of curses we have and returns information back to the slot machine. `max_curse_amount` is set by the slot machine itself. @@ -123,7 +125,6 @@ span_notice("The smoke slowly clears from [owner.name]..."), span_notice("Your skin finally settles down and your throat no longer feels as dry... The brand disappearing confirms that the curse has been lifted."), ) - QDEL_NULL(particle_effect) qdel(src) /// If our owner's stat changes, rapidly surge the damage chance. @@ -140,10 +141,8 @@ /datum/status_effect/grouped/cursed/proc/on_death(mob/living/source, gibbed) SIGNAL_HANDLER - if(gibbed) - return - - QDEL_NULL(particle_effect) + if(!gibbed && smoke_path) + owner.remove_shared_particles(smoke_path) /datum/status_effect/grouped/cursed/update_particles() var/particle_path = /particles/smoke/steam/mild @@ -156,9 +155,10 @@ if(smoke_path == particle_path) return - QDEL_NULL(particle_effect) + if (smoke_path) + owner.remove_shared_particles(smoke_path) + owner.add_shared_particles(particle_path) smoke_path = particle_path - particle_effect = new(owner, particle_path) /datum/status_effect/grouped/cursed/tick(seconds_between_ticks) if(curse_count <= 1) diff --git a/code/datums/status_effects/debuffs/fire_stacks.dm b/code/datums/status_effects/debuffs/fire_stacks.dm index 83510a2ded2..8a629dc14ef 100644 --- a/code/datums/status_effects/debuffs/fire_stacks.dm +++ b/code/datums/status_effects/debuffs/fire_stacks.dm @@ -135,6 +135,8 @@ var/obj/effect/dummy/lighting_obj/moblight /// Type of mob light emitter we use when on fire var/moblight_type = /obj/effect/dummy/lighting_obj/moblight/fire + /// Cached particle type + var/cached_state /datum/status_effect/fire_handler/fire_stacks/get_examine_text() if(owner.on_fire) @@ -153,6 +155,8 @@ /datum/status_effect/fire_handler/fire_stacks/on_remove() UnregisterSignal(owner, COMSIG_ATOM_TOUCHED_SPARKS) + if (cached_state) + owner.remove_shared_particles(cached_state) /datum/status_effect/fire_handler/fire_stacks/tick(seconds_between_ticks) if(stacks <= 0) @@ -177,15 +181,23 @@ deal_damage(seconds_between_ticks) /datum/status_effect/fire_handler/fire_stacks/update_particles() - if(on_fire) - if(!particle_effect) - particle_effect = new(owner, /particles/embers) - if(stacks > MOB_BIG_FIRE_STACK_THRESHOLD) - particle_effect.particles.spawning = 5 - else - particle_effect.particles.spawning = 1 - else if(particle_effect) - QDEL_NULL(particle_effect) + if (!on_fire) + if (cached_state) + owner.remove_shared_particles(cached_state) + cached_state = null + return + + var/particle_type = /particles/embers/minor + if(stacks > MOB_BIG_FIRE_STACK_THRESHOLD) + particle_type = /particles/embers + + if (cached_state == particle_type) + return + + if (cached_state) + owner.remove_shared_particles(cached_state) + owner.add_shared_particles(particle_type) + cached_state = particle_type /** * Proc that handles damage dealing and all special effects @@ -300,7 +312,7 @@ enemy_types = list(/datum/status_effect/fire_handler/fire_stacks) stack_modifier = -1 - ///If the mob has the TRAIT_SLIPPERY_WHEN_WET trait, the mob gets this component while it's wet + /// If the mob has the TRAIT_SLIPPERY_WHEN_WET trait, the mob gets this component while it's wet var/datum/component/slippery/slipperiness /datum/status_effect/fire_handler/wet_stacks/on_apply() @@ -312,12 +324,14 @@ if(HAS_TRAIT(owner, TRAIT_SLIPPERY_WHEN_WET)) become_slippery() ADD_TRAIT(owner, TRAIT_IS_WET, TRAIT_STATUS_EFFECT(id)) + owner.add_shared_particles(/particles/droplets) /datum/status_effect/fire_handler/wet_stacks/on_remove() . = ..() REMOVE_TRAIT(owner, TRAIT_IS_WET, TRAIT_STATUS_EFFECT(id)) if(HAS_TRAIT(owner, TRAIT_SLIPPERY_WHEN_WET)) no_longer_slippery() + owner.remove_shared_particles(/particles/droplets) /datum/status_effect/fire_handler/wet_stacks/proc/update_wet_stack_modifier() SIGNAL_HANDLER @@ -342,10 +356,5 @@ if(stacks <= 0) qdel(src) -/datum/status_effect/fire_handler/wet_stacks/update_particles() - if(particle_effect) - return - particle_effect = new(owner, /particles/droplets) - /datum/status_effect/fire_handler/wet_stacks/check_basic_mob_immunity(mob/living/basic/basic_owner) return !(basic_owner.basic_mob_flags & IMMUNE_TO_GETTING_WET) diff --git a/code/datums/status_effects/debuffs/slime/slime_food.dm b/code/datums/status_effects/debuffs/slime/slime_food.dm index 538e62e27c5..259f6c8829a 100644 --- a/code/datums/status_effects/debuffs/slime/slime_food.dm +++ b/code/datums/status_effects/debuffs/slime/slime_food.dm @@ -21,9 +21,11 @@ RegisterSignal(owner, COMSIG_COMPONENT_CLEAN_ACT, PROC_REF(on_feeder_deleted)) RegisterSignal(owner, COMSIG_SLIME_DRAINED, PROC_REF(on_drained)) RegisterSignal(owner, COMSIG_ATOM_EXAMINE, PROC_REF(on_examine)) - return ..() +/datum/status_effect/slime_food/on_remove() + feeder = null + ///Handles the source of the pheromones getting deleted, or the owner getting washed /datum/status_effect/slime_food/proc/on_feeder_deleted(datum/source) SIGNAL_HANDLER @@ -51,6 +53,3 @@ draining_slime.befriend(feeder) new /obj/effect/temp_visual/heart(draining_slime.loc) qdel(src) - -/datum/status_effect/slime_food/on_remove() - feeder = null diff --git a/code/datums/status_effects/debuffs/slime/slimed.dm b/code/datums/status_effects/debuffs/slime/slimed.dm index 6cd107c74a9..32fa9048223 100644 --- a/code/datums/status_effects/debuffs/slime/slimed.dm +++ b/code/datums/status_effects/debuffs/slime/slimed.dm @@ -66,6 +66,14 @@ to_chat(owner, span_userdanger("You have been covered in a thick layer of slime! Find a way to wash it off!")) return ..() +/datum/status_effect/slimed/on_remove() + owner.remove_shared_particles(rainbow ? "slimed_rainbow" : "slimed_[slime_color]") + +/datum/status_effect/slimed/update_particles() + var/obj/effect/abstract/shared_particle_holder/holder = owner.add_shared_particles(rainbow ? /particles/slime/rainbow : /particles/slime, rainbow ? "slimed_rainbow" : "slimed_[slime_color]") + if (!rainbow) + holder.particles.color = "[slime_color]a0" + /datum/status_effect/slimed/proc/remove_stacks(stacks_to_remove = 1) slime_stacks -= stacks_to_remove // lose 1 stack per second if(slime_stacks <= 0) diff --git a/code/game/objects/effects/particles/fire.dm b/code/game/objects/effects/particles/fire.dm index 99046858073..481849c00eb 100644 --- a/code/game/objects/effects/particles/fire.dm +++ b/code/game/objects/effects/particles/fire.dm @@ -34,6 +34,9 @@ spin = generator(GEN_NUM, list(-15,15), NORMAL_RAND) scale = generator(GEN_VECTOR, list(0.5,0.5), list(2,2), NORMAL_RAND) +/particles/embers/minor + spawning = 1 + /particles/embers/spark count = 3 spawning = 2 diff --git a/code/game/objects/effects/shared_particle_holder.dm b/code/game/objects/effects/shared_particle_holder.dm new file mode 100644 index 00000000000..2d5bbbb8c3a --- /dev/null +++ b/code/game/objects/effects/shared_particle_holder.dm @@ -0,0 +1,86 @@ +#define SHARED_PARTICLE_HOLDER_INDEX 1 +#define SHARED_PARTICLE_USER_NUM_INDEX 2 +// Assoc list of particle type/key -> list(list of particle holders, number of particle users) +GLOBAL_LIST_EMPTY(shared_particles) + +//A more abstract version of particle holder not bound to a specific object +/obj/effect/abstract/shared_particle_holder + name = "shared particle holder" + desc = "How are you reading this? Please make a bug report :)" + appearance_flags = KEEP_APART|KEEP_TOGETHER|TILE_BOUND|PIXEL_SCALE|LONG_GLIDE + vis_flags = VIS_INHERIT_PLANE + layer = ABOVE_ALL_MOB_LAYER + mouse_opacity = MOUSE_OPACITY_TRANSPARENT + anchored = TRUE + /// Holds info about how this particle emitter works + /// See \code\__DEFINES\particles.dm + var/particle_flags = NONE + +/obj/effect/abstract/shared_particle_holder/Initialize(mapload, particle_path = /particles/smoke, particle_flags = NONE) + . = ..() + // Shouldn't exist outside of nullspace + loc = null + src.particle_flags = particle_flags + particles = new particle_path() + +/obj/effect/abstract/shared_particle_holder/Destroy(force) + QDEL_NULL(particles) + return ..() + +/* Adds (or creates and adds) a shared particle holder + * Shared particle holders are held in nullspace and added to vis_contents of all atoms using it + * in order to save clientside performance by making clients only render 3-5 particle holders + * for 400 objects using them. This should be prioritized over normal particles when possible if it is known + * that there will be a lot of objects using certain particles. + * custom_key can be used to create a new pool of already existing particle type in case you're planning to edit holder's color or properties + * pool_size controls how many particle holders per type are created. Any objects over this cap will pick an existing holder from the pool + */ +/atom/movable/proc/add_shared_particles(particle_type, custom_key = null, particle_flags = NONE, pool_size = 3) + var/particle_key = custom_key || "[particle_type]" + if (!GLOB.shared_particles[particle_key]) + GLOB.shared_particles[particle_key] = list(list(new /obj/effect/abstract/shared_particle_holder(null, particle_type, particle_flags)), 1) + vis_contents += GLOB.shared_particles[particle_key][SHARED_PARTICLE_HOLDER_INDEX][1] + return GLOB.shared_particles[particle_key][SHARED_PARTICLE_HOLDER_INDEX][1] + + if (length(GLOB.shared_particles[particle_key][SHARED_PARTICLE_HOLDER_INDEX]) < pool_size) + var/obj/effect/abstract/shared_particle_holder/new_holder = new(null, particle_type, particle_flags) + GLOB.shared_particles[particle_key][SHARED_PARTICLE_HOLDER_INDEX] += new_holder + vis_contents += new_holder + GLOB.shared_particles[particle_key][SHARED_PARTICLE_USER_NUM_INDEX] += 1 + return new_holder + + var/obj/effect/abstract/shared_particle_holder/particle_holder = pick(GLOB.shared_particles[particle_key][SHARED_PARTICLE_HOLDER_INDEX]) + if (particle_holder in vis_contents) + return particle_holder + vis_contents += particle_holder + GLOB.shared_particles[particle_key][SHARED_PARTICLE_USER_NUM_INDEX] += 1 + return particle_holder + +/* Removes shared particles from object's vis_contents and disposes of it if nothing uses that type/key of particle + * particle_key can be either a type (if no custom_key was passed) or said custom_key + */ +/atom/movable/proc/remove_shared_particles(particle_key, delete_on_empty = TRUE) + if (!particle_key) + return + + if (ispath(particle_key)) + particle_key = "[particle_key]" + + if (!GLOB.shared_particles[particle_key]) + return + + var/list/type_holders = GLOB.shared_particles[particle_key][SHARED_PARTICLE_HOLDER_INDEX] + for (var/obj/effect/abstract/shared_particle_holder/particle_holder as anything in type_holders) + if (!(particle_holder in vis_contents)) + continue + + vis_contents -= particle_holder + GLOB.shared_particles[particle_key][SHARED_PARTICLE_USER_NUM_INDEX] -= 1 + + if (delete_on_empty && GLOB.shared_particles[particle_key][SHARED_PARTICLE_USER_NUM_INDEX] <= 0) + QDEL_LIST(type_holders) + GLOB.shared_particles -= particle_key + return + +#undef SHARED_PARTICLE_HOLDER_INDEX +#undef SHARED_PARTICLE_USER_NUM_INDEX diff --git a/tgstation.dme b/tgstation.dme index 229dc47983c..12698d296b1 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -2262,6 +2262,7 @@ #include "code\game\objects\effects\poster_motivational.dm" #include "code\game\objects\effects\powerup.dm" #include "code\game\objects\effects\rcd.dm" +#include "code\game\objects\effects\shared_particle_holder.dm" #include "code\game\objects\effects\spiderwebs.dm" #include "code\game\objects\effects\step_triggers.dm" #include "code\game\objects\effects\wanted_poster.dm"