0

I was working on a simple RPG type game in C#, it is mainly a prototype, just testing ideas out (I am aware that the code could be improved and is crude in places, however the bulk of it does work).

I have currently hit a roadblock with the battle system.

Currently, you control 1 character. You can face up to 3 enemies. It is a turn based, ATB system, and this part is working fine. Once it is the players turn, they can select an attack and use it.

I wanted to add a new feature to have damage over time effects. However, this has broken the combat.

I made a class to represent a DOT effect:

 public class DoT
{
    public int Duration { get; set; }
    public int Elapsed { get; set; }
    public int Damage { get; set; }
    public Enemy Target { get; set; }
    public String DName { get; set; }
    public DoT()
    {
        Duration = 3;
        Elapsed = 0;
        DName = "";
    }
    public void Tick()
    {
        Elapsed++;
    }

}

This is added to abilities when they are created, and this part works, I can add a DOT effect to an ability.

In the combat system, when selecting an attack, I create a list of DOT effects called DOTS. When the user selects an attack, if the attack has a DOT effect attached to it, it will be added to the list of DOT effects:

try
        {
            string s = ((Control)sender).Text;//Stores name of label that triggered the event
            int i = s.Length;//can't remember why I added this

            if (playerTarget != null)//check that we have a target
            {
                if (currentTurn.owner == c && canAttack)//if the current turn is ours and we can attack
                {
                    if (c.GetAbility(s).AbilityCost <= c.currentMP)//if we have enough MP for the attack
                    {
                       if(c.GetAbility(s).DamageOverTime!=null)//checks if the attack has a DOT effect
                       {
                           c.GetAbility(s).DamageOverTime.Target = playerTarget;//the DOT now targets the target
                           AddDOT(c.GetAbility(s).DamageOverTime );//Adds the DOT effect to DOTS
                           //Trace statements I was using for debugging
                           UpdateTest("Added "+ c.GetAbility(s).AbilityName + " to DOT");
                           UpdateTest("Time left- " + (c.GetAbility(s).DamageOverTime.Duration-c.GetAbility(s).DamageOverTime.Elapsed).ToString() );
                       }
                        currentTurn.ability = c.GetAbility(s);//Sets the current ability to be used
                        UseTurn();//Executes a turn
                    }
                    else
                    {
                        MessageBox.Show("Not enough MP!");
                    }
                }
            }
            else
            {
                MessageBox.Show("You must select a target!");
            }


        }
        catch (Exception ex)
        {
            MessageBox.Show("Error" + ex.ToString());
        }

The method that adds a DOT effect is this:

