nope, the problem of the patrolling state is back, and I have absolutely no idea why…
this patrolling problem is becoming seriously interesting. Right now as we speak, the enemies in the AggroGroup stop patrolling after the first time the player dies and respawns
However, the enemies outside the AggroGroup stop patrolling after the SECOND TIME that the player dies and respawns…
Any idea how to fix these?
oh, and the damage dealt to the player, displayed by the damage UI, is half the actual damage dealt for some reason…
Not going to lie, I hate that edit limit. It makes documenting and understanding my problems significantly harder than it has to be. I’d appreciate if you guys can take that limit out, just a humble request
so… the main problem I have right now is with the patrolling for the enemies UNDER THE AGGROGROUP. If one of them was fighting you and he killed you, that enemy is never ever patrolling again unless you fight him
However, if he was just peaceful and you died and respawned, he’ll patrol like nothing matters…
They have absolutely no issues respawning and patrolling, but they have a serious problem when the player Respawns… they’re terrible at re-patrolling for some reason, and this is only for the enemies under “AggroGroup.cs”. Here is my “AggroGroup.cs” script so far:
using System.Collections.Generic;
using UnityEngine;
using Unity.VisualScripting;
using RPG.Attributes;
using RPG.States.Enemies;
namespace RPG.Combat
{
public class AggroGroup : MonoBehaviour
{
[Header("This script aggrevates a group of enemies,\n if you attack one of their friends.\n The player must be in their 'PlayerChasingRange' though\n otherwise they won't try attacking you")]
[SerializeField] List<EnemyStateMachine> enemies = new List<EnemyStateMachine>();
[Tooltip("Set this to true only for groups of enemies that you want naturally aggressive towards the player")]
[SerializeField] bool activateOnStart = true;
[Tooltip("Set to true only if you want enemies to forget any problems they had with the player when he dies")]
[SerializeField] bool resetIfPlayerDies = false; // this is left as a boolean because it allows us to choose which aggroGroup members reset their settings on players' death, and which groups may not reset themselves on death (depending on your games' difficulty)
private bool isActivated; // Activates/deactivates the 'aggroGroup'
private Health playerHealth;
private void Start()
{
// Ensures guards are not active to fight you, if you didn't trigger them:
Activate(activateOnStart);
}
private void OnEnable()
{
if (resetIfPlayerDies)
{
playerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<Health>();
if (playerHealth != null) playerHealth.onDie.AddListener(ResetGroupMembers);
}
}
public void OnDisable()
{
if (resetIfPlayerDies)
{
Health playerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<Health>();
if (playerHealth != null) playerHealth.onDie.RemoveListener(ResetGroupMembers);
}
}
public void ResetGroupMembers()
{
Activate(false);
}
public void Activate(bool shouldActivate)
{
enemies.RemoveAll(enemy => enemy == null || enemy.IsDestroyed());
foreach (EnemyStateMachine enemy in enemies)
{
enemy.SetHostile(shouldActivate);
}
}
public void AddFighterToGroup(EnemyStateMachine enemy)
{
// If you got the fighter you want to add on your list, return:
if (enemies.Contains(enemy)) return;
// For other fighters on the list, add them lah:
enemies.Add(enemy);
// enemy.enabled = !isActivated;
enemy.IsHostile = !isActivated;
enemy.GetComponent<Health>().OnTakenHit += OnTakenHit; // NEW LINE
// if (enemy.TryGetComponent(out CombatTarget target)) target.enabled = isActivated;
}
public void RemoveFighterFromGroup(EnemyStateMachine enemy)
{
// Remove fighters from the list
if (enemy == null) return;
enemies.Remove(enemy);
// enemy.enabled = isActivated;
enemy.IsHostile = isActivated;
enemy.GetComponent<Health>().OnTakenHit -= OnTakenHit;
}
void OnTakenHit(GameObject instigator)
{
Activate(true);
}
}
}
The non-aggroGroup enemy so far is doing just fine on his own, after that “Respawner.cs” line at the end of “ResetEnemies()”
fight him or get close to him if he’s hostile*
nope, not the case. The moment you die, unless you fight and defeat him, he will have serious patrolling issues
Speaking as somebody who just had to catch up with a day’s worth of posts… Going back and editing previous posts confuses the living daylights out of me. It makes understanding YOUR problem significantly harder than it has to be. There are spots in here where you have correct information further up the chain that is incorrect later by reference. I have no control over the Edit limits, but I’d be more likely to lower the edit limit than raise it… This goes double for when I’m responding to a post in real time, save it and what I replied to is completely changed. Now imagine anybody else trying to follow the thread.
This is functionally equivalent to:
enemyControllers.GetComponent<NavMeshAgent>().enabled = true; //no if needed
The goal is to eliminate the AIController… it has no job anymore… so it makes more sense to find EnemyStateMachines…
This is the Respawner that goes on the Player, I’m assuming…
Maybe something like
private void ResetEnemies()
{
foreach(EnemyStateMachine enemyStateMachine in FindObjectsOfType<EnemyStateMachine>())
{
if(enemyStateMachine.Health.IsDead()) continue;
enemyStateMachine.Heal(enemyStateMachine.GetMaxHealthPoints() * enemyHealthRegenPercentage/100f);
enemyStateMachine.ResetPosition();
enemyStateMachine.SwitchState(new EnemyIdleState(enemyStateMachine);
}
}
Now you’re going to have to write the ResetPosition() method on EnemyStateMachine.cs… which is going to require some setup before that…
You’ll need a:
- private Vector3 with the original position.
- To assign that Vector3 in Start()
- To Assign that original position within ResetPosition(). Remember that both the NavMeshAgent and the CharacterController will fight you if you simply move the transform. You’ll have to turn off the NavMeshAgent and CharacterController, then set the Transform, then re-enable the Agent and CharacterController… This will, quite naturally reset the NavMeshAgent
[NOTE: I Solved ALL the problems I mentioned below. Feel free to skip if you wish, but I’d seriously appreciate if you just gave it a quick look through (ESPECIALLY FOR THE VERY LAST PROBLEM IN THE VERY LAST COMMENT, BECAUSE I’M NOT SURE WHAT IMPACT THIS WILL HAVE ON ANIMATIONS DOWN THE LINE) and see if anything can be done more elegantly, or changed to avoid future problems]
these edit limits used to cover serious programming crimes I did in the past, with proper solutions… - basically, they used to make it seem like my job was clean (although it was a serious mess). Lowering them just exposes my mistakes and will more likely than not baffle you even more (I seriously don’t mean to make your life harder. I apologize for being a total trainwreck from time to time, but please bear with me )
Anyway, back to fixing bugs at 6AM, I go (I’ll update you with the updates over the next comment)
OK so… here’s what I came up with (which kind of does not work exactly as expected. I’m sure I messed something up somewhere. Respawning-wise for the aggroGroup? Works well now. However, I have two problems, which will be discussed after the code I programmed):
in ‘EnemyStateMachine.cs’:
[field: SerializeField] public Vector3 resetPosition {get; private set;}
// in 'Start()':
// TEST:
resetPosition = transform.position;
public void ResetPosition()
{
Agent.enabled = false;
CharacterController.enabled = false;
transform.position = resetPosition;
Agent.enabled = true;
CharacterController.enabled = true;
}
and in ‘Respawner.cs’, as you mentioned above:
foreach (EnemyStateMachine enemyStateMachine in FindObjectsOfType<EnemyStateMachine>())
{
if (enemyStateMachine.Health.IsDead()) continue;
enemyStateMachine.Health.Heal(enemyStateMachine.Health.GetMaxHealthPoints() * enemyHealthRegenPercentage / 100f);
enemyStateMachine.ResetPosition();
enemyStateMachine.SwitchState(new EnemyIdleState(enemyStateMachine));
}
But I still have three problems:
- For the moment, resetting the enemies when the player dies work perfectly fine (thank you). However, the “IsHostile()” function of these enemies sometimes acts a bit randomly when the player has just respawned. Although one of them is hostile and the other isn’t, sometimes they both can flip (so the hostile isn’t hostile, and the non-hostile now is hostile), sometimes they work correctly and sometimes they don’t. Can you please have a look at my ‘AggroGroup.cs’, and see if something suspicious is going on?
using System.Collections.Generic;
using UnityEngine;
using Unity.VisualScripting;
using RPG.Attributes;
using RPG.States.Enemies;
namespace RPG.Combat
{
public class AggroGroup : MonoBehaviour
{
[Header("This script aggrevates a group of enemies,\n if you attack one of their friends.\n The player must be in their 'PlayerChasingRange' though\n otherwise they won't try attacking you")]
[SerializeField] List<EnemyStateMachine> enemies = new List<EnemyStateMachine>();
[Tooltip("Set this to true only for groups of enemies that you want naturally aggressive towards the player")]
[SerializeField] bool activateOnStart = false;
[Tooltip("Set to true only if you want enemies to forget any problems they had with the player when he dies")]
[SerializeField] bool resetIfPlayerDies = false; // this is left as a boolean because it allows us to choose which aggroGroup members reset their settings on players' death, and which groups may not reset themselves on death (depending on your games' difficulty)
private bool isActivated; // Activates/deactivates the 'aggroGroup'
private Health playerHealth;
private void Start()
{
// Ensures guards are not active to fight you, if you didn't trigger them:
Activate(activateOnStart);
}
private void OnEnable()
{
if (resetIfPlayerDies)
{
playerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<Health>();
if (playerHealth != null) playerHealth.onDie.AddListener(ResetGroupMembers);
}
}
public void OnDisable()
{
if (resetIfPlayerDies)
{
Health playerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<Health>();
if (playerHealth != null) playerHealth.onDie.RemoveListener(ResetGroupMembers);
}
}
public void ResetGroupMembers()
{
Activate(false);
}
public void Activate(bool shouldActivate)
{
enemies.RemoveAll(enemy => enemy == null || enemy.IsDestroyed());
foreach (EnemyStateMachine enemy in enemies)
{
enemy.SetHostile(shouldActivate);
}
}
public void AddFighterToGroup(EnemyStateMachine enemy)
{
// If you got the fighter you want to add on your list, return:
if (enemies.Contains(enemy)) return;
// For other fighters on the list, add them:
enemies.Add(enemy);
enemy.GetComponent<Health>().OnTakenHit += OnTakenHit; // NEW LINE
if (enemy.TryGetComponent(out CombatTarget target)) target.enabled = isActivated;
}
public void RemoveFighterFromGroup(EnemyStateMachine enemy)
{
// Remove fighters from the list
if (enemy == null) return;
enemies.Remove(enemy);
enemy.GetComponent<Health>().OnTakenHit -= OnTakenHit;
}
void OnTakenHit(GameObject instigator)
{
Activate(true);
}
}
}
-
When I hit the non-AggroGroup enemy, who is a projectile enemy (this really doesn’t matter here), and walk away, he will no longer return to patrolling (this was not a problem yesterday as far as I can recall, but the AggroGroup enemies were also a problem yesterday… I think we flipped the issues )
-
When I pause and unpause the game, and my enemy is either chasing me or patrolling between points, my enemy animation turns into sliding and he no longer plays the movement animation. He still moves as expected when I unpause the game, but the animations are a complete mess
How do I solve these?
Problem 1:
I solved this one, in a funky way. Here’s how I went around it
- I introduced a brand new variable called “InitiallyHostile” in “EnemyStateMachine.cs”, which holds the initial hostility state of the player. You’ll see why this is necessary later
The way this works, is that isHostile, at the start of the game, is set equal to “InitiallyHostile”, and it also acts as a holder for information when I want to reset my group members’ hostility if the player dies. So all I basically changed in “EnemyStateMachine.cs” is the following:
// new variable:
[field: SerializeField] public bool InitiallyHostile {get; private set;}
// in 'EnemyStateMachine.Start()':
// Initially, enemy hostility = enemy initialHostility
IsHostile = InitiallyHostile;
// Getter for the Initial Hostility:
public bool GetInitialHostility => InitiallyHostile;
I wouldn’t hide the ‘IsHostile’ from the inspector though, for debugging purposes
Next up, in AggroGroup.cs, I made some major changes in ‘Start()’ (for when you start the game), and ‘ResetGroupMembers()’ (I have a boolean called ‘resetIfPlayerDies’. If this is ticked, I want to reset my enemies to their initial hostility state, so that everyone goes back to what they started as, which is also why I created “InitiallyHostile”). The new “Start()” looks as follows:
private void Start()
{
// Ensures guards are not active to fight you, if you didn't trigger them:
if (groupHatesPlayer) Activate(true);
else
{
foreach (EnemyStateMachine enemy in enemies)
{
enemy.SetHostile(enemy.GetInitialHostility);
}
}
}
(for the curious minds, ‘groupHatesPlayer’ is my renaming of ‘activateOnStart’. I just find it easier to read with that name)
So currently, this solves the whole hostility issue when the game starts (or so I think… I’ll let Brian be the judge)
As for when the player resets on his death, this is what I ended up coding it to be:
/// <summary>
/// Called when the player dies, and everyone is reset to their original state
/// </summary>
public void ResetGroupMembers()
{
if (groupHatesPlayer) Activate(true);
else foreach (EnemyStateMachine enemy in enemies)
{
if (enemy.HoldsGrudgesAfterPlayerDeath) enemy.SetHostile(true);
else enemy.SetHostile(enemy.GetInitialHostility);
}
}
and to make sure it’s only called when ‘resetIfPlayerDies’, these are my OnEnable (called when the game starts), and OnDisable (called when the game ends. I don’t even know why it’s here anymore…):
private void OnEnable()
{
if (resetIfPlayerDies)
{
playerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<Health>();
if (playerHealth != null) playerHealth.onDie.AddListener(ResetGroupMembers);
}
}
public void OnDisable()
{
if (resetIfPlayerDies)
{
Health playerHealth = GameObject.FindGameObjectWithTag("Player").GetComponent<Health>();
if (playerHealth != null) playerHealth.onDie.RemoveListener(ResetGroupMembers);
}
}
I think for now it works perfectly fine, unless Brian has any comments about it
Almost forgot… also plug this line in ‘Respawner.ResetEnemies()’:
enemyStateMachine.SetHostile(enemyStateMachine.GetInitialHostility);
otherwise he will remember that you had a fight and hold the grudge, even after you respawn. If you want him to hold grudges, keep that one out
Problem 2:
anyway, I’ll go try solve this problem now
Edit: 3 hours later, I don’t even know how to start solving this one… Please send help
Edit 2: The NavMeshAgent gets turned off, and never back on… Now I have to hunt down why (I genuinely have not a single clue as of why. And no, it has nothing to do with him being a ranger or what not)
Edit 3: This was an easy fix, but I’m not sure if that’s the right way or not to do it. To fix this one, I went to ‘EnemyIdleState.Enter()’, and placed this line of code at the top of the function:
if (!stateMachine.Agent.enabled) stateMachine.Agent.enabled = true;
and it seemed to solve it
Problem 3:
My last problem with this system for now is this one. When the game is paused, the “FreeLookSpeed” turns into “NaN”, and it’s not recoverable until I quit the game scene (to the main menu) and return… Can you please help me fix it?
Edit 3: After a bit of squandering around the code, I found this line in “EnemyPatrolState.Tick()”:
stateMachine.Animator.SetFloat(FreeLookSpeedHash, grossSpeed / stateMachine.MovementSpeed, stateMachine.AnimatorDampTime, deltaTime);
which, I replaced with this for the time being (I’m still not sure what ‘grossSpeed’ is, or how to freeze the FreeLookSpeedHash):
if (Time.timeScale == 0.0f) stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0f, stateMachine.AnimatorDampTime, deltaTime);
else stateMachine.Animator.SetFloat(FreeLookSpeedHash, 1f, stateMachine.AnimatorDampTime, deltaTime);
[Note: My HumanoidWalk threshold is set to 1 in the Animator, hence why it’s 1 in the second parameter above. Feel free to tune it to your own threshold]
to compensate for when the game is paused. I tried replacing the ‘1f’ with ‘grossSpeed/stateMachine.MovementSpeed’, but that terribly failed… (I don’t know how to fix the division by zero issue here)
and so far this if-else statement seems to be working (mainly because 1f is constant for all animations… this may be a problem down the line if ‘FreeLookSpeedHash’ changes to other non-1 values)
For the Dwelling State, there’s no issues when the game pauses (because there’s no movement in there), but the chasing state will also need to undergo the same operation (because there is movement), as it has the same sliding problem
Actually, the ultimate arbiter of whether or not this solves the issue is…
if it works…
See previous comment…
I gotta tell you… when some guy in a player outfit shows up and starts swinging his sword at me, then dies, then has the audacity to come back to life… I remember that sort of thing… I’m still mad at that guy… for quite a while… I’d probably put a timer in CooldownTokenManager, something like “AngryAtPlayer” or “HeyRememberWhenThatGuyAttackedMe” with like a half hour on it…
I left that part out in the end, without a timer. If he holds a grudge against you, then he has a grudge against you until you kill him and he actually forgets about it. I left that code in the comment though for anyone interested in implementing it
However, now that you brought up timers, I might add that
Tomorrow though I will start investigating ways around getting the enemy to find ways around obstacles. Today, one of them attacked me through a huge fence between us, and that’s when I noticed I have a serious problem
It works, so I think all 3 problems mentioned above are in the clear now… Although I’d have loved to see how you’d go around when gross Speed /stateMachine.MovementSpeed is equal to zero.
For me, it causes the animations to bug
I’ll have to admit, you and @bixarrio come up with incredible solutions, so I always ask for reviews from either of you when I try something on my own, but first I have to try it on my own, so I can learn better - another reason I always ask, is to make sure whatever I did doesn’t accidentally mess my code with the other lessons you have coming up soon
Why not? It’s totally realistic
Usually, this is either because the Surface hasn’t been rebaked since the fence went in, or the Surface isn’t picking up the Fence as an obstacle…
Also known as when gross speed == 0? We covered that in the Player movement states, forgot to handle it in the Enemy movement states.
Vector3 deltaMovement = lastPosition - stateMachine.transform.position;
float deltaMagnitude = deltaMovement.magnitude;
if (deltaMagnitude > 0)
{
FaceTarget(stateMachine.transform.position - deltaMovement, deltaTime);
float grossSpeed = deltaMagnitude / deltaTime;
stateMachine.Animator.SetFloat(FreeLookSpeedHash, grossSpeed/stateMachine.MovementSpeed, stateMachine.AnimatorDampTime, deltaTime);
}
else
{
FaceTarget(stateMachine.Player.transform.position, deltaTime);
stateMachine.Animator.SetFloat(FreeLookSpeedHash, 0f);
}
Good morning. Yup, that worked - alright off to the NavMeshAgent issue, because my enemy needs to start finding ways to move around obstacles
And I also found an interesting mechanic that I’d love to utilize. It’s a “bug”, but it’s a funny little bug (basically, if the enemy swings his sword and there’s another enemy in his path, and that hit actually registers, I figured that if he’s hostile (the enemy that just got hit), they can fight it out until one of them dies and respawns, it’d be hilarious to get them to fight each other instead of chasing me down), and whoever wins shall continue to pursue me once the other is dead. They’ll dissolve it out after the respawn though, because they’ll need to focus on killing the player again eventually
Apart from that, I hope the projectile system is almost ready (and the banking system down the line… these two will be essential for me)
I don’t think this is the case. At first, it was, but then I went and made the fence static and changed the LayerMask on them to “Obstacle”, to be able to use this algorithm we developed a while ago:
bool HasLineOfSight() {
Ray ray = new Ray(transform.position + Vector3.up, target.transform.position - transform.position);
if (Physics.Raycast(ray, out RaycastHit hit, Vector3.Distance(transform.position, target.transform.position - transform.position), layerMask)) {
return false;
}
return true;
}
(it’s out of course context, and the LayerMask we assigned back then was “Obstacles”)
which was used in ‘Fighter.AttackBehaviour()’, as follows:
if (!HasLineOfSight()) {
while (!HasLineOfSight() && GetComponent<Mover>().CanMoveTo(target.transform.position)) {
GetComponent<Mover>().MoveTo(target.transform.position, 1);
return;
}
}
Sometimes he’ll find his way around it, sometimes he won’t (and when I added a “NavMeshObstacle” on the fence, he stopped hitting me through it, so that’s a plus one)… is there a way to increase his NavMesh searching length, so he searches for longer distances in case he gets stuck?
Increase the MaxNavMeshPath in Mover.cs. The higher the number, the greater the search.
done that… some paths he just can’t find (can’t blame him though, my NavMesh is terrible to some extent, at some areas)
And the enemies need a jumping state, in case they get stuck on the NavMeshAgent (because sometimes they do, on uneven surfaces). Let’s keep that in considerations
Minecraft does this with different types of enemies. If a skeleton shoots you but hits a zombie, the zombie will turn around and attack the skeleton which - in turn - will start defending himself. Winner continues to pursue the player
I didn’t even know anyone else tried that, but I’d LOVE to have this implemented in my own game… it’ll make combat a whole lot more fun. Would you be interested in helping me out?
I may also need to implement my own blocking state to the enemy as well… Just to keep things exciting