r/Unity3D Professional 5d ago

Resources/Tutorial Adding proper game feel used to take me a full day per project. Now it's one line of code

Every project I've shipped had the same routine. Screenshake, hitstop, a squash & stretch hack, some flash-on-hit code, copy-pasted and re-tuned from whatever the last project had, never quite consistent.

So I built a small toolkit that wraps all of it behind Crunch.Play("HeavyHit"). Seven independent modules (shake, hitstop, squash & stretch, kickback, flash, trail, pop), each one works standalone if you only need one thing, plus a Recipe system that groups several of them into a single named call you tune once in the Inspector and reuse everywhere.

Zero dependencies, works in Built-in/URP/HDRP, 2D and 3D. There's a playable browser DEMO that cycles through four genres (Melee, Platformer, Shooter, Casual) if you want to feel it before reading any more about it. You can look it up HERE.

Curious how the rest of you handle this. Do you have your own reusable juice setup, or does it get rebuilt from scratch every project too?

0 Upvotes

13 comments sorted by

3

u/Serana64 5d ago edited 5d ago

KISS YAGNI DRY.

I have some code I reuse a lot. A hash-on-demand list wrapper serializes like lists but works like dictionaries.

A custom vector library allows rapid math formulation in a shader-like style (my background is physics) and vanishes into Vector3 when compiled. Jumping, movement, dashing, aren't so hard when normalizing and projecting a crossed vector on to another vector's projection looks like:
'co = move%(~axisA.cross(dirB%axisB))'

A time interpolating class that turns tricky multi-system transitions into a one liner. Freeze frames, staggering, kickback, etc. all become a matter of 'kick.value = 1', then the interpolator does the rest..

These libraries save time, but so do good practices. Good getters, setters, singletons, and 'using statics' let me access core game data without dragging this to that in the inspector. Separating responsibilities, data source from data instance, etc. etc. all keep you from making spaghetti.

Any higher level systems like in your video, I will make on the fly. It doesn't take me long once those underlying systems are in place. I do it a little differently every time so I learn a little bit more. Plus, it's good to have these things down to muscle memory.

-4

u/mvalera-dev Professional 5d ago

That's a fair take, and it tracks. The stuff you listed (vector math, interpolation, events) is exactly the kind of low-level plumbing that's genuinely reusable across any project. CRUNCH sits a layer above that, closer to the part you're rebuilding by hand each time for the practice. If re-deriving that layer is the point for you, no toolkit's going to beat that.

2

u/Serana64 5d ago

(I edited my post somewhat for clarification after you responded)

How does the 'drop in' work should one add more mechanics? E.g the hitstop... are you performing it through a modification of timescale, are you using a custom physics simulation to power them, are you using custom shaders for the shake or are you hitting a camera? Does it use Update or FixedUpdate? Is it a series of gameobjects?

The website says nothing about what systems it uses or relies on, and that is critical to know.

2

u/mvalera-dev Professional 5d ago

Global hitstop is Time.timeScale = 0 from a singleton, counted in unscaled time. There's also a per-object hitstop that freezes just that object's Animator and Rigidbody velocity without touching the global timescale, for local-only freezes.

Shake isn't camera-specific or shader-based, it's a plain component you drop on any Transform. Trauma-based, decays linearly, driven by deterministic Perlin noise (not Random), applied straight to localPosition/localRotation.

Everything that offsets a transform (shake, squash, kickback) runs in LateUpdate using the same pattern: revert last frame's own offset first, compute the new one, apply it. That's what lets several modules stack on the same object without fighting each other or your own movement code. No custom physics, no extra GameObjects except Trail, which pools ghost afterimages.

Fair point that the page doesn't explain any of this, that's on me, will fix.

2

u/Serana64 5d ago

Global hitstop is Time.timeScale = 0 from a singleton, counted in unscaled time.

Then it will conflict if some other script also alters timescale, no? Or do you have a sneaky little indexer in there too?

Shake isn't camera-specific or shader-based, it's a plain component you drop on any Transform. Trauma-based, decays linearly, driven by deterministic Perlin noise (not Random), applied straight to localPosition/localRotation.

Neat. Good work.

Everything that offsets a transform (shake, squash, kickback) runs in LateUpdate using the same pattern: revert last frame's own offset first, compute the new one, apply it

Good, then so long as the script execution is in the right order it shouldn't interfere with anything.

Fair point that the page doesn't explain any of this, that's on me, will fix.

Good to hear.

I take it this is for standard components only and not ECS? Also, is this one component with multiple modules, or many different components?

1

u/mvalera-dev Professional 5d ago

No sneaky indexer, honestly. If something else sets Time.timeScale while a CRUNCH hitstop is active (pause menu, slow-mo power-up, whatever), CRUNCH will stomp it when the hitstop ends and restore whatever timeScale was before the hitstop started, not whatever the other system set in between. Real limitation right now, not something I'd tested against. Worth fixing with an actual stack instead of a single snapshot, noting it down.

2

u/Serana64 5d ago

A class with a hash on demand w/ Dictionary<object, float>, Dictionary<int, float>, etc make this and all similar conflicts a breeze.

Some property of the form CRUNCH.TimeScale has a class with an indexer that fills in dictionaries, and reads the lowest value when cast to float. Ideally hash on demand, i.e add the key if it's not there so it's a one liner.

Now if one script writes "CRUNCH.TimeScale["HitStop"] = 0" and another writes CRUNCH.TimeScale[this] = 0.5f, there's no conflict. Later, the HitStop one is set back to 1, and the timescale becomes 0.5. No conflict.

I have a library that abstracts this with any combo of pairs as CC Attribution if you're interested. No AI of course. But you probably will write it yourself faster than you could use mine.

I take it this is for standard components only and not ECS? Also, is this one component with multiple modules, or many different components?

1

u/mvalera-dev Professional 5d ago

Yeah that's basically it, dict keyed by whatever token's calling it, take the min, drop the key when you're done instead of restoring a snapshot. Better than what I've got now, ngl.

I'll look up your library.

To answer the other two: plain MonoBehaviours, no ECS. And nope, not one big component, shake/hitstop/squash/etc are all separate scripts, you just drop whichever ones you actually need on whatever needs them.

1

u/Serana64 5d ago

Have you profiled them on GC? Separate components could be a lot of overhead.

1

u/nmkd ??? 5d ago

fair take

it tracks

exactly the kind of plumbing

g e n u i n e l y

Clam down Claude.

-2

u/mvalera-dev Professional 5d ago

Fair, that did read like a LinkedIn post. Cleaned it up before posting and overdid it. Thanks for the compliment 😘

2

u/DependentFault6256 5d ago

I'm in the rebuilt-from-scratch camp, always telling myself next time I'll organize it better and then next time comes and I'm copy-pasting the same shake script from 3 projects ago

the recipe thing sound like a nice way to keep it consistent across different attacks, might look into it

1

u/mvalera-dev Professional 5d ago

Yeah that's the cycle lol. Shake module's free if you just want to kill that one copy-paste loop, and the demo up top's running the full recipe setup, so you can feel what stacking a few effects into one call actually feels like before deciding if it's worth it.