Search Unity

Scripts structure, communication and organization - "Weapon System"

Discussion in 'Scripting' started by ouraf, Nov 22, 2019.

  1. ouraf

    ouraf

    Joined:
    Sep 4, 2019
    Posts:
    18
    i need some help with code planning, communication and organization.

    obs: I'm a beginner scripter, please be kind with me.

    I'm making a variant of the space shooter game (the one GameDevHQ and a lot of people makes on their tutorial). On this one i want the weapon change to work with ammo (if ammo < 1, change the prefab to the default shot).

    I also want to change the delay between shots based on which weapon (to make a machinegun and slow shotgun behavior, for example).

    That isn't much, but it has some implications that are confusing me.

    When i had only one weapon, i made a "PlayerShoot" script and nested a bunch of variables there:

    Code (CSharp):
    1. public GameObject projectile;
    2.     [SerializeField]
    3.     private float allowFire = -1f;
    4.     private float shotCooldown = 0.3f;
    The idea was that if i only changed the shot shape, trajectory or speed, all of this would go on the projectile prefab loaded on the editor.

    But if i want to make a lot of different weapons, i need to either:
    1. make a gigantic class with all values from all weapons there (i don't want to do that)
    2. make a variable to EVERY SINGLE WEAPON, attach all weapons to the player and do a lot of true/false to change bewteen them (i don't know if that's right)
    3. to write and use the shotCooldown and other variables on the other weapon script inside the prefab loaded at the time. (the design patterns says each object must deal with only one thing, so would this be the right way to do it?)
    And here's where things kinda get out of place for me: i don't know the right method of referencing a variable inside a script that change names with every weapon i have
    upload_2019-11-22_12-39-38.png
    now i have three of these, but could become a dozen. and the work needed to implement them shouldn't increase with the ammount of weapons i create.

    i'd also have to make logic for the weapon transition while avoiding a the usage of GameObject.Find as much as possible.

    the main doubts are
    1. which ways are "right" and which are "bad practices" for this kind of system
    2. a smart way of referencing variables that control the same thing, but are in different scripts and different prefabs
    thanks for the support

    OBS: the "weapon change" is also the basic for this kind of game: collide with the powerup, destroy the powerup and the powerup call some method to make the change and set everything in place
     
  2. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    Polymorphism to the rescue! This is 100% the exact problem that polymorphism is designed to solve.

    So what polymorphism means is basically this:
    STEP 1: THE BASE CLASS
    You'll have one class, BaseWeapon, that has all the common stuff. Cooldown time, etc.
    This class also has something called abstract and virtual methods. This is sort of a placeholder for functionality where, every weapon will have it, but it'll all function differently. I'll stick to virtual methods because they're a little easier for beginners to handle. So, you might have:
    Code (csharp):
    1. public class BaseWeapon : MonoBehaviour {
    2. public float cooldownTime;
    3. public float weaponLastFired = -9999f;
    4. public int maxAmmo;
    5. public int currentAmmo;
    6. public virtual void Fire() {
    7. Debug.Log("BaseWeapon firing code. This can be left empty.");
    8. }
    9. }
    STEP 2: DERIVED CLASSES
    OK, now it's time to make the individual weapons. These weapons will derive from the base class (note the "BaseWeapon" after the colon), and then override the functions where something changes:
    Code (csharp):
    1. public class Gun : BaseWeapon {
    2. public GameObject bulletPrefab; //this will be assignable in the inspector, alongside the maxAmmo, cooldownTime, etc from the base class
    3. public override void Fire() {
    4. Instantiate(bullet, transform.position, transform.rotation);
    5. Debug.Log("Gun firing code goes here. BTW my cooldown time is "+cooldownTime);
    6. }
    7. }
    In the derived classes, you can still access all the parent class's members and methods (as demonstrated with cooldownTime above), except for the private ones. (Sidenote: There's a middle ground between "public" and "private" called "protected", which is not accessible from the outside, but is accessible to derived classes).
    Code (csharp):
    1. public class Knife : BaseWeapon {
    2. public override void Fire() {
    3. Debug.Log("Knife knife knife!");
    4. }
    5. }
    You can have multiple levels of inheritance, too. If you want to add a machinegun, you can derive MachineGun from Gun and only need to override the bits of the behavior that have changed.

    STEP 3: TAKING ADVANTAGE OF THE SETUP
    So now how does all this help you? What's the point of making the BaseWeapons class rather than just having all the weapons be separate standard scripts? It's all about the virtual/override stuff. Because your player manager script whatever that is can now have an array of BaseWeapon, call Fire() on one of these weapons without knowing anything about the Gun or Knife classes, and it will call the Fire() method on those classes.
    The "not knowing about derived classes" bit is important. In good practice, you'll never directly reference the derived classes; rather, you call the virtual method, and allow the class itself to determine how to handle that call.
    (This script assumes that each kind of weapon you create is attached to a child transform)
    Code (csharp):
    1. public class PlayerWeaponManager : MonoBehaviour {
    2. public BaseWeapon[] allWeapons;
    3. public int currentWeaponIndex = 0;
    4. void Start() {
    5. allWeapons = GetComponentsInChildren<BaseWeapon>();
    6. }
    7. void Update() {
    8. // place code here to change currentWeaponIndex and select weapons however you see fit
    9. BaseWeapon currentWeapon = allWeapons[currentWeaponIndex];
    10.  
    11. if (Input.GetButtonDown("Fire") && Time.time > currentWeapon.weaponLastFired + currentWeapon.cooldownTime) {
    12. currentWeapon.Fire();
    13. currentWeapon.weaponLastFired = Time.time;
    14. }
    15. }
    16. }
    So let's talk about the variable currentWeapon. That variable is of type BaseWeapon. But, it will actually be pointing to an object which is derived from BaseWeapon (Gun, Knife, etc). If it points to a Gun, when you call Fire(), it will instantiate that bullet.

    And here's the best part: extensibility. Adding a flamethrower? You don't need to touch PlayerWeaponManager, nor any of the other scripts. Just add a new script that derives from BaseWeapon, and it'll all Just Work(TM).

    You can combine this with other ways of differentiating your weapons. For example, you might have a bunch of different guns that all behave basically the same logically, but do different damage, have different cooldowns, etc. All those things can just be assigned in the inspector, so you can make 5 different object all with a "Gun" script attached representing 5 different guns. Then you can drag to assign your bullet prefabs, type in the different cooldowns, etc.

    It took me a couple of readthroughs to first get a grasp on inheritance, so don't worry too much if this seems like a confusing concept. Read it as many times as you need to, try playing around with the code, you'll start to wrap your head around it as you go.
     
    hkalterkait likes this.
  3. hkalterkait

    hkalterkait

    Joined:
    Jul 20, 2016
    Posts:
    11
    Thank you this was really helpful!