0

Im currently writing a weapon script for a FPS and I want to switch weapons with the mouse wheel. I created an array with the weapons in it and everytime I scroll up with the mouse wheel the index of the weapon increases by one. My problem is that when I'm at the last weapon I get IndexOutOfBounds error message. I've tried to reset the weapon index to 0 if its at the end of the array but for some reason that didn't work. I've also tried to do it with a while loop instead of an if-statement but that didn't work as well. Here's the code:

public class WeaponManager : MonoBehaviour
{

    [SerializeField]
    private WeaponHandler[] weapons;

    private int current_weapon_index;

    void Start()
    {
        current_weapon_index = 0;
        weapons[current_weapon_index].gameObject.SetActive(true);
    }

    
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Alpha1))
        {
            TurnOnSelectedWeapon(0);
        }

        if (Input.GetKeyDown(KeyCode.Alpha2))
        {
            TurnOnSelectedWeapon(1);
        }

        if (Input.GetKeyDown(KeyCode.Alpha3))
        {
            TurnOnSelectedWeapon(2);
        }

        if (Input.GetKeyDown(KeyCode.Alpha4))
        {
            TurnOnSelectedWeapon(3);
        }

        if (Input.GetKeyDown(KeyCode.Alpha5))
        {
            TurnOnSelectedWeapon(4);
        }

        if (Input.GetKeyDown(KeyCode.Alpha6))
        {
            TurnOnSelectedWeapon(5);
        }

        if(Input.mouseScrollDelta.y > 0)
        {
            SwitchToNextWeapon();
        }

        if (Input.mouseScrollDelta.y < 0)
        {
            SwitchToPreviousWeapon();
        }
    }

    void TurnOnSelectedWeapon(int weaponIndex)
    {
        weapons[current_weapon_index].gameObject.SetActive(false);

        weapons[weaponIndex].gameObject.SetActive(true);

        current_weapon_index = weaponIndex;
    }

    void SwitchToNextWeapon()
    {
        weapons[current_weapon_index].gameObject.SetActive(false);

        current_weapon_index++;
       
        weapons[current_weapon_index].gameObject.SetActive(true);

        if (current_weapon_index >= weapons.Length)
        {
            current_weapon_index = 0;
        }
    }

    void SwitchToPreviousWeapon()
    {
        weapons[current_weapon_index].gameObject.SetActive(false);

        current_weapon_index--;

        weapons[current_weapon_index].gameObject.SetActive(true);
    }
}
derHugo
  • 83,094
  • 9
  • 75
  • 115
L1NTHALO
  • 23
  • 5

4 Answers4

1

It would be quite straightforward to implement a class which handles this for you, just maintaining an internal List<> of items. Add a Current property to read the currently selected item, as well as a MoveNext/MovePrevious methods to be called from your mouse wheel handler

public class ContinuousList<T>
{
    private List<T> internalList = new List<T>();
    private int currentIndex = 0;

    public void Add(T item) => internalList.Add(item);

    public T Current { get => internalList[currentIndex]; }

    public void MoveNext()
    {
       currentIndex++;
       if(currentIndex >= internalList.Count) currentIndex = 0;
    }

    public void MovePrevious()
    {
       currentIndex--;
       if(currentIndex <= 0) currentIndex = internalList.Count - 1;
    }
}

Usage assuming you maybe have some weapons which have a base class Weapon:

var weaponList = new ContinuousList<Weapon>();
weaponList.Add(new Sword()); 
weaponList.Add(new Axe());
var currentWeapon = weaponList.Current; // gets Sword
weaponList.MoveNext();
var currentWeapon = weaponList.Current; // gets Axe
weaponList.MoveNext();
var currentWeapon = weaponList.Current; // Back to Sword

Live example: https://dotnetfiddle.net/Ji7rkt

Note that it is very easy to implement IEnumerable<T> on this ContinuousList so that it can be used in any enumerations and with LINQ methods. I did not want to complicate a simple example with this but to see this in action check here: https://dotnetfiddle.net/NtdfDi

Jamiec
  • 133,658
  • 13
  • 134
  • 193
  • As a small improvement, you can look at implementing `IEnumerable` or `IEnumerator`, which makes the class functional with `foreach` construct and a few other places just like a normal list. – Alejandro Aug 31 '21 at 12:53
  • @Alejandro indeed. I held off doing so as to not compicate the code. – Jamiec Aug 31 '21 at 12:53
1
void SwitchToNextWeapon()
{
    weapons[current_weapon_index].gameObject.SetActive(false);
    var temp = current_weapon_index + 1;
    current_weapon_index = temp >= weapons.Count() ? 0 : temp;
    
    weapons[current_weapon_index].gameObject.SetActive(true);
}

