#Will this be good code architecture for unity game?

1 messages · Page 1 of 1 (latest)

foggy berry
#

So for example:
I have MonoBehaviour Enemy class. Also I have MonoBehaviors: Health, Movement and KnockbackController. All this classes are the components on an enemy object. Enemy class handles communication between other components. For example Health class have OnDamaged event, and Enemy class have something like:

void OnEnable() {
  if (health != null && knockbackController != null)
    health.OnDamaged += knockbackController.Knockback();
}

The problem there - many null checks for components in Enemy class but actually I can just do components like Health and Movement - required.

Also there is EnemyData scriptable object which contain base data like max health and speed. And enemy class has reference of EnemyData. So, Enemy class filling out Health with max health from EnemyData and Movement with speed from EnemyData.
I don't like here that, for example, Health script will not be working without something that fill out max health through script. What I can do for this is creating IHealthData and implement it in EnemyData SO script. And in Health script make field of type IHealthData and fill it out with EnemyData through inspector window.

Also there is EnemyAI MonoBehavior class which is a state machine and, for example, will be using Movement component for executing movement behavior.

Is this good architecture? It sounds good for me but maybe there is problems which I didn't saw.

novel citrus
#

Personally I find no purpose in creating separate scripts for health management or other stats and effects. It just clutters the inspector more.

#

Interfaces are a better way of handling this since it allows you to customize the behavior more per object if need be (maybe the player has damage absorbtion and enemies don't)

#

It also doesn't create any dependencies

#

Or what I did which could be quite overkill is create a whole gameplay effect system that allows you to define any kind of attributes and modify them trough scriptable objects

marble granite
# foggy berry So for example: I have MonoBehaviour Enemy class. Also I have MonoBehaviors: Hea...

You make MonoBehaviour components when you need editor composable behavior, if not, keep it in one component and compose with plain C# components inside that host. You may also go a step further and treat MonoBehaviours purely as debug/authoring/config/reference/callback providers and the actual logic in a separate system. Depends on the project how much value you gain from either end of that spectrum.

foggy berry
marble granite
# foggy berry What do you mean by "editor composable behavior"?

if you always and only combine "healthComponent" with "characterComponent" there is no reason to have health be a separate component, if you want healthComponent to be usable with character- and buildingComponent, you would split health into its own component (assuming they both will need the same type of health and respond to a unified idea of 'damage')

foggy berry
foggy berry
novel citrus
#

it really depends on the size of your game and extensebility you need

foggy berry
novel citrus
#

The responsability of the entity class is purely to define the base implementation of what an entity is

#

Movement should be handled by a separate script

marble granite
foggy berry
fervent haven
#

A game that only has one player and no other similar entities, might even include ALL the logic in one script. And it would be fine.

novel citrus
fervent haven
#

If you find that you need to rewrite a lot of the same code again and again, it probably means you need some base/shared functionality. And there are many ways to achieve that: base class with common behavior, shared components, shared poco objected referenced by different classes, etc...

foggy berry
#

Ok guys, thanks) Actually I wanted more specific answers and maybe comparisons but I guess as long as I don't violate the basic principles and make things logic for my project it's okay?

novel citrus
#

I came up with a pretty generic architecture that fits most projects.

#

It's like a combination of composition and singleton pattern

#

but with a bit of unity twist

#

I call it the Modular Manager Hierarchy Pattern

novel citrus
foggy berry
#

with singleton - yes, with composition - no

novel citrus
#

Composition is essentially structuring scripts as a tree

#

You have the root high level class and from there it branches off into multiple classes with their own responsabilities

foggy berry
#

It sounds like inheritance

novel citrus
#

yes, but there is no inheritence involved

#

it's more of a reference tree

#

In my case it is like this

#

At the root you have the GameManager, which is the only singleton in the entire scene

#

Then under it you have some sub managers like UI Manager, Player Manager, Audio Manager

#

Then something like the Player Manager has other scripts under it, like Player Movement, Player Interaction, Player Footsteps, etc.

#

And this way you are able to call the game manager as a singleton and grab a reference of anything under it (within reason)

#

And it still making use of the single responsabillity principle.

#

The GameManager will only handle global level stuff so it is not a god object, it only holds the entry point of the hierarchy

#

As for health I just place that logic in the PlayerManager since there isn't enough code to justify a different component

#

And if I need reusability I use an interface

#

Or a completely different attribute and gameplay effect system, which is what I went for eventually

foggy berry
#

Why are you using composition pattern? What if you just do UI Manager, Player Manager etc as fields in GameManager?

novel citrus
#

thats what they are

#

The top level managers are referenced in the GameManager, as properties

#

then those managers reference any scripts under them

foggy berry
#

oh, ok) how does player manager interact with player object for example? It it component on player object? Or you do something like finding player object in player manager?

novel citrus
#

The player manager is attached to the player object in the inspector

#

This pattern is only for core scene components

foggy berry
#

ok, got you, thanks for sharing your method

gaunt sedge
#

