Search Unity

Design and architecture questions on top-down shooter game

Discussion in 'Game Design' started by OkitoDev, Apr 28, 2023.

  1. OkitoDev

    OkitoDev

    Joined:
    Sep 29, 2017
    Posts:
    2
    Hello, to everyone reading this - I hope you're having a nice day!

    So recently I was working on my new project. I want to push my skills a bit further so that I can have something decent to show on my resume to get my first job. I was hoping to get feedback on my architecture design choices from more experienced developers and see if I am on the right track.

    - The game is a top-down shooter where the player can run around, kill enemies, collect coins, and upgrade their character and weapons.
    - The player earns gold to purchase and upgrade different types of weapons, such as guns, melee weapons, explosives, energy weapons, traps, etc.
    - The player can only use one weapon per type at once
    - Every weapon has a different attack pattern and can be upgraded with increased damage, projectile speed, projectile hit radius, cooldown reduction, etc.

    I wanted to mainly focus on gun-like-weapons for this post.

    So, for example, some guns are aimed by mouse, some auto-target monsters, and others shoot bubbles that float near the mouse. Upgrades to those guns include increased damage, projectile speed, hit radius, and cooldown reduction. One gun shoots projectiles straight from the player position, while another shoots in a spiral and targets nearby enemies, etc.

    As for my architecture design, I switched to more data-driven approach. I'm aiming for easier development down the line. As for the general overview:
    - Projectiles are spawned using a ProjectileFactory class, with each projectile having its own unique stats assigned at the start. Additionally, each projectile has its own IProjectilePattern set. So, every weapon/enemy that wants to shoot projectiles will have to create it's own instance of the factory.
    - All gun-like weapons inherit from the abstract class BaseGunWeapon, with each weapon being able to override the Fire() function for a unique style of shooting.
    - Base stats for weapons are set from a ScriptableObject, with the exception of IProjectilePattern interface. To set these, an abstract function must be implemented in each class that inherits from BaseGunWeapon. That is because Unity doesn't allow for assigning interfacecs from the inspector.
    - Finally, BaseGunWeapon inherits from BaseWeapon, which handles functionalities for all weapons, such as damage calculation and handling some upgrades.
    Because of my design decisions I can now easily create new types of gun, upgrade them, create new projectile patterns etc.

    Some performance concerns:
    - I introduced a new weapon - bubble gun. Very simple, It shoots bubbles. I tried messing around with it and by accident I created cool effect. I accidentally enabled collisions between the bubbles and reduced the spawning cooldown to a very low value. This caused a lot of bubbles to be shot in a dense line, creating a really cool effect. However, this created a lot of lag spikes once too many bubbles have spawned - over 60% of lags are because of physics (more specifically, collisions). I couldn't come up with how to replicate this effect without the use of collisions and physics.

    Thank you for your time!
     
  2. CodeSmile

    CodeSmile

    Joined:
    Apr 10, 2014
    Posts:
    5,989
    The most important question: how many projectiles are active at most at any time? Provide a rough guesstimate.
    Anything below maybe ~1,000 or just a few thousand projectiles may work well enough when each projectile is a GameObject. But you will see better performance when doing the following:
    • Use object pooling. You haven't mentioned it so I'm not sure if you deem this self-understood. If you don't: Master Object Pooler v2 from the asset store (free) is a great choice.
    • Have a central class manage all of the bullets, ie updating their positions, velocity, and determine collisions.
      • The more GameObjects, the more the overhead of "each GameObject runs their own script(s)" becomes.
    If the number of active projectiles may reach or exceed the 10k mark you are best advised to look into DOTS, specifically Entities, just to boost the speed for managing projectiles. However, you will have to have every physics object as an Entity because Entities physics is not compatible with regular physics, even though there are ways to make it work for simple cases like collision detection.

    Absolutely not! This goes against the idea of a Factory. The factory class should be static, and returns the created object to the caller. It doesn't need state at all, it's just an elaborate switch statement moved away to a class so you don't have to look at the messy switch anymore.

    This doesn't sound too data-driven actually. It sounds like you want to create a subclass for each kind of weapon, and possibly one for each kind of weapon mod/improvement too.
    Note that "Base" typically is a prefix, not a suffix. Also your "GunWeapon" is a case of name duplication: it's either a gun or a weapon but not both. I think what you wanted to express here is "ProjectileWeapon" as there may be other weapons that aren't projectile based such as melee weapons.

    The data-driven way to design this is to encode the type of weapon in the data. There's generally two approaches you can from here:
    1. WeaponShooter class looks at the data for the current weapon and does the processing based on weapon type, ammo type, fire frequency, last time fired, and so on. You may end up with one method per weapon type and lots of utility methods.
    2. WeaponShooter class is a container for weapon shoot processing components. It goes through a sequence of shooting where it checks each component and stops processing when one of the components signal a "fail" state. Example processing and components may include - however this is not fully thought out because this is more like a statemachine if you consider edge cases like (manual or auto) reloading, ammo capacity rules, shot frequency modifiers, buffs or debuffs applied (such as slowness, quad damage):
      1. CheckFireFrequencyElapsed => If frequency is 10 times per second and last shot fired was within less than 0.1s then it will return false, otherwise true.
      2. CheckAmmoInInventory => Returns true, if there is ammo left in the "inventory". Special version may return false if the weapon fires 10 projectiles and requires all of them to fire at once, so it will fail for 0-9 ammo left.
      3. CheckAmmoInClip => Returns true if enough ammo is in the inventory.
      4. PerformReload => Triggers automatic reload or checks for player input for manual reload, otherwise fails. Will also fail during a reload until reloading the weapon / clip is complete.
      5. Fire => Fires projectiles, instantiating them with weapon stats passed onto them (ie type, damage amount, initial velocity, initial position). Projectiles are spawned via ProjectileSpawner. The Fire component may use further FirePattern components that set the velocity, and add data to the projectile determining its flight path (ie wobble movement).
    3. In both cases:
      1. ProjectileSpawner relays spawning to the ProjectileFactory and performs any other initialization tasks, specifically registering each projectile with the ProjectileMotion class.
      2. ProjectileMotion is a central component that takes the data for each projectile passed in, such as type, initial position, velocity and type of flight pattern and so on and then updates each projectile according to that data every frame. In the same way as the WeaponShooter class the ProjectileMotion class can relay determining behaviour to other classes, such as CheckTimeToLive and DestroyProjectile (most projectiles will simply be disabled, but others may explode even if their TTL has run out eg grenades).
    Note that the "components" I mentioned don't need to be Unity components/MonoBehaviour. In fact, they should be using your own interface at best, and are created/processed based on the weapon's data.

    Using ScriptableObjects as data containers is a sound strategy but you will also need state per projectile, such as its current time to live (time of death). So you also need a transient (short-lived) ProjectileData method that gets a reference to its immutable data (the SO) and has other per-projectile properties.

    Overall, if you want a truly data-driven approach, you should try and see where and how you can work without subclassing / abstract base classes. This kind of design will also make it easier to adapt to Entities or just bursted Jobs for processing.

    Lastly, the physics performance issue is typical for regular Unity physics. You may see an improvement handling collision yourself using simple Vector3 distance checks and processing all projectiles from a central class rather than each projectile running its own projectile MonoBehaviour. Finally, if you do move to Entities these performance issues with collisions are practically lifted entirely, you can easily scale up by a factor of 100 or more given a decent DOTS (Entities or bursted Jobs) implementation.
     
    Last edited: Apr 28, 2023
    Socrates and OkitoDev like this.
  3. OkitoDev

    OkitoDev

    Joined:
    Sep 29, 2017
    Posts:
    2
    Firstly, thank you for your time, I appreciate it.
    So when it comes to projectiles, I would estimate that there may be at most ~1000-2000 projectiles active at once, so with that in mind, using object pooling and a central class to manage all the projectiles as you mentioned will probably be the way to go for me. I will look into the asset you mentioned, thanks for the recommendation.

    It seems like I got it all wrong. Creating subclasses for each kind of weapon was actually what I wanted to do, but now I see that's definitely not a data-driven approach at all. The ones that you suggested make sense in my head for now, I'll make sure to try and understand it a little bit better before implementing it, but I was thinking about something else related to object pooling with projectiles.
    So, that's most likely not a concern in my case as I wouldn't be spawning tons of projectiles per frame, but I was curious, suppose I want different sprites for projectiles, such as projectiles fired by enemies having different sprites from the ones fired by the player's weapon. Would it make sense to have separate pools of projectiles with different sprites as opposed to having one big pool of projectiles and changing their sprites as needed?

    And again, thank you for your helpful insights
     
  4. BIGTIMEMASTER

    BIGTIMEMASTER

    Joined:
    Jun 1, 2017
    Posts:
    5,181
    Not adding anything new here, but basically the question I ask is, "does it do anything different, or does it just look different?"

    So all guns do the same thing, only thing changes is visual representation. Bullets may fly slower or faster, but that's just change numbers in a spreadsheet type of thing. Same thing with gun behavior, one may reload slower or only hold two bullets, but again that can be accomplished just by changing input data.

    The major goal for me is, if I want to change the way the game plays, I don't want to have to remember fifty different steps and fifty different places. I want to open one config file and change some numbers. So whatever decision I make, I see if there is a way that it can accomplish that goal.
     
    christh and OkitoDev like this.