void SwitchToPreviousWeapon()
{
    weapons[current_weapon_index].gameObject.SetActive(false);
    var temp = current_weapon_index - 1;
    current_weapon_index = temp < 0 ? weapons.Count() - 1 : temp;

    weapons[current_weapon_index].gameObject.SetActive(true);
}

Just add a check before increasing or decreasing current weapon index. If it reaches max, then revert to 0 and if it's reached min (0) then set index to max.

Abhilash Augustine
  • 4,128
  • 1
  • 24
  • 24
  • 1
    Since `weapons` is an array I would say it is better to use `Length`. – Guru Stron Aug 31 '21 at 13:14
  • 1
    It worked! Thanks! Could you maybe explain why it didn't work the way I did it? I just want to know so that I can avoid similar mistakes. You don't have to, of course. – L1NTHALO Aug 31 '21 at 14:29
  • @L1NTHALO Glad that it helped! In your code you were trying to access the item using new increased and decreased index before checking for it's validity. – Abhilash Augustine Aug 31 '21 at 14:36
1

Cyclic values are easily handled using a modulo (%) operator.

int mod = 5;

for (int i = 0; i < 10; i++)
{
    Console.WriteLine(i % mod);
}

You'll see that the output cycles from 0 to mod-1: 0,1,2,3,4,0,1,2,...

This covers the case for incrementing by one:

int index = 4;
int mod = myArray.Length; // assume 5 items in the array

// Increment and cycle
index = ++index % mod;

You'll see that index is now 0 because you were at the end of the list, so the next item should be at the start of the list.


However, there is a bit of an issue for decrementing cyclical values. For a reason I don't understand, C# has opted to allow for negative modulo values, i.e.:

-1 % 5 = -1

... instead of 4, which is what you'd expect.
Edit: It is contended in the comments that 4 is not what everyone would expect. From experience when I was tackling this issue for the first time, I found a lot of confusion/annoyance online at the existence of negative modulo results, but I cannot disprove that this is observation bias on my part.

I've tackled this issue in the past, and the easiest way to solve this is to:

  • Take the modulo
  • Add the modulo
  • Take the modulo again

In essence, if the first step ends up with a negative result (e.g. -1), we simply add the modulo, therefore pushing the value above zero. However, if the first step was already a positive result, we've now made the value too high. Therefore, by taking the modulo again, we are able to cancel out the potentially too high value. This covers both cases.

Here is a dotnetfiddle to prove that it works.

In other words:

public int Increment(int current, int mod)
{
    return ((++current % mod) + mod) % mod;
}

public int Decrement(int current, int mod)
{
    return ((--current % mod) + mod) % mod;
}

For the sake of DRY, you can reshape it so you only use this complex formula once

public int Cycle(int current, int mod)
{
    return ((current % mod) + mod) % mod;
}

... but then you have to manually in/decrement the value first. Which version you prefer is up to you.

Flater
  • 12,908
  • 4
  • 39
  • 62
  • "which is what you'd expect" - it might be what *you* expect, but it is already functioning in the way that I would expect; expectations may be a little subjective. – Marc Gravell Aug 31 '21 at 14:02
  • @MarcGravell: I would expect that most people expect that `% x` outputs an integer value between `0` and `x-1`, not between `-x+1` and `x-1`. My expectation is based on the sheer amount of confusion and annoyance I found online when I was tackling this negative modulo issue the first time. However, maybe if you start from a specific use case where negative results make sense, it could be different and more intuitive. I can't attest to that since I started from using them for simple cyclical values. – Flater Aug 31 '21 at 14:46
  • Isn't this basically what I also did [here](https://stackoverflow.com/a/68999275/7111561)? – derHugo Aug 31 '21 at 17:00
  • @derHugo: In terms of the specific way to account for negative moduli, yes that is the same formula. But an answer is more than just its code snippet, and I wanted to provide a more in-depth explanation regarding the underlying math. – Flater Sep 02 '21 at 21:43
0

This answer is for swift but basically applies in the same way in c#.

So in general you can wrap any given index to an array length by applying modulo twice.

This works in more general cases, not only for moving a single step up and down:

public static class ArrayUtils
{
    public static void Forward<T>(ref int currentIndex, T[] array, int amount)
    {
        currentIndex = WrapIndex(currentIndex + amount, array);
    }
    
    public static void Backward<T>(ref int currentIndex, T[] array, int amount)
    {
        currentIndex = WrapIndex(currentIndex - amount, array);
    }
    
    public static int WrapIndex<T>(int newIndex, T[] array)
    {
        var length = array.Length;
        return ((newIndex % length) + length) % length;
    }
}

See Fiddle

Which you can now use for any array.

derHugo
  • 83,094
  • 9
  • 75
  • 115