I'll just answer from the perspective of sound integration here. Not quite sure what all would be included in the graphics side of your cross-cutting concerns.
I've seen these types of cross-cutting sound integrations take a few different shapes, generally. Here's a breakdown on how I see the pros/cons of each approach.
Singleton Sound Manager
A globally available, "master mix" style sound manager that receives calls from various systems / Update()
methods directly via SoundManager.Trigger(soundId)
.
Pros
- Initially easy to reason about
- All things sound related are here except the triggers; it's centralized and easily modified in broad strokes - assuming a consistent API in consuming code
- Likely higher performance w/r/t fewer disparate sound systems being instanced all at once
Cons
- Dangerously easy to bloat with complexity (voice counts, choke groups, not to mention larger sound integration concerns like occlusion, dynamic music, etc.)
- Likely to evolve into a number of more specialized
[Foo]SoundManager
singletons, which need to be provided more context as external systems request sound playback (or cancellation, or looping, etc.)
- Consuming classes become more complex as a result
Sounds in Systems
A list of sounds that can be triggered by a system is stored close to the system and used internally. Some form of ad-hoc configuration might accompany the list of sounds, but it is otherwise fairly fire-and-forget.
Pros
- Easy to reason about, even at scale (bloat notwithstanding)
- Sound assets are assigned, managed, and triggered close to where they are used
+/-
- As you mentioned re: collision, suddenly your
CollisionManager
isn't just physics; it's now concerned with sound too.
- Some technical sound designers would argue that this is A Good Thing, as it allows for more nuanced sound integration within each system (e.g. different sounds are triggered based on impact velocity, insert effects are tweaked based on collision state.)
Cons
- Systems become larger in scope, potentially resulting in more abstraction:
CollisionManager
becomes the tip of an architecture subtree with CollisionSoundManager
managing sound with knowledge of state provided by separate CollisionPhysicsManager
, and so on
- Cross cutting concerns (e.g. voice count, choke groups, etc.) are either repeated (bad) or broken into more disparate sound subsystems (e.g.
SoundPlaybackManager
) which sort of just leads you back to the Singleton Sound Manager approach
- Without proper abstraction, systems needing sound can get messy, quickly
Event Sourcing / SoundEventManager
Essentially every notable event in your game becomes logged to some central EventStore
. This may already be in place for save systems or other databasing needs, but this can be an extremely powerful well from which a SoundEventManager
can parse events, decide if they warrant a sound, infer context from surrounding events, and trigger + manage sounds appropriately.
Pros
- Lots of power from a centralized place, though proper abstraction of sound playback concerns is absolutely necessary.
- Enables adding, modifying, or removing sounds long after other systems are created without modifying said systems.
+/-
- Rather than relying on systems to trigger a sound themselves (Sounds in Systems), or request playback from an orchestrator (Singleton Sound Manager), the systems just publish events with reckless abandon.
- Can go well if the core
EventStore
makes intelligent use of debounces / throttles and otherwise keeps it's memory footprint in check via streaming to disk as needed.
- But it can also go poorly, with far too many useless event records and a monstrous memory footprint to boot.
Cons
- Absolutely the more complex of the architectures to get right.
- Aside from
EventStore
hygiene gotchas mentioned above, latency can be an issue if the event queue isn't processed on nearly every tick.
Personal Opinion
I'm a big fan of the Event Sourcing approach because I believe it lends itself to good design in other areas of gameplay, despite the complexity and need to carefully manage + persist incoming events. Having centralized communication is ultimately always needed for systems to cooperate and doing so via event logs is a tried-and-true approach.
All that said, I won't hesitate to recommend mature sound integration platforms like WWise or FMOD. Sound integration is such a deep and nuanced practice that tooling around it has become very full-featured and truly helps lighten the load as a [solo] developer just trying to get work done.
CollisionSound
component attached which subscribes toCollision
events on that object, and contains data about what sound to play. ACollisionEventSystem
could iterate over all such collision event subscribers for each collision that occurs, and invoke them. \$\endgroup\$