If you want to go pure composition approach, you can make Knock back controller run completely independent to the Enemy. Do damage then Get component the Knock back component.

marble granite
gaunt sedge
#

Usually I just want to give the enemy the interface imo and implement custom Knock back per type.

novel citrus
#

you will do something like GameManager.Instance.PlayerManager.PlayerMovement

#

the reference tree will look something like this

marble granite
novel citrus
#

Not really massive, since it is only intended for core scene components

#

I do have a bunch of rule sets for it so it doesn't get messy

#

Those rules are:

Limit globally accessible references
Don't tie everything to the hierarchy

#

I am gonna copy paste the exact text I have written in my guidelines

marble granite
#

i wonder how you would teach that kind of thing to a team.

novel citrus
#

Limit globally accessible references:
In the case of manager subsystems that are part of the hierarchy, if that subsystem doesn’t make sense to be referenced by other systems then the reference should stay private. For example you have the PlayerFootsteps class that's part of the PlayerManager, if the PlayerFootsteps doesn’t make sense to be accessed by other systems then its PlayerManager reference should stay private. This will limit the amount of global references. If there are any niche cases where the subsystem needs to be referenced, consider a direct reference rather than a global one or create an abstract way of doing the needed logic of that class in the manager via the facade pattern.

Don’t tie everything in the hierarchy:
Only core scene systems should be part of the hierarchy, systems like Interactables, Minigames, World Streaming, etc. may reference the hierarchy but not be part of it, but while those systems are not part of the hierarchy they should still make use of the composition pattern.
Anything that's instantiated at runtime should also not be part of the hierarchy.

novel citrus
marble granite
#

i wonder why you would want to expose the hierarchy at all. a component would have no need to know about it. you could collapse this to "component only knows its owner" and asks its owner for whatever it needs, the owner does the same with its owner.

marble granite
novel citrus
#

This hierarchy is basically components that may be commonly referenced throughout the project, rather then having everything be a singleton, having to constantly drag references in the inspector or having to find said type in the scene every time

marble granite
#

why not use a service locator where you register services via interfaces?

#

what you have there effectively is just a collection of singletons

novel citrus
#

It's less code and a clearer hierarchy.

novel citrus
marble granite
#

but they are singletons, you're just pretending they arent. everything is coupled, everything is globally accessible from anywhere

novel citrus
#

yes, that's the point. But again, only restricted to components that would have to be commonly accessed throughout the scene lifetime

marble granite
#

its probably very convenient to use, until it isnt.

novel citrus
#

I did ship a game with it, and I didn't have any issues with it

#

I am not saying it's perfect, but it works for me

gaunt sedge
marble granite
#

if you have "no issues" with your archiecture, i'm immediately sceptical. no architecture has "no issues".

novel citrus
#

I guess 6-7 people help develop it, but it is mostly my project and I did all the coding

novel citrus
gaunt sedge
#

Architecture is fine for smaller games / jams. Obviously most optimial is without any static bindings but like if I had all the time in the world

novel citrus
gaunt sedge
#

as long as the team is aware that there is strict instance dependencies outside of the prefab

marble granite
#

i'd be curious how it behaves when you have 4+ programmers of mixed seniority working with it.

#

wondering if the explicit coupling helps or hinders, could be either.

novel citrus
#

Well, I didn't get to try it out too much, but I also did use it when I was a senior at a freelance programming team, but I left before the projects got huge, but still left them with all the guideline docs and the setup in place

#

and already explained to the other lead how the architecture worked

marble granite
#

i think the strongest aspect of it is that you implicitly create a "systems" layer in your project, it almost doesnt matter what shape that has.

#

you basically add an opinionated custom layer on top of unity that solves your common problems in a way you like, and that can be powerful.

#

also you likely standardize many things which makes communication easier.

novel citrus
#

This architecture is something that I just developed overtime troughout all my projects by myself, it's definetely something that will keep evolving overtime as I use it in bigger and bigger projects

marble granite
#

what i would say is that your system is not really modular or pluggable. which may be fine. i.e. i would expect all your components and subsystems eventually depend on each other and you cannot easily drop stuff into a scene in isolation.

#

i would personally prefer a system that is fundamentally cooperative and synergetic with minimal coupling between systems, and an orchestration layer that exists purely per project for message routing and shaping the project specific interop.

novel citrus
#

I guess yeah, it is not really modular, I did kinda name it on the spot to be fair

#

But the thing is that pretty much every game has a Player/UI and Audio systems that have to communicate with each other, so I don't know how would you make something without these being coupled

gaunt sedge
#

Again, if you had all the time in the world you would always delegate up, but I dont have time for that

novel citrus
#

Delegates still require you to reference the class that contains said delegate

gaunt sedge
#

You'd have some reference at the time of instantiation to bind those delegates yeah. But UI never needs to know about the player, it should just invoke the event. But like I would just bi-directional reference

novel citrus
#

Well, for stuff like that I mostly use a scriptable event system

#

So there isn't actually any direct coupling, it is done trough a middle man instead

#

But this is only for editor events not code events

gaunt sedge
#

That's fine too, but more boilerplate I can't be bothered with lol

marble granite
novel citrus
#

All the components in the hierarchy are guaranteed to have the same lifecycle

marble granite
#

if your UI wants to play a click sound on hover, why should that call be GameManager.AudioStuff.OverlayAudio.PlayClick() instead of audioService.Play(click)

#

you could inject audioService when the component registers with the manager that owns it or when it is spawned. this also allows you to decide locally and per instance which service instance to pass, instead of forcing a single global one that the component accesses through a hierarchy-path.

novel citrus
#

Both are valid

#

I just like the clarity of the hierarchy

#

With services you have to know that service exists in the first place, with the hierarchy you can see everything in one place pretty much

#

I know there was 1 more thing I didn't like about the services but can't remember what, it's too late right now for me and I am tired 😭

marble granite
#

my priority would be "ease of replacing any amount of code with something different", i.e. limiting the blast radius if deleting and rewriting something entirely. meaning if i change something in a system, i only want its dependents to be affected and those dependents should not be any other systems.

novel citrus
# marble granite i think thats exactly the point that makes it flexible. you can't have discovera...

I guess the other thing is also related to the lack of a clear structure for services, that in a team you can end up having a ton of services to manage if not careful, it's pretty much the same problems that you would from making everything part of the hierarchy or making everything a singleton, the difference is that with a clear structure that can be more easily avoided compared to a service where you just have to guess or properly document everything.

#

While yes in the tree stuff are quite coupled, you can still write the code in a reusable and modular way so you can remove something from the tree, you just need to make sure to also remove any references from it, which a simple F12 can help with that

#

and there is also the option of using a middle man like the scriptable event system to detach a lot of coupling

#

Also for services I know there is some lifecycle stuff that you can handle, but how does it work exactly? In my case only main gameplay scenes have this tree, main menus and other similar stuff don't. And the tree only exists for the scene's lifetime, there is no don't destroy on load or additive scenes involved

#

But I am still able to use some submanagers in other scenes. For example in the main menu, there is no game manager but I can still use the Audio and Input managers, I only need to get a direct reference instead.

novel citrus
novel citrus
#

Big systems I design them completely independent, separating them from the main game code with assembly definitions. For example I recreated unreal's Gameplay Ability System, and it is not dependent on anything from the hierarchy.

#

But I can attach it to the PlayerManager who uses this system and expose the main component to the tree

marble granite
#

I think what you have is a very fancy game manager that solves some of the common game dev issues in Unity in a way you like. This isn’t a general architecture or principle that aims at explicit signal flow, testability and standardization across any feature. It’s more like some orchestration and bootstrap you standardize to solve coordination, discovery and the way you prefer to do set up in a project. Which is necessary, just a bit overselling on the general architecture.

novel citrus
#

I guess yeah, I never wanted to imply this was some next level architecture, just wanted to share how I do stuff, what fits me and why, maybe it can fit some other people as well, if not that's perfectly fine. But I do think this is a viable way of doing stuff and just another tool in the toolbox.

#

Like I said before this is just a combination of the singleton and composition design patterns.

#

And kind of service locator but done trough a reference hierarchy, maybe it is a bit of a stretch to call it a service locator?

marble granite
novel citrus
#

Pretty much yeah

#

But it needs a better name lol

marble granite
#

Call it FancyMan Boots and Vestments

#

(i find weird high-level metaphors can help focus a team on what you're trying to actually achieve with a system, which is more difficult when you use generic terms from engineering/CS. Call it a MessageBus and everyone has a different opinion of what that might be, call it the PonyExpress and you invite a more specific mental model of an object that travels async along a predictable route and changes its wrapper at each stop while being intercepted by hostile locals it passes by.)

novel citrus
#

So basically a nickname

marble granite
#

yeah

#

other example "Pawn", thats way more evocative than "ControllableEntity"

#

i always dislike the term GameManager, i find its more of a handwave at a whole bunch of problems that shouldn't really be one thing. and your breakdown is an example of that. But if you keep the "GameManager" name at the root, its just a garbage bag of 100 ideas. but if you call it "TheHierarchy" you make your foundational principle the mental anchor instead of attaching it to the rather unspecfic "GameManager" anchor. I think any system/module benefits from a name that implies its shape AND the core abstraction or principle it leverages.

novel citrus
#

This makes sense for systems especially for branding

#

Oddly enough Unity pretty much stopped doing that after a while

marble granite
#

it depends on how specific your thing is, if its really a infrastructure feature, say a MessagePipe to build a PonyExpress with, the "brand name" is a mistake

novel citrus
#

Like Unreal has Lumen and the Unity implementation is just called Surface Cache GI

marble granite
#

i think about it this way: if its opinonated, give it a name, if its generic and wants to be configurable into everything, use a generic technical name

marble granite
#

its also great if you can tell your investor "we have TheHierarchy, therefore we can ship 10% quicker", vs "Our generic project architecture convention makes us 10% quicker".