Hi, I’m making a top down shooter, and I’m currently working on a new enemy type.
Basically it’s an enemy that spawns in a group of 5-10 pawns, that follow eachother in a chain pattern.
I’m thinking of giving them an array of all the pawns in the chain, and store it in the one that’s in the lead. The 2nd pawn follows the 1st, the 3rd follows the 2nd and so on.
If the one in the lead that holds the array dies, the array gets subtracted by the one in [0] and each pawn moves up one slot in the chain, and number 2 now becomes the lead etc.
Now, I’m also thinking of splitting the chain into a new chain if a pawn that isn’t the lead dies.
Say pawn number 5 dies in a chain of 10, now you have 2 chains of enemies with 2 seperate leaders, the original that is now just pawn 1-4, and a new chain with what used to be 6-10.
To do this I’m thinking of copying array slots [5-9] into the 6th pawn’s array from [0-4] and designating it leader, then deleting 5-9 from the original leader.
Does this seem like a feasible method, or am I overcomplicating/oversimplifying this?
Ok, so I’m having some issues with how I should deal with the last two entities in my chain and my head is starting to spin, so I suspect I am doing this way more complicated than I need to.
Basically, if the last on in the chain gets killed, it cannot generate a new chain, as there are no entities to follow it. So I have the last one get a bool "lastInChain’ true. Then when it dies, the second last gets the bool, etc.
Now I also have to handle the second to last, as if it dies, the last one still shouldn’t generate a new chain, as that too has no enteties to follow it. Then when it dies, it’s tag also gets moved forward one step.
However, the one in front of it is now actually last, not the one second to last, and I have to check for how many entities we still have in front, etc, etc.
I find myself and conditional after contional, catching all sorts of orders of edge cases and catch 22’s and it’s quicky becoming a mess.
Personally I’d spawn an actor that creates and manages the groups of pawns. Death of a pawn can call into the managing actor and you can do your magic from there.
That’s practically how I’m handling it, except from using a seperate actor, all the work is handled from the pawn that’s in the lead. If it dies, I just move that responsibility to #2.
I was able to fix and simplify a few things by assigning each pawn in the chain with it’s own ID number upon generating a chain to do some shortcuts, rather than fully relying on and working with the arrays only.
So it’s almost working so far, but there seems to be some contingencies I haven’t caught that causes out of bounds crashes. This is the code so far, I’m sure a lot of it could be simplified:
Once the chained enemies are spawned, I run this:
InitChain:
void AWSNPC::initChain()
{
bIsChainLeader = true;
//start at 1, as leader does not follow
for (int i = 1; i < ChainPawns.Num(); i++)
{
ChainPawns[i]->followTarget = ChainPawns[i - 1];
ChainPawns[i]->bIsChainLeader = false;
ChainPawns[i]->chainLeader = this;
ChainPawns[i]->chainID = i;
if (i == ChainPawns.Num() - 2)
ChainPawns[i]->bSecondToLast = true;
if (i == ChainPawns.Num() - 1)
ChainPawns[i]->bLastInChain = true;
}
}
This is run upon entity death to handle new bool tags:
void AWSNPC::OnDeath()
{
if (bIsChain)
{
if (bIsChainLeader && ChainPawns.Num() > 1)
{
ChainPawns.RemoveAt(0); //remove self from array
ChainPawns[0]->bIsChainLeader = true;
ChainPawns[0]->ChainPawns = ChainPawns; //copy modified array to next in line
ChainPawns[0]->chainID--;
for (int i = 1; i < ChainPawns.Num(); i++)
{
ChainPawns[i]->chainLeader = ChainPawns[0];
ChainPawns[i]->chainID--;
}
}
else if (bLastInChain && !bIsChainLeader) //does not generate new chain
{
//Second to last and last in chain gets redesignated to new entities
if (chainID > 2)
{
if (chainLeader->ChainPawns[chainID - 2]->bIsChainLeader == false)
chainLeader->ChainPawns[chainID - 2]->bSecondToLast = true;
}
if (chainID > 1)
{
if (chainLeader->ChainPawns[chainID - 1]->bIsChainLeader == false) //more than 1?
{
chainLeader->ChainPawns[chainID - 1]->bLastInChain = true;
chainLeader->ChainPawns[chainID - 1]->bSecondToLast = false;
}
}
chainLeader->ChainPawns.RemoveAt(chainID); //make sure this instance is not being used in further assessments after death
}
else if (bSecondToLast && !bIsChainLeader)//cannot let last generate new chains as it will be alone.
{
chainLeader->ChainPawns[chainID + 1]->bIsChainLeader = true; //last in chain becomes solo "leader".
chainLeader->ChainPawns[chainID + 1]->bLastInChain = false; //new leader should not cause last in chain conditionals
chainLeader->ChainPawns[chainID + 1]->chainID = 0;
chainLeader->ChainPawns[chainID + 1]->bSecondToLast = false; //new leader is never second to last
//the one in front becomes last in current chain, assuming not leader
if (chainLeader->ChainPawns[chainID - 1]->bIsChainLeader == false)
chainLeader->ChainPawns[chainID - 1]->bLastInChain = true; //as chain breaks, the one in front actually becomes last, not second
if (chainID > 2)
chainLeader->ChainPawns[chainID - 2]->bSecondToLast = true;
//remove self and last from leader's pools of entities
chainLeader->ChainPawns.RemoveAt(chainID);
chainLeader->ChainPawns.RemoveAt(chainID);
}
else if (!bIsChainLeader && !bLastInChain && !bSecondToLast)
{
//notify new chain entities of new ID and new leader
/*other than leader has no chainpawns content, we need to do this from original leader*/
chainLeader->reformChains(chainID + 1); //new leader should be the one after dying pawn
}
}
Super::OnDeath();
}
If a chain is split, we run this on the dying enemy to notify both old and new leaders for status of new chains:
void AWSNPC::reformChains(int newLeader)
{
ChainPawns[newLeader]->ChainPawns.Add(ChainPawns[newLeader]); //add self to [0] for start of new chain
ChainPawns[newLeader]->bLastInChain = false;
ChainPawns[newLeader]->bSecondToLast = false;
for (int i = newLeader + 1; i < ChainPawns.Num(); i++)
{
ChainPawns[i]->chainLeader = ChainPawns[newLeader];
ChainPawns[newLeader]->ChainPawns.Add(ChainPawns[i]); //populate new chain array for new leader
}
ChainPawns[newLeader]->bIsChainLeader = true;
ChainPawns[newLeader]->initChain();
//delete new chain entities from old chain
for (int i = newLeader - 1; i < ChainPawns.Num(); i++) //-1 as we also want to remove the dying entity from being referenced
{
ChainPawns.RemoveAt(newLeader - 1); //we need to remove the same ID# repeatedly, as removals automatically contracts array for us
}
//we should now theoretically have 2 independent chains, this entity should not be contained in either and is therefore ready for destruction
}
make use of check(condition); to verify that your assumptions/expectations are met. This will trigger a breakpoint if you have the debugger attached if one of the checked conditions are violated. Checks will crash the game if running without debugger unless they are not compiled in certain build configurations
refactor the chaining logic into a separate class for which you can easily write unittests without having to spawn AWSNPCs
I’d probably use a simple (double)linked list instead of the array so that each AWSNPC only remembers the previous and next AWSNPC. Unless you absolutely need to have an array of all the AWSNPC in the chain, since iterating the linked list everytime would be slightly more expensive since you have to follow all of the links instead of just looking it up from the array. But this has the benefit of making the chaining logic much simpler (note nullptr’s written out for clarity):
Checking whether some AWSNPC is at the begining of the list Previous == nullptr.
Checking whether some AWSNPC is the leader AND actually has a chain Previous == nullptr && Next != nullptr.
Checking whether some AWSNPC is at the end of the list Next == nullptr.
Checking whether some AWSNPC is last in a chain AND actually has a chain Previous != nullptr && Next == nullptr.
Checking whether some AWSNPC is in any chain Previous != nullptr || Next != nullptr.
Checking whether some AWSNPC doesn’t have a chain at all IsInAnyChain() == false.
Splitting the chain when some AWSNPC dies
Oooh, I see what you mean. I technically shouldn’t need more than the next and previous in line indeed. The reason why I had to keep the entire chain in mind is because I used the array in the first place.
I think I caught the last contingency right now, but if I keep encountering issues, I will definitely try this way instead, or if I try something similar in the future.