public void AddDOT(DoT d)
    {
        int exists = 0;
        if (DOTS.Count > 0)
        {
            foreach (DoT dot in DOTS)
            {
                if (dot.Equals(d))
                {
                    dot.Elapsed = 0;
                    exists++;
                }
            }
            if(exists==0)
            {
                DOTS.Add(d);
            }
        }
        else
        {
            DOTS.Add(d);
        }

This is to stop multiples of the same DOT being applied, if it is already there, we just reset the time elapsed on the current DOT.

I also have a method to apply DOT effects on each turn:

 public void UpdateDots()
    {
        try
        {
            if (DOTS.Count > 0)//Check that we have a DOT
            {
                foreach (DoT d in DOTS)//Loop through each DOT
                {
                    DotDamage(d);//Apply the DOT damage to the DOT's target
                    d.Tick();//call the tick method of DOT to add 1 to elapsed
                    //Trace statement to help debug
                    UpdateTest("Duration on " + d.DName + " is " + (d.Duration - d.Elapsed).ToString());
                    if (d.Elapsed == d.Duration)//Check if we have ticks left
                    {
                        DOTS.Remove(d);//If not, remove the DOT effect                            
                    }
                }
            }
            else
            {
                UpdateTest("No DOTS active");//Trace statement
            }
        }
        catch(Exception ex)
        {
            MessageBox.Show(ex.ToString());
        }

The UpdateDots method is called here in the code to use a turn:

 public void UseTurn()
    {
        UpdateDots();//Execute any DOT effects
        pause.Tick += new EventHandler(PauseTick);//just a timer to delay each turn, I need to move this
        UpdateTest("Owner is..."+currentTurn.owner.characterName );//Trace statement

        if (currentTurn.owner == c)//Checks if the current turn is by the player (c=player)
        {
            if (currentTurn.ability.DamageOverTime == null)//if we aren't applying a DOT
            {
                if (currentTurn.ability.MultiTarget == false)//If it is single target
                {
                    Attack(c, playerTarget, currentTurn.ability);//Single target attack
                }
                else
                {
                    MultiAttack(playerTargets, currentTurn.ability);//Multi target attack
                }

            }

            lblPlayerTimer.Width = 0;//Resets the label showing the players ATB bar
            currentTurn.owner = null;//Free the current owner to be claimed by other characters
            canAttack = false;//Can no longer attack
            playerTimer.Start();//Start the players timer again
            UpdateAbilities();//Simply used to update the display of abilities
        }
        else if(currentTurn.owner is Enemy)//If an enemy is attacking
        {
            Enemy en = (Enemy)currentTurn.owner;//Get the details of the enemy attacking
            string n = en.characterName;//this was from previous code             
            Label l = new Label();
            l = Controls.Find(en.timer.lblName ,true).FirstOrDefault() as Label;//Get the label being used as the enemy timer
            PickEnemyAttack(en);//Select an attack for the enemy
            Attack(en, c, currentTurn.ability);//Execute attack for enemy
            l.Width = 0;//Reset the enemy ATB bar
            currentTurn.owner = null;//Free up the current turn

            //This part is just used to cause a short delay before the next turn
            pause.Start();
            enemyCanAttack = false;                
            en.timer.Start();               

        }
        ShowTurn();//Updates display of the current turn
        turnCount += 1;
        CheckWinner();//Checks if the player has won, or lost
    }

The issue is, when I apply a DOT, it is applied, and damages the enemy. It continues to tick until the DOT expires, at which point all of the timers stop. If there are no DOT effects, then the combat works as normal.

It seems to be caused when the DOT is removed from DOTS. It is throwing an invalidoperation exception, stating that the collection was modified. Is it because I removed an item, but as I am in a ForEach loop, removing this messes with the enumeration?

As you can tell, I am not an expert with lists, and I can't see the issue, I was hoping someone with a more expert eye could help me fix this issue.

Thanks.

David
  • 208,112
  • 36
  • 198
  • 279
rm46
  • 21
  • 1
  • 4
  • Possible duplicate of [InvalidOperationException after removing an element in an arrayList](https://stackoverflow.com/questions/12987799/invalidoperationexception-after-removing-an-element-in-an-arraylist) – mjwills Jul 17 '17 at 13:13

3 Answers3

1

foreach won't let you modify the collection, but you can loop over it manually with a simple for loop and then modify whatever you like (since you control the iteration). So instead of this:

foreach (DoT d in DOTS)
{
    // do things with d
}

You would have this:

for (var i = 0; i < DOTS.Count; i++)
{
    // do things with DOTS[i]
}

Just note that with this manual control comes a little more responsibility on your part. If you want to continue to iterate over DOTS after removing an element from it, you'll need to manually modify i (decrement it by one) to reflect the fact that you're now re-iterating to the same index, since the collection was modified. But if, after removing the item from the collection, you simply exit the loop then it's not a problem. Something like this:

DOTS.Remove(DOTS[i]);
break;
David
  • 208,112
  • 36
  • 198
  • 279
  • 2
    Assuming that the order in which the items are processed is not critical, an alternative to having to change to value of `i` when you remove an item is to iterate the items from last to first. This way, when you remove the current item you are only affecting the position of those items you have already processed and are no longer concerned with, and you can move on to the next iteration knowing the indexes of the items you have still go to process have not been affected. – Andy Hames Jul 17 '17 at 13:44
0

You can not modify array while using foreach. Use for instead.

Sv Sv
  • 325
  • 3
  • 12
0

One option to consider would be to change:

foreach (DoT d in DOTS)

to:

foreach (DoT d in DOTS.ToList())

This way, the thing you are iterating over is different to the 'original' DOTS array / list. Which means you can alter DOTS (add / remove items etc).

mjwills
  • 23,389
  • 6
  • 40
  